The answer. Supabase is not a new database. It is a plain Postgres instance with a ring of open-source services around it, each in its own container on a private network: GoTrue for authentication (/auth/v1), PostgREST turning exposed schemas into a REST API (/rest/v1), Realtime streaming changes over WebSockets (/realtime/v1), Storage for files with metadata rows in Postgres (/storage/v1), Edge Functions on Deno (/functions/v1), pg-meta for schema management, and Studio as the UI. Kong is the only thing that faces the internet and routes each path to its service. The idea that makes the rest work: authorization lives in the database. A request carries a JWT, PostgREST verifies it, switches to the anon or authenticated role, and row-level-security policies decide which rows exist for that caller. The same from("tickets").select() is therefore equally safe from a browser and a server.
The pattern.
browser / server ──► Kong (gateway) ──► PostgREST ──► set role authenticated ──► RLS policies ──► rows
├─► GoTrue (login, sessions, JWTs) ├─► Realtime (changes, broadcast, presence)
├─► Storage (+ image proxy) └─► Edge Functions (Deno)
publishable key sb_publishable_… safe in browsers, RLS applies (legacy: anon JWT)
secret key sb_secret_… servers only, bypasses RLS entirely (legacy: service_role JWT)
Watch out.
- Enabling RLS on a table hides every row until a policy exists; the API then returns
[], not an error. - The direct Postgres connection bypasses Kong, GoTrue and RLS. Treat it like root.
- Do not create a Postgres role per app user. One shared
authenticatedrole plusauth.uid()in policies is the model.
Related: first-table-enable-rls-write-one-policy-query-it · publishable-and-secret-keys-replace-anon-and-service-role · self-hosted-only-kong-faces-the-internet