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.
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:
railiance-master/schemas/rapp.schema.jsonrailiance-master/docs/adr/ADR-0007-rapp-declaration-contract.mdrailiance-master/docs/repo-family-bootstrap-contract.md
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:
- What a managed Railiance application is.
- How rails, reefs, and existing rapps already do it.
- Which files, declarations, and operator surfaces a new wrapper needs.
- How
user-engineshould be wrapped asrapp-user-engine. - 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".
- 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.
- Is wrapping distinct from ownership? If the only thing you would
put in the rapp already belongs in
railiance-platformorrailiance-appsas shared policy, do not create a rapp. - Do members share fate? One rapp per bounded context that deploys and rolls back together. Independent rollback → separate rapp.
- Which rail? Default
rail-kubernetes. Chooserail-knativeonly for request-activation / scale-to-zero / revision-traffic semantics. - Which reef? New compute workloads bind
reef-railiance. Object storage isreef-storageand is consumed as a capability, not as a placement. - What package type? One of the schema enums (see §6).
- 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:
- Would this asset survive replacing the product? → ownership / platform repo.
- Does any other workload depend on it? → platform or rail.
- Does it encode who may approve, not how to apply? → platform / reef admission.
- 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 valuesconsumer_contract/consumers— only if this rapp provisions isolated units to others (rapp-postgresis the model)exposure— omit for private.publicrequires 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-apihelm-apply-path/helm-repo-accesstraefik-ingresscert-managerforgejo-oci-registrypvc-storageoperator-kubeconfig-accessknative-servingkeycape-oidc/key-cape-oidc(spellings have already drifted — pick one and stay consistent inside a rapp)flex-authopenbaoopenbao-database-secrets-engines3-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-nexusis 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
/healthzand/readyz /metricscluster-internal and authorized- probe
Hostheader 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-engine → rapp-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 uid10001, port8080python -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.
10.2 Recommended decisions
| 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_URIUSER_ENGINE_FLEX_AUTH_URL,USER_ENGINE_FLEX_AUTH_TOKEN_FILEUSER_ENGINE_PROXY_SECRETUSER_ENGINE_LOGIN_URLUSER_ENGINE_EVENT_URL,USER_ENGINE_EVENT_TOKENUSER_ENGINE_PROVISIONING_URL,USER_ENGINE_PROVISIONING_TOKEN
Optional / fail-closed when absent:
USER_ENGINE_TENANT_MANAGEMENT_URL+USER_ENGINE_TENANT_MANAGEMENT_TOKENUSER_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:
- Keep dedicated. Move
user-engine-pgmanifests into the rapp. Declarepvc-storage(and laters3-backup-targetif backup is attached). Do not add a postgres consumer. - Become a consumer. Add
rapp-postgres/consumers/user-engine.yaml(same shape asaudit-core.yaml), delete the dedicated Cluster after a reviewed cutover. Then droppvc-storagefrom 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
Containerfileand image publish instructions- SQL migrations
- portal/OIDC behavior
docs/operability.mddiagnostics
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.mdas a package boundary, not a product manifesto. - Author
declarations/rapp.yamlto the schema. Startreadiness_state: draft. - Add
railiance/app.tomlif the primary rail israil-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
exposureunset.
Handoffs
- Request a
rapp-postgresconsumer 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 declared → deployed
tools/validate-family-declarations.py --root ..passes.make renderandmake server-dry-runpass.make deployonreef-railianceonly after the reef binding is intended.make verify-liveproves everysmoke_contract.requireditem.- 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--inventoryis run.
Do not
- Commit credentials, kubeconfigs, or tokenized registry URLs.
- Add the workload to
reef.yamlbound_rappsby 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: publicto 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 draft → declared. |
| 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-consistencyafter the files exist. - No secret values, ever.
- Do not invent package types, rails, or reefs.
- Idempotent: re-running
initon 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-engineis the pilot.
13. Open questions and known drift
Recorded so the scaffolder does not silently pick a side.
ownership_repotightness. Schema currently allows a layer repo (railiance-platform) or an application repo. For application rapps, prefer the application repo. (railiance-master/schemas/README.md)- No application package_type. First-party Helm apps reuse
helm-managed-platform-service. A futurerailiance-masterschema change may addhelm-managed-application. rapp-qontois not a copy target. It is verified in production but does not yet conform to ADR-0007 (composition,bound_reefs,package_type, workload name). Copyrapp-policy-nexus+rapp-openbaoinstead.- Capability vocabulary is free-form. Spellings already drift
(
keycape-oidcvskey-cape-oidc). The scaffolder should emit a small allow-list and refuse new names without a note. rapp-vergabe-teilnahmewas the planned user-facing proof and was never built. Extracting it remains arailiance-appsresidual. Do not pretendrapp-user-enginecloses that ADR-0003 item. Other planned-but-unbuilt names (rapp-forgejo,rapp-coulomb-social,rapp-netkingdom-identity) are not this workplan.- Reef
bound_rappsonreef-railiancehas been stale. Never teach the scaffolder to edit it. - Metrics and Host-validated apps need extra chart care. The
scaffolder should read probe paths from the app if present
(
/healthz,/readyzfor user-engine) and not assume/health. - Grouped identity vs single-engine stubs. Architecture still
mentions folding
rapp-user-engineintorapp-netkingdom-identity. ADR-0007 only allows that if members share rollout and rollback fate.rapp-secrets-engineis also a suspect duplicate ofrapp-openbao/ S3 custody, not a fourth identity member. The pilot declaresrapp-user-enginealone; a later composition change is a new declaration, not a rename of the stub.
14. References
Architecture and contracts:
railiance-master/docs/repository-axes.mdrailiance-master/docs/adr/ADR-0001-repository-prefix-architecture.mdrailiance-master/docs/adr/ADR-0003-rapp-first-wave-selection.mdrailiance-master/docs/adr/ADR-0005-derived-rail-composition.mdrailiance-master/docs/adr/ADR-0006-reef-production-admission.mdrailiance-master/docs/adr/ADR-0007-rapp-declaration-contract.mdrailiance-master/docs/adr/ADR-0008-private-by-default-exposure.mdrailiance-master/docs/repo-family-bootstrap-contract.mdrailiance-master/docs/rapp-first-wave-candidates.mdrailiance-master/schemas/README.md
Rails and reefs:
rail-kubernetes/declarations/rail.yamlrail-kubernetes/docs/deployment-lifecycle.mdrail-kubernetes/docs/app-toml-contract.mdrail-kubernetes/docs/overlay-repo-pattern.mdrail-knative/declarations/rail.yamlreef-railiance/declarations/reef.yamlreef-storage/declarations/reef.yaml
Proven wrappers and operator recipes:
railiance-platform/docs/rapp-platform-service-pattern.mdrapp-openbao/declarations/rapp.yamlrapp-postgres/declarations/rapp.yamlrapp-policy-nexus/declarations/rapp.yamlrapp-policy-nexus/docs/runtime-contract.mdrailiance-apps/docs/s5-app-onboarding-checklist.mdrailiance-apps/docs/django-on-railiance.mdrailiance-apps/docs/operator-recipes.mdrailiance-apps/Makefile(policy-nexus-deploydelegate)rail-kubernetes/docs/create-overlay-command.mdrailiance-fabric/docs/rail-rapp-reef-adaptation.md(discovery only)
Worked example:
user-engine/INTENT.mduser-engine/docs/configuration.mduser-engine/docs/operability.mduser-engine/docs/postgres-durable-store-consumer-requirements.mduser-engine/Containerfileuser-engine/src/user_engine/runtime.pynet-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)