dom4in.net
← All resources
Guide · Supabase

Start a Supabase project: Postgres, auth, and API in one

One project gives you a real Postgres database, user auth, and instant APIs — the trick is knowing that Row Level Security is the actual security model, not the API keys.

The process

The full path from new project to authenticated users reading their own data. RLS in step three is the load-bearing step — everything else is convenience.

Directions

1Create the project and find your keys

Create a project at supabase.com — pick the region near your users, set a strong database password, and save it (it's the raw Postgres credential, separate from everything else). Provisioning takes a couple of minutes.

In Settings → API live the two keys that confuse everyone: the anon key ships in your frontend and is safe only because Row Level Security constrains it; the service_role key bypasses RLS entirely and belongs only in server-side secrets — a leaked service key is a leaked database.

2Create tables in the SQL editor

Use the SQL editor (or the table UI) to create your schema. Keep a habit from the start: every user-owned table gets a user_id uuid column referencing auth.users, because that column is what RLS policies key on.

The auto-generated REST API exists the moment a table does — every table in the public schema is queryable through PostgREST with the client library, filtered by whatever policies you write next.

create table notes (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users not null default auth.uid(),
  content text,
  created_at timestamptz default now()
);

3Turn on Row Level Security and write policies

Enable RLS on every table, then write policies expressing who can do what. Without RLS enabled, any table in an exposed schema is readable and writable by anyone holding the anon key — which is everyone, because it's in your JS bundle. This is the most common real-world Supabase security hole, and it's entirely avoidable.

The standard owner pattern is two policies: select where user_id = auth.uid(), and insert/update/delete with the same check. The dashboard's security advisor flags tables with RLS off — keep it at zero findings.

alter table notes enable row level security;
create policy "own rows" on notes
  for all using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

4Wire up sign-in

Email/password and magic links work out of the box; OAuth providers (Google, GitHub) need a client ID and secret from each provider's console, pasted into Authentication → Providers, using the callback URL Supabase displays.

On the frontend the client library handles the whole session: signInWithOAuth or signInWithOtp, then every subsequent query automatically carries the user's JWT — which is what your RLS policies see as auth.uid(). Set your site URL and redirect allow-list in auth settings so production links don't bounce to localhost.

5Point your app at it and mind the tiers

Install @supabase/supabase-js, initialize with the project URL and anon key from env vars, and CRUD reads like a query builder: supabase.from('notes').select() returns only rows the policies allow. Server-side code (Workers, functions) that must act across users uses the service key — from a secret store, never bundled.

Free-tier projects pause after a week of inactivity, which is fine for prototypes and surprising in production. Before real users arrive: turn on backups, check the security advisor once more, and know your project's egress and connection limits.

Common issues & fixes

Queries return empty arrays but the data exists.

RLS is on with no select policy matching, or the user isn't signed in so auth.uid() is null. Check policies first, then the session.

Everything works — including reading other users' rows.

RLS is off on that table. Enable it and add the owner policies; the security advisor page lists every offender.

OAuth redirect lands on localhost in production.

Site URL and redirect URLs in auth settings still point at dev. Add the production URLs to the allow-list.

'Too many connections' from a serverless backend.

Every invocation opened a raw Postgres connection. Use the pooled connection string (transaction mode) or the HTTP client library instead of direct Postgres from functions.

Project paused and the app is down.

Free-tier inactivity pause. Restore it in the dashboard, and upgrade the project if it's serving anything real.

Honesty note: articles are drafted with AI assistance, then a human checks each one against the vendor's current setup flow before it publishes — nothing goes live unreviewed. Vendor interfaces still change; if a step looks different, the underlying record is what matters.