NK-ADR-0015 accepted · 1 net-kingdom reviewed 2026-08-22generated from canonical source — do not edit

NetKingdom Railiance Workload Packaging and Relational Platform

Source: net-kingdom · docs/adr/ADR-0015-netkingdom-railiance-workload-packaging-and-relational-platform.md · 9781102e2971d762ae42fdd5085a6647afd1cd66

Review due: 2027-02-22

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 a declarations/rapp.yaml, a binding in reef-railiance/bindings/rapps.yaml, and its own readiness evidence progression (declaredinstalledverifiedproduction-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.