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.
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:
railiance-master/schemas/rapp.schema.jsonrailiance-master/docs/adr/ADR-0007-rapp-declaration-contract.mdrailiance-master/docs/repo-family-bootstrap-contract.md
Worked example (absorb path): user-engine → rapp-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:
- How to wrap a new app from scratch (§0).
- Which rail to start on, and when Knative is justified (§3.1).
- What a managed Railiance application is.
- Which files, declarations, and operator surfaces a wrapper needs.
- How
user-enginewas absorbed asrapp-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 |
|---|---|
draft → declared → deployed → verified |
package readiness |
omitted / private → operator → public |
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:
- The semantics are intrinsic: request activation, scale-to-zero, revision traffic splits, or previous-revision rollback — not just fewer replicas.
- A Kubernetes profile cannot say it safely. If a Deployment + Service
- HPA is enough, it is not a new rail.
- 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.
- Callers tolerate cold-start. Measure activator/buffering timeouts,
concurrency, and retry ownership before those numbers are
load-bearing (
docs/qonto-knative-runtime-contract.md). - Egress is restricted. Unrestricted HTTPS egress is not production-approved for critical Knative workloads.
- 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-qontouses 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".
- 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.tomlonly 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/skeletonover hand-copying. - 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 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 draft → declared. |
| 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/wraprefuse to overwrite a live declaration.tenant-engineis theRMGR-WP-0007pilot.rapp-secrets-engineis not a wrap target.
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)