0007 — Shared CloudNativePG, Database-per-Service¶
Status: Accepted
Context¶
The core stateful tenants need durable Postgres: Synapse (homeserver), MAS (auth), the bridge (the mautrix StateStore, so ghost registrations survive restarts — ADR 0005), kagent, the optional Keycloak reference IdP, and the composed knowledge store. Every enabled external-network bridge adds another stateful tenant. The project principle is "bridging infra, not embedded databases": shared infrastructure exposed to independent apps without coupling them.
Alternatives considered and rejected:
- Bundled/embedded database per component (e.g. ESS's in-chart Postgres, a sidecar DB per app). Simplest to bootstrap, but multiplies operators, backup policies, and connection configs — and contradicts the shared-infra principle by making each app own its own DB lifecycle.
- One shared database with a schema per service. Lighter, but Synapse's
Ccollation requirement is database-scoped, not schema-scoped, and mixing tenants in one DB muddies backup/restore and role isolation. - A separate managed cloud DB (e.g. Cloud SQL). Breaks provider independence — a hard non-goal for this platform.
Decision¶
Run one shared CloudNativePG cluster (platform-pg, ns postgres) that exposes a dedicated database + purpose-scoped role set per tenant:
synapse— created withCcollation (Synapse's hard requirement).mas— the Matrix Authentication Service store.bridge— the mautrix StateStore.kagent— agent and session persistence.keycloak— the optional reference IdP's realm, client, user, and session state.knowledge— Fgentic's owned chunk/ACL schema, composed from pgvector in the same PostgreSQL operand.knowledge_ownerowns migrations and ingestion objects;knowledge_retrievalis the separately credentialed read-only consumer. The exact schema and ranking boundary are in Sovereign Grounding Store.
Opt-in Kustomize components append one role/database without replacing the canonical Postgres layer. The shipped references use slackbridge and telegrambridge; their password Secrets do not exist in a normal core bootstrap.
TLS and the database/role pairing are enforced at connection admission. pg_hba contains one hostssl <database> <role> … scram-sha-256 rule per admitted role, then rejects every other TLS or plaintext connection. Optional network components prepend only their own pair and remove it during the NOLOGIN offboard phase. This compensates for PostgreSQL's default CONNECT grant to PUBLIC instead of relying on schema ownership after a cross-database login has already succeeded. A single operator owns backups, connection policy, and failover for all stateful tenants; role passwords are SOPS-encrypted secrets decrypted in-cluster by Flux.
Consequences¶
- One operator, one backup policy, one connection posture — the shared-infra pattern demonstrated concretely.
Ccollation is set per-database, so Synapse's requirement is satisfied without forcing it onmas/bridge.- Scoped roles plus exact HBA pairs reject cross-database logins, so a compromised
bridgecredential cannot connect to Synapse's database or read its tables through the supported network path. - Interop bridges each have their own database — mautrix requires unrelated programs to avoid sharing a database, so Slack and Telegram compose
slackbridgeandtelegrambridgeinto this cluster rather than sharingbridgeor each other. - The knowledge store adds a database, extension, roles, schema, and indexes to the existing operand—not another database operator, StatefulSet, or bundled RAG platform. Empty catalog objects consume no steady-state connection, CPU, or memory.
- The pattern mirrors the sibling
dev.fmindbridging-infra principle, adapted from schema-per-service to database-per-service precisely because of Synapse's collation constraint.