supabase: the self-hosted stack as one module, its secrets rendered into files #169

Open
mesh-admin wants to merge 1 commits from feat/supabase-for-ace into main
Contributor

New module: supabase, upstream's self-hosted stack of 13 containers, which ace runs under HAL.

  • The database stays inside the module (Supabase's own Postgres image, superuser, reserved roles and _supabase). A postgres-database grant cannot hold it, so ace's 2.2 GB data directory moves as a copy.
  • Secrets reach five containers as files and never through their environment: Kong (a rendered kong.yml, uid 100), GoTrue (auth -c dotenv), PostgREST (config file), Vector (the Logflare key in its yml), and the db (POSTGRES_PASSWORD_FILE plus a rendered jwt.sql, uid 105). The other seven declare secrets-in-environment and give the reason.
  • Every public URL is https://${bound:route:name}, so this depends on mesh-controller #149. On main the render is refused rather than written empty. On ace, HAL's SITE_URL is broken today ("https://supabase.").
  • Three run-once steps. The first seeds /etc/postgresql-custom with cp -n, which never overwrites the pgsodium key. The other two wait for the db and for Logflare, replacing compose's depends_on: service_healthy.
  • settings.json (merge:json) sets the pooler's tenant and pool sizes, so ace keeps the zurag tenant. pooler.exs repoints a tenant whose db host is not supabase-db. HAL created ace's tenant with host db.
  • Secrets that can never be minted: anon-key and service-role-key (JWTs signed with jwt), and pooler-vault (must be exactly 32 bytes). On ace every secret the data already holds is accepted.
  • Vector reads only this module's containers. Upstream's config read every container's logs on the machine.
  • Not carried: Kong 8443 and Logflare 4000 on all interfaces.

Verified: the catalogue tests pass with MESH_CATALOGUE set, on main and on #149. The #149 render was run as a throwaway stack of all 13 pinned digests with dummy secrets:

  • the db initialised through the rendered scripts;
  • REST, auth, storage, GraphQL, pg-meta, functions and realtime all answer through Kong;
  • Studio returns 401 without the dashboard login and 200 with it;
  • the pooler accepts connections in session and transaction mode as postgres.zurag;
  • a tenant pointed at host db was repointed by the bootstrap.
New module: **supabase**, upstream's self-hosted stack of 13 containers, which ace runs under HAL. - The database stays inside the module (Supabase's own Postgres image, superuser, reserved roles and `_supabase`). A `postgres-database` grant cannot hold it, so ace's 2.2 GB data directory moves as a copy. - Secrets reach five containers as files and never through their environment: Kong (a rendered kong.yml, uid 100), GoTrue (`auth -c` dotenv), PostgREST (config file), Vector (the Logflare key in its yml), and the db (`POSTGRES_PASSWORD_FILE` plus a rendered jwt.sql, uid 105). The other seven declare `secrets-in-environment` and give the reason. - Every public URL is `https://${bound:route:name}`, so this **depends on mesh-controller #149**. On main the render is refused rather than written empty. On ace, HAL's SITE_URL is broken today ("https://supabase."). - Three run-once steps. The first seeds `/etc/postgresql-custom` with `cp -n`, which never overwrites the pgsodium key. The other two wait for the db and for Logflare, replacing compose's `depends_on: service_healthy`. - `settings.json` (merge:json) sets the pooler's tenant and pool sizes, so ace keeps the `zurag` tenant. `pooler.exs` repoints a tenant whose db host is not `supabase-db`. HAL created ace's tenant with host `db`. - Secrets that can never be minted: `anon-key` and `service-role-key` (JWTs signed with `jwt`), and `pooler-vault` (must be exactly 32 bytes). On ace every secret the data already holds is accepted. - Vector reads only this module's containers. Upstream's config read every container's logs on the machine. - Not carried: Kong 8443 and Logflare 4000 on all interfaces. Verified: the catalogue tests pass with MESH_CATALOGUE set, on main and on #149. The #149 render was run as a throwaway stack of all 13 pinned digests with dummy secrets: - the db initialised through the rendered scripts; - REST, auth, storage, GraphQL, pg-meta, functions and realtime all answer through Kong; - Studio returns 401 without the dashboard login and 200 with it; - the pooler accepts connections in session and transaction mode as `postgres.zurag`; - a tenant pointed at host `db` was repointed by the bootstrap.
mesh-admin added 1 commit 2026-09-30 10:12:40 +00:00
ace runs Supabase under HAL as upstream's 13-container compose: a 2.2 GB
database (1.8 GB of it the dormant `novox` schema, 5.8 M rows in its largest
table), Kong at supabase.zurag.be, the pooler on 5433/6543. This is that stack
as a catalogue module, same images (the digests ace runs), same container
names so an assignment holds the running ones and a take replaces them.

The database stays inside the module. Supabase is a Postgres distribution: its
own image with pgsodium, pg_graphql, pg_net, vault and timescale preloaded, a
superuser (supabase_admin), a dozen reserved roles and a second database
(_supabase). A postgres-database grant - one database, one unprivileged role -
cannot hold it, so ace's data directory moves as a copy, not a dump/restore.

What HAL did by shell and environment the mesh now renders as files:
- kong.yml carries the anon/service keys and the dashboard login (owned by
  kong's uid 100), instead of an entrypoint that eval'd the environment;
- GoTrue reads a dotenv file (auth -c), PostgREST a config file, Vector its yml
  with the Logflare key in it, the database POSTGRES_PASSWORD_FILE and a
  jwt.sql rendered with the secret (owned by postgres, uid 105). None of these
  five containers has a secret in its environment.
- realtime, storage, meta, functions, analytics, studio and supavisor read
  their credentials from the environment only; each declares
  secrets-in-environment with the reason (ADR 0086).
- Upstreams are container names (supabase-db, supabase-kong, ...) instead of
  compose service names, which the mesh does not have.
- SITE_URL / API_EXTERNAL_URL / SUPABASE_PUBLIC_URL are
  https://${bound:route:name} (mesh-controller #149); on ace HAL rendered them
  as "https://supabase." - broken today.
- Vector reads the docker socket, as upstream does, but now includes only this
  module's containers instead of every container's logs on the machine.

Three things compose did that a declaration cannot, done as steps: a run-once
seed copies the image's /etc/postgresql-custom into the placed config
directory with cp -n (a named volume did that implicitly; never overwrites the
pgsodium root key), and two run-once gates wait for the database and for
Logflare, which compose expressed as depends_on: service_healthy.

The pooler bootstrap (pooler.exs) takes the tenant id and pool sizes from
settings.json, the module's one merge:json file, so ace keeps its tenant
"zurag"; and it repoints an existing tenant whose database host is not
supabase-db - HAL created ace's with host "db", which no longer resolves.

Secrets (vault, requires "secret"): postgres, jwt, anon-key,
service-role-key, dashboard-user, dashboard, logflare, pooler-vault,
key-base-a + key-base-b (concatenated: Phoenix wants 64+ bytes, a minted
secret is 40), openai. Three cannot be minted on any machine: anon-key and
service-role-key are JWTs signed with jwt, and pooler-vault must be exactly
32 bytes (AES-256-GCM, found in the bed). They are accepted. On ace every
secret the data already knows is accepted (all but key-base-a/b).

Not carried: Kong's 8443 and Logflare's 4000 on all interfaces (nothing
outside the module uses them); realtime's DB_ENC_KEY stays upstream's constant
(realtime deletes and re-seeds that tenant from its environment every start,
and the key must be exactly 16 bytes).

Verified: catalogue tests with MESH_CATALOGUE pointed here on mesh-controller
main and #149 (on main the render is refused for "name", never written
empty). The #149 resolution with stub providers, turned into a throwaway
stack of all 13 pinned digests with dummy secrets and the rendered files at
their owners and modes: the database initialised through the rendered scripts
(jwt setting applied, _analytics/_supavisor created, roles' password from
POSTGRES_PASSWORD_FILE); through Kong: REST 200 with the anon key and 401
without, auth health and settings 200, storage buckets 200, GraphQL 200, pg-meta
200, an edge function 200, Studio 401 without and 200 with the dashboard login,
realtime tenant health 200; the pooler in session and transaction mode as
postgres.zurag; a tenant set to host "db" was repointed to supabase-db by the
bootstrap and connections worked.
You are not authorized to merge this pull request.
This pull request can be merged automatically.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/supabase-for-ace:feat/supabase-for-ace
git checkout feat/supabase-for-ace
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: novox/mesh-catalog#169