Skip to content

Glossary

Supabase’s vocabulary spans several layers stacked on plain Postgres: a role and grant layer (anon/authenticated/service_role, [api] schemas) that decides whether a request reaches a table at all; Row Level Security (using/with check, permissive/restrictive) that decides which rows once it does; Auth (GoTrue, JWTs, Auth Hooks) that decides who a request claims to be; the auto-generated Data API (PostgREST, pg_graphql, RPC) that turns the schema into an endpoint; Edge Functions (Deno, the isolate model) for logic that does not belong in SQL; Realtime and Storage (WAL replication, realtime.messages, storage.objects) built as ordinary Postgres rows and policies underneath; and the client and production tooling (supabase-js, Supavisor, migrations, branching) that ties it all together. When a lesson uses a term you’re unsure about, look it up here — every definition below is sourced from a lesson that proved it against this course’s real local Supabase stack (Postgres 17.6, PostgREST, GoTrue, Realtime, Storage, Edge Runtime), not from memory.

TermMeaningLesson
aal / amrTwo JWT claims about how a session authenticated: aal (Authenticator Assurance Level) is aal1 for a single factor, aal2 once MFA is completed; amr (Authentication Methods Reference) is an array recording each method and timestamp ([{"method":"password","timestamp":...}]), growing a second entry if a session is later upgraded with MFA.Sessions and JWTs
admin API (supabase.auth.admin.*)The service_role-only /auth/v1/admin/* endpoints — proved with a real 403 not_admin when called with the anon key. Distinct from signUp(): admin.createUser() needs service_role and is for a trusted server creating accounts for other people, not something safe to expose to a browser.Authentication Methods
anon key / anon roleThe public API key, safe to ship in client code, that identifies every unauthenticated Data API request as the Postgres anon role — subject to Row Level Security like any other role, never treated as special by RLS itself. Since late 2025 new projects also issue this as a shorter sb_publishable_... string, playing the identical role.Project Setup and the Supabase CLI
anonymous sign-inA real, distinct authenticated-role session with its own auth.users row and is_anonymous: true — not the same as an unauthenticated anon-role request, since auth.uid() returns a real value for it. Lets a product delay the signup wall, at the cost of an auth.users row for someone who may never convert until an explicit linkIdentity flow converts it.Authentication Methods
asymmetric signing (ES256 vs HS256)The real default since October 1, 2025 for every new project (quoted from supabase.com/blog/jwt-signing-keys): a real session token decodes to {"alg":"ES256","kid":"..."}, verifiable against a public JWKS key with no shared secret. The static anon/service_role API keys are a separate, older mechanism — {"alg":"HS256"} with no kid, signed with the legacy shared JWT_SECRET, and were never migrated to the new per-session scheme.Sessions and JWTs
auth.identitiesA table separate from auth.users, one row per (provider, person) pair — the same person can authenticate multiple ways over time (email, then GitHub), each proof its own row pointing at one auth.users row.Authentication Methods
auth.uid() / auth.jwt() / auth.role()The three functions every RLS policy is built from, all reading request.jwt.claims forwarded into the Postgres session by PostgREST: auth.uid() returns the sub claim (null for anon/service_role, since neither is a real signed-in user), auth.role() always has a value (every request carries a role claim), and auth.jwt() returns the whole decoded claims object — which is why a custom claim like tenant_id is read as (auth.jwt() ->> 'tenant_id') rather than through a dedicated function.Row Level Security Fundamentals
auth.usersThe table Supabase’s Auth service (GoTrue) manages for you; the standard pattern for attaching application data to a user is a profiles table whose id is a foreign key to auth.users(id).Tables, Schema, and Migrations
Auth Hook (Customize Access Token Claims)A Postgres function GoTrue calls during token issuance, configured via [auth.hook.custom_access_token] in config.toml, taking a jsonb event (user_id + assembled claims) and returning {"claims": {...}} — the only mechanism that puts a custom claim like tenant_id into a JWT, since GoTrue never adds one on its own. Only supabase_auth_admin may call it; the hook only affects new tokens (sign-in or the next refresh), never retroactively updating an already-issued one.RLS Patterns and Pitfalls
authenticated roleThe Postgres role every logged-in request carries, with its JWT claims (including auth.uid()) available inside RLS policies — distinct from anon only in which role PostgREST executes the query as, per the request’s JWT.The Supabase Architecture
authenticator roleThe single Postgres login role PostgREST actually opens its database connection as — proved via \du: anon/authenticated/service_role all say Cannot login, authenticator is the one role that can. PostgREST does a real SET LOCAL ROLE to whichever of the three the request’s JWT claims, per transaction — session_user stays authenticator the whole time, only current_user changes.Postgres Roles and Grants
auto_expose_new_tablesThe config.toml setting controlling whether a brand-new public-schema table is auto-granted to anon/authenticated/service_role with no explicit grant. Proved live: the CLI’s own scaffolded local default falls back to true even though the hosted platform is rolling out false as the new default on a phased schedule — opt-in April 28 2026, default for new projects May 30 2026, applied to all existing projects October 30 2026 (changelog).Securing the Data API
TermMeaningLesson
branch (Supabase branch)A genuinely separate Postgres instance — its own schema, Data API, Auth, and Storage — provisioned for the lifetime of a Git branch or pull request, not a feature flag inside a shared database. supabase branches create --persistent stays around across pushes; --with-data clones production data into it.Branching and the Migrations Workflow
broadcastRealtime’s ephemeral, database-independent messaging layer: channel.on('broadcast', ...) receives arbitrary JSON payloads sent with channel.send(), with no postgres_changes-style table wiring needed at all.Realtime Broadcast and Presence
broadcast_changes() (realtime.broadcast_changes())A Postgres function called from a trigger that pushes a postgres_changes-shaped payload (payload.payload.record) over Broadcast instead of the per-subscriber RLS path — proved to always write with private = true, so it needs the exact same realtime.messages RLS setup as any other private channel; there is no public-broadcast mode for it.Realtime Broadcast and Presence
bucketA top-level Storage container, just a row in storage.buckets (createBucket is a thin wrapper around the same insert), markable public (readable by URL with no auth) or private (access controlled per-request via storage.objects RLS).Storage Buckets and Policies
BYPASSRLSThe Postgres role attribute service_role holds, proved to skip RLS evaluation entirely and independently of any policy — Postgres never even looks for a matching policy for a BYPASSRLS role, regardless of Data API schema exposure.Securing the Data API
TermMeaningLesson
cache-controlA plain HTTP response header, set per-object at upload time via the cacheControl option and echoed verbatim — proved that cacheControl: '3600' (the docs’ own example) produces the literal, non-standard header cache-control: 3600, not max-age=3600, with no validation catching the mismatch. Works identically whether or not image transformation is enabled.Image Transformations and CDN
channelA named Realtime connection (supabase.channel('name')) that can carry postgres_changes, broadcast, and presence subscriptions at once; private-channel authorization and postgres_changes filters both key off the exact channel/topic string, which is why this course scopes names deliberately (rt:private-room-1).Realtime Broadcast and Presence
CI (migrations through CI)Running supabase db push as part of the same pipeline that already tested a migration on a preview branch, instead of someone pasting SQL into production by hand — the step that keeps a migration applied “the same way every time” rather than drifting out of sync with version control.Branching and the Migrations Workflow
config.tomlThe project’s local CLI configuration file — [api] schemas, auto_expose_new_tables, [auth.hook.custom_access_token], [functions.<name>] verify_jwt, refresh_token_reuse_interval, and more — every gate this course proves as a real error or real behavior traces back to a real key in this one file.Securing the Data API
CORS preflightThe browser-only OPTIONS request sent before a non-trivial cross-origin call, checked entirely client-side — curl never enforces it, so a 200 from curl proves a function ran but proves nothing about whether a real cross-origin browser client can read the response. On this local stack, the CLI’s own Kong gateway answers OPTIONS before a function’s own req.method === 'OPTIONS' branch is ever reached — a local-only quirk; hosted Edge Functions expect the function itself to answer it.Writing and Invoking a Function
count=exact / count=planned / count=estimatedThree Prefer: count=... modes reported via Content-Range: exact runs a real count(*); planned reads the planner’s own row estimate (proved 720 on a real 3-row unanalyzed table — not a bug, exactly what the planner guesses); estimated uses an exact count below db-max-rows and falls back to planned above it.The REST API (PostgREST)
createSignedUrl / signed URLA time-limited URL for a private-bucket object, embedding a real signed JWT with an exp claim checked server-side — proved that a request after expiry gets a genuine InvalidJWT error, not a client-side hint that silently still works. getPublicUrl by contrast builds a URL with zero validation, even against a private object.Storage Buckets and Policies
TermMeaningLesson
Data API exposureThe first of two independent gates a request passes: a Postgres grant (on anon/authenticated/service_role) that decides whether a table or function is reachable through PostgREST at all, evaluated before RLS ever runs. A table with permissive RLS but no grant returns a real 42501, not an RLS-filtered result — RLS was never consulted.Securing the Data API
Database WebhookA Postgres trigger, built on pg_net, calling supabase_functions.http_request(url, method, headers, body, timeout) on insert/update/delete — proved fully asynchronous (a real insert returned in 2.767ms, far less than the HTTP round trip it triggered). Sends { type, table, record, schema } and has no built-in retry, unlike Auth Hooks; a receiver must be idempotent on its own.Background Tasks and Webhooks with Functions
db diff (supabase db diff --schema public)Captures a schema change made through the Studio GUI as a proper migration .sql file after the fact, so a ClickOps edit does not stay invisible to code review and version control.Tables, Schema, and Migrations
db lint (supabase db lint)A plpgsql type-checker over function bodies — not an RLS or security advisor, despite the name inviting that assumption. Proved live: run against a schema with a real, live RLS gap (a table with RLS enabled and zero policies), it reported zero results; run against a function with a genuine internal type mismatch, it caught a real 42804 before the function was ever called.Verification Tools
db push (supabase db push)Applies every migration that has not yet run against whichever project or branch it is pointed at — the same command whether the target is a preview branch or production, which is exactly what makes it safe to run from CI.Tables, Schema, and Migrations
db reset (supabase db reset)Replays every migration file in supabase/migrations/ from scratch against a fresh database, then runs supabase/seed.sql — the mechanism that makes a schema change typed directly into psql/Studio and never captured as a migration file vanish completely on the next reset, proved with a real “ClickOps with extra steps” example.Tables, Schema, and Migrations
DenoThe runtime Edge Functions actually run on — not Node.js. Ships a global Deno.serve, first-class TypeScript with no build step, and secure-by-default sandboxing; code assuming Node.js APIs or bare npm-style imports does not work unmodified.What Are Edge Functions
deno checkA pre-deploy type-check gate over a function’s own module graph — catches the same import-resolution failures a boot-time 503 would, before ever calling supabase functions serve or deploy.Writing and Invoking a Function
deno.json (import map)A per-function import map sitting next to one function’s index.ts, scoped to just that function since every function gets its own module graph — remapping a bare specifier like 'supabase' to a real jsr: specifier; a sibling function needs its own copy, with no shared root config to fall back on.Writing and Invoking a Function
TermMeaningLesson
Edge FunctionServer-side TypeScript running on Deno inside the Edge Runtime, for logic that does not fit close to Postgres — a third-party API call, a secret, CPU-bound work, or a fully custom HTTP endpoint — reachable via supabase.functions.invoke(). Not the right tool for “aggregate/filter/join rows,” which a Postgres RPC function does faster with no network round trip.What Are Edge Functions
Edge Runtime (main runtime / user runtime, isolate)The open-source engine (github.com/supabase/edge-runtime) both supabase functions serve and the hosted platform run, splitting into a main runtime (proxies requests, no limits, reads every env var) and a user runtime — a sandboxed Deno isolate per function with real memory/CPU limits. Proved live: a hostname resolving fine from docker exec into the container fails with ENOTFOUND from inside a function’s isolate — a real, observable sandbox boundary, not a theoretical one. A cold isolate measured ~10x slower than a warm one on this Mac.What Are Edge Functions
EdgeRuntime.waitUntilMarks a promise as background work that keeps running after the function’s response is already sent — proved with real timestamps: the response returned in 60ms while a waitUntil-handed 3-second task kept running underneath, invisible to the caller. awaiting it directly instead defeats the entire mechanism.Background Tasks and Webhooks with Functions
embedding / !innerPostgREST following a real foreign-key constraint to nest a related row in one round trip (?select=title,director:directors(name)) — default behavior is a left join (an unmatched row still appears, with the embedded field null); appending !inner forces an inner join, dropping unmatched rows entirely, and is also how you filter on a column of the embedded table.The REST API (PostgREST)
error.code (PostgrestError)The stable field (a Postgres SQLSTATE like 23505, or a PostgREST-specific code like PGRST205) to branch on programmatically — never error.message, which is human-readable text that can change wording between versions. A populated PostgrestError also carries message, details, and hint.Error Handling and Typed Queries
exposed schemas ([api] schemas)The config.toml list (default ["public", "graphql_public"]) of schemas PostgREST introspects at all, independent of per-table grants. A fully granted table in an unlisted schema still returns a real 404 PGRST205 "Could not find the table" — indistinguishable from a genuinely nonexistent table, which is exactly the trap in an AI-generated “expose via service_role” comment that never checked this layer.Securing the Data API
explain analyzeThe direct way to see whether Postgres used an index scan or a sequential scan for a query, plus real execution time — proved on a 100,000-row table dropping from 86.8ms (Seq Scan, auth.uid() re-evaluated per row) to 3.5ms (wrapped in select, cached as an InitPlan) to 0.096ms (adding an index on top). Worth running against any new RLS-protected query before it ships, not after it is slow in production.RLS Patterns and Pitfalls
TermMeaningLesson
functions.invoke()The supabase-js method for calling an Edge Function instead of a plain fetch — serializes body to JSON and automatically attaches an Authorization header (the signed-in user’s access token if there is one, otherwise the project’s anon key).Writing and Invoking a Function
TermMeaningLesson
gen types (supabase gen types typescript --project-id <ref>)Inspects the live schema and emits a Database TypeScript type with Row/Insert/Update shapes per table; passed as a generic to createClient<Database>(), it turns a misspelled column or a missing required insert field into a compile-time error instead of a runtime surprise.The supabase-js Client
getSession() vs getUser() vs getClaims()getSession() reads local storage with no signature verification — proved to trust a tampered token’s forged role/sub completely. getUser() sends the token to the real Auth server (GET /auth/v1/user), which rejected the same tampered token with a real bad_jwt crypto-verification error. getClaims() gets the same cryptographic guarantee locally, verifying against the cached JWKS public key instead of a network round trip — Supabase’s current official Next.js middleware examples call getClaims(), not getUser(), for this reason.Sessions and JWTs
GoTrueThe open-source server Supabase Auth is built on — every sign-in method (password, magic link, OTP, anonymous, OAuth) is a plain HTTP call to a real GoTrue server that ends the same way: a matched or created auth.users row.Authentication Methods
grant / revokeThe ordinary Postgres statements underneath every form of Data API exposure — auto-grant, dashboard, or hand-written SQL. Revoking anon’s select on a table produces a real 42501 with a hint naming the exact grant statement that fixes it — proved, no RLS ever ran because the request never got that far.Securing the Data API
TermMeaningLesson
imgproxyThe separate service Storage delegates image transformation to, gated behind ENABLE_IMAGE_TRANSFORMATION (off by default on this local stack and on hosted plans below Pro) — transform URLs build with zero validation that transformation is even enabled, only failing with a real 404 when a client actually requests them.Image Transformations and CDN
import specifier (jsr: / npm:)Deno has no node_modules-style bare-specifier resolution — a bare import { createClient } from '@supabase/supabase-js' fails at boot with a real 503 BOOT_ERROR, before any handler code runs. jsr:@supabase/supabase-js@2 and npm:nanoid@5 both work, pinned to a version; unpinned resolves to “whatever is latest right now” with no lockfile.Writing and Invoking a Function
index scan / sequential scanThe two query plans explain analyze’s top line names: Seq Scan reads every row and checks each one against the filter (including an RLS policy condition); Index Scan/Bitmap Heap Scan jumps straight to matching rows via an index. An unindexed column referenced by an RLS policy silently forces the former on every query against that table, regardless of table size.Performance and Indexing
TermMeaningLesson
JWKSThe public-key endpoint (/auth/v1/.well-known/jwks.json) a service checks an asymmetric (ES256) token’s signature against, with no shared secret needed anywhere — the real local response’s kid matches every real session token’s header kid exactly.Sessions and JWTs
JWT claims (sub/role/aud/exp/iat/session_id)The standard claims a real decoded access token carries: sub is what auth.uid() reads; role is what auth.role() reads and what Postgres actually set roles to; aud must be authenticated for PostgREST to accept the token; exp/iat are Unix expiry/issued-at timestamps; session_id ties the token to one sign-in session, used by refresh-token rotation.Sessions and JWTs
TermMeaningLesson
KongThe CLI’s local API gateway sitting in front of Edge Functions — proved to intercept and answer OPTIONS preflight requests to /functions/v1/* with its own permissive CORS headers before a function’s own code is ever reached, a local-only behavior that does not reflect the hosted platform.Writing and Invoking a Function
TermMeaningLesson
magic link / OTPTwo related but mechanically different sign-in flows: a magic link is a clickable GET URL with the token baked in; a phone OTP is a short code typed back in and exchanged via verifyOtp (SMS has no browser context to click into). The /auth/v1/otp API response is deliberately empty ({}) — the real one-time token only ever appears inside the actual email, proved by fetching it from Mailpit.Authentication Methods
MailpitThe local email-capture inbox this course’s proofs fetch a real magic-link email from via its own API — proof that the token really is only delivered through the email, never leaked in the API response itself.Authentication Methods
MCP execute_sqlThe Supabase MCP server’s SQL tool, reachable at the real MCP_URL supabase start prints — proved to run as postgres, the actual database superuser (current_user/session_user both report postgres), which bypasses Row Level Security exactly like a leaked service_role key: a table whose only row is scoped to one specific user via RLS returned that row through execute_sql with no session or key of its own beyond the MCP connection.Verification Tools
migration (supabase/migrations/)A plain, timestamped .sql file created with supabase migration new <name> (empty, written by hand) or supabase db diff (captured from a GUI change) — the mechanism that turns a schema change into something reviewable, repeatable, and deployable to another environment via supabase db push, instead of invisible ClickOps.Tables, Schema, and Migrations
TermMeaningLesson
OAuth providerHands the user to a third-party (Google, GitHub, …) via signInWithOAuth(); mechanically lands in the same two tables as every other method — a new auth.identities row with provider = '<name>', linked to a new or existing auth.users row if the returned email already matches one. Not reachable from this credential-free local stack ([auth.external.<provider>] blocks default enabled = false).Authentication Methods
onAuthStateChangeA single subscription, typically wired at app startup, that fires for every auth-related event (SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED, and the initial session on load) — keeps UI state synced with the client’s real session without manually re-checking it after every auth action.Auth in the Client
TermMeaningLesson
permissive / restrictive policyThe default permissive policies for one operation combine with OR — any one matching allows the row. A restrictive policy (as restrictive) combines with AND on top of every permissive match — proved live: a banned-user restrictive policy blocked even a row a permissive public-read policy would otherwise have shown, useful for a mandatory cross-cutting rule without repeating it inside every permissive policy.Row Level Security Fundamentals
pg_cronRuns SQL on a schedule entirely inside Postgres, no external scheduler — proved with cron.schedule('name', '* * * * *', $$select 1$$) firing and landing in cron.job_run_details at the real top of the next minute. Calling an Edge Function on a schedule reuses the same supabase_functions.http_request(...) call a Database Webhook trigger uses.Background Tasks and Webhooks with Functions
pg_graphqlThe extension exposing the same schema as GraphQL at /graphql/v1 — not enabled by default on this local stack (create extension pg_graphql required first), but once on, enforces the exact same RLS as REST through the same Postgres connection and role: proved with the identical two-user notes table returning different rows for different GraphQL callers, no separate GraphQL authorization layer to configure.The REST API (PostgREST)
pg_netThe Postgres extension a Database Webhook trigger is built on — proved genuinely asynchronous with real timing (a 2.767ms insert far shorter than the HTTP call it enqueued); the actual response lands later in net._http_response, read after the fact rather than awaited.Background Tasks and Webhooks with Functions
PGRST102”All object keys must match” — the real error a bulk insert with heterogeneous JSON keys across rows gets by default; fixed with Prefer: missing=default plus an explicit ?columns= list, which fills any row’s missing key with that column’s own default.The REST API (PostgREST)
PGRST205”Could not find the table … in the schema cache” — the real 404 PostgREST returns both for a genuinely nonexistent table and for a fully-granted, real table sitting in a schema never added to [api] schemas; it says “does not exist,” never “forbidden,” for either case.Securing the Data API
pgTAP / supabase test dbA real, pgtap-extension-powered test framework for Postgres — plan(), ok(), is() assertions inside a rolled-back transaction, runnable per-file with real TAP output (ok 1 - ...) or summarized via supabase test db for CI. Proved with a real two-assertion test: one checking a table has exactly its expected RLS policy count, one switching to set local role anon mid-test to assert a genuinely RLS-empty table returns zero rows — not installed by default on a fresh project (create extension pgtap first).Verification Tools
PITR (point-in-time recovery)Restores a project to any specific moment within its retention window, not just to the most recent daily snapshot — matters specifically when the incident to undo happened hours after the last backup was taken.Security Checklist and Production Readiness
PKCE (flowType: 'pkce')supabase-js’s default auth flow (the older implicit flow remains available but is not the default) — a magic link or OAuth redirect carries a ?code= authorization code, not tokens directly, and the client calls exchangeCodeForSession(code) to turn it into a real session, proved end to end from a real Mailpit-captured email through to a signed-in session.Auth in the Client
policy (RLS policy)A create policy statement scoping one operation (select/insert/update/delete) on one table to a using and/or with check condition, evaluated per row, per operation — a missing policy for an operation denies it by default (a silent DELETE 0 for select/delete, a loud 42501 for insert/update), never falls back to another policy’s coverage.Row Level Security Fundamentals
PostgRESTThe service turning an HTTP request into one SQL statement, executed as whatever role the request carries — enforces nothing of its own; every access decision is Postgres’s own grants and RLS. !inner, count=planned, and resolution=merge-duplicates all compile to real SQL a hand-written query could express, not a separate framework-level permission layer.The REST API (PostgREST)
postgres_changesRealtime tailing Postgres’s own write-ahead log via logical replication — proved completely silent for a table not yet added to the supabase_realtime publication (5 real seconds, zero events, no error). Re-checks the subscriber’s own RLS select policy per event per subscriber, the same policy PostgREST would run for a plain select.Realtime: Postgres Changes
Prefer: headerRequest headers changing PostgREST’s response shape or write semantics: return=representation (return the affected row instead of an empty 201/204), resolution=merge-duplicates (turn a conflicting insert into an upsert, needs ?on_conflict=), missing=default (fill absent bulk-insert keys with column defaults), and tx=rollback — proved to silently do nothing on this stack, since it is gated behind a server-only db-tx-end setting that defaults to commit with no client override allowed.The REST API (PostgREST)
presenceRealtime’s in-memory, synchronized view of who is connected to a channel and what they last track()-ed via presenceState() — not backed by any table, gone the instant a socket disconnects (an automatic leave event), never a queryable, persistent record.Realtime Broadcast and Presence
prepare: falseThe postgres npm driver option required over Supavisor transaction mode, since a prepared statement created on one pooled backend connection does not exist on whichever different backend the next query lands on — the documented fix, matching Prisma’s own ?pgbouncer=true, for a real, cited incompatibility.Edge Functions and the Database
private channelA channel (config: { private: true }) restricted to authenticated clients, with authorization routed through RLS on realtime.messages rather than Realtime’s own logic. realtime.messages ships with RLS enabled and zero policies — proved to produce a real CHANNEL_ERROR with no policy at all, and needs an explicit client.realtime.setAuth(token) call, unlike postgres_changes’ automatic session pickup.Realtime Broadcast and Presence
publication (supabase_realtime)The Postgres publication alter publication supabase_realtime add table <t> wires a table into — the single opt-in step without which postgres_changes streams absolutely nothing for a table, no matter how correct the client subscription code looks.Realtime: Postgres Changes
publishable key / secret keySince late 2025, the newer key format (sb_publishable_.../sb_secret_...) replacing the legacy anon/service_role JWTs — same trust roles (publishable safe for clients, secret bypasses RLS and stays server-side), shorter, non-JWT strings that can be rotated without downtime; existing keys keep working.Project Setup and the Supabase CLI
TermMeaningLesson
query builder (supabase-js) / .range()A thin, typed URL-and-header assembler — nothing executes until await, proved by logging query.url.toString() before awaiting and seeing the exact request about to fire. .range() is provably not the HTTP Range header despite the name — it sends limit/offset query parameters instead, PostgREST’s own pagination mechanism.Querying with the Client SDK
TermMeaningLesson
realtime.messagesThe real Postgres table every private-channel and broadcast_changes()/realtime.send() message is inserted into, gated by RLS using realtime.topic() (a function standing in for “the channel name this connection is authorizing”) in place of an ordinary column comparison.Realtime Broadcast and Presence
realtime.send()The more primitive function broadcast_changes() is built on — realtime.send(payload, event, topic, private) broadcasts any JSON with no row-change shape and no trigger or table at all; its fourth argument decides whether realtime.messages RLS is required (true) or skipped entirely (false, delivered to any subscriber on that public topic).Realtime Broadcast and Presence
refresh token rotationEvery successful token refresh returns a new refresh token and invalidates the one just used. refresh_token_reuse_interval (10 seconds on this local stack) tolerates the immediately-previous token within that grace window (real network retries should not cost a session); a token two generations stale gets a real, specific refresh_token_already_used error, not a generic 401.Sessions and JWTs
REPLICA IDENTITYControls how much of the “old” row Postgres writes into the WAL for UPDATE/DELETE — by default just the primary key, proved directly: payload.old came back as only {"id":...} until alter table ... replica identity full was applied, after which it carried every column’s pre-update value.Realtime: Postgres Changes
RLS (Row Level Security)Never on by default — alter table ... enable row level security must be run explicitly, and once on with zero policies the table is completely locked (proved: both anon and a real signed-in user get [], only service_role sees rows). Not the whole security model on its own — it is the second of two gates, evaluated only after Data API exposure already let a request reach the table.Row Level Security Fundamentals
RPC (/rpc/<function_name>)PostgREST auto-exposing a Postgres function the same way it exposes a table — no routes file, no deploy step. returns setof/returns table shape what curl actually sees back; an atomic RPC (a single update ... set count = count + 1) avoids the read-then-write race a client-side increment would hit.Database Functions as RPC
TermMeaningLesson
search_pathThe purely textual resolution order Postgres uses for an unqualified name like orders — becomes a real security hole the instant a security definer function leaves it unpinned: proved with a planted agapi_attacker.agapi_accounts table hijacking an unqualified lookup and returning a fabricated balance, fixed entirely by set search_path = '' plus fully qualifying every relation in the function body.Database Functions as RPC
security definerThe Postgres function attribute making it run with its owner’s privileges instead of the caller’s — proved to see rows a caller’s own RLS would have hidden (an admin report crossing users), which is exactly why it must pin search_path and remains only as safe as whoever is granted execute on it.Database Functions as RPC
security invokerThe default for a Postgres function with no security clause — runs as whichever role called it, so RLS keeps applying exactly as it would to a direct query. Proved with a function whose body has no where clause at all still returning only each caller’s own rows, because Postgres applied using per row regardless of the function body.Database Functions as RPC
security_invoker (view option, Postgres 15+)Makes a create view evaluate RLS using the querying role’s identity instead of the view’s (RLS-exempt) owner’s — proved: the same view showed a second user every row before this option, and only their own row after alter view ... set (security_invoker = true). Every migration-created view defaults to the pre-15 behavior unless this is set explicitly.Row Level Security Fundamentals
seed.sql (supabase/seed.sql)A fixed set of rows loaded automatically after supabase db reset (and optionally db push --include-seed) — for local development and CI fixtures, explicitly not something a real migration file should carry, since migrations describe schema history and seed data is neither.Tables, Schema, and Migrations
service_role key / roleBypasses Row Level Security entirely via the real Postgres BYPASSRLS attribute — meant only for trusted server-side code (an Edge Function, a backend process), never client bundles, since anyone holding it can read and write every row of every exposed table.Project Setup and the Supabase CLI
session mode (Supavisor, port 5432)Gives a client a dedicated Postgres connection for as long as it stays connected — full feature parity including prepared statements, at the cost of one pool slot held per client, so it does not scale to huge numbers of short-lived clients the way transaction mode does.Connection Pooling and Supavisor
Smart CDNHosted-only edge caching for transformed images, keyed by file and exact transform parameters — per the docs, up to ~60 seconds for an update’s cache invalidation to propagate, and it applies to signed-URL requests too, not just public ones (docs-sourced, not reproducible on a local Docker stack with no CDN edge).Image Transformations and CDN
sslmode=disableA required query parameter on this local stack’s db push --db-url/pooler connection strings — proved live in a real db push --dry-run invocation — because this CLI’s local Postgres and pooler containers do not terminate TLS the way a hosted project’s connection string does; omitting it on a local connection fails to connect at all.Branching and the Migrations Workflow
stable / immutable / volatileA Postgres function’s volatility promise, used only by PostgREST to decide which HTTP verbs are safe (stable/immutable unlock cacheable GETs) — enforced at the transaction level, not the route: a mislabeled function that tries to write inside a GET’s read-only transaction fails with a real error (25006) rather than being rejected earlier with a friendly message.Database Functions as RPC
Storage (storage.objects / storage.buckets)Not a separate permission system — a thin API over an S3-compatible blob store for bytes plus a real Postgres table, storage.objects, for everything about them (owner, path, bucket, content type). Every access decision about rows is RLS, full stop, proved with the identical 403/42501 shape as any other table’s RLS.Storage Buckets and Policies
storage.foldername()Splits an object’s path into an array of segments (storage.foldername(name))[1]) — the standard pattern makes the first segment the uploading user’s auth.uid(), so a policy comparing it scopes every user to their own folder; proved live with one user’s write into another’s folder rejected with the exact same RLS error family as ordinary table policies.Storage Buckets and Policies
StudioThe web GUI (Table Editor, SQL Editor, Query Performance, Advisors, Database > Replication) for configuring a project without writing every change by hand — a GUI schema edit is real and takes effect immediately but is invisible to git log until captured as a migration with db diff.Tables, Schema, and Migrations
@supabase/ssrThe cookie-based client bridging browser and server code (Server Components, Route Handlers, middleware) that a plain localStorage-backed client cannot reach — both sides read/write the same cookie, and getting the setAll/cache-header plumbing wrong is the docs’ own repeatedly-cited cause of randomly logged-out users.Sessions and JWTs
SupavisorSupabase’s connection pooler, sitting in front of Postgres’s hard cap on concurrent connections (low hundreds by default) — exactly the problem serverless/Edge Function traffic creates by each trying to open its own direct connection.Connection Pooling and Supavisor
TermMeaningLesson
tenant_id claim patternA custom claim (not a default GoTrue claim — evaluates to null in a policy unless an Auth Hook puts it there) letting a multi-tenant policy check one JWT read instead of a join on every query — proved to correctly hide another tenant’s rows once the hook wires it in, and to only affect new tokens, never retroactively.RLS Patterns and Pitfalls
transaction mode (Supavisor, port 6543)Holds a Postgres connection only for the duration of one transaction, then returns it to the pool — lets a small pool serve a very large number of short-lived clients (the default recommendation for Edge Functions and serverless), at the cost of no session-level features like prepared statements.Connection Pooling and Supavisor
TUSThe resumable-upload protocol Storage implements at /storage/v1/upload/resumable, recommended above roughly 6MB or on unreliable connections — proved chunk by chunk with raw curl: POST to create, PATCH with an Upload-Offset to send bytes, HEAD to confirm exactly how many bytes the server already has after a simulated dropped connection.Storage Buckets and Policies
TermMeaningLesson
using / with checkTwo separate gates on two separate moments: using gates which existing rows can be targeted at all (select/update/delete), failing silently — a delete with no matching policy returns a real DELETE 0, no error. with check gates what a row is allowed to become (insert/update), failing loudly with a real 42501. Postgres reuses using as with check when the latter is omitted, so an ownership-scoped using is already safe with no separate with check at all.Row Level Security Fundamentals
TermMeaningLesson
verify_jwtPer-function config (true by default) that rejects a request before the function’s own code runs at all if it lacks a valid Supabase JWT — proved with real, structured error bodies (UNAUTHORIZED_NO_AUTH_HEADER, UNAUTHORIZED_INVALID_JWT_FORMAT) distinct from anything the function’s own code could produce. Only checks the token is a real, validly signed JWT — the project’s own anon key passes it, since it is itself a valid JWT; verify_jwt = false is required for a webhook receiver with no Supabase JWT to present at all.Writing and Invoking a Function
TermMeaningLesson
WAL (write-ahead log)Postgres’s own durable, ordered record of every write, kept for crash recovery and replication — the exact stream Realtime’s postgres_changes taps into via logical replication, which is why it inherits Postgres’s own properties (nothing streams until the table is in a publication; the old row is limited to the replica identity unless widened).Realtime: Postgres Changes