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.
mesh-catalog
The Novox Mesh catalogue. The modules the mesh builds, provisions and runs — as manifests, one
per module under modules/.
This is data, not a control-plane concern. The manifests describe what a module is: what it
provides, what it requires, the seats it claims, the resources the host applies for it. The
engine that reads them — parsing, eligibility resolution, sealing, declaration emission — lives
in the control plane (novox/mesh-controller, internal/catalogue), which consumes this repository
as a build source. The host (novox/mesh-host) applies the declarations the control plane emits.
Neither is here.
What a module is, and is not
A module is one thing the mesh can run, named once, described completely by its manifest. A
manifest names its image (pinned by digest), the resources the host owns for it (directories,
files, the container, the private network it joins), what it requires from a provider and what
it provides to consumers, and the sealed secrets it needs filled on the machine.
- Core mesh components are not modules. The node host, the foundation, the control-plane
contexts and the surfaces are the mesh itself; they ship as their own repositories
(
mesh-host,mesh-foundation,mesh-controller,mesh-surfaces,mesh-sdk), not from here. - Standalone applications are not here either. A larger application lives in its own repository with its manifest at the root, registered with the mesh as a build source (novox/hq ADR 0010). This repository holds the modules the mesh maintains as its shared catalogue; an application the mesh merely hosts keeps its manifest beside its own code.
So there is one home for the catalogue the mesh owns, and every application that runs on the mesh rather than being of it carries its own — both reach the pipeline the same way, as a registered source.
Layout
modules/<name>.json one manifest per module
Flat, because the catalogue's shape carries no meaning: a module is found by its name and described by its manifest, and what relates two modules — a shared seat, a claim, a provider/consumer edge — is data inside the manifests, not a directory the tree encodes (novox/hq, the domain-grouping question closed in favour of seats, claims and tags).
The manifest contract
The shape a manifest must satisfy is owned by the control plane's catalogue engine and is what
validates a manifest before a machine ever sees it — a stray key, a consumer contributing the
wrong provision field, an image that nothing builds. That validation belongs with this
repository and is being re-homed here from mesh-controller; until it is, the pipeline is the
gate — it builds each module and refuses a manifest it cannot resolve.
Where the reasoning lives
Design and decisions are in novox/hq:
02-DECISIONS/0002-everything-is-a-module.md— one unit, no second mechanism02-DECISIONS/0010-applications-live-in-their-own-repository.md— why applications are not here02-DECISIONS/0030-the-repository-structure.md— the repositories, and the open tier-4 question this repository answers03-DESIGN/00-as-is/10-module-catalogue.md— the catalogue's shape, and what it records