diff --git a/docs/adr/ADR-0015-netkingdom-railiance-workload-packaging-and-relational-platform.md b/docs/adr/ADR-0015-netkingdom-railiance-workload-packaging-and-relational-platform.md new file mode 100644 index 0000000..05d2c4f --- /dev/null +++ b/docs/adr/ADR-0015-netkingdom-railiance-workload-packaging-and-relational-platform.md @@ -0,0 +1,166 @@ +# ADR-0015 - NetKingdom Railiance Workload Packaging and Relational Platform + +**Status:** Accepted +**Date:** 2026-08-11 +**Deciders:** Bernd Worsch, Claude + +## Context + +NetKingdom's runtime services are deployed today outside the Railiance +reef/rail/`rapp` model. `tenant-engine` and `user-engine` run on the coulomb +substrate with digest-pinned images +(`forgejo.coulomb.social/coulomb/{tenant,user}-engine@sha256:…`), but their +Kubernetes manifests live in this repo at `sso-mfa/k8s//runtime.yaml` +— a canon repo holding runtime YAML — applied imperatively, with +`verify-t0*.sh` scripts as verification. Neither service declares +`railiance/app.toml`, appears in `reef-railiance/bindings/rapps.yaml`, or is +reconciled by a GitOps controller. + +The decision to bring NetKingdom under Railiance governance forces two +questions this ADR settles. + +**Packaging granularity.** `railiance-master/docs/repository-axes.md` maps +`rapp-*` to "one Railiance-managed workload" and states that a `rapp` "does +not replace the responsibility repo that owns the broader domain. It is +explicitly about managed wrapping, not ownership." Precedent exists in two +shapes: `rapp-qonto` (`ownership_repo: qonto-assistant`, one workload, thin +package repo pointing back at the domain repo) and `rapp-postgres` +(`ownership_repo: railiance-platform`, one workload with a `consumers:` list +and a `consumer_contract`). The open question was whether NetKingdom's engines +are one workload or several. + +**Relational storage.** The engines have diverged. `user-engine` already runs +CloudNative PG (`kind: Cluster` in its own namespace) with NetworkPolicies and +a `migrations/` surface. `tenant-engine` runs SQLite on a 1 Gi RWO PVC, chosen +in TEN-WP-0004 for expedience. `railiance-platform` (S3) declares CloudNative +PG the canonical database operator, and `rapp-postgres` packages it with a +`database-per-consumer` boundary unit and OpenBao dynamic credentials. +Separately, `rail-kubernetes`'s wave-1 contract explicitly states the rail must +**not** assume "a generic persistent-storage contract" — so a per-workload PVC +is unsupported by the rail regardless of which database is chosen. + +TEN-WP-0005 was drafted against PostgreSQL and implemented against SQLite, +because T05 must roll out against the runtime that exists. That workplan's +`TenantStore` Protocol was kept as an explicit seam for this decision. + +## Decision + +### 1. Separate `rapp-*` repos per engine + +Each deployed NetKingdom engine gets its own managed workload package repo: + +```text +rapp-tenant-engine ownership_repo: tenant-engine +rapp-user-engine ownership_repo: user-engine +``` + +Following `rapp-qonto`'s shape: the `rapp` owns Railiance packaging, rail +compatibility, workload-specific smoke/health checks, rollout and rollback +expectations, secret references, and dependency declarations. The engine repo +retains domain ownership; `net-kingdom` retains canon. + +This follows the one-workload rule rather than working around it. The engines +are already independent on every axis a `rapp` declaration must state: +separate namespaces, separate image digests, separate release cadence, and +(until decision 2 lands) different storage. A single `rapp-netkingdom` would +have to declare one `workload_identity`, one `rollout_contract`, and one +`rollback_contract` across services that genuinely differ. + +The decisive property is **independent rollback**: rolling back a bad +`tenant-engine` revision must not force a `user-engine` revision change. A +single rapp would make that either impossible or fictional. + +Rollout ordering between the engines is a declared constraint between rapps, +not a reason to merge them: `tenant-engine` before `user-engine`, following the +dependency direction. + +### 2. `secrets-engine` is not packaged as a `rapp` + +`secrets-engine` has no deployed workload — no Containerfile, no Makefile, no +Kubernetes manifests. It is a catalog/policy/workflow layer over OpenBao, whose +packaging already belongs to `rapp-openbao` with custody and lane policy in +`railiance-platform`. + +A `rapp` wraps a workload; `secrets-engine` has none to wrap. Revisit only if +it becomes a runtime service, at which point this decision reopens for that +repo alone and not for the packaging model. + +### 3. CloudNative PG is the default relational platform for production + +Production NetKingdom services requiring relational storage use CloudNative PG +via `rapp-postgres`, consuming the `database-per-consumer` boundary unit and +the `openbao-dynamic-database-credential` lane, with tenant-keying per +`business-app-service-contract_v0.1` section 1.3. + +Per-workload SQLite-on-a-PVC is not a production pattern. It remains acceptable +for local development and tests. + +`tenant-engine` migrates from SQLite to cnpg. Its `TenantStore` Protocol makes +this a backend swap behind an existing seam rather than a rewrite; the +lifecycle semantics proven in TEN-WP-0005 (atomic compare-and-swap, durable +idempotency receipts, forward-only migration) are the conformance bar the +PostgreSQL backend must meet, and its store-conformance suite is already +parametrised across backends to enforce exactly that. + +## Consequences + +- Two new `rapp-*` repos to create, each requiring a `declarations/rapp.yaml`, + a binding in `reef-railiance/bindings/rapps.yaml`, and its own readiness + evidence progression (`declared` → `installed` → `verified` → + `production-approved`). +- Per-binding evidence multiplies, but per-reef evidence does not — the + `reef-production-readiness-contract` puts substrate, ingress, storage, + network, and backup evidence on the reef, once. What multiplies is the + critical-workload gate (threat model, negative authorization tests, rollback + rules, residual-risk owner), which identity-plane services warrant + individually. A shared NetKingdom threat model may be referenced by both + bindings rather than duplicated. +- Runtime manifests move out of `net-kingdom/sso-mfa/k8s/` into the respective + `rapp` repos. This repo stops holding runtime YAML and returns to canon, + standards, and ADRs. +- `tenant-engine` requires a data migration from SQLite to cnpg, including + migration of existing tenants, grants, plan assignments, and idempotency + receipts. TEN-WP-0005-T05's rollout plan is affected: it currently targets + the SQLite runtime. +- NetKingdom services become subject to the `railiance/app.toml` staged + promotion contract (Stage 1 local → Stage 2 canary → Stage 3 production), + including declared rollback commands and health endpoints. Digest-pinned + images already satisfy `digest_policy = "required"`. +- The reef must accept or mitigate its single-server and shared-control-plane + risk for each NetKingdom binding, as it must for `rapp-qonto`. The reef name + and a `high` criticality label are not evidence. + +## Alternatives Considered + +**One `rapp-netkingdom` for all associated repos.** Rejected. It would reduce +binding-evidence count and match the current coupled deployment (both engines' +manifests share one tree; `flex-auth` runs per-consumer instances +`flex-auth-tenant-engine` and `flex-auth-user-engine`). But it contradicts the +one-workload rule in `repository-axes.md`, and would force a single rollback +contract across independently versioned services. A rollback contract that +depends on which service failed is not a contract. This alternative would be +correct only if the engines were always promoted and rolled back as one atomic +cutover — which their separate digests and release cadences contradict. + +**Keep SQLite for `tenant-engine`, standardise later.** Rejected. It leaves the +two engines operationally dissimilar, which is the opposite of the intent, and +`rail-kubernetes` does not support the per-workload persistent-storage contract +the PVC depends on. Deferring also grows the migration: every tenant, grant, +and receipt written between now and the cutover is data to move. + +**A `rapp` per NetKingdom concern with a `consumers:` list, mirroring +`rapp-postgres`.** Rejected as a category error. `rapp-postgres`'s consumers +are consumers *of one workload* (PostgreSQL); NetKingdom's engines are distinct +workloads, not consumers of a shared one. + +## Follow-Up + +- Create `rapp-tenant-engine` and `rapp-user-engine`; move runtime manifests + out of `net-kingdom/sso-mfa/k8s/`. +- Add both bindings to `reef-railiance/bindings/rapps.yaml` at `declared`. +- Add `railiance/app.toml` to `tenant-engine` and `user-engine`. +- Open a `tenant-engine` workplan for the cnpg backend and data migration; + reconcile with TEN-WP-0005-T05, whose rollout currently targets SQLite. +- Confirm whether `secrets-engine` is intended to remain a non-deployed control + layer. This ADR assumes it is. +- Record the NetKingdom-wide threat model that both bindings will reference.