Skip to content

The Supabase Architecture

Every request to Supabase’s auto-generated API arrives at Postgres carrying a Postgres roleanon, 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.

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, including auth.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.profiles
for select
to authenticated
using (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 auth schema — auth.users, sessions, and related tables.
  • Storage manages its own metadata tables (buckets, objects, permissions) in the storage schema; 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
A request's role travels through PostgREST into Postgres; other services share the same database
Which Postgres role does an unauthenticated request use?
Where is access control actually enforced for Supabase's auto-generated API?
What role does trusted server-side code use to bypass Row Level Security?
How do Auth, Storage, and Realtime relate to the underlying Postgres database?