Architecture
What it is
The existing legacy-shared topology uses one gateway to check API keys and forward requests to the original Supabase services of the right environment. Each environment has its own database, Auth and REST; PostgreSQL itself and Storage are shared processes. The diagrams and sharing rationale below describe that topology.
The experimental native-dedicated placement contract records an environment's own engine, application database postgres, and a distinct maintenance database with separate credential references. The catalog is the declaration authority; discovering an engine does not authorize its adoption. Every native declaration currently resolves as unadmitted, so declaring it does not activate provisioning, routing, service consumers, pools, recovery or external effects. See the native placement contract and gauntlet ledger.
Why
The choice is to share the heavy engines (PostgreSQL, Storage) and keep separate the services that hold an environment's identity and API surface (Auth, PostgREST, the database and its logins). Auth and PostgREST are small, and running them per environment means no request-time database switching and no fork of either service.
Rejected alternatives:
- One Auth and one PostgREST for all environments, switching database per request. Neither upstream service supports that as an established mode, and making it work means rewriting security-critical code.
- A full stack per environment. Kept as the isolation baseline and historical independent-engine fallback. The native-dedicated candidate is a separate compatibility requirement for original Supabase features, with its own admission and evidence contract.
How we built it
PostgreSQL and Storage run once; each environment adds its own database, a Storage tenant and a small Auth and REST.
The gateway checks every request against the environment named in its path, and refuses rather than queues.
The full routing, including the management path and the provisioning worker:
One proxy, one loopback server, three paths.
Request path. An app calls /<environment>/rest/v1/... with its publishable key; a server may send a secret key instead. The gateway reads the environment's routing record (is it in maintenance? where does it live?), verifies the key against stored hashes, admits the request if the environment has fewer than its concurrent limit in flight, caps the body at 1 MiB (a Storage file upload streams through up to the upload limit, 50 MiB by default) and forwards it to that environment's own PostgREST. Storage requests go to the one shared Storage process with a trusted tenant header naming the environment. Realtime sockets and Edge Functions calls take the same path to that environment's own containers. Without an API key the gateway passes only public and signed Storage downloads, the Auth steps a browser reaches by a link (verify, authorize, callback) and Edge Functions deployed with verify_jwt off.
Management path. Operators log in against a dedicated management Auth realm that is never an application environment's Auth. The control API derives the actor from that login, never from the request body, and applies owner, admin and viewer roles. Agents reach the same control API through /v1 and /mcp with a personal access token, acting as the person who made it (AI agents).
Trust boundary. Only the TLS proxy faces the network; the loopback server and every container behind it listen on loopback or internal Docker networks, with no published ports. App traffic enters only through the gateway and needs an API key of that environment; operator traffic goes only through the separate management realm and the control API. The server's operators are trusted, the visitors of the apps are not. What that means for shared components is in isolation and trust.
Background work. Creating an environment queues a job. A single provisioning worker, holding an exclusive lock, creates the database and scoped logins, writes the connection rules and starts the services. See provisioning.
| Shared | Separate per environment |
|---|---|
| PostgreSQL engine (one cluster) | Database, three scoped service logins, connection rules |
| Storage process | Storage tenant, its signing keys and metadata |
| Gateway and control API | Auth, PostgREST, publishable and secret keys, routing record |
| Vault's root key | Realtime, Edge Functions and Studio containers, when turned on |
| Host CPU, memory, disk | Resource tier limits per container |
Code: src/control/application.ts (routing between the three paths), src/gateway/handler.ts and src/gateway/managed.ts (gateway), src/gateway/concurrency.ts (admission), src/control/http.ts and src/control/key-http.ts (management API), src/control/catalog.ts (catalog), lab/worker.py and lab/durable_runtime.py (runtime). Routes are listed in the API reference.
Limits
- Admission is per gateway process, with no queue: an overloaded environment gets
429and a busy server503. Several gateway processes do not share counts. - Uploads stream through up to the upload limit (50 MiB by default); resumable (TUS) uploads are not routed.
- Realtime and Edge Functions run one container per environment that turns them on; Studio runs per environment on demand. The legacy workflow does not expose pooler or cron support. Cron libraries in the native image are not evidence of a supported operator feature.
- All processes share one local catalog; there is no multi-server coordination.
Go deeper
- Control plane, persistent routing, gateway overload, gateway drain.
- Combined runtime and resource policy.
- Studio integration specification: the implemented on-demand upstream Studio workflow and its evidence.