Ship It: Supabase + Vercel · lesson 1 of 3
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
| Product | What it gives you |
|---|---|
| Database | Postgres + extensions (pgvector, pg_cron, …) |
| Auth | PKCE, OAuth/OTP/password, JWT sessions in cookies |
| PostgREST | REST API generated from your schema, RLS-enforced |
| Realtime | Websocket streams of database changes |
| Storage | Object storage with RLS-style policies |
| Edge Functions | Deno 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/createServerClientwith 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.servehandler deployed viasupabase functions deploy, with secrets fromsupabase secrets set. - ca_ prefix pattern — namespacing one app's tables in a shared project by prefix, with RLS providing per-user isolation.
See also
- supabase-pgvector — vector search on this stack
- vercel-deployment — where the Next.js frontend lives
- how-this-site-works — this stack in production