Overview
Supabase bundles managed Postgres, Auth, Storage, Realtime, and an auto-generated REST and GraphQL Data API. Use it when you want a Postgres-backed backend with built-in auth and row-level security and no database cluster to run. Because the Data API exposes tables directly, the key model and supabase-rls are the security boundary.
Use publishable and secret keys, not anon and service_role
New projects use sb_publishable_... (low privilege, safe in browser and mobile code) and sb_secret_... (elevated, server only). The legacy JWT-based anon and service_role keys are being deprecated by the end of 2026, and both systems coexist until then. A secret key maps to the service_role Postgres role, which has BYPASSRLS, so it skips every policy.
- Keep secret keys in server environments only: API handlers, Edge Functions, CI. Supabase returns 401 when a secret key is used from a browser, but never rely on that. See secrets-and-env.
- Send the new keys in the
apikeyheader. They are not JWTs, so do not verify them as JWTs or put them inAuthorization: Bearer. - To rotate a leaked key: create a replacement, deploy it, then delete the old key.
// Browser-safe
const supabase = createClient(url, process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!)
// Never: createClient(url, process.env.NEXT_PUBLIC_SUPABASE_SECRET_KEY!)Grant Data API access explicitly
New tables in the public schema are no longer exposed to the Data API automatically. The change is the default for new projects from 2026-05-30 and is enforced on all existing projects on 2026-10-30; tables that already exist keep their grants. Grant each role what it needs, then enable RLS:
grant select on public.posts to anon;
grant select, insert, update, delete on public.posts to authenticated;
alter table public.posts enable row level security;Without a GRANT, a role cannot reach the table at all, and a missing grant fails before any policy runs. Add the grants to your migrations so they are reviewed and reproducible. Policy details are in supabase-rls.
Initialize and link with the CLI
npm install -D supabase # global npm install is unsupported
npx supabase init
npx supabase link # link to a hosted project
supabase start # full local stack in Dockersupabase init creates supabase/ with config.toml, a seed file, and the migrations folder. supabase link is required before pushing migrations. The local stack is ephemeral and reseeds from supabase/seed.sql.
Change the schema through migrations only
supabase migration new add_posts_table # writes supabase/migrations/<timestamp>_add_posts_table.sql
supabase db push # apply pending migrations to the linked projectDashboard schema edits in production bypass version control. Prototype locally, then capture the change in a migration. See migrations for the general contract.
Pick the connection mode by workload
| Mode | Endpoint | Use for |
|---|---|---|
| Direct | db.<ref>.supabase.co:5432 (IPv6, or IPv4 with the add-on) | Persistent servers, migrations |
| Session pooler | aws-<n>-<region>.pooler.supabase.com:5432 (IPv4) | Persistent clients that cannot use IPv6 |
| Transaction pooler | aws-<n>-<region>.pooler.supabase.com:6543 (IPv4) | Serverless and edge functions |
Transaction mode shares server connections across clients and does not support prepared statements or query pipelining, so disable both in the driver. Port 6543 serves transaction mode only; session mode on 6543 was deprecated on 2025-02-28. In serverless code, create the client once at module scope and keep its pool to one connection. For Prisma settings see prisma-pooling; for Postgres-side pooling see postgres.