docs: add Railiance app wrapper best-practice guide

Record how rapp-* repos wrap first-party apps for Railiance, using
user-engine as the pilot. Opens RMGR-WP-0006 (T01 done) and includes
fix-consistency ID writeback for WP-0004/0005/0006.
This commit is contained in:
tegwick 2026-08-18 12:18:45 +02:00
parent c7fbd57800
commit 5197aa7425
6 changed files with 1002 additions and 0 deletions

View file

@ -0,0 +1,843 @@
# 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 throughout: `user-engine` + the empty stub `rapp-user-engine`.
---
## 1. Purpose
Repo Manager should make it easy to set up a managed Railiance application
wrapper. The product of that setup is a `rapp-<app>` repository: a
packaging and operations home that can deploy, verify, roll back, and bind
the app onto a rail and a reef.
This guide answers:
1. What a managed Railiance application **is**.
2. How rails, reefs, and existing rapps already do it.
3. Which files, declarations, and operator surfaces a new wrapper needs.
4. How `user-engine` should be wrapped as `rapp-user-engine`.
5. What Repo Manager must later automate, in order.
It is the first deliverable of `RMGR-WP-0006`. Implementation of the
scaffolder comes after this shape is accepted.
---
## 2. Vocabulary
Railiance classifies repositories on four complementary axes
(`railiance-master/docs/repository-axes.md`, ADR-0001):
| Prefix | Axis | Question it answers |
| --- | --- | --- |
| `railiance-*` | ownership | Who owns this capability or layer? |
| `rail-*` | execution contract | How does a workload run here? |
| `rapp-*` | managed workload package | What is packaged and operated? |
| `reef-*` | substrate | Where does it run, and what is bound there? |
A `rapp` is **managed packaging, not ownership**. The application repo
keeps domain logic, tests, image build, and schema migrations. The rapp
owns Helm/manifests, rail compatibility, smoke and rollback, secret
*references*, and reef binding.
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 | verified | Scale-to-zero, revision traffic, request activation (today: Qonto) |
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`.
`railiance-platform` retains cross-workload policy, credential custody,
lane approval, and the postgres credential broker.
---
## 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.
---
## 5. Ownership split
Use the S3 ownership test from
`railiance-platform/docs/rapp-platform-service-pattern.md`, generalized
to applications:
1. Would this asset survive replacing the product? → ownership / platform
repo.
2. Does any other workload depend on it? → platform or rail.
3. Does it encode *who may approve*, not *how to apply*? → platform /
reef admission.
4. Otherwise it is the package's.
### Applied to a first-party app
| Asset | Home |
| --- | --- |
| Domain code, tests, migrations, Containerfile, OpenAPI | app repo (`user-engine`) |
| Helm chart, values, NetworkPolicy, Ingress, ServiceAccount | rapp |
| `declarations/rapp.yaml`, `railiance/app.toml` | rapp |
| Deploy / dry-run / status / verify / rollback Makefile | rapp |
| Workload-specific smoke outcomes | rapp |
| Image **build** and digest publication | app repo + forge |
| Image **pin** in the release | rapp (digest, never `latest`) |
| Postgres cluster, backup, credential broker | `rapp-postgres` + `railiance-platform` |
| Consumer declaration (`database-per-consumer`) | `rapp-postgres/consumers/<app>.yaml` requested by the rapp |
| Schema migrations | app repo, invoked by the rapp at rollout |
| OIDC / flex-auth / tenant policy | those ownership repos |
| Secret **values** | OpenBao via the platform broker |
| Secret **references** | rapp `secret_references` + `railiance/app.toml` |
| Public hostname grant | rapp `exposure.grant` + reef public surface |
| DNS / TLS / Traefik | reef / `railiance-cluster` |
| Shared S5 recipes (probe Host header, landing pages) | copied into the rapp; source of the lesson stays in `railiance-apps` |
The rapp repo must never become a shadow ownership repo. If a paragraph
in `INTENT.md` starts explaining *why the product exists*, it belongs in
the app repo.
---
## 6. Required repo baseline
Every `rapp-*` starts with the first-wave bootstrap contract:
```text
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:
```text
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:
```yaml
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:
```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-<app>` for a single-repo rapp |
| `ownership_repo` | the accountable app or layer repo, **never** the rapp itself. Application: `user-engine`. Platform service: `railiance-platform`. |
| `contract_version` | start at `1.0.0` |
| `readiness_state` | `draft` while scaffolding → `declared` once valid → `deployed` / `verified` only with evidence. There is no `production-approved` on a rapp. |
| `workload_identity.name` | the workload (`user-engine`), **not** `rapp-user-engine`. The schema forbids the `rapp-` prefix. |
| `workload_identity.package_type` | one of `helm-managed-platform-service`, `manifest-managed-platform-service`, `knative-managed-service`, `grouped-composition` |
| `data_classification` | highest class the workload handles |
| `criticality` | `low` \| `medium` \| `high` \| `critical` |
| `primary_rail` | must also appear in `supported_rails` |
| `bound_reefs` | `[]` until placed; then `[reef-railiance]` |
| `runtime_dependencies` | capability names, not the resource that satisfies them |
| `composition.purpose` | one sentence; if it needs "and also", split the rapp |
| `composition.member_repos` | one member for a single-repo rapp; each lists `deployables` |
| `composition.upstream_components` | pinned third-party charts/images. Exact version or digest. Never `latest`. |
| `rollout_contract.default_mode` | `helm-upgrade-install` or `kubectl-server-side-apply` (or `revision-canary` on Knative) |
| `smoke_contract.required` | **outcomes** that must be true, not a command diary |
| `rollback_contract.order` | preferred strategies first |
| `source_documents` | repo + path of the docs this declaration was derived from |
Optional, use when true:
- `secret_references` — OpenBao **paths**, never values
- `consumer_contract` / `consumers` — only if this rapp provisions isolated units to others (`rapp-postgres` is the model)
- `exposure` — omit for private. `public` requires a grant, `binding_admission: production-approved`, and a reef that has granted a public surface.
### Package-type choice
The schema has no `helm-managed-application` enum. Until that is added in
`railiance-master`, first-party Helm apps use
`helm-managed-platform-service` (as `rapp-policy-nexus` does). Do not
invent a local type. Record the naming mismatch in `source_documents` /
workplan notes if you want the schema extended later.
### Capability names in use today
There is not yet a controlled vocabulary shared with
`rail.required_substrate_capabilities`. Prefer names already live in
declarations rather than minting near-duplicates:
- `kubernetes-api`
- `helm-apply-path` / `helm-repo-access`
- `traefik-ingress`
- `cert-manager`
- `forgejo-oci-registry`
- `pvc-storage`
- `operator-kubeconfig-access`
- `knative-serving`
- `keycape-oidc` / `key-cape-oidc` (spellings have already drifted — pick one and stay consistent inside a rapp)
- `flex-auth`
- `openbao`
- `openbao-database-secrets-engine`
- `s3-backup-target`
Who provisions the capability is **outside** the rapp:
`resource-control` procures, the reef publishes non-secret attributes,
`railiance-platform` vends the credential.
---
## 8. Service wrapper patterns
Three live patterns. Copy the one that matches the rail and package type.
### 8.1 Helm platform / app service (`rail-kubernetes`)
References: `rapp-openbao`, `rapp-policy-nexus`,
`railiance-apps/docs/s5-app-onboarding-checklist.md`.
Operator surface (minimum Makefile targets):
| Target | Meaning |
| --- | --- |
| `make check` / `make test` / `make lint` | contract and `helm lint` with dummy digests |
| `make render` | write rendered manifests locally |
| `make server-dry-run` | `kubectl apply --dry-run=server` against a representative API |
| `make deploy` | `helm upgrade --install --atomic --wait` |
| `make status` | helm/kubectl status |
| `make verify-live` | named smoke outcomes from `smoke_contract.required` |
| `make rollback REVISION=…` | previous Helm revision |
Pins:
- image by **digest**, not tag (`rapp-policy-nexus` is the model)
- chart version and app version in `workload_identity`
- forge-owned registry path (`forgejo.coulomb.social/...`)
Runtime hardening that should be the default in new charts:
- non-root numeric user
- read-only root filesystem where the app allows it
- dropped capabilities
- NetworkPolicy default-deny plus explicit ingress/egress
- unauthenticated `/healthz` and `/readyz`
- `/metrics` cluster-internal and authorized
- probe `Host` header set when the app validates Host (Django lesson)
### 8.2 Manifest-managed platform service
Reference: `rapp-postgres` (`kubectl-server-side-apply` of CNPG Cluster
and NetworkPolicies; Helm only for the *operator*, which is cluster-owned).
Use this when the workload is a set of reviewed manifests rather than a
chart you upgrade as a release.
### 8.3 Knative managed service
Reference: `rapp-qonto` (`runtime/knative/`, revision-canary,
previous-revision rollback). Only when `rail-knative` is the primary rail.
### 8.4 Overlay compatibility path
`rail-kubernetes/docs/overlay-repo-pattern.md` still describes
`<app>-railiance-overlay/` with `railiance/app.toml`. 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. Declare 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`).
### 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. It 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, and secret custody
`rapp-user-engine` is an empty git repository (2026-08-11). ADR-0007
already lists it as undeclared.
Sister stubs `rapp-secrets-engine` and `rapp-tenant-engine` should follow
the same guide after user-engine proves the path. Do not batch-scaffold
all three before the first declaration validates.
### 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` | `helm-managed-platform-service` | only Helm-shaped enum available today |
| `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) | operator tunnel later if needed; public grant only with a hostname and residual-risk owner |
| Granularity | single-repo rapp | one deployable; do not group with tenant-engine or secrets-engine |
### 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: helm-managed-platform-service
chart: rapp-user-engine/user-engine
chart_version: 0.1.0
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
- openbao-database-secrets-engine
- keycape-oidc
- flex-auth
composition:
purpose: Package and operate the user-engine identity-domain service on Railiance.
member_repos:
- repo: rapp-user-engine
role: Helm package, smoke, and rollback for the user-engine runtime
deployables:
- user-engine
- repo: user-engine
role: application source, image, and schema migrations
deployables:
- user-engine-migrate
upstream_components:
- name: user-engine
source: forgejo.coulomb.social/<org>/user-engine
version: sha256:<pin-at-first-release>
rollout_contract:
default_mode: helm-upgrade-install
commands:
- make render
- 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
- postgres-consumer-database-present
commands:
- make verify-live
rollback_contract:
order:
- previous-helm-revision
- apply-reviewed-git-revision
commands:
- make rollback REVISION=<previous-helm-revision>
source_documents:
- repo: user-engine
path: INTENT.md
- repo: user-engine
path: docs/configuration.md
- repo: user-engine
path: docs/operability.md
- repo: user-engine
path: docs/postgres-durable-store-consumer-requirements.md
- repo: railiance-master
path: schemas/rapp.schema.json
- repo: repo-manager
path: docs/RailianceAppDeploymentGuide.md
```
Cardinality note: `user-engine` the *Deployment* belongs to this rapp.
`user-engine-migrate` is a Job owned by the same rapp. Do not also list
the Deployment under another rapp.
If the migrate Job is considered the same deployable as the app, collapse
to a single `user-engine` deployable and treat migration as a rollout
step, not a second live unit. Prefer the single-deployable form unless
the Job must be inventoried separately for the coverage check.
### 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 Postgres consumer (to be added in `rapp-postgres`)
Sketch only — the owning package is `rapp-postgres`:
```yaml
apiVersion: rapp-postgres.railiance.io/v1alpha1
kind: PostgresConsumer
metadata:
name: user-engine
spec:
database: user_engine
schema: user_engine
costAttributionKey: platform:user-engine
clientNamespaces: [user-engine]
roles:
owner: user_engine_owner
migration: user_engine_migrate
runtime: user_engine_app
tenantKeyingRequired: true
```
### 10.6 What stays in `user-engine`
- domain model, adapters, tests, conformance harness
- `Containerfile` and image publish instructions
- SQL migrations
- portal/OIDC behavior
- `docs/operability.md` diagnostics
The app repo should grow a short "consumed by `rapp-user-engine`" note
pointing here. It should not grow Helm charts.
### 10.7 First-wave `railiance/app.toml` for this rapp
Minimum: `schema_version = "railiance.app.v1"`, `app.id = "user-engine"`,
source digest policy `required`, postgres + oidc + flex-auth + openbao
dependencies, secret references by route, `/healthz` and `/readyz`
checks, Stage 1 (`make test` in the app repo + `helm template`), Stage 2
canary requiring approval, Stage 3 promote with a recorded rollback
target. Copy structure from
`rail-kubernetes/examples/railiance/app.toml`.
---
## 11. Operator checklist (new rapp)
Use this when filling a stub or extracting a wrapper from
`railiance-apps`.
### Shape
- [ ] Confirm the workload passes §4.
- [ ] Create or fill `rapp-<app>` with the §6 baseline.
- [ ] Write `INTENT.md` / `SCOPE.md` as a **package** boundary, not a
product manifesto.
- [ ] Author `declarations/rapp.yaml` to the schema. Start
`readiness_state: draft`.
- [ ] Add `railiance/app.toml` if the primary rail is `rail-kubernetes`.
- [ ] Add the Helm chart or manifest set. Pin images by digest.
- [ ] Add Makefile targets from §8.1.
- [ ] Name smoke **outcomes** and rollback **order**.
- [ ] Leave `exposure` unset.
### Handoffs
- [ ] Request a `rapp-postgres` consumer when a durable store is needed.
- [ ] Request credential lanes / CCRs in `railiance-platform`. Commit
paths only.
- [ ] Confirm forge image publish path in the **app** repo.
- [ ] Confirm cluster pull before the first deploy.
- [ ] Record `source_documents`.
### Evidence before `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 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):
```text
rmgr rapp init --app user-engine --ownership-repo user-engine \
--rail rail-kubernetes --classification confidential --criticality high
rmgr rapp skeleton --path ../rapp-user-engine --from-app ../user-engine
rmgr rapp validate --path ../rapp-user-engine --family-root ..
```
Constraints on the scaffolder:
- Files stay authoritative. The hub is not written to except via
`fix-consistency` after the files exist.
- No secret values, ever.
- Do not invent package types, rails, or reefs.
- Idempotent: re-running `init` on an already-declared rapp must refuse
or update in a reviewed diff, not overwrite a live contract.
- The three engine stubs (`user-engine`, `secrets-engine`,
`tenant-engine`) are the first cohort; `user-engine` is the pilot.
---
## 13. Open questions and known drift
Recorded so the scaffolder does not silently pick a side.
1. **`ownership_repo` tightness.** Schema currently allows a layer repo
(`railiance-platform`) or an application repo. For application rapps,
prefer the application repo. (`railiance-master/schemas/README.md`)
2. **No application package_type.** First-party Helm apps reuse
`helm-managed-platform-service`. A future `railiance-master` schema
change may add `helm-managed-application`.
3. **`rapp-qonto` is not a copy target.** It is verified in production
but does not yet conform to ADR-0007 (`composition`, `bound_reefs`,
`package_type`, workload name). Copy `rapp-policy-nexus` +
`rapp-openbao` instead.
4. **Capability vocabulary is free-form.** Spellings already drift
(`keycape-oidc` vs `key-cape-oidc`). The scaffolder should emit a
small allow-list and refuse new names without a note.
5. **`rapp-vergabe-teilnahme` was the planned user-facing proof and was
never built.** Extracting it remains a `railiance-apps` residual. Do
not pretend `rapp-user-engine` closes that ADR-0003 item.
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`.
---
## 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`
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`