Architecture

One process manages many local Supabase stacks. Two entry points route traffic to them, and a reserved port block keeps every stack apart.

System overview

Supabase Local Manager is a single Node and Express process. It serves the React dashboard, the management API and a WebSocket, and it orchestrates any number of local Supabase stacks. Each stack is a normal Docker Compose stack started by the open-source Supabase CLI. The manager does not reimplement Postgres or the Supabase services; it decides where each stack runs, writes its ports into the project's configuration, and gives you one place to operate it.

The process keeps its own state in a SQLite database loaded into memory with sql.js. That state covers projects, manager users, settings, port allocations, backups metadata and vault entries. Project data itself never enters the manager database; it stays in the project's Docker volumes.

Three ideas explain most of the design. First, ports are allocated deterministically from a reserved band, so projects cannot collide. Second, every project is reachable through a shared entry point, so clients never depend on a volatile service port. Third, access is evaluated once, in front of both entry points, so the routing mechanism cannot weaken the policy.

Subsystem map

Subsystem component map The browser app talks to one Express backend. The backend owns the manager API, the front door, the subdomain proxy, the shared pooler and the SQLite manager database, and drives the Supabase CLI for each project stack. Browser React SPA (apps/web) manager UI + Studio links Express backend packages/supabase-wrapper manager API + WebSocket front door :6000 / proxy :9000 shared PgBouncer :6432 Manager SQLite (sql.js) projects / users / settings port allocations / vault metadata Supabase CLI start / stop / db driven per project no Docker API client Docker stack Kong / Postgres / Auth Storage / Realtime Functions / Studio one per project /api /ws state spawn containers Responsibilities Web app: pages, forms, live streams. Backend: auth, tenancy, lifecycle, proxying, introspection. Manager database: durable product state. CLI: the stack itself. Docker: containers and volumes. The front door and subdomain proxy share one access evaluator; neither forwards before a decision. backend state

Diagram: the browser reaches the manager API and the React dashboard; the manager owns the proxy layer, the project lifecycle and port allocator, the manager database and the secret vault; each project is a Supabase CLI stack in Docker.

Manager processRouting layerExternal dependency

The two entry points

The path-routed front door listens on FRONT_DOOR_PORT, 6000 by default. The first URL segment is the project's database link: a 16-character identifier drawn from a 38-symbol lowercase alphabet, about 84 bits of entropy, generated with cryptographic randomness. The subdomain proxy listens on PROXY_PORT, 9000 by default, and resolves the project from the Host header as <slug>.<base-domain>.

Both entry points resolve a project and then call the same access evaluator. A manager session grants access only to projects the session owns or to an administrator. A project's own anon or service-role key grants access to Kong service paths without a manager session, matching Supabase's gateway. The postgres-meta route requires the service-role key. An unknown link returns a 404 before credentials are considered, and a non-owner receives a 404 of the same shape, so existence is not disclosed.

Request flow

Request flow sequence A browser calls the manager API with a session cookie and CSRF header. Middleware verifies the session and CSRF token, the route checks project ownership, the backend queries the manager database or project Postgres, and the response returns as JSON to the browser. A manager API request, end to end BrowserExpressMiddlewareRouteStore GET /api/projects/:id/status session cookie verify + CSRF + ownership route handler manager DB / Postgres rows / status 200 JSON Failure paths Missing or invalid session gives 401. CSRF mismatch gives 403. A non-owner gets a non-disclosing 404. An upstream failure goes through central error redaction; no stack trace reaches the browser.

Diagram: a browser request enters the front door, is resolved to a project by path, is checked by the shared access evaluator, is forwarded to the project's Kong gateway or Studio, and the response returns with its cookie path and location header rewritten.

ClientAccess decisionProject service

Port bands

Every managed project owns 11 contiguous ports. The formula is 60000 + slot × 11 + offset, with a fixed offset for each service: gateway, Postgres, shadow database, Studio, mail catcher, SMTP, POP3, analytics, project pooler, edge runtime and edge inspector. The allocator finds the first slot whose ports do not intersect an active allocation. Released slots are reusable, and a slot already used by a released audit row is reclaimed in place rather than duplicated.

The values are constants in portAllocator.ts: base 60000 and block size 11. The example environment file lists PORT_BAND_BASE and PORT_BLOCK_SIZE, and the setup script checks that they exist, but the running allocator does not read them. The code wins, and it reserves 11 ports, not 10.

How the band is laid out

Port-band allocation Each project receives a contiguous block of eleven ports starting at 60000. Slot zero begins at 60000, slot one begins at 60011, and slot two begins at 60022. The eleven offsets are fixed service slots. 60000 band · 11 ports per project · base + slot × 11 + offset slot 0 · project A 60000 api60001 db60002 shadow60003 studio… 60010 edge_inspector slot 1 · project B 60011 api60012 db60013 shadow60014 studio… 60021 edge_inspector slot 2 · project C 60022 api60023 db60024 shadow60025 studio… 60032 edge_inspector +11 +11 Offset map inside every block 0 api · 1 db · 2 shadow · 3 studio · 4 inbucket · 5 smtp 6 pop3 · 7 analytics · 8 pooler · 9 edge_runtime · 10 edge_inspector port = 60000 + slot × 11 + offset The legacy 54321 band is preserved for imported stacks; managed blocks never start there. active block next slot

Diagram: the 60000 band split into eleven-port blocks, with the service offset inside each block and the legacy 54321 range shown separately.

Managed blockLegacy rangeService offsets

Process and storage

The backend workspace is packages/supabase-wrapper. It owns the HTTP routes, the proxy services, project lifecycle, port allocation, Postgres introspection and the manager database. The frontend is a React and Vite single-page application in apps/web; in production the backend serves the built bundle on the manager port.

The manager database sits in the platform data directory. Because the entire database is held in memory and rewritten on mutation, a safe file copy needs the manager stopped or taken between writes. Project Postgres data lives in each project's Docker volumes. Project backups are .sql files under the project's supabase/backups directory, with metadata tracked in the manager database.

Security principles

Manager sessions are JWTs signed with JWT_SECRET, carried in an httpOnly cookie, with a token version for revocation. Cookie-authenticated state changes require a double-submit CSRF token. Project ownership is enforced before a request reaches a project, and the denial path does not disclose existence. Project and edge-function secrets are encrypted at rest with scrypt and AES under VAULT_MASTER_KEY.

The HTTP layer applies strict security headers, central error redaction, rate limiting and a proxy that strips its own management token before forwarding. CLI processes are spawned without a shell. The goal is a small, legible security surface: one policy in front of both entry points, one encryption boundary for secrets, and no ambient authority for a project over the manager.