Supabase Core
Shared model for the supabase cluster. The feature-build, current-API, and performance
spokes all depend on these interlocking concepts — keep them consistent here so no spoke
contradicts another.
1. The authorization model (Supabase's defining decision)
Authorization lives in the database, as Postgres Row-Level Security. Supabase exposes your
tables directly (the auto-generated Data API / PostgREST), so a request can reach a table
without passing through any app server. RLS is the boundary every product enforces:
Request ──carries──> Role + JWT claims ──checked by──> RLS Policy(ies) ──on──> Table/Storage object
- RLS is default-deny once enabled, but OFF by default on a new table. Enable RLS on every table in an exposed schema (especially
public), then write policies that match the actual access model — don't reflexively stamp the same auth.uid() policy on every table. → supabase, supabase-developer
- A policy is per-command. An
UPDATE first has to SELECT the row — without a SELECT policy, updates silently affect 0 rows (no error). Storage upsert needs INSERT + SELECT + UPDATE; INSERT alone makes file replacement fail silently.
- Views and privileged code bypass RLS. Create views
WITH (security_invoker = true) (Postgres 15+); keep security definer functions in a private/unexposed schema; on older Postgres, revoke view access from anon/authenticated.
Rule: the narrowest policy that satisfies the real access model — and treat enabling the
Data API on a schema, adding a permissive policy, or exposing a view as a security change
worth stating.
2. Keys & roles (who reaches the database, and as what)
| Key / role |
Where it belongs |
Notes |
| publishable (frontend) |
public clients, browser |
preferred for client code; subject to RLS as anon/authenticated |
| legacy anon |
compatibility only |
same RLS roles; prefer publishable |
| service_role / secret |
server-only, never shipped |
bypasses RLS — never in a public client; in Next.js, never behind NEXT_PUBLIC_ |
The role + JWT decide which policies apply. service_role ignores RLS entirely, which is
exactly why it must never reach the browser.
3. Auth claims & sessions (the silent traps)
- Never use
user_metadata / raw_user_meta_data in authorization. It is user-editable
and can appear in auth.jwt(). Put authorization data in app_metadata / raw_app_meta_data. → supabase
- JWT claims aren't fresh until the user's token refreshes —
app_metadata/auth.jwt() can lag.
- Deleting a user does not invalidate existing access tokens — sign out / revoke sessions,
keep JWT expiry short, validate
session_id against auth.sessions for sensitive ops.
- On the client, distinguish
getSession (fast, trusts local) from getUser/getClaims
(revalidated); use @supabase/ssr for cookie-based SSR. → supabase
4. Products map onto the same boundary
| Product |
What it is |
Access controlled by |
| Database / Data API |
Postgres + auto REST (PostgREST), optional pg_graphql |
RLS policies |
| Auth |
email/OAuth/magic-link/phone, issues the JWT |
claims feed RLS |
| Storage |
S3-compatible objects |
RLS on storage objects |
| Realtime |
WebSocket DB-change / broadcast / presence |
RLS on the underlying tables |
| Edge Functions |
Deno serverless (TS) |
the key the function uses |
| Extensions |
pgvector, pg_cron, pg_graphql, queues |
SQL / schema |
Feature wiring across these → supabase-developer. Current per-product API/config (which drifts
between versions) → supabase. Query/index/pooling performance underneath → supabase-postgres-best-practices.
5. The schema-change workflow (iterate freely, commit cleanly)
- Iterate with
execute_sql (MCP) or supabase db query (CLI) — runs SQL directly, no
migration-history entry, so you can change your mind. Do NOT use apply_migration to
iterate a local schema — it writes history on every call and breaks db diff/db pull.
- Run advisors →
supabase db advisors (CLI v2.81.3+) or MCP get_advisors; fix findings.
- Re-check the security traps in §1–§3 if the change touches views, functions, triggers, storage, or auth.
- Generate the migration →
supabase db pull <name> --local --yes, then supabase migration list --local to verify. → supabase
CLI gotchas: supabase db query needs v2.79.0+; supabase db advisors needs v2.81.3+ —
fall back to MCP execute_sql / get_advisors. Always discover commands via --help; the CLI
structure changes between versions.
6. Version / conventions
- Supabase changes frequently — do not trust training data. Function signatures,
config.toml, and API conventions drift between versions. Look up the topic first:
MCP search_docs → fetch any docs page as markdown (append .md to the URL) → web search. → supabase
- Verify your work: after any fix, run a test query confirming it. A fix without verification is incomplete.
- Recover, don't loop: if an approach fails 2–3 times, change method / check docs / read logs — don't re-run the same command.
7. Shared guardrails
- RLS-by-default: enable it on every table in an exposed schema; policies match the real access model.
- Remember per-command policies:
UPDATE needs SELECT; storage upsert needs INSERT + SELECT + UPDATE.
app_metadata, never user_metadata, for authorization claims.
- Never expose
service_role/secret keys in public clients; publishable keys for the browser.
- Views
security_invoker; security definer functions out of exposed schemas.
- State every data-access-surface change (RLS enable/policy, exposed schema, view, storage policy, key).
- Verify against live docs before implementing; verify the result with a test query after.
- Supabase vs a hand-rolled backend: pick Supabase for Postgres + the RLS boundary + batteries-included
Auth/Storage/Realtime; if you need heavy custom server logic beyond Edge Functions, weigh a
dedicated backend cluster alongside it.
1---2name: supabase-core3description: Shared reference for the Supabase cluster: the authorization model (Row-Level Security as the one boundary every product enforces), the keys/roles that reach the database, the auth-claims rule (app_metadata not user_metadata), the security-checklist traps, and the schema-change → advisor → migration workflow. USE WHEN designing tables, writing RLS policies, choosing keys, reviewing security, or shipping a Supabase schema change — the interlocking rules every Supabase spoke shares.4---56# Supabase Core78Shared model for the `supabase` cluster. The feature-build, current-API, and performance9spokes all depend on these interlocking concepts — keep them consistent here so no spoke10contradicts another.1112## 1. The authorization model (Supabase's defining decision)1314**Authorization lives in the database, as Postgres Row-Level Security.** Supabase exposes your15tables directly (the auto-generated Data API / PostgREST), so a request can reach a table16without passing through any app server. RLS *is* the boundary every product enforces:1718```19Request ──carries──> Role + JWT claims ──checked by──> RLS Policy(ies) ──on──> Table/Storage object20```2122- **RLS is default-deny once enabled, but OFF by default on a new table.** Enable RLS on **every** table in an exposed schema (especially `public`), then write policies that match the *actual* access model — don't reflexively stamp the same `auth.uid()` policy on every table. → `supabase`, `supabase-developer`23- **A policy is per-command.** An `UPDATE` first has to `SELECT` the row — **without a `SELECT` policy, updates silently affect 0 rows** (no error). Storage **upsert needs INSERT + SELECT + UPDATE**; INSERT alone makes file replacement fail silently.24- **Views and privileged code bypass RLS.** Create views `WITH (security_invoker = true)` (Postgres 15+); keep `security definer` functions in a private/unexposed schema; on older Postgres, revoke view access from `anon`/`authenticated`.2526**Rule:** the narrowest policy that satisfies the real access model — and treat *enabling the27Data API on a schema*, *adding a permissive policy*, or *exposing a view* as a security change28worth stating.2930## 2. Keys & roles (who reaches the database, and as what)3132| Key / role | Where it belongs | Notes |33|---|---|---|34| **publishable** (frontend) | public clients, browser | preferred for client code; subject to RLS as `anon`/`authenticated` |35| legacy **anon** | compatibility only | same RLS roles; prefer publishable |36| **service_role** / secret | server-only, never shipped | **bypasses RLS** — never in a public client; in Next.js, never behind `NEXT_PUBLIC_` |3738The role + JWT decide which policies apply. `service_role` ignores RLS entirely, which is39exactly why it must never reach the browser.4041## 3. Auth claims & sessions (the silent traps)4243- **Never use `user_metadata` / `raw_user_meta_data` in authorization.** It is **user-editable**44 and can appear in `auth.jwt()`. Put authorization data in `app_metadata` / `raw_app_meta_data`. → `supabase`45- **JWT claims aren't fresh** until the user's token refreshes — `app_metadata`/`auth.jwt()` can lag.46- **Deleting a user does not invalidate existing access tokens** — sign out / revoke sessions,47 keep JWT expiry short, validate `session_id` against `auth.sessions` for sensitive ops.48- On the client, distinguish `getSession` (fast, trusts local) from `getUser`/`getClaims`49 (revalidated); use `@supabase/ssr` for cookie-based SSR. → `supabase`5051## 4. Products map onto the same boundary5253| Product | What it is | Access controlled by |54|---|---|---|55| **Database / Data API** | Postgres + auto REST (PostgREST), optional `pg_graphql` | RLS policies |56| **Auth** | email/OAuth/magic-link/phone, issues the JWT | claims feed RLS |57| **Storage** | S3-compatible objects | RLS on storage objects |58| **Realtime** | WebSocket DB-change / broadcast / presence | RLS on the underlying tables |59| **Edge Functions** | Deno serverless (TS) | the key the function uses |60| **Extensions** | `pgvector`, `pg_cron`, `pg_graphql`, queues | SQL / schema |6162Feature wiring across these → `supabase-developer`. Current per-product API/config (which drifts63between versions) → `supabase`. Query/index/pooling performance underneath → `supabase-postgres-best-practices`.6465## 5. The schema-change workflow (iterate freely, commit cleanly)66671. **Iterate** with `execute_sql` (MCP) or `supabase db query` (CLI) — runs SQL directly, **no**68 migration-history entry, so you can change your mind. Do **NOT** use `apply_migration` to69 iterate a local schema — it writes history on every call and breaks `db diff`/`db pull`.702. **Run advisors** → `supabase db advisors` (CLI v2.81.3+) or MCP `get_advisors`; fix findings.713. **Re-check the security traps** in §1–§3 if the change touches views, functions, triggers, storage, or auth.724. **Generate the migration** → `supabase db pull <name> --local --yes`, then `supabase migration list --local` to verify. → `supabase`7374CLI gotchas: `supabase db query` needs **v2.79.0+**; `supabase db advisors` needs **v2.81.3+** —75fall back to MCP `execute_sql` / `get_advisors`. Always discover commands via `--help`; the CLI76structure changes between versions.7778## 6. Version / conventions7980- **Supabase changes frequently — do not trust training data.** Function signatures,81 `config.toml`, and API conventions drift between versions. Look up the topic first:82 MCP `search_docs` → fetch any docs page as markdown (append `.md` to the URL) → web search. → `supabase`83- **Verify your work**: after any fix, run a test query confirming it. A fix without verification is incomplete.84- **Recover, don't loop**: if an approach fails 2–3 times, change method / check docs / read logs — don't re-run the same command.8586## 7. Shared guardrails8788- **RLS-by-default**: enable it on every table in an exposed schema; policies match the *real* access model.89- Remember per-command policies: `UPDATE` needs `SELECT`; storage upsert needs INSERT + SELECT + UPDATE.90- **`app_metadata`, never `user_metadata`**, for authorization claims.91- **Never expose `service_role`/secret keys** in public clients; publishable keys for the browser.92- Views `security_invoker`; `security definer` functions out of exposed schemas.93- State every data-access-surface change (RLS enable/policy, exposed schema, view, storage policy, key).94- Verify against **live docs** before implementing; verify the result with a **test query** after.95- Supabase vs a hand-rolled backend: pick Supabase for Postgres + the RLS boundary + batteries-included96 Auth/Storage/Realtime; if you need heavy custom server logic beyond Edge Functions, weigh a97 dedicated backend cluster alongside it.