The Supabase Architecture
The idea in one sentence
Section titled “The idea in one sentence”Every request to Supabase’s auto-generated API arrives at Postgres carrying a Postgres role — anon, authenticated, or service_role — and that role is the architectural backbone that everything else (the Data API, Row Level Security) is built on top of.
Every request carries a role
Section titled “Every request carries a role”Supabase’s auto-generated API authenticates every request, or treats it as explicitly anonymous. Whichever it is, the request reaches Postgres as one of three roles:
anon— an unauthenticated request. No user is logged in.authenticated— a request from a logged-in user, carrying their JWT claims, includingauth.uid()for identifying which user it is.service_role— trusted server-side code. This role bypasses Row Level Security entirely, the same key you already met in the previous lesson.
-- Inside a Row Level Security policy, this is exactly how you tell roles apart.create policy "Users can view their own profile"on public.profilesfor selectto authenticatedusing (auth.uid() = id);PostgREST translates HTTP into SQL — it does not enforce anything itself
Section titled “PostgREST translates HTTP into SQL — it does not enforce anything itself”PostgREST is the service that turns an HTTP request like GET /rest/v1/profiles?select=* into an actual SQL query, and executes that query as the role the request carries. This detail matters more than it looks: PostgREST does not implement its own separate permission system. It relies entirely on Postgres’s own access control — plain grant/revoke permissions, and Row Level Security policies — to decide what that role is actually allowed to read or write. Access control lives in the database, not in the API layer sitting in front of it.
// This client call becomes an HTTP request to PostgREST, executed as// whatever role the current session carries (anon or authenticated).const { data, error } = await supabase .from('profiles') .select('id, username') .eq('id', userId);The other services also just read and write Postgres
Section titled “The other services also just read and write Postgres”Auth, Storage, and Realtime are separate services (separate processes), but none of them invent their own storage layer — they all ultimately read and write the same Postgres database:
- Auth (GoTrue) manages the
authschema —auth.users, sessions, and related tables. - Storage manages its own metadata tables (buckets, objects, permissions) in the
storageschema; the files themselves live in an object store, but access rules are Postgres rows. - Realtime reads Postgres’s write-ahead log (WAL) to stream row changes, in addition to offering ephemeral broadcast/presence channels.
flowchart LR
client["Client request (anon / authenticated / service_role)"] --> postgrest["PostgREST"]
postgrest -->|"executes SQL as that role"| pg[("Postgres
(grants + RLS enforced here)")]
auth["Auth (GoTrue)"] --> pg
storage["Storage"] --> pg
realtime["Realtime (reads WAL)"] --> pg