@volter/twin-supabase
v1.0.0
Published
Local Supabase twin: the Management API (api.supabase.com/v1) and a project's data plane (<ref>.supabase.co: PostgREST at /rest/v1, Storage at /storage/v1, Auth at /auth/v1) served over the World's own Postgres, which supabase-js talks to unmodified. Buil
Readme
@volter/twin-supabase
A local Supabase: the Management API (api.supabase.com/v1) the Supabase CLI and management clients call, and a
project's data plane (https://<ref>.supabase.co) that supabase-js calls unmodified — PostgREST at /rest/v1,
Storage at /storage/v1 and Auth at /auth/v1 — over the World's own Postgres. A Protocol 3 derived pack
(architecture); no real Supabase is contacted.
bun run src/cli.ts serve --database <postgres url> # both planes; without --database the data plane is refused (503)Units
| Unit | Serves | Spec | State |
|---|---|---|---|
| supabase (src/) | the Management API | spec/openapi.json.gz, api.supabase.com's own document (170 operations) | organizations, projects, their keys (the tree) |
| supabase/rest | PostgREST (/rest/v1) | rest/spec/client-ops.json, the calls @supabase/postgrest-js makes (PostgREST publishes no fixed document) | the project's tables (the World's Postgres) |
| supabase/storage | Storage (/storage/v1) | storage/spec/openapi.json, storage-api's document as Supabase's docs publish it | storage.buckets, storage.objects (the World's Postgres) and objects' bytes (the World's blobs) |
| Auth (auth/) | GoTrue (/auth/v1) | GoTrue v2.197.0's openapi.yaml, two routes patched in (auth/spec) | auth.* (the World's Postgres) |
The two lanes are over the vendor's state: they read the project (its ref and JWT secret) from the Management API's
state and are walked by the pack's one life (journeys/).
What is modelled
- Management API: organizations made and listed; projects made in one (
organization_slug,db_pass,name, a region), answeredUNKNOWN,COMING_UPand thenACTIVE_HEALTHYtwo minutes after they were made on the World clock, read, listed and deleted (a delete of one still coming up is refused, as Supabase refuses it); a project's API keys — the legacyanonandservice_roleJWTs, adefaultpublishable and secret key, keys made, read and deleted, and the legacy pair turned off and on; its database through the API — migrations applied (recorded in the Supabase CLI'ssupabase_migrations.schema_migrations) and listed, and a query run. The account's token is a personal access token the World issued: the application's (POST /_twin/app-credentials, the credential door, which also makes the application's project and answers its URL and keys) or one the Access Tokens door made (POST /_twin/access-tokens). A project's JWT secret is drawn from a secret the World holds, so no one forges its keys from its ref. A project's Auth settings are read and changed (/config/auth): a new project confirms email before sign-in, as a hosted one does, and the settings the twin's Auth acts on (autoconfirm, sign-ups, the Site URL and redirect list, token and password rules, the custom SMTP its mail goes out through) take effect. - Gateway: every data-plane request names its project by host; PostgREST and Auth need an API key (
apikey), a legacy JWT or a publishable or secret key, which gives the request its role (anonorservice_role); Storage authenticates itself. - PostgREST: select with columns, aliases, casts and JSON paths; embeds to-one, to-many and many-to-many, with
!hintand!inner; filters andand/or; order, limit, offset andRange;count=exact; the object media type; insert, bulk insert, upsert on conflict, update and delete withreturn=representation;rpc/<fn>. Each request is one transaction as the caller's role with its claims set, so row-level security is Postgres's own. - Storage: buckets made, read, listed, changed, emptied and deleted; objects uploaded (with user metadata), replaced, read (as the caller, publicly with no key, through a signed URL, with ranges), their information read, listed, moved, copied and removed; signed download and upload URLs.
- Not served (the gap): every other Management API operation (branches, Postgres, pooler and storage configuration, secrets, functions, domains, backups, pausing …), Storage's resumable, S3, image-transformation, analytics and vector APIs, PostgREST's OpenAPI root, Realtime and Edge Functions (their paths are not claimed, so the injector refuses them).
The World's Postgres
The data plane is architecture's declared exception to kernel-held state
(managed infrastructure): the runtime binds the World's managed Postgres
(managedDatabase: serve --database <url>), the World's seed lays Supabase's own schema on it
(POST /_twin/data-plane/schema: the API roles and grants, GoTrue v2.197.0's and storage-api v1.79.22's migrations,
vendored under vendor/ with their licences), and the application's migrations follow. The database's now() and
gen_random_uuid() are the host's, not the World's.
Evidence
The vendor's documents and recordings are under each unit's spec/ (its SOURCE.md). The vendor's own servers are the
data plane's conformance oracle on a host, never World infrastructure (oracle/: PostgREST 16.4, GoTrue v2.197.0 and
storage-api v1.79.22 against the twin; measured 2026-09-28: 72/72, 31/32 and 34/34 identical answers).
