repo-manager/docs/RailianceAppDeploymentGuide.md
tegwick f953b1ebf5 feat: greenfield rapp wrap path and rail-sequence rules
Add the §0 playbook and kubernetes-then-knative gate to the guide.
Implement rmgr rapp skeleton/wrap/place, draft postgres consumers,
and copy the fleet image workflow when missing.
2026-08-18 13:03:16 +02:00

44 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 (absorb path): user-enginerapp-user-engine (§10). Greenfield default: rmgr rapp wrap (§0). Rail choice: §3.1.

RMGR-WP-0006 proved the contract. RMGR-WP-0007 is the wrap path.


0. Greenfield playbook

Use this section to wrap an app that has no rapp yet. Do not start in §10 unless you are absorbing an existing apply home.

decide (§4, §3.1) → rmgr rapp wrap → human reviews drafts
                 → CI publishes a digest → rmgr rapp pin-image
                 → rmgr rapp place → operator deploy
rmgr rapp wrap \
  --path ../rapp-<app> \
  --app <app> \
  --ownership-repo <app> \
  --from-app ../<app>

rmgr rapp validate --path ../rapp-<app> --family-root ..
# after CI prints a digest:
rmgr rapp pin-image --path ../rapp-<app> --digest sha256:<64 hex>
# only when you intend to bind compute:
rmgr rapp place --path ../rapp-<app> --reef reef-railiance

wrap runs init → skeleton (or absorb deploy/) → app image.yaml if missing → postgres consumer draft → validate. It stops. It does not set bound_reefs, apply to a cluster, or grant public exposure.

Defaults:

Decision Default
Rail rail-kubernetes (§3.1). Not Knative.
Package type manifest-managed-platform-service. Helm only if a chart already exists.
Database rapp-postgres consumer draft. No dedicated Cluster.
Image fleet .forgejo/workflows/image.yaml in the app repo. Pin by digest in the rapp.
railiance/app.toml optional; not part of the first wrap
Exposure omitted (private)

Still human: the purpose sentence, classification/criticality, NetworkPolicy review, CCR / consumer apply, production gate, any public grant.


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. How to wrap a new app from scratch (§0).
  2. Which rail to start on, and when Knative is justified (§3.1).
  3. What a managed Railiance application is.
  4. Which files, declarations, and operator surfaces a wrapper needs.
  5. How user-engine was absorbed as rapp-user-engine (§10).

RMGR-WP-0006 recorded the contract and the first wrap. RMGR-WP-0007 is the greenfield command path.


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.

This is a default plus a specialization gate, not a maturity ladder. An app does not graduate from Kubernetes to Knative.

rail-kubernetes          ← start here, stay here unless proven otherwise
        │
        └── rail-knative ← only if the execution model itself must change

rail-kubernetes is the base rail. rail-knative is derived from it: same identity, smoke, promotion vocabulary, and recovery; it only overrides activation, scale-to-zero, concurrency, revision traffic, cold-start, and revision rollback (railiance-master/docs/rail-composition-contract.md).

There is no next rail after Knative. Names such as rail-keda, rail-fission, and rail-nuclio are examples of when a new rail would be justified, not a planned path. Do not invent a rail for an adapter or a values profile.

Do not mix rail choice with the other progressions:

Sequence What it is
draftdeclareddeployedverified package readiness
omitted / privateoperatorpublic who may reach the listener
kubernetes → knative how the process is executed

Stay on Kubernetes when the app is a long-running portal or API; it has a durable store, in-process migrations, or startup that cannot die between requests; ordinary Deployment rolling updates and digest rollback are enough; “scale down at night” can be an HPA / replica count; or no cold-start SLO has been measured.

Move to Knative only if all of these hold:

  1. The semantics are intrinsic: request activation, scale-to-zero, revision traffic splits, or previous-revision rollback — not just fewer replicas.
  2. A Kubernetes profile cannot say it safely. If a Deployment + Service
    • HPA is enough, it is not a new rail.
  3. The workload can die between requests. No migration-on-boot that must finish before the first probe; no sticky in-memory session that cannot cold-start.
  4. Callers tolerate cold-start. Measure activator/buffering timeouts, concurrency, and retry ownership before those numbers are load-bearing (docs/qonto-knative-runtime-contract.md).
  5. Egress is restricted. Unrestricted HTTPS egress is not production-approved for critical Knative workloads.
  6. The reef will admit it. Knative Serving is installed and verified on Railiance01; production approval is still blocked (single-node control plane). Today only rapp-qonto uses this rail.

Qonto is the worked example: internet-reachable, scales to zero, holds a bank credential, needs revision canaries. user-engine stays on Kubernetes — long-running portal plus Postgres.

New wraps set primary_rail: rail-kubernetes. Change it only when the app owner can write: this process must not exist until a request arrives, and rollback is a previous Knative revision, not a previous image digest. If that sentence needs “and also it has a database that migrates on start,” keep Kubernetes.

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. app.toml is not required on the first wrap.

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 yes manifest-managed-platform-service kubernetes user-engine verified First-party absorb wrap. Apply home for the portal.
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 proven once as an absorb (rapp-user-engine) and once as a static Helm site (rapp-policy-nexus). rapp-qonto is drifted. rapp-vergabe-teilnahme was never extracted. rapp-tenant-engine is the greenfield/absorb pilot for RMGR-WP-0007.

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 only if you need Stage 1/2/3 rail promotion. It is not part of the first wrap.
  • Add the Helm chart or manifest set. Pin images by digest. Prefer rmgr rapp wrap / skeleton over hand-copying.
  • 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 automates

Implemented under RMGR-WP-0006 (P1, P4) and RMGR-WP-0007 (P2, P3, P5, compose).

Phase Command Still human
P0 — this guide Accept or amend the shape.
P1 — bootstrap rmgr rapp init Purpose sentence, classification.
P2 — package skeleton rmgr rapp skeleton --from-app NetworkPolicy and resource review.
P3 — platform drafts consumer draft + app image.yaml CCR / consumer apply.
P4 — validate rmgr rapp validate Promote draftdeclared.
P5 — place rmgr rapp place --reef reef-railiance Production gate, exposure grant.
rmgr rapp wrap --path ../rapp-<app> --app <app> \
  --ownership-repo <app> --from-app ../<app>
rmgr rapp validate --path ../rapp-<app> --family-root ..
rmgr rapp pin-image --path ../rapp-<app> --digest sha256:<64 hex>
rmgr rapp place --path ../rapp-<app> --reef reef-railiance

Constraints:

  • Files stay authoritative. The hub is only updated via fix-consistency.
  • No secret values, ever.
  • Do not invent package types, rails, or reefs.
  • init / wrap refuse to overwrite a live declaration.
  • tenant-engine is the RMGR-WP-0007 pilot. rapp-secrets-engine is not a wrap target.

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)