repo-manager/docs/RailianceAppDeploymentGuide.md
tegwick 77452492ec docs: treat live net-kingdom user-engine manifests as the wrap source
user-engine already runs from net-kingdom/sso-mfa/k8s/user-engine.
The rapp absorbs those manifests (manifest-managed), keeps or
cutovers the dedicated CNPG cluster, and becomes the only apply home.
2026-08-18 12:21:53 +02:00

40 KiB

Railiance App Deployment Guide

Best-practice guide for wrapping an ordinary application as a Railiance managed workload (rapp-*) so Repo Manager can later scaffold the same shape for rapp-some-app.

This document is a practice and work-structure guide. It does not restate the family schemas. Where a field list and a schema disagree, the schema wins:

Worked example throughout: user-engine + the empty stub rapp-user-engine.


1. Purpose

Repo Manager should make it easy to set up a managed Railiance application wrapper. The product of that setup is a rapp-<app> repository: a packaging and operations home that can deploy, verify, roll back, and bind the app onto a rail and a reef.

This guide answers:

  1. What a managed Railiance application is.
  2. How rails, reefs, and existing rapps already do it.
  3. Which files, declarations, and operator surfaces a new wrapper needs.
  4. How user-engine should be wrapped as rapp-user-engine.
  5. What Repo Manager must later automate, in order.

It is the first deliverable of RMGR-WP-0006. Implementation of the scaffolder comes after this shape is accepted.


2. Vocabulary

Railiance classifies repositories on four complementary axes (railiance-master/docs/repository-axes.md, ADR-0001):

Prefix Axis Question it answers
railiance-* ownership Who owns this capability or layer?
rail-* execution contract How does a workload run here?
rapp-* managed workload package What is packaged and operated?
reef-* substrate Where does it run, and what is bound there?

A rapp is managed packaging, not ownership. The application repo keeps domain logic, tests, image build, and schema migrations. The rapp owns Helm/manifests, rail compatibility, smoke and rollback, secret references, and reef binding. The architecture term is managed workload package; "service wrapper" is informal.

Do not derive a rapp from a Forgejo organization or a State Hub domain. Rapp context is its own dimension (ADR-0007):

  • repos ↔ rapps is many-to-many
  • deployables ↔ rapps is one-to-one

Grouping is legitimate only where members share rollout and rollback fate.


3. Current inventory (2026-08-18)

3.1 Rails

Rail Kind Architecture Readiness Use when
rail-kubernetes base Kubernetes verified Default for platform services and managed applications
rail-knative derived from rail-kubernetes Knative Serving on Kubernetes rail.yaml verified; reef binding not production-approved Scale-to-zero, revision traffic, request activation (today: Qonto). Serving 1.22.0 is installed on Railiance01; single-node control plane still blocks production approval.

Default bias: use rail-kubernetes unless the workload truly needs Knative lifecycle semantics. Do not invent a new rail for an adapter or values profile.

rail-kubernetes also owns the generic staged-promotion contract railiance/app.toml (Stage 1 local, Stage 2 canary, Stage 3 promote) and the compatibility overlay-repo pattern. That overlay is the migration-era wrapper. Durable first-class packaging belongs in rapp-*.

3.2 Reefs

Reef Kind State What it is
reef-railiance server-group active Home compute reef (Railiance01). Hosts rail-kubernetes and rail-knative. Public host ports 80/443 (and 2224 for the provider agent) are granted.
reef-storage object-storage planned Provider-delegated S3 (Scaleway). Hosts no rail. Backup target, not a compute placement.

A rapp names its placement in bound_reefs. The reef's bound_rapps is a derived projection of that field, not a hand list.

Topology is not readiness (ADR-0006). Binding a rapp to a reef does not admit it to production and does not make it public (ADR-0008).

3.3 Live and stub rapps

Repo Declaration Package type Rail Ownership repo Readiness Notes
rapp-openbao yes helm-managed-platform-service kubernetes railiance-platform verified Third-party platform service. Proven S3 pattern.
rapp-postgres yes helm-managed-platform-service kubernetes railiance-platform verified Shared CNPG + per-consumer surface. Apps consume this; they do not run their own Postgres.
rapp-qonto yes, pre-schema drift knative-managed-service (implied) knative qonto-assistant verified First-party Knative app. Missing composition, bound_reefs, package_type; workload_identity.name is still rapp-qonto.
rapp-policy-nexus yes helm-managed-platform-service kubernetes policy-nexus declared Closest first-party Helm app wrapper. Public grant at policy.coulomb.social.
rapp-user-engine no stub Empty git repo. Target of this guide.
rapp-secrets-engine no stub README only. Same family as user-engine.
rapp-tenant-engine no stub README only. Same family.
rapp-vergabe-teilnahme never built Planned user-facing proof; workload still lives in railiance-apps.
rapp-forgejo not built Deferred; forge still mixed into railiance-forge / railiance-apps.

The platform-service wrapper is proven twice. The first-party application wrapper is only half-proven (rapp-policy-nexus declared; rapp-qonto drifted; rapp-vergabe-teilnahme never extracted). rapp-user-engine is the next application-shaped proof, not a substitute for the missing Vergabe extraction.

3.4 Ownership repos that still hold wrappers

railiance-apps is still the S5 release surface for workloads that have not been extracted (vergabe-teilnahme, coulomb-social, core-hub, reuse-surface, inter-hub, Forgejo values). Its docs/s5-app-onboarding-checklist.md remains the operator recipe for Helm, probes, landing pages, and image pins. New first-class wrappers should copy those recipes into a rapp-* repo, not add another chart to railiance-apps.

When a rapp already exists, S5 becomes a thin delegate rather than a second chart. railiance-apps make policy-nexus-deploy calls make -C ../rapp-policy-nexus deploy. Production approval may still be recorded in railiance-apps bindings; the package and apply path belong to the rapp.

railiance-platform retains cross-workload policy, credential custody, lane approval, and the postgres credential broker.

railiance-fabric projects supports_rail / hosts_rail / binds_rapp from family declarations. That is discovery, not a deploy engine. Do not treat "register in Fabric" as wrapping the app.

3.5 Three wrap paths (only one is the target)

Path Where it lives Use
A. rapp-* repo declarations/rapp.yaml + package Makefile Canonical destination for a managed app or platform service. This guide.
B. Overlay + railiance/app.toml bin/railiance create-overlay in rail-kubernetes Migration-era third-party wrap. Produces <app>-railiance-overlay/. Graduate it to Path A; do not start new overlays.
C. S5 chart in railiance-apps charts/<app>/ + helm/<app>-values.yaml How user-facing apps actually ship today. Stay here only for an already-extracted-or-not decision; new managed apps should leave as Path A.

flex-auth is a live Path B example (flex-auth/railiance/app.toml) with no rapp-* repo. vergabe-teilnahme is a live Path C example. rapp-policy-nexus is the only live A←C bridge.

There is no railiance register-rapp. A rapp exists when declarations/rapp.yaml is committed and validate-family-declarations.py accepts it.


4. Decide before creating a rapp

Ask these in order. Stop at the first "no".

  1. Does the workload have a stable identity? A name that is not the repo prefix, a package surface already visible in Git, and a reason to version and roll back on its own.
  2. Is wrapping distinct from ownership? If the only thing you would put in the rapp already belongs in railiance-platform or railiance-apps as shared policy, do not create a rapp.
  3. Do members share fate? One rapp per bounded context that deploys and rolls back together. Independent rollback → separate rapp.
  4. Which rail? Default rail-kubernetes. Choose rail-knative only for request-activation / scale-to-zero / revision-traffic semantics.
  5. Which reef? New compute workloads bind reef-railiance. Object storage is reef-storage and is consumed as a capability, not as a placement.
  6. What package type? One of the schema enums (see §6).
  7. What exposure? Omit the field (private) until a grant exists.

A service that fails (1) or (2) can still be operated from its ownership repo. It is not blocked; it is just not ready to be a rapp. Keep it on Path C (S5) or in the ownership repo until those answers are yes.

If the workload is already an S5 chart and is not yet ready for a rapp, do not generate an overlay (Path B) on top of it. Path B is only a third-party compatibility wrap.


5. Ownership split

Use the S3 ownership test from railiance-platform/docs/rapp-platform-service-pattern.md, generalized to applications:

  1. Would this asset survive replacing the product? → ownership / platform repo.
  2. Does any other workload depend on it? → platform or rail.
  3. Does it encode who may approve, not how to apply? → platform / reef admission.
  4. Otherwise it is the package's.

Applied to a first-party app

Asset Home
Domain code, tests, migrations, Containerfile, OpenAPI app repo (user-engine)
Helm chart, values, NetworkPolicy, Ingress, ServiceAccount rapp
declarations/rapp.yaml, railiance/app.toml rapp
Deploy / dry-run / status / verify / rollback Makefile rapp
Workload-specific smoke outcomes rapp
Image build and digest publication app repo + forge
Image pin in the release rapp (digest, never latest)
Postgres cluster, backup, credential broker rapp-postgres + railiance-platform
Consumer declaration (database-per-consumer) rapp-postgres/consumers/<app>.yaml requested by the rapp
Schema migrations app repo, invoked by the rapp at rollout
OIDC / flex-auth / tenant policy those ownership repos
Secret values OpenBao via the platform broker
Secret references rapp secret_references + railiance/app.toml
Public hostname grant rapp exposure.grant + reef public surface
DNS / TLS / Traefik reef / railiance-cluster
Shared S5 recipes (probe Host header, landing pages) copied into the rapp; source of the lesson stays in railiance-apps

The rapp repo must never become a shadow ownership repo. If a paragraph in INTENT.md starts explaining why the product exists, it belongs in the app repo.


6. Required repo baseline

Every rapp-* starts with the first-wave bootstrap contract:

rapp-<app>/
  .repo-classification.yaml
  README.md
  INTENT.md
  SCOPE.md
  AGENTS.md
  CLAUDE.md
  LICENSE
  Makefile
  declarations/rapp.yaml          # family contract (normative schema)
  workplans/
  workplans/archived/

Then add the package surface. For a rail-kubernetes first-party app the proven layout is the union of rapp-policy-nexus (Helm + digest pins + make render|deploy|verify-live|rollback) and the rail-kubernetes overlay contract:

  railiance/app.toml              # staged promotion (rail contract)
  helm/<app>/                     # chart owned by the rapp
    Chart.yaml
    values.yaml
    templates/
      deployment.yaml
      service.yaml
      serviceaccount.yaml
      networkpolicy.yaml
      ingress.yaml                # only if exposure will become public/operator
  consumers/                      # only if this rapp provisions units
  docs/
    runtime-contract.md
    retained-handoff.md           # if extracting from railiance-apps / platform
  tests/                          # render / contract tests, no cluster required
  tools/                          # verify_live.sh, smoke helpers
  bindings/                       # production binding records when needed

Implementation files come after the baseline. An empty rapp-* git directory is not a wrapper; railiance-master already treats rapp-user-engine as undeclared.

Classification

Copy the shape used by live rapps. Domain follows the workload, not the wrapper:

repo_classification:
  standard: Repo Classification Standard
  version: "1.0"
  classified_at: "YYYY-MM-DD"
  classified_by: agent
  category: project
  domain: <workload domain>
  secondary_domains:
    - infotech
  capability_tags:
    - platform
    - operations
    - configuration
  business_stake:
    - technology
    - operations
  business_mechanics:
    - coordination
    - operation

7. declarations/rapp.yaml

Normative shape: railiance-master/schemas/rapp.schema.json. Validate with:

# from railiance-master, with sibling family repos checked out
tools/validate-family-declarations.py --root ..

Required meaning, not a second schema

Field Practice
kind always managed-workload-package
repo_family always rapp
rapp_id / repo rapp-<app> for a single-repo rapp
ownership_repo the accountable app or layer repo, never the rapp itself. Application: user-engine. Platform service: railiance-platform.
contract_version start at 1.0.0
readiness_state draft while scaffolding → declared once valid → deployed / verified only with evidence. There is no production-approved on a rapp.
workload_identity.name the workload (user-engine), not rapp-user-engine. The schema forbids the rapp- prefix.
workload_identity.package_type one of helm-managed-platform-service, manifest-managed-platform-service, knative-managed-service, grouped-composition
data_classification highest class the workload handles
criticality low | medium | high | critical
primary_rail must also appear in supported_rails
bound_reefs [] until placed; then [reef-railiance]
runtime_dependencies capability names, not the resource that satisfies them
composition.purpose one sentence; if it needs "and also", split the rapp
composition.member_repos one member for a single-repo rapp; each lists deployables
composition.upstream_components pinned third-party charts/images. Exact version or digest. Never latest.
rollout_contract.default_mode helm-upgrade-install or kubectl-server-side-apply (or revision-canary on Knative)
smoke_contract.required outcomes that must be true, not a command diary
rollback_contract.order preferred strategies first
source_documents repo + path of the docs this declaration was derived from

Optional, use when true:

  • secret_references — OpenBao paths, never values
  • consumer_contract / consumers — only if this rapp provisions isolated units to others (rapp-postgres is the model)
  • exposure — omit for private. public requires a grant, binding_admission: production-approved, and a reef that has granted a public surface.

Package-type choice

The schema has no helm-managed-application enum. Until that is added in railiance-master, first-party Helm apps use helm-managed-platform-service (as rapp-policy-nexus does). Do not invent a local type. Record the naming mismatch in source_documents / workplan notes if you want the schema extended later.

Capability names in use today

There is not yet a controlled vocabulary shared with rail.required_substrate_capabilities. Prefer names already live in declarations rather than minting near-duplicates:

  • kubernetes-api
  • helm-apply-path / helm-repo-access
  • traefik-ingress
  • cert-manager
  • forgejo-oci-registry
  • pvc-storage
  • operator-kubeconfig-access
  • knative-serving
  • keycape-oidc / key-cape-oidc (spellings have already drifted — pick one and stay consistent inside a rapp)
  • flex-auth
  • openbao
  • openbao-database-secrets-engine
  • s3-backup-target

Who provisions the capability is outside the rapp: resource-control procures, the reef publishes non-secret attributes, railiance-platform vends the credential.


8. Service wrapper patterns

Three live patterns. Copy the one that matches the rail and package type.

8.1 Helm platform / app service (rail-kubernetes)

References: rapp-openbao, rapp-policy-nexus, railiance-apps/docs/s5-app-onboarding-checklist.md.

Operator surface (minimum Makefile targets):

Target Meaning
make check / make test / make lint contract and helm lint with dummy digests
make render write rendered manifests locally
make server-dry-run kubectl apply --dry-run=server against a representative API
make deploy helm upgrade --install --atomic --wait
make status helm/kubectl status
make verify-live named smoke outcomes from smoke_contract.required
make rollback REVISION=… previous Helm revision

Pins:

  • image by digest, not tag (rapp-policy-nexus is the model)
  • chart version and app version in workload_identity
  • forge-owned registry path (forgejo.coulomb.social/...)

Runtime hardening that should be the default in new charts:

  • non-root numeric user
  • read-only root filesystem where the app allows it
  • dropped capabilities
  • NetworkPolicy default-deny plus explicit ingress/egress
  • unauthenticated /healthz and /readyz
  • /metrics cluster-internal and authorized
  • probe Host header set when the app validates Host (Django lesson)

8.2 Manifest-managed platform service

Reference: rapp-postgres (kubectl-server-side-apply of CNPG Cluster and NetworkPolicies; Helm only for the operator, which is cluster-owned).

Use this when the workload is a set of reviewed manifests rather than a chart you upgrade as a release.

8.3 Knative managed service

Reference: rapp-qonto (runtime/knative/, revision-canary, previous-revision rollback). Only when rail-knative is the primary rail.

8.4 Overlay compatibility path

rail-kubernetes/docs/overlay-repo-pattern.md still describes <app>-railiance-overlay/ with railiance/app.toml. The existing scaffold is:

# from rail-kubernetes; cluster/bootstrap only keep shims
bin/railiance create-overlay --app-id <id> --upstream-url <url> …
bin/railiance run <overlay-dir>                 # Stage 1
bin/railiance deploy --stage 2 <overlay-dir>    # Stage 2 canary
bin/railiance promote <overlay-dir>             # Stage 3
bin/railiance rollback <overlay-dir>

create-overlay writes local files only. It does not create declarations/rapp.yaml, a Forgejo remote, or a family classification. Repo Manager's later rmgr rapp init replaces this for first-party apps.

New work should not create overlay repos. If you find one, graduate it to rapp-<app> and keep railiance/app.toml inside the rapp.


9. Platform handoffs every app wrapper needs

9.1 Database

Do not run a sidecar Postgres. The default for a new app is a consumer against rapp-postgres:

  • boundary unit: database-per-consumer
  • credential lane: railiance-platform-credential-broker
  • roles split: owner / migration / runtime
  • tenant-keying required even for a single current tenant

The consumer file lives in rapp-postgres/consumers/<app>.yaml. The rapp records the capability (openbao-database-secrets-engine or equivalent) and the OpenBao path in secret_references. The app repo keeps migrations and applies them with the migration role.

If the app consumes a single DATABASE_URL, URL-encode the password. Prefer discrete POSTGRES_* env vars when the framework allows it (railiance-apps/docs/django-on-railiance.md).

A dedicated CNPG Cluster is allowed only when placement policy already says so (live user-engine-pg is this case). Then the Cluster manifests belong in the rapp, not in net-kingdom or the app repo. Do not be both a rapp-postgres consumer and a dedicated Cluster.

9.2 Secrets

The rapp never stores values. Standing secrets go through a CCR and secret_references. Short-lived leases go through the grant catalog and rapp_id. See railiance-platform/docs/rapp-credential-lane-binding.md.

In railiance/app.toml, declare secrets.references with route ids (openbao-api-key, key-cape-oidc-login, …) and Kubernetes Secret / ExternalSecret names.

9.3 Identity and authorization

Apps that speak OIDC / flex-auth declare those as runtime capabilities and receive issuer, audience, client id, and token-file paths through the broker. The rapp does not mint tenants or tokens. workload_identity.tenant is an opaque NetKingdom id when one is needed; the rapp never parses or authorizes on it.

9.4 Images

Build stays in the app repo. The rapp consumes a forge-published digest. Verify the cluster can pull the image before the first non-smoke deploy (railiance-apps/tools/check-oci-image.sh is the existing helper).

9.5 Ingress, TLS, landing pages

Private by default. When a public hostname is later granted:

  • name host, namespace, release, ingress, TLS secret in the runbook
  • cert-manager owns certificates
  • every public / has an intentional browser response (S5 landing-page rule)
  • do not break /healthz, /readyz, API prefixes, or OIDC callbacks

9.6 Exposure (ADR-0008)

Posture Who reaches the listener
omitted / private in-cluster DNS, same-reef services
operator same, plus a named ops-bridge / SSH tunnel
public internet, only with grant + production-approved binding + reef public surface

data_classification: public is not exposure. Port 6443 is never a grantable public surface.


10. Worked example: user-enginerapp-user-engine

10.1 Current facts

user-engine is a headless user-domain / identity-domain service with a production WSGI portal. The app repo already has:

  • Containerfile — Python 3.12 slim, non-root uid 10001, port 8080
  • python -m user_engine.runtime
  • /healthz, /readyz, /metrics (metrics require the trusted proxy marker)
  • Postgres store + in-tree migrations
  • fail-closed production adapters: OIDC, flex-auth, outbox, provisioning, optional tenant-management and registration verification
  • explicit non-ownership of IdP, authorization PDP, secret custody, and deployment (docs/release.md)

rapp-user-engine is an empty Forgejo clone (forgejo-remote:coulomb/rapp-user-engine.git). No README, no declaration. ADR-0007 already lists it as undeclared.

Live packaging is not in the rapp. It lives in net-kingdom/sso-mfa/k8s/user-engine/:

File Role
runtime.yaml Namespace, dedicated CNPG user-engine-pg, Deployment, Service, SA, NetworkPolicies
ingress.yaml Traefik Ingress users.92-205-62-239.nip.io (README still says no public Ingress until KeyCape PKCE is configured — treat exposure as unresolved)
openbao-runtime.yaml ExternalSecrets from platform/workloads/user-engine/runtime
backup.yaml, restore-drill.yaml Logical dump / restore
verify-operability.sh Live checks

Image already published: forgejo.coulomb.social/coulomb/user-engine@sha256:e3b5f65bafc1c0260dfdf2567a52766e67506ceb878a51759a2e9a307c4b5eb8. flex-auth for this consumer is pinned inside flex-auth (values/user-engine.yaml, examples/user-engine/), not in a rapp. Closest sibling analogue: tenant-engine/deploy/ (digest-pinned manifests, but that app is a rapp-postgres consumer).

The wrapper job is therefore absorb and declare, not invent a second production target. Same rule as qonto-assistant/docs/knative-runtime-and-rapp-handoff.md: once the rapp exists, net-kingdom keeps IAM/provisioner contracts and stops being the apply home.

Sister stubs rapp-secrets-engine and rapp-tenant-engine follow after this path validates. rapp-secrets-engine is likely a grouping mistake against rapp-openbao / S3, not a fourth identity member.

Decision Value Why
Create a rapp? yes stable identity, clear app/wrapper split, own rollout fate
ownership_repo user-engine application accountability, same pattern as policy-nexus / qonto-assistant
workload_identity.name user-engine schema rule
package_type manifest-managed-platform-service live assets are raw YAML/CNPG, not a Helm chart. Do not invent Helm just to match policy-nexus.
primary_rail rail-kubernetes long-running portal + durable store; no Knative need
bound_reefs [] at declared; [reef-railiance] when placed empty is legal and means "not yet placed"
data_classification confidential identity and account data
criticality high shared identity-domain service; not the secrets/postgres critical path
exposure omit (private) until the KeyCape PKCE edge is decided an Ingress object already exists; that is not a grant (ADR-0008). Decide hostname + residual-risk owner before public.
Granularity single-repo rapp for the pilot one deployable; do not group with tenant-engine or secrets-engine unless they share rollback fate. rapp-first-wave-candidates.md still flags a future grouped rapp-netkingdom-identity (user-engine + tenant-engine + key-cape + flex-auth). That grouping is only legal if those members cannot roll back independently — they almost certainly can. Treat the grouped name as a later composition question, not as a reason to leave rapp-user-engine undeclared.

10.3 Draft declaration (index, not authority)

The live file will be rapp-user-engine/declarations/rapp.yaml. This is the intended fill of the schema, to be validated before it is treated as declared.

kind: managed-workload-package
repo_family: rapp
rapp_id: rapp-user-engine
repo: rapp-user-engine
ownership_repo: user-engine
contract_version: 1.0.0
readiness_state: draft
workload_identity:
  name: user-engine
  package_type: manifest-managed-platform-service
  app_version: 0.1.0
data_classification: confidential
criticality: high
primary_rail: rail-kubernetes
supported_rails:
  - rail-kubernetes
bound_reefs: []
runtime_dependencies:
  - kubernetes-api
  - helm-apply-path
  - forgejo-oci-registry
  - pvc-storage
  - keycape-oidc
  - flex-auth
  - openbao
  - audit-core
  - tenant-engine
secret_references:
  - platform/workloads/user-engine/runtime
composition:
  purpose: Package and operate the user-engine identity-domain service on Railiance.
  member_repos:
    - repo: rapp-user-engine
      role: Kubernetes manifests, smoke, and rollback for the user-engine runtime
      deployables:
        - user-engine
  upstream_components:
    - name: user-engine
      source: forgejo.coulomb.social/coulomb/user-engine
      version: sha256:e3b5f65bafc1c0260dfdf2567a52766e67506ceb878a51759a2e9a307c4b5eb8
rollout_contract:
  default_mode: kubectl-server-side-apply
  commands:
    - make server-dry-run
    - make deploy
  production_gate: reef-production-approved
smoke_contract:
  required:
    - healthz-ok
    - readyz-ok-when-store-ready
    - metrics-requires-proxy-marker
    - local-issuer-rejected-in-production
    - live-image-digest-match
  commands:
    - make verify-live
rollback_contract:
  order:
    - previous-immutable-image-digest
    - apply-reviewed-git-revision
  commands:
    - make rollback DIGEST=<previous-image-digest>
source_documents:
  - repo: user-engine
    path: INTENT.md
  - repo: user-engine
    path: docs/configuration.md
  - repo: user-engine
    path: docs/operability.md
  - repo: net-kingdom
    path: sso-mfa/k8s/user-engine/README.md
  - repo: railiance-master
    path: schemas/rapp.schema.json
  - repo: repo-manager
    path: docs/RailianceAppDeploymentGuide.md

Cardinality note: the live deployable name is user-engine (the Deployment). Migrations run in-process before serve, so do not invent a second user-engine-migrate deployable unless a separate Job is added. user-engine-pg is a Cluster owned by this package only if the dedicated-cluster decision stands; if it is inventoried as its own deployable it still belongs to this rapp.

ownership_repo: user-engine already names the source repo. Do not also claim the Deployment from net-kingdom. A single-member composition matches every live rapp; add user-engine as a second member only if it contributes a different deployable.

10.4 Runtime inputs the chart must accept

From user-engine runtime.py and docs/configuration.md. All values arrive as secret-backed env or files. The chart names the keys; OpenBao fills them.

Required for production assembly:

  • USER_ENGINE_DATABASE_URL (or discrete Postgres vars + a rendered DSN)
  • USER_ENGINE_OIDC_ISSUER, USER_ENGINE_OIDC_AUDIENCE, USER_ENGINE_OIDC_CLIENT_ID, USER_ENGINE_OIDC_REDIRECT_URI
  • USER_ENGINE_FLEX_AUTH_URL, USER_ENGINE_FLEX_AUTH_TOKEN_FILE
  • USER_ENGINE_PROXY_SECRET
  • USER_ENGINE_LOGIN_URL
  • USER_ENGINE_EVENT_URL, USER_ENGINE_EVENT_TOKEN
  • USER_ENGINE_PROVISIONING_URL, USER_ENGINE_PROVISIONING_TOKEN

Optional / fail-closed when absent:

  • USER_ENGINE_TENANT_MANAGEMENT_URL + USER_ENGINE_TENANT_MANAGEMENT_TOKEN
  • USER_ENGINE_MAIL_URL + USER_ENGINE_MAIL_TOKEN
  • public-registration adapter settings

Port: USER_ENGINE_HTTP_PORT (default 8080). Do not use USER_ENGINE_PORT — Kubernetes service-link injection occupies that name.

Logical secret names the app already documents: database.url, event.signing_key, webhook.shared_secret. Map those to broker routes in railiance/app.toml; do not invent a second naming scheme in the chart.

10.5 Database decision (do not do both)

Live user-engine uses a dedicated CNPG Cluster user-engine-pg (postgresql 17.5, db user_engine), not a rapp-postgres consumer. tenant-engine and audit-core are platform consumers.

Two legal next states:

  1. Keep dedicated. Move user-engine-pg manifests into the rapp. Declare pvc-storage (and later s3-backup-target if backup is attached). Do not add a postgres consumer.
  2. Become a consumer. Add rapp-postgres/consumers/user-engine.yaml (same shape as audit-core.yaml), delete the dedicated Cluster after a reviewed cutover. Then drop pvc-storage from this rapp.

Pick one in T04. The current live state is (1).

10.6 Absorb the NetKingdom apply home

T03 should copy net-kingdom/sso-mfa/k8s/user-engine/ into rapp-user-engine/manifests/ (or helm/ only if a real chart is justified), add Makefile server-dry-run / deploy / verify-live / rollback DIGEST=…, and leave a pointer README in NetKingdom so there is one apply path. Do not Helm-rewrite the live YAML in the same change.

10.6 What stays in user-engine

  • domain model, adapters, tests, conformance harness
  • Containerfile and image publish instructions
  • SQL migrations
  • portal/OIDC behavior
  • docs/operability.md diagnostics

The app repo should grow a short "consumed by rapp-user-engine" note pointing here. It should not grow Helm charts.

10.7 First-wave railiance/app.toml for this rapp

Minimum: schema_version = "railiance.app.v1", app.id = "user-engine", source digest policy required, postgres + oidc + flex-auth + openbao dependencies, secret references by route, /healthz and /readyz checks, Stage 1 (make test in the app repo + helm template), Stage 2 canary requiring approval, Stage 3 promote with a recorded rollback target. Copy structure from rail-kubernetes/examples/railiance/app.toml.


11. Operator checklist (new rapp)

Use this when filling a stub or extracting a wrapper from railiance-apps.

Shape

  • Confirm the workload passes §4.
  • Create or fill rapp-<app> with the §6 baseline.
  • Write INTENT.md / SCOPE.md as a package boundary, not a product manifesto.
  • Author declarations/rapp.yaml to the schema. Start readiness_state: draft.
  • Add railiance/app.toml if the primary rail is rail-kubernetes.
  • Add the Helm chart or manifest set. Pin images by digest.
  • Add Makefile targets from §8.1.
  • Name smoke outcomes and rollback order.
  • Leave exposure unset.

Handoffs

  • Request a rapp-postgres consumer when a durable store is needed.
  • Request credential lanes / CCRs in railiance-platform. Commit paths only.
  • Confirm forge image publish path in the app repo.
  • Confirm cluster pull before the first deploy.
  • Record source_documents.

Evidence before declareddeployed

  • tools/validate-family-declarations.py --root .. passes.
  • make render and make server-dry-run pass.
  • make deploy on reef-railiance only after the reef binding is intended.
  • make verify-live proves every smoke_contract.required item.
  • Rollback is rehearsed at least once on a non-destructive revision.
  • Deployable names match the live inventory (railiance-platform/docs/evidence/reef-railiance-deployables.json) when --inventory is run.

Do not

  • Commit credentials, kubeconfigs, or tokenized registry URLs.
  • Add the workload to reef.yaml bound_rapps by hand.
  • Put Helm charts in the app repo "just for now".
  • Create a new rail.
  • Create a reef because a hostname exists.
  • Group unrelated engines into one rapp.
  • Set exposure.posture: public to make a smoke test easier.

12. What Repo Manager should automate

This is the work structure for RMGR-WP-0006. Each step is a later command or command flag on the existing governed-mutation path (RMGR-WP-0004 scaffolding, specialized for the rapp family).

Phase Repo Manager does Still human
P0 — this guide Record the shape. Accept or amend the shape.
P1 — bootstrap stub Create or complete rapp-<app> with §6 baseline files, classification, and a draft rapp.yaml from a questionnaire (ownership repo, rail, classification, criticality, purpose). Confirm ownership split and purpose sentence.
P2 — package skeleton Generate Helm chart + Makefile targets from app facts: port, health paths, Containerfile user, image repository. Generate railiance/app.toml skeleton. Review NetworkPolicy and resource requests.
P3 — platform bindings Draft rapp-postgres consumer and secret_references from the app's documented logical secret names. Approve CCR / consumer apply in the owning packages.
P4 — validate Run family-declaration validation and helm lint / render. Refuse readiness_state promotions that skip evidence. Promote draftdeclared.
P5 — place Set bound_reefs: [reef-railiance] only on an explicit place command. Production gate and any exposure grant.

Command sketch (not implemented):

rmgr rapp init --app user-engine --ownership-repo user-engine \
  --rail rail-kubernetes --classification confidential --criticality high

rmgr rapp skeleton --path ../rapp-user-engine --from-app ../user-engine

rmgr rapp validate --path ../rapp-user-engine --family-root ..

Constraints on the scaffolder:

  • Files stay authoritative. The hub is not written to except via fix-consistency after the files exist.
  • No secret values, ever.
  • Do not invent package types, rails, or reefs.
  • Idempotent: re-running init on an already-declared rapp must refuse or update in a reviewed diff, not overwrite a live contract.
  • The three engine stubs (user-engine, secrets-engine, tenant-engine) are the first cohort; user-engine is the pilot.

13. Open questions and known drift

Recorded so the scaffolder does not silently pick a side.

  1. ownership_repo tightness. Schema currently allows a layer repo (railiance-platform) or an application repo. For application rapps, prefer the application repo. (railiance-master/schemas/README.md)
  2. No application package_type. First-party Helm apps reuse helm-managed-platform-service. A future railiance-master schema change may add helm-managed-application.
  3. rapp-qonto is not a copy target. It is verified in production but does not yet conform to ADR-0007 (composition, bound_reefs, package_type, workload name). Copy rapp-policy-nexus + rapp-openbao instead.
  4. Capability vocabulary is free-form. Spellings already drift (keycape-oidc vs key-cape-oidc). The scaffolder should emit a small allow-list and refuse new names without a note.
  5. rapp-vergabe-teilnahme was the planned user-facing proof and was never built. Extracting it remains a railiance-apps residual. Do not pretend rapp-user-engine closes that ADR-0003 item. Other planned-but-unbuilt names (rapp-forgejo, rapp-coulomb-social, rapp-netkingdom-identity) are not this workplan.
  6. Reef bound_rapps on reef-railiance has been stale. Never teach the scaffolder to edit it.
  7. Metrics and Host-validated apps need extra chart care. The scaffolder should read probe paths from the app if present (/healthz, /readyz for user-engine) and not assume /health.
  8. Grouped identity vs single-engine stubs. Architecture still mentions folding rapp-user-engine into rapp-netkingdom-identity. ADR-0007 only allows that if members share rollout and rollback fate. rapp-secrets-engine is also a suspect duplicate of rapp-openbao / S3 custody, not a fourth identity member. The pilot declares rapp-user-engine alone; a later composition change is a new declaration, not a rename of the stub.

14. References

Architecture and contracts:

  • railiance-master/docs/repository-axes.md
  • railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md
  • railiance-master/docs/adr/ADR-0003-rapp-first-wave-selection.md
  • railiance-master/docs/adr/ADR-0005-derived-rail-composition.md
  • railiance-master/docs/adr/ADR-0006-reef-production-admission.md
  • railiance-master/docs/adr/ADR-0007-rapp-declaration-contract.md
  • railiance-master/docs/adr/ADR-0008-private-by-default-exposure.md
  • railiance-master/docs/repo-family-bootstrap-contract.md
  • railiance-master/docs/rapp-first-wave-candidates.md
  • railiance-master/schemas/README.md

Rails and reefs:

  • rail-kubernetes/declarations/rail.yaml
  • rail-kubernetes/docs/deployment-lifecycle.md
  • rail-kubernetes/docs/app-toml-contract.md
  • rail-kubernetes/docs/overlay-repo-pattern.md
  • rail-knative/declarations/rail.yaml
  • reef-railiance/declarations/reef.yaml
  • reef-storage/declarations/reef.yaml

Proven wrappers and operator recipes:

  • railiance-platform/docs/rapp-platform-service-pattern.md
  • rapp-openbao/declarations/rapp.yaml
  • rapp-postgres/declarations/rapp.yaml
  • rapp-policy-nexus/declarations/rapp.yaml
  • rapp-policy-nexus/docs/runtime-contract.md
  • railiance-apps/docs/s5-app-onboarding-checklist.md
  • railiance-apps/docs/django-on-railiance.md
  • railiance-apps/docs/operator-recipes.md
  • railiance-apps/Makefile (policy-nexus-deploy delegate)
  • rail-kubernetes/docs/create-overlay-command.md
  • railiance-fabric/docs/rail-rapp-reef-adaptation.md (discovery only)

Worked example:

  • user-engine/INTENT.md
  • user-engine/docs/configuration.md
  • user-engine/docs/operability.md
  • user-engine/docs/postgres-durable-store-consumer-requirements.md
  • user-engine/Containerfile
  • user-engine/src/user_engine/runtime.py
  • net-kingdom/sso-mfa/k8s/user-engine/ (current live apply home)
  • tenant-engine/deploy/ (closest sibling manifest package)
  • qonto-assistant/docs/knative-runtime-and-rapp-handoff.md (one apply home)