<p>NetKingdom's runtime services are deployed today outside the Railiance reef/rail/<code>rapp</code> model. <code>tenant-engine</code> and <code>user-engine</code> run on the coulomb substrate with digest-pinned images (<code>forgejo.coulomb.social/coulomb/{tenant,user}-engine@sha256:…</code>), but their Kubernetes manifests live in this repo at <code>sso-mfa/k8s/<service>/runtime.yaml</code> — a canon repo holding runtime YAML — applied imperatively, with <code>verify-t0*.sh</code> scripts as verification. Neither service declares <code>railiance/app.toml</code>, appears in <code>reef-railiance/bindings/rapps.yaml</code>, or is reconciled by a GitOps controller.</p>
<p>The decision to bring NetKingdom under Railiance governance forces two questions this ADR settles.</p>
<p><strong>Packaging granularity.</strong><code>railiance-master/docs/repository-axes.md</code> maps <code>rapp-*</code> to "one Railiance-managed workload" and states that a <code>rapp</code> "does not replace the responsibility repo that owns the broader domain. It is explicitly about managed wrapping, not ownership." Precedent exists in two shapes: <code>rapp-qonto</code> (<code>ownership_repo: qonto-assistant</code>, one workload, thin package repo pointing back at the domain repo) and <code>rapp-postgres</code> (<code>ownership_repo: railiance-platform</code>, one workload with a <code>consumers:</code> list and a <code>consumer_contract</code>). The open question was whether NetKingdom's engines are one workload or several.</p>
<p><strong>Relational storage.</strong> The engines have diverged. <code>user-engine</code> already runs CloudNative PG (<code>kind: Cluster</code> in its own namespace) with NetworkPolicies and a <code>migrations/</code> surface. <code>tenant-engine</code> runs SQLite on a 1 Gi RWO PVC, chosen in TEN-WP-0004 for expedience. <code>railiance-platform</code> (S3) declares CloudNative PG the canonical database operator, and <code>rapp-postgres</code> packages it with a <code>database-per-consumer</code> boundary unit and OpenBao dynamic credentials. Separately, <code>rail-kubernetes</code>'s wave-1 contract explicitly states the rail must <strong>not</strong> assume "a generic persistent-storage contract" — so a per-workload PVC is unsupported by the rail regardless of which database is chosen.</p>
<p>TEN-WP-0005 was drafted against PostgreSQL and implemented against SQLite, because T05 must roll out against the runtime that exists. That workplan's <code>TenantStore</code> Protocol was kept as an explicit seam for this decision.</p>
</section>
<sectionid="decision"><h2>Decision</h2>
<h3>1. Separate <code>rapp-*</code> repos per engine</h3>
<p>Each deployed NetKingdom engine gets its own managed workload package repo:</p>
<p>Following <code>rapp-qonto</code>'s shape: the <code>rapp</code> 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; <code>net-kingdom</code> retains canon.</p>
<p>This follows the one-workload rule rather than working around it. The engines are already independent on every axis a <code>rapp</code> declaration must state: separate namespaces, separate image digests, separate release cadence, and (until decision 2 lands) different storage. A single <code>rapp-netkingdom</code> would have to declare one <code>workload_identity</code>, one <code>rollout_contract</code>, and one <code>rollback_contract</code> across services that genuinely differ.</p>
<p>The decisive property is <strong>independent rollback</strong>: rolling back a bad <code>tenant-engine</code> revision must not force a <code>user-engine</code> revision change. A single rapp would make that either impossible or fictional.</p>
<p>Rollout ordering between the engines is a declared constraint between rapps, not a reason to merge them: <code>tenant-engine</code> before <code>user-engine</code>, following the dependency direction.</p>
<h3>2. <code>secrets-engine</code> is not packaged as a <code>rapp</code></h3>
<p><code>secrets-engine</code> 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 <code>rapp-openbao</code> with custody and lane policy in <code>railiance-platform</code>.</p>
<p>A <code>rapp</code> wraps a workload; <code>secrets-engine</code> 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.</p>
<h3>3. CloudNative PG is the default relational platform for production</h3>
<p>Production NetKingdom services requiring relational storage use CloudNative PG via <code>rapp-postgres</code>, consuming the <code>database-per-consumer</code> boundary unit and the <code>openbao-dynamic-database-credential</code> lane, with tenant-keying per <code>business-app-service-contract_v0.1</code> section 1.3.</p>
<p>Per-workload SQLite-on-a-PVC is not a production pattern. It remains acceptable for local development and tests.</p>
<p><code>tenant-engine</code> migrates from SQLite to cnpg. Its <code>TenantStore</code> 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.</p>
</section>
<sectionid="consequences"><h2>Consequences</h2>
<ul><li>Two new <code>rapp-*</code> repos to create, each requiring a <code>declarations/rapp.yaml</code>, a binding in <code>reef-railiance/bindings/rapps.yaml</code>, and its own readiness evidence progression (<code>declared</code> → <code>installed</code> → <code>verified</code> → <code>production-approved</code>).</li><li>Per-binding evidence multiplies, but per-reef evidence does not — the <code>reef-production-readiness-contract</code> 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.</li><li>Runtime manifests move out of <code>net-kingdom/sso-mfa/k8s/</code> into the respective <code>rapp</code> repos. This repo stops holding runtime YAML and returns to canon, standards, and ADRs.</li><li><code>tenant-engine</code> 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.</li><li>NetKingdom services become subject to the <code>railiance/app.toml</code> staged promotion contract (Stage 1 local → Stage 2 canary → Stage 3 production), including declared rollback commands and health endpoints. Digest-pinned images already satisfy <code>digest_policy = "required"</code>.</li><li>The reef must accept or mitigate its single-server and shared-control-plane risk for each NetKingdom binding, as it must for <code>rapp-qonto</code>. The reef name and a <code>high</code> criticality label are not evidence.</li></ul>
<p><strong>One <code>rapp-netkingdom</code> for all associated repos.</strong> Rejected. It would reduce binding-evidence count and match the current coupled deployment (both engines' manifests share one tree; <code>flex-auth</code> runs per-consumer instances <code>flex-auth-tenant-engine</code> and <code>flex-auth-user-engine</code>). But it contradicts the one-workload rule in <code>repository-axes.md</code>, 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.</p>
<p><strong>Keep SQLite for <code>tenant-engine</code>, standardise later.</strong> Rejected. It leaves the two engines operationally dissimilar, which is the opposite of the intent, and <code>rail-kubernetes</code> 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.</p>
<p><strong>A <code>rapp</code> per NetKingdom concern with a <code>consumers:</code> list, mirroring <code>rapp-postgres</code>.</strong> Rejected as a category error. <code>rapp-postgres</code>'s consumers are consumers <em>of one workload</em> (PostgreSQL); NetKingdom's engines are distinct workloads, not consumers of a shared one.</p>
</section>
<sectionid="follow-up"><h2>Follow-Up</h2>
<ul><li>Create <code>rapp-tenant-engine</code> and <code>rapp-user-engine</code>; move runtime manifests out of <code>net-kingdom/sso-mfa/k8s/</code>.</li><li>Add both bindings to <code>reef-railiance/bindings/rapps.yaml</code> at <code>declared</code>.</li><li>Add <code>railiance/app.toml</code> to <code>tenant-engine</code> and <code>user-engine</code>.</li><li>Open a <code>tenant-engine</code> workplan for the cnpg backend and data migration; reconcile with TEN-WP-0005-T05, whose rollout currently targets SQLite.</li><li>Confirm whether <code>secrets-engine</code> is intended to remain a non-deployed control layer. This ADR assumes it is.</li><li>Record the NetKingdom-wide threat model that both bindings will reference.</li></ul>