Every client project runs on Supabase; these are the defaults that keep them safe and consistent. When a project's own docs conflict, the project wins — but say so.
When to use
- Creating or altering tables, policies, functions, or triggers on a Supabase project
- Wiring a client app or server to Supabase (keys, environment, SSR)
- Reviewing a Supabase project before ship or handoff
API keys
- New code uses the current key family:
sb_publishable_…in clients,sb_secret_…on servers only. Do not introduce the legacyanon/service_roleJWT keys into new code — they are deprecated with removal planned for late 2026 (supabase.com/docs → migrating-to-new-api-keys). - Touching a project still on legacy keys? Flag the migration as its own task; don't mix key families within one codebase.
- Secret keys never reach a browser, a client bundle, or a repo. Publishable keys are fine to expose — RLS is the security boundary, not key secrecy.
Schema & RLS
- RLS on from the first migration, deny-by-default; every policy names its table's
tenant predicate explicitly (e.g.
org_id = (select auth.jwt() ->> 'org_id')::uuidor a sharedis_org_member(org_id)helper — one pattern per project, reused). - Multi-tenant tables carry
org_id(or the project's tenant column) NOT NULL with a foreign key; server-side code derives the tenant from the credential, never from the request body. security definerfunctions pinsearch_pathand get an explicitrevoke execute … from publicdecision, stated in the migration.
Migrations
- Schema changes are migration files in the repo (CLI-generated), applied in order — never hand-run statements that bypass the migration history.
- Don't pin extension versions in
create extension— version clauses are ignored on the platform (changelog 2026-08); state required extensions by name.
Before ship
- Run the security and performance advisors and act on every finding or record why
it stands (
security definerviews, missing indexes on FKs, permissive policies). - Verify tenant isolation with a real cross-tenant read attempt, not by reading the policy: query as tenant A for tenant B's rows and expect zero.
Examples
Good: policy "orders_select" on orders for select using (org_id = app.current_org_id())
— named predicate, reused helper, deny-by-default table.
Bad: alter table orders disable row level security; -- "we filter in the API"
— one forgotten endpoint and every tenant reads every order.