Claude Academy
Sign in

Supabase for App Developers

Supabase is hosted Postgres plus a product suite around it: Auth (GoTrue), auto-generated REST (PostgREST), Realtime (websocket change feeds), Storage (S3-compatible objects), and Edge Functions (Deno). The mental model that keeps you safe: the database is directly reachable from browsers, so Postgres itself — via Row Level Security — is your authorization layer.

Auth: the current rules

Flow. Supabase uses the PKCE OAuth flow. After a sign-in redirect, your callback route exchanges the code:

// app/auth/callback/route.ts
const { error } = await supabase.auth.exchangeCodeForSession(code);

Clients. Use @supabase/ssr's createBrowserClient / createServerClient. The server client takes a cookies adapter with { getAll, setAll } — the old get/set/remove adapter is deprecated:

createServerClient(url, key, {
  cookies: {
    getAll: () => cookieStore.getAll(),
    setAll: (list) => list.forEach(({ name, value, options }) =>
      cookieStore.set(name, value, options)),
  },
});

The session lives in the sb-<project-ref>-auth-token cookie.

Server-side identity — the exam-favorite rule: on the server, trust getClaims() (verifies the JWT locally, no network) or getUser() (round-trip to the auth server), and NEVER getSession()getSession() reads the cookie without verifying it, so a forged cookie passes.

Keys. New projects issue sb_publishable_... and sb_secret_... API keys, replacing the legacy anon and service_role JWTs. Same trust model: publishable is safe in browsers because RLS constrains it; secret bypasses RLS and must never ship to a client.

RLS: mandatory, not optional

Any table exposed through the API must have RLS enabled, or every row is readable with the publishable key:

alter table ca_progress enable row level security;

create policy "own rows" on ca_progress
  for select to authenticated
  using ((select auth.uid()) = user_id);

Idioms embedded in that snippet:

  • to authenticated — policy scoped to logged-in users only.
  • (select auth.uid()) — subquery form lets Postgres cache the function result per statement instead of per row (an initPlan), a significant performance win.
  • One policy per operation (for select, for insert with check ..., etc.).

This very site uses ca_-prefixed tables with RLS in a shared Supabase project — prefix-namespacing plus per-user policies is a cheap multi-tenancy scheme (contrast physical isolation in local-first-databases). Details in how-this-site-works.

Edge Functions

Deno-runtime functions deployed to the edge:

Deno.serve(async (req) => {
  return new Response(JSON.stringify({ ok: true }),
    { headers: { "Content-Type": "application/json" } });
});
supabase functions new my-fn
supabase functions serve my-fn      # local dev
supabase functions deploy my-fn
supabase secrets set OPENAI_KEY=... # runtime secrets

Typical AI-app roles: webhook receivers, embedding generation on insert (feeding supabase-pgvector), and server-side calls that need the secret key.

The stack at a glance

ProductWhat it gives you
DatabasePostgres + extensions (pgvector, pg_cron, …)
AuthPKCE, OAuth/OTP/password, JWT sessions in cookies
PostgRESTREST API generated from your schema, RLS-enforced
RealtimeWebsocket streams of database changes
StorageObject storage with RLS-style policies
Edge FunctionsDeno HTTP handlers, globally deployed

Key terms

  • PKCE flow — the OAuth authorization-code flow with proof key that Supabase Auth uses; completed server-side via exchangeCodeForSession(code).
  • @supabase/ssr — the package providing createBrowserClient/createServerClient with the cookies {getAll, setAll} adapter (the old get/set/remove adapter is deprecated).
  • sb-<ref>-auth-token — the cookie holding the Supabase session for project <ref>.
  • getClaims() vs getUser() vs getSession() — local JWT verification vs auth-server round-trip vs unverified cookie read; on the server use the first two, never the third.
  • sb_publishable_ / sb_secret_ — the new API-key pair replacing anon/service_role; publishable is RLS-constrained and browser-safe, secret bypasses RLS.
  • RLS (Row Level Security) — Postgres row-filtering policies; mandatory on every API-exposed table because the database is browser-reachable.
  • (select auth.uid()) — the subquery idiom in policies that caches the auth function per statement instead of evaluating per row.
  • PostgREST — the layer auto-generating a REST API from the schema; every request passes through RLS.
  • Edge Function — a Deno Deno.serve handler deployed via supabase functions deploy, with secrets from supabase secrets set.
  • ca_ prefix pattern — namespacing one app's tables in a shared project by prefix, with RLS providing per-user isolation.

See also