Tutorials / Backend integrations / Set up Supabase (database + auth)
πŸ“ Written ● Beginner Updated 2026-05-13

How do I set up Supabase for database and auth?

TL;DR: Sign up at supabase.com, create a project, copy the Project URL and anon key from Settings β†’ API into your env vars, and call createClient(url, key). Database, auth, storage, and a JS SDK all come from that one signup. Free tier: 500 MB DB, 50K active users.

Postgres + Auth + Storage + a JS SDK, one signup. Most "I just need a backend" projects start here. Free tier is real β€” 500 MB DB, 50K active users, 1 GB storage. Ten minutes to a working sign-up flow.

Why Supabase (and when not)

0
  • Pick Supabase when β€” you want a working backend without setting up Postgres + auth + storage separately. The integration is the value.
  • Skip Supabase when β€” you need very specific Postgres extensions Supabase doesn't enable, you're tied to a different auth provider, or you have a team-level standard for managed Postgres (Neon, RDS, etc.). For pure Postgres without auth, Neon β†— or PlanetScale β†— (MySQL) are common alternatives.

Create a project

1

Sign up at supabase.com β†— (free; GitHub OAuth or email).

Click New Project. Pick:

  • Name β€” anything.
  • Database password β€” strong; save it. You'll need it for direct Postgres connections.
  • Region β€” closest to your users / your hosting.
  • Plan β€” Free tier is fine to start.

Provisioning takes ~2 min. Then you're in the project dashboard.

Grab your URL and anon key

2

Project dashboard β†’ Project Settings β†’ API. Copy two values:

  • Project URL β€” https://<ref>.supabase.co
  • anon public key β€” long JWT-shaped string. Safe to ship in frontend bundles; row-level security limits what it can do.
  • service_role key β€” also visible here. Never ship this to a browser. Backend-only; bypasses all row-level security.

Backend env:

SUPABASE_URL=https://<ref>.supabase.co
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIs...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIs...   # backend ONLY

Install the SDK

3
npm install @supabase/supabase-js

Other languages: Python β†—, Dart/Flutter β†—, Swift β†—, Kotlin β†—, C# β†—.

Initialize the client

4
import { createClient } from "@supabase/supabase-js";

export const supabase = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_ANON_KEY
);

This client is safe to use in both frontend and backend. For backend code that needs to bypass row-level security (admin operations), create a separate client with the service-role key.

Create your first table

5

Supabase dashboard β†’ Table Editor β†’ New table. Or use SQL Editor:

CREATE TABLE notes (
  id BIGSERIAL PRIMARY KEY,
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
  title TEXT NOT NULL,
  body TEXT,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Enable Row Level Security (RLS)
ALTER TABLE notes ENABLE ROW LEVEL SECURITY;

-- Allow users to read their own notes
CREATE POLICY "users read own notes" ON notes
  FOR SELECT USING (auth.uid() = user_id);

-- Allow users to insert their own notes
CREATE POLICY "users insert own notes" ON notes
  FOR INSERT WITH CHECK (auth.uid() = user_id);

Row Level Security (RLS) is the most important Supabase concept. Without RLS, your anon key lets anyone read/write everything. With RLS, the database itself enforces who-can-do-what. Always enable RLS on tables that store user data.

RLS docs β†—.

Read and write data

6
// Read
const { data: notes, error } = await supabase
  .from("notes")
  .select("*")
  .order("created_at", { ascending: false });

// Write
const { data: newNote, error } = await supabase
  .from("notes")
  .insert({ title: "Hello", body: "First note" })
  .select()
  .single();

// Update
await supabase
  .from("notes")
  .update({ title: "Updated title" })
  .eq("id", newNote.id);

// Delete
await supabase.from("notes").delete().eq("id", newNote.id);

If the user isn't signed in (next step), these will return empty arrays because the RLS policies require auth.uid(). That's the system working correctly.

Add authentication

7

Supabase Auth supports email/password, magic links, phone, and dozens of OAuth providers β€” all from the same SDK.

// Sign up
const { data, error } = await supabase.auth.signUp({
  email: "[email protected]",
  password: "secure-password",
});

// Sign in
const { data, error } = await supabase.auth.signInWithPassword({
  email: "[email protected]",
  password: "secure-password",
});

// Sign out
await supabase.auth.signOut();

// Get current user (returns null if not signed in)
const { data: { user } } = await supabase.auth.getUser();

After sign-in, the SDK stores a JWT in local storage. Every subsequent supabase.from(...) call carries it; RLS policies use auth.uid() to filter.

Enable OAuth providers

8

Project dashboard β†’ Authentication β†’ Providers. Toggle Google, GitHub, Apple, etc. Each provider asks for Client ID + Client Secret from that provider's console β€” see Sign in with Google and Sign in with GitHub for how to get those.

Then in your app:

await supabase.auth.signInWithOAuth({
  provider: "google",
  options: { redirectTo: "https://yourapp.com/auth/callback" },
});

That's the entire flow β€” Supabase handles the redirect dance, JWT issuance, and session refresh.

React to auth state changes

9
supabase.auth.onAuthStateChange((event, session) => {
  // event: "SIGNED_IN" | "SIGNED_OUT" | "TOKEN_REFRESHED" | ...
  if (event === "SIGNED_IN") {
    console.log("Signed in as", session.user.email);
  }
});

Subscribe once at app startup. Useful for syncing UI state (show "Sign out" button when signed in, etc.).

Realtime subscriptions

10

Supabase publishes Postgres changes over WebSockets. Subscribe to a table:

const channel = supabase
  .channel("notes-changes")
  .on(
    "postgres_changes",
    { event: "*", schema: "public", table: "notes" },
    (payload) => {
      console.log("Change:", payload);
    }
  )
  .subscribe();

// Later, to disconnect:
supabase.removeChannel(channel);

Combine with React state: live multiplayer cursors, real-time chat, collaborative docs β€” all without writing a WebSocket server. Realtime docs β†—.

Storage (file uploads)

11

Supabase ships an S3-compatible object store. For file uploads inside Supabase apps:

// Create a bucket once in the dashboard: Storage β†’ Create bucket
await supabase.storage
  .from("avatars")
  .upload(`${user.id}/profile.png`, fileBlob, { upsert: true });

const { data } = supabase.storage
  .from("avatars")
  .getPublicUrl(`${user.id}/profile.png`);

console.log(data.publicUrl);   // ready to put in an <img>

If you're already on AWS or want zero egress, see Upload files to S3 or Cloudflare R2. Otherwise Supabase Storage is the easiest path within the same project.

Local development with the CLI

12

For real projects, develop against a local Supabase instance, not production:

brew install supabase/tap/supabase   # or via npm: npm i -g supabase
supabase init                          # in your project root
supabase start                         # spins up Postgres + Studio + Auth in Docker

Run migrations as SQL files in supabase/migrations/. Push to production with supabase db push. CLI docs β†—.

RLS is opt-in per table. Tables you create through the SQL editor don't have RLS enabled by default. If you forget, your anon key reads everything. Always run ALTER TABLE foo ENABLE ROW LEVEL SECURITY; immediately after creating a user-data table.

Pricing reality

13

Free tier: 500 MB database, 50,000 monthly active users, 1 GB file storage, 5 GB bandwidth, 2 free projects. Pauses inactive projects after a week of no requests (you can wake them up; not destroyed).

Pro tier: $25/mo β€” 8 GB DB, 100K MAU, 100 GB storage, projects never pause. Supabase pricing β†—.

Self-hosted: full source is open. Self-hosting docs β†—. Production self-hosting is real work; most people stay on managed.

Official references

What's next