Supabase Standards
Supabase is Postgres underneath, so this domain co-loads with database (generic
Postgres) and the auth / security concerns. It owns only the platform
contract: RLS, the key boundary, Postgres/Edge function security, and the CLI
migration workflow. Generic Postgres rules (types, constraints, indexing-in-general,
expand-contract, N+1, transactions) live in database; auth flows (JWT verification,
OAuth, password reset) live in global/refs/auth.md — this domain links to them
rather than restating them.
Priority: P0 — Row-Level Security
- RLS is OFF by default. Every table reachable through the API/PostgREST must
ENABLE ROW LEVEL SECURITYin the same migration that creates it. A table left unguarded behind a publicanonkey is a full data leak. Signal: acreate tableinsupabase/migrationswith no matchingalter table … enable row level security. - RLS-on denies by default — write an explicit policy per operation
(
select/insert/update/delete). Don't rely on onefor allpolicy where operations need different predicates. Signal: a table with RLS enabled and no policy, or a blanketfor all using (true). - Pair every
UPDATE/DELETEpolicy with aSELECTpolicy. Postgres must read the existing row to evaluate theUSINGclause; withoutSELECTthe row is invisible and the write silently affects nothing. Signal: anupdate/deletepolicy on a table with noselectpolicy. - Use
WITH CHECKonINSERT/UPDATEso a user can't write a row they couldn't own or read (e.g. inserting someone else'suser_id). Signal: aninsert/updatepolicy withusingbut nowith check. - Wrap auth calls as
(select auth.uid())/(select auth.jwt())in policy predicates so the planner caches the result per-statement instead of re-evaluating per row. Signal: a bareauth.uid()in a policy on a table that gets scanned. - Never base a policy on
auth.jwt() -> 'user_metadata'—user_metadatais editable by the authenticated user. Useapp_metadata(server-controlled) or a roles table joined via asecurity definerhelper. Signal: a policy readinguser_metadatafor an authorization decision. - Index every column referenced in an RLS predicate (
user_id,tenant_id, …). RLS turns these into per-query filters; an unindexed predicate column is a full scan on every request. (Sharpens thedatabaseindexing rule.) Signal: an RLS predicate column absent from any index.
Priority: P0 — Keys & Client Boundary
- Use publishable keys (
sb_publishable_...) for public clients; treat legacyanonas the compatibility form. Public clients are only safe when RLS protects every exposed table for theanon/authenticatedroles. Signal: a publishable or legacyanonclient used against a table with RLS disabled. - Secret keys (
sb_secret_...) and legacyservice_rolekeys bypass RLS — backend / Edge Functions only. Never ship them to a browser or mobile bundle, never put them in a client-public env (NEXT_PUBLIC_*,EXPO_PUBLIC_*,VITE_*), and never pass them in URLs or query params. A leaked secret key is root on the database. Signal:sb_secret,SUPABASE_SECRET_KEYS,service_role, orSUPABASE_SERVICE_ROLE_KEYreferenced in client-bundled code, a public-prefixed env var, a URL/query param, or unsanitized logs. - A user-session client and an admin client are separate instances. A client
carrying a user session sends the user JWT (RLS applies); do not attach a user
session to the admin client, and the user session must not override the admin API
key. In SSR, build a dedicated admin client from a secret key / legacy
service_rolekey. Signal: one shared client mixing a user session withsb_secret/service_role.
Priority: P0 — Postgres & Edge Functions
- Prefer
SECURITY INVOKER(the default) for Postgres functions. If a function must beSECURITY DEFINER, setsearch_path = '', schema-qualify every relation (public.table), and never create it in an API-exposed schema. Signal:security definerwith noset search_path = '', or such a function in an exposed schema. - Edge Functions are publicly invokable by default — match
verify_jwtto the caller credential and verify inside the handler when needed. Keepverify_jwton for user-JWT calls. Turn it off for webhooks or API-key service calls, then verify the provider signature orapikeyheader in code. Publishable/secret keys are not JWTs and must not be sent asAuthorization: Bearer .... Signal: an Edge Function reading user data withverify_jwt = falseand no signature /apikey/ authorization check; a publishable or secret key sent as a bearer token; a literal key in function source. - Treat Postgres as a pooled remote from Edge Functions — use the connection
pooler / serverless-friendly client; don't open a fresh direct connection per
invocation.
Signal: a
new Pool/ direct-connect per request in a function.
Priority: P1 — Migrations & Workflow
- All schema and RLS changes go through Supabase CLI migrations in
supabase/migrations, version-controlled — never schema-edit only in the dashboard (silent drift). Write RLS policies as explicit SQL; ORM-generated migrations don't capture them. Signal: a dashboard-only schema change with no migration file. - Storage buckets are private by default; gate access with storage RLS policies.
Signal: a
public = truebucket holding user/private data. - P1 (design): enable Realtime per-table deliberately, and remember RLS applies to
Realtime too — a
postgres_changessubscription only emits rows the subscriber can read.
Anti-Patterns
- Table exposed with RLS disabled
- RLS on with no policy, or a blanket
for all using (true) update/deletepolicy without aselectpolicyinsert/updatepolicy withoutwith check- bare
auth.uid()per-row in a policy - policy keyed on
user_metadatafor authorization - unindexed RLS predicate column
sb_secret/service_rolekey in a client bundle, public-prefixed env var, URL, or log- user session attached to the admin client
security definerwith nosearch_path = '', or in an exposed schema- Edge Function on user data with
verify_jwtoff and no in-code check - hardcoded secrets in Edge Function source
- dashboard-only schema edits (drift)
- public storage buckets holding private data
References
Load only what the task requires:
- rls — enable-RLS migration pattern, per-operation policies,
SELECT+UPDATEpairing,WITH CHECK,(select auth.uid())wrapping,app_metadatavsuser_metadata, indexing predicate columns - keys-and-clients — publishable/anon vs secret/service_role, browser/mobile boundary, SSR admin-client separation, public-env pitfalls
- database-functions —
SECURITY INVOKERdefault,SECURITY DEFINER+search_path = ''+ schema qualification, exposed-schema rule,auth.uid()in helpers - edge-functions — Deno runtime, user JWT vs API-key auth,
verify_jwt/ in-code auth, project secrets, connection pooling for Postgres - migrations — CLI workflow, RLS-as-SQL, dashboard drift, storage and Realtime policies
- checklist — pre-deploy review checklist