2026-08-18 12:18:45 +02:00
# 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 )
2026-08-18 13:03:16 +02:00
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 > \
--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.
2026-08-18 12:18:45 +02:00
---
## 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:
2026-08-18 13:03:16 +02:00
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).
2026-08-18 12:18:45 +02:00
2026-08-18 13:03:16 +02:00
`RMGR-WP-0006` recorded the contract and the first wrap.
`RMGR-WP-0007` is the greenfield command path.
2026-08-18 12:18:45 +02:00
---
## 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
2026-08-18 12:19:47 +02:00
*references*, and reef binding. The architecture term is *managed
workload package*; "service wrapper" is informal.
2026-08-18 12:18:45 +02:00
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 |
2026-08-18 12:20:27 +02:00
| `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. |
2026-08-18 12:18:45 +02:00
2026-08-18 13:03:16 +02:00
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.
2026-08-18 12:18:45 +02:00
`rail-kubernetes` also owns the generic staged-promotion contract
`railiance/app.toml` (Stage 1 local, Stage 2 canary, Stage 3 promote) and
2026-08-18 13:03:16 +02:00
the compatibility overlay-repo pattern. That overlay is the
migration-era wrapper. `app.toml` is **not** required on the first wrap.
2026-08-18 12:18:45 +02:00
### 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` . |
2026-08-18 13:03:16 +02:00
| `rapp-user-engine` | yes | manifest-managed-platform-service | kubernetes | `user-engine` | verified | First-party absorb wrap. Apply home for the portal. |
2026-08-18 12:18:45 +02:00
| `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
2026-08-18 13:03:16 +02:00
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` .
2026-08-18 12:18:45 +02:00
### 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` .
2026-08-18 12:19:47 +02:00
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.
2026-08-18 12:18:45 +02:00
`railiance-platform` retains cross-workload policy, credential custody,
lane approval, and the postgres credential broker.
2026-08-18 12:19:47 +02:00
`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.
2026-08-18 12:18:45 +02:00
---
## 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
2026-08-18 12:19:47 +02:00
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.
2026-08-18 12:18:45 +02:00
---
## 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
2026-08-18 12:19:47 +02:00
`<app>-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 < 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.
2026-08-18 12:18:45 +02:00
---
## 9. Platform handoffs every app wrapper needs
### 9.1 Database
2026-08-18 12:21:49 +02:00
Do not run a sidecar Postgres. The default for a new app is a consumer
against `rapp-postgres` :
2026-08-18 12:18:45 +02:00
- 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` ).
2026-08-18 12:21:49 +02:00
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.
2026-08-18 12:18:45 +02:00
### 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
2026-08-18 12:21:49 +02:00
production WSGI portal. The **app repo** already has:
2026-08-18 12:18:45 +02:00
- `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
2026-08-18 12:21:49 +02:00
- 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.
2026-08-18 12:18:45 +02:00
2026-08-18 12:21:49 +02:00
**Live packaging is not in the rapp.** It lives in
`net-kingdom/sso-mfa/k8s/user-engine/` :
2026-08-18 12:18:45 +02:00
2026-08-18 12:21:49 +02:00
| 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.
2026-08-18 12:18:45 +02:00
### 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 |
2026-08-18 12:21:49 +02:00
| `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. |
2026-08-18 12:18:45 +02:00
| `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 |
2026-08-18 12:21:49 +02:00
| `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` . |
2026-08-18 12:20:27 +02:00
| 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. |
2026-08-18 12:18:45 +02:00
### 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
2026-08-18 12:21:49 +02:00
package_type: manifest-managed-platform-service
2026-08-18 12:18:45 +02:00
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
2026-08-18 12:21:49 +02:00
- pvc-storage
2026-08-18 12:18:45 +02:00
- keycape-oidc
- flex-auth
2026-08-18 12:21:49 +02:00
- openbao
- audit-core
- tenant-engine
secret_references:
- platform/workloads/user-engine/runtime
2026-08-18 12:18:45 +02:00
composition:
purpose: Package and operate the user-engine identity-domain service on Railiance.
member_repos:
- repo: rapp-user-engine
2026-08-18 12:21:49 +02:00
role: Kubernetes manifests, smoke, and rollback for the user-engine runtime
2026-08-18 12:18:45 +02:00
deployables:
- user-engine
upstream_components:
- name: user-engine
2026-08-18 12:21:49 +02:00
source: forgejo.coulomb.social/coulomb/user-engine
version: sha256:e3b5f65bafc1c0260dfdf2567a52766e67506ceb878a51759a2e9a307c4b5eb8
2026-08-18 12:18:45 +02:00
rollout_contract:
2026-08-18 12:21:49 +02:00
default_mode: kubectl-server-side-apply
2026-08-18 12:18:45 +02:00
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
2026-08-18 12:21:49 +02:00
- live-image-digest-match
2026-08-18 12:18:45 +02:00
commands:
- make verify-live
rollback_contract:
order:
2026-08-18 12:21:49 +02:00
- previous-immutable-image-digest
2026-08-18 12:18:45 +02:00
- apply-reviewed-git-revision
commands:
2026-08-18 12:21:49 +02:00
- make rollback DIGEST=< previous-image-digest >
2026-08-18 12:18:45 +02:00
source_documents:
- repo: user-engine
path: INTENT.md
- repo: user-engine
path: docs/configuration.md
- repo: user-engine
path: docs/operability.md
2026-08-18 12:21:49 +02:00
- repo: net-kingdom
path: sso-mfa/k8s/user-engine/README.md
2026-08-18 12:18:45 +02:00
- repo: railiance-master
path: schemas/rapp.schema.json
- repo: repo-manager
path: docs/RailianceAppDeploymentGuide.md
```
2026-08-18 12:21:49 +02:00
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.
2026-08-18 12:18:45 +02:00
2026-08-18 12:21:49 +02:00
`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.
2026-08-18 12:18:45 +02:00
### 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.
2026-08-18 12:21:49 +02:00
### 10.5 Database decision (do not do both)
2026-08-18 12:18:45 +02:00
2026-08-18 12:21:49 +02:00
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.
2026-08-18 12:18:45 +02:00
2026-08-18 12:21:49 +02:00
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.
2026-08-18 12:18:45 +02:00
### 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` .
2026-08-18 13:03:16 +02:00
- [ ] 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.
2026-08-18 12:18:45 +02:00
- [ ] 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.
---
2026-08-18 13:03:16 +02:00
## 12. What Repo Manager automates
2026-08-18 12:18:45 +02:00
2026-08-18 13:03:16 +02:00
Implemented under `RMGR-WP-0006` (P1, P4) and `RMGR-WP-0007` (P2, P3, P5,
compose).
2026-08-18 12:18:45 +02:00
2026-08-18 13:03:16 +02:00
| Phase | Command | Still human |
2026-08-18 12:18:45 +02:00
| --- | --- | --- |
2026-08-18 13:03:16 +02:00
| **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. |
2026-08-18 12:18:45 +02:00
```text
2026-08-18 13:03:16 +02:00
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
2026-08-18 12:18:45 +02:00
```
2026-08-18 13:03:16 +02:00
Constraints:
2026-08-18 12:18:45 +02:00
2026-08-18 13:03:16 +02:00
- Files stay authoritative. The hub is only updated via `fix-consistency` .
2026-08-18 12:18:45 +02:00
- No secret values, ever.
- Do not invent package types, rails, or reefs.
2026-08-18 13:03:16 +02:00
- `init` / `wrap` refuse to overwrite a live declaration.
- `tenant-engine` is the `RMGR-WP-0007` pilot. `rapp-secrets-engine` is
not a wrap target.
2026-08-18 12:18:45 +02:00
---
## 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
2026-08-18 12:19:47 +02:00
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.
2026-08-18 12:18:45 +02:00
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` .
2026-08-18 12:20:27 +02:00
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.
2026-08-18 12:18:45 +02:00
---
## 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`
2026-08-18 12:19:47 +02:00
- `railiance-apps/Makefile` (`policy-nexus-deploy` delegate)
- `rail-kubernetes/docs/create-overlay-command.md`
- `railiance-fabric/docs/rail-rapp-reef-adaptation.md` (discovery only)
2026-08-18 12:18:45 +02:00
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`
2026-08-18 12:21:49 +02:00
- `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)