TEN-WP-0009: make database placement portable rather than settled

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-16 18:59:30 +02:00
parent 121968d65b
commit cf7ca1a692
2 changed files with 94 additions and 17 deletions

View file

@ -17,6 +17,7 @@
| workplan | TEN-WP-0006 | finished | — | workplans/TEN-WP-0006-guardrail-quota-policy.md |
| workplan | TEN-WP-0007 | finished | — | workplans/TEN-WP-0007-guardrail-production-rollout.md |
| workplan | TEN-WP-0008 | ready | — | workplans/TEN-WP-0008-staged-promotion-onboarding.md |
| workplan | TEN-WP-0009 | ready | — | workplans/TEN-WP-0009-postgres-production-store.md |
| task | ADHOC-2026-07-24-T01 | done | — | workplans/ADHOC-2026-07-24.md |
| task | TEN-WP-0001-T01 | done | — | workplans/TEN-WP-0001-statehub-bootstrap.md |
| task | TEN-WP-0001-T02 | done | — | workplans/TEN-WP-0001-statehub-bootstrap.md |
@ -53,3 +54,9 @@
| task | TEN-WP-0008-T02 | todo | — | workplans/TEN-WP-0008-staged-promotion-onboarding.md |
| task | TEN-WP-0008-T03 | todo | — | workplans/TEN-WP-0008-staged-promotion-onboarding.md |
| task | TEN-WP-0008-T04 | todo | — | workplans/TEN-WP-0008-staged-promotion-onboarding.md |
| task | TEN-WP-0009-T01 | todo | — | workplans/TEN-WP-0009-postgres-production-store.md |
| task | TEN-WP-0009-T02 | todo | — | workplans/TEN-WP-0009-postgres-production-store.md |
| task | TEN-WP-0009-T03 | todo | — | workplans/TEN-WP-0009-postgres-production-store.md |
| task | TEN-WP-0009-T04 | todo | — | workplans/TEN-WP-0009-postgres-production-store.md |
| task | TEN-WP-0009-T05 | todo | — | workplans/TEN-WP-0009-postgres-production-store.md |
| task | TEN-WP-0009-T06 | todo | — | workplans/TEN-WP-0009-postgres-production-store.md |

View file

@ -13,6 +13,7 @@ depends_on:
- TEN-WP-0007
unblocks:
- TEN-WP-0008
state_hub_workstream_id: "3bf3b4ab-a116-4914-b2d8-d3c430754956"
---
# TEN-WP-0009 - PostgreSQL production store
@ -57,33 +58,88 @@ So **no secret enters this repo, this workplan, or any evidence dump**, and no
or rotate. Runtime secret custody remains OpenBao's per `SCOPE.md`; an
operator-minted in-cluster secret is outside that lane.
## T01 - Decide the topology and write the cluster manifest
## Placement: shared by default, movable by design
Dedicated-vs-shared is **not a decision this workplan should settle once**, and
`tenant-engine` is the wrong place to settle it for the platform. The estate
has distinct elements — railiance, NetKingdom, HelixForge, Coulomb — plus
tenants layered on top, and which repo belongs to which grouping is not yet
organised. On top of that, isolation level is heading toward being a
*product* property: plans will differ in how much isolation they buy.
So the requirement here is **portability, not placement**:
- start on a **shared** cluster, because it is cheaper on a single node and
nothing yet justifies dedicated capacity;
- make moving to a dedicated cluster — or to a different shared one — an
**operational change, not a code change**;
- never let the decision leak into the application.
What that demands of this repo, and all of it is cheap if done now and
expensive to retrofit:
1. **Connect by injected URL only.** No cluster name, namespace, host, or
database name anywhere in `src/`. The application must be unable to tell
whether it is on a shared or dedicated cluster.
2. **Own a whole database, not a set of tables in someone else's.** Sharing a
*cluster* is a capacity decision; sharing a *database* would entangle
schemas and make relocation a merge rather than a move. A dedicated
database inside a shared cluster relocates with a dump and a restore.
3. **No cross-database joins or co-location assumptions**, which the repo
boundary already forbids — `tenant_id` is the only key shared with
`user-engine`. Placement must not quietly become a dependency.
4. **Idempotent schema creation**, so a fresh target comes up correct without a
hand-built database.
Then "move `tenant-engine` to dedicated" is: create the target cluster, dump,
restore, swap the `secretKeyRef`, restart. No rebuild, no release, no code
review.
**What this repo does not own:** the placement *policy* — which element or
tenant gets dedicated capacity and when. That is a platform concern, and
`tenant-engine` implementing its own would be the same boundary error as
building a fleet drift-detector because we got bitten. Raised with
`railiance-platform` separately; see T01.
## T01 - Make placement portable and provision on shared capacity
```task
id: TEN-WP-0009-T01
status: todo
priority: high
state_hub_task_id: "b4701256-f235-4474-9dcf-7cb09f62b873"
```
Decide between a dedicated `tenant-engine-pg` cluster in our namespace and
sharing an existing one (`net-kingdom-pg` or `apps-pg` in `databases`).
Provision a dedicated **database** on an existing **shared** cluster, per the
principles above. Candidates are `net-kingdom-pg` (already exists, and
`tenant-engine` is a NetKingdom service) and `apps-pg`. Prefer the one whose
grouping matches the product family, since that is the axis a future split
would most likely follow — moving is cheap, but moving less often is cheaper.
Argue it rather than defaulting. Points to weigh: `user-engine` chose dedicated
and is the closest precedent; a dedicated cluster keeps blast radius and backup
policy per-service; a shared cluster is cheaper on a single node and
`net-kingdom-pg` already exists for this product family. Note that
`tenant-engine` is consulted by `flex-auth` on the authorization path, so its
availability envelope is not purely its own.
Note the deviation from precedent and why: `user-engine` runs a dedicated
`user-engine-pg`. We are deliberately *not* copying that. Their choice is fine
and ours is reversible in an afternoon; picking dedicated now would spend
capacity on an isolation guarantee nothing has asked for yet.
Write the CNPG `Cluster` manifest into `deploy/`, matching the fleet's existing
image and storage conventions rather than inventing new ones.
Record what would trigger a move to dedicated, so the reversal is a judgement
already made rather than one improvised under pressure. Candidate triggers:
a noisy neighbour affecting the authorization path, a compliance or residency
requirement, a plan tier that sells isolation, or the shared cluster's backup
policy no longer fitting.
Also decide **backups**. The `scheduledbackups.postgresql.cnpg.io` CRD is
available; SQLite-on-a-PVC had no backup story at all, so this is a gain to
claim explicitly rather than inherit by accident.
Also decide **backups**: whether the shared cluster's existing
`scheduledbackups` policy covers us, or we need our own. SQLite-on-a-PVC had no
backup story at all, so this is a gain to claim explicitly rather than inherit
by accident — and a shared cluster means inheriting *someone else's* retention
choice, which is worth checking rather than assuming.
Done when the topology decision is recorded with its reasoning and the manifest
is committed.
Write whatever manifest the choice needs into `deploy/` — a CNPG `Database`
resource against the shared cluster, not a new `Cluster`, if that is the
correct shape.
Done when the database exists on shared capacity, the connection reaches the
app only through an injected URL, and the move-to-dedicated trigger list and
runbook are recorded.
## T02 - Implement `PostgresTenantStore`
@ -91,6 +147,7 @@ is committed.
id: TEN-WP-0009-T02
status: todo
priority: high
state_hub_task_id: "d7428bc1-2e5f-4a4c-93f5-9368ebb691d5"
```
Implement the full `TenantStore` Protocol — including
@ -128,6 +185,7 @@ Done when all three backends pass the same conformance suites unchanged.
id: TEN-WP-0009-T03
status: todo
priority: high
state_hub_task_id: "89c121c6-8bc5-4abe-80f0-d11406aeffc2"
```
`Settings` currently carries only `database_path`. Add a database URL and make
@ -156,6 +214,7 @@ by tests.
id: TEN-WP-0009-T04
status: todo
priority: high
state_hub_task_id: "51a83eae-9e9f-4676-bc1d-9c2d884bac0d"
```
Move the live SQLite database to PostgreSQL. Currently small — two tenants
@ -191,6 +250,7 @@ recorded here.
id: TEN-WP-0009-T05
status: todo
priority: high
state_hub_task_id: "03aeae3b-5e0f-4c77-a763-787ae08078f5"
```
Update `deploy/tenant-engine.yaml`:
@ -223,6 +283,7 @@ service.
id: TEN-WP-0009-T06
status: todo
priority: medium
state_hub_task_id: "e7adbc64-ad1b-4f28-bfd0-0b07034629db"
```
- `flex-auth`: availability characteristics of their data source changed. No
@ -243,4 +304,13 @@ Done when the notes are sent and TEN-WP-0008-T02 is updated.
change at the same time makes any regression ambiguous.
- **Sharing a database with `user-engine`.** `tenant_id` is the only key the
two services share, and it stays that way — separate stores are the boundary,
not an implementation detail.
not an implementation detail. Sharing a *cluster* is fine; sharing a
*database* is not.
- **Fleet placement policy.** Which element or tenant gets dedicated capacity,
and how isolation maps to plan tiers, is a platform decision. This workplan
makes `tenant-engine` movable and stops there.
- **Per-tenant isolation.** A future plan tier may sell dedicated
infrastructure. `tenant-engine` already records plan assignment by id, and
`adaptive-pricing` owns what a plan *means*, so neither the tier definition
nor the placement that implements it belongs here. Worth designing for —
which the portability principles above do — not worth building for.