diff --git a/README.md b/README.md
index 0a67a79..87ff039 100644
--- a/README.md
+++ b/README.md
@@ -38,9 +38,10 @@ it special-cases none of it.
## Binaries
- `portal` — the server (Leptos SSR + hydrate, Axum underneath).
-- `question_lint` — headless content validation, run by the
- `questions` repo's CI against a prebuilt copy this repo's deploy
- publishes; also works offline: `question_lint --path
`.
+- `question_lint` — headless content validation, shipped inside every
+ release artifact so each content repo's CI lints with the exact
+ portal version its instance runs; also works offline:
+ `question_lint --path `.
## Development
@@ -50,17 +51,55 @@ cargo test --features ssr # engine tests
cargo build --features ssr --bin question_lint
```
-Runtime configuration is env vars (see `src/main.rs` and
-`.gitea/workflows/deploy.yml`): `NATS_URL`, `CONTENT_REPO`/
-`CONTENT_BRANCH`, Kanidm OIDC (`KANIDM_URL`, `OAUTH2_CLIENT_*`),
-optional `GARAGE_*` for uploads, `GITEA_API_TOKEN` for authenticated
-resource pulls, `AUTOMATION_READ_TOKEN` for the automation KV read
-endpoint.
+Runtime configuration is env vars (see `src/main.rs`, and each
+content repo's `.gitea/workflows/deploy.yml` for the values in
+production): `NATS_URL`, `CONTENT_REPO`/`CONTENT_BRANCH`, Kanidm OIDC
+(`KANIDM_URL`, `OAUTH2_CLIENT_*`), optional `GARAGE_*` for uploads,
+`GITEA_API_TOKEN` for authenticated resource pulls,
+`AUTOMATION_READ_TOKEN` for the automation KV read endpoint.
-## Deploy
+## Release and deploy
-Pushing `main` triggers `.gitea/workflows/deploy.yml` on the
-bare-metal runner: release build, ship to
-`/srv/app/uhhm-portal/releases/`, flip the `current` symlink,
-restart `app@uhhm-portal`, reload Caddy. Content changes never come
-through here — they hot-reload live from the `questions` repo.
+Portal doesn't deploy itself — it publishes versions, and each site
+decides when to take one. The whole day-to-day surface is two
+commands:
+
+**Cut a release** (here):
+
+```sh
+cargo release patch # or minor / major
+```
+
+That bumps `Cargo.toml`, tags `v`, and pushes; CI
+(`.gitea/workflows/publish.yml`) reacts to the tag, builds once, and
+attaches `portal-v.tar.gz` (`portal` + `question_lint` +
+`site/`) to the Gitea release. Plain pushes to `main` only run the
+tests (`test.yml`) — nothing reaches production from this repo.
+
+**Roll it out** (in a content repo): edit one line in that repo's
+`.gitea/workflows/deploy.yml` —
+
+```yaml
+env:
+ PORTAL_RELEASE: v0.1.1 # <- bump, commit, push
+```
+
+Its CI downloads the pinned artifact, ships
+`/srv/app//releases/`, flips `current`, rewrites the
+instance env from that repo's own Actions variables/secrets, restarts
+`app@`, and refreshes the Caddy route. Every rollout is a
+commit, so reverting a bad version is `git revert` + push. Sites
+upgrade independently: uhhm.no (uhhm/questions → `app@uhhm-portal`,
+:3010) and redoal.com (redoal/questions → `app@redoal-portal`,
+:3020) can pin different versions.
+
+Content changes never come through any of this — they hot-reload
+live from the content repos over NATS.
+
+**Add a site**: new content repo with a copy of an existing
+`deploy.yml` (change `INSTANCE`, port, domains), Actions variables
+(`KANIDM_URL`, `OAUTH2_CLIENT_ID`, `PUBLIC_URL`, `SITE_NAME`) and
+secrets (`NATS_URL`, `OAUTH2_CLIENT_SECRET`,
+`PORTAL_GITEA_API_TOKEN` — Gitea reserves the `GITEA_` prefix for
+secret names), a Kanidm OAuth2 client, and DNS. `site.yaml` in the
+content repo handles all branding; no portal changes needed.