# 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.json`](../../railiance-master/schemas/rapp.schema.json) - [`railiance-master/docs/adr/ADR-0007-rapp-declaration-contract.md`](../../railiance-master/docs/adr/ADR-0007-rapp-declaration-contract.md) - [`railiance-master/docs/repo-family-bootstrap-contract.md`](../../railiance-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. ```text decide (§4, §3.1) → rmgr rapp wrap → human reviews drafts → CI publishes a digest → rmgr rapp pin-image → rmgr rapp place → operator deploy ``` ```bash rmgr rapp wrap \ --path ../rapp- \ --app \ --ownership-repo \ --from-app ../ rmgr rapp validate --path ../rapp- --family-root .. # after CI prints a digest: rmgr rapp pin-image --path ../rapp- --digest sha256:<64 hex> # only when you intend to bind compute: rmgr rapp place --path ../rapp- --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-` 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. ```text 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:** 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 `-railiance-overlay/`. Graduate it to Path A; do not start new overlays. | | **C. S5 chart in `railiance-apps`** | `charts//` + `helm/-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/.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: ```text rapp-/ .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: ```text railiance/app.toml # staged promotion (rail contract) helm// # 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: ```yaml repo_classification: standard: Repo Classification Standard version: "1.0" classified_at: "YYYY-MM-DD" classified_by: agent category: project 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: ```bash # 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-` 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 `-railiance-overlay/` with `railiance/app.toml`. The existing scaffold is: ```bash # from rail-kubernetes; cluster/bootstrap only keep shims bin/railiance create-overlay --app-id --upstream-url … bin/railiance run # Stage 1 bin/railiance deploy --stage 2 # Stage 2 canary bin/railiance promote # Stage 3 bin/railiance rollback ``` `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-` 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/.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 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. ### 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. ```yaml 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= 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-` 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 `declared` → `deployed` - [ ] `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 `draft` → `declared`. | | **P5 — place** | `rmgr rapp place --reef reef-railiance` | Production gate, exposure grant. | ```text rmgr rapp wrap --path ../rapp- --app \ --ownership-repo --from-app ../ rmgr rapp validate --path ../rapp- --family-root .. rmgr rapp pin-image --path ../rapp- --digest sha256:<64 hex> rmgr rapp place --path ../rapp- --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)