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/<service>/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:
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 adeclarations/rapp.yaml, a binding inreef-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-contractputs 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 respectiverapprepos. This repo stops holding runtime YAML and returns to canon, standards, and ADRs. tenant-enginerequires 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.tomlstaged promotion contract (Stage 1 local → Stage 2 canary → Stage 3 production), including declared rollback commands and health endpoints. Digest-pinned images already satisfydigest_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 ahighcriticality 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-engineandrapp-user-engine; move runtime manifests out ofnet-kingdom/sso-mfa/k8s/. - Add both bindings to
reef-railiance/bindings/rapps.yamlatdeclared. - Add
railiance/app.tomltotenant-engineanduser-engine. - Open a
tenant-engineworkplan for the cnpg backend and data migration; reconcile with TEN-WP-0005-T05, whose rollout currently targets SQLite. - Confirm whether
secrets-engineis intended to remain a non-deployed control layer. This ADR assumes it is. - Record the NetKingdom-wide threat model that both bindings will reference.