Cloudron deployment plan

This commit is contained in:
adilallo
2026-04-22 18:54:32 -06:00
parent c7f22a0990
commit 4d066dad0e
5 changed files with 220 additions and 22 deletions
+5
View File
@@ -15,6 +15,11 @@
Use `npx prisma studio` to inspect the database.
Deploying to staging or production (MEDLab Cloudron) — see
[docs/guides/ops-backend-deploy.md](docs/guides/ops-backend-deploy.md)
for the admin handoff and the linked Linear tickets for the actual
deployment-pipeline work.
### Prisma migrations
- **Never edit** a migration that has already been applied to staging,
+1
View File
@@ -23,6 +23,7 @@ These will be deleted once the backend services are stood up:
- [guides/backend-roadmap.md](./guides/backend-roadmap.md)
- [guides/backend-linear-tickets.md](./guides/backend-linear-tickets.md)
- [guides/template-recommendation-matrix.md](./guides/template-recommendation-matrix.md)
- [guides/ops-backend-deploy.md](./guides/ops-backend-deploy.md) — admin handoff for deploying to MEDLab's Cloudron.
## Cursor rules
+53 -17
View File
@@ -11,7 +11,8 @@ A backend review was merged into **[docs/backend-roadmap.md](backend-roadmap.md)
### Audit note (Linear CR-72+ vs repo, 2026-04)
- **Done in Linear and shipped:** **CR-72CR-76**, **CR-77** (publish from create flow), **CR-78** (template seed), **CR-79**, **CR-88**, **CR-89**. The **CR-72 → CR-83** numbering is the original **sequential plan**, not current blocking order; the **core product vertical** through publish + templates is effectively complete in-repo.
- **Backlog (still open):** **CR-80** (web vitals — file-based route remains), **CR-81** (public rule detail — no `GET /api/rules/[id]` or marketing detail page yet), **CR-82** (CI migrate smoke), **CR-83** (no `docs/ops-backend-deploy.md` yet), **CR-84** / **CR-85** (parallel hygiene), **CR-86** (profile + account + draft resume — UI mostly placeholder), **CR-90** / **CR-91**, **CR-93** (template grid facets on marketing).
- **Backlog (still open):** **CR-80** (web vitals — file-based route remains), **CR-81** (public rule detail — no `GET /api/rules/[id]` or marketing detail page yet), **CR-82** (CI migrate smoke), **CR-84** / **CR-85** (parallel hygiene), **CR-86** (profile + account + draft resume — UI mostly placeholder), **CR-90** / **CR-91**, **CR-93** (template grid facets on marketing).
- **CR-83 Done (admin handoff scope):** [`docs/guides/ops-backend-deploy.md`](ops-backend-deploy.md) shipped as the **admin handoff sheet** (access, env vars, platform settings, open decisions). The full deploy runbook is intentionally split out — see the new follow-up tickets in [Ticket 12 / CR-83 follow-ups](#follow-up-tickets-filed-under-cr-83) below.
- **CR-86** is **no longer blocked** by publish — **CR-77** is **Done**; profile work is gated by **implementation**, not waiting on publish wiring.
- **Not in this ticket list** but called out in **[docs/backend-roadmap.md](backend-roadmap.md):** shared **rate-limit store** (e.g. Redis) before multi-instance; **`GET /api/create-flow/methods`** exists for facet scoring (Ticket 16 / CR-88) but is not duplicated as a separate doc ticket.
@@ -535,29 +536,58 @@ _Section B — Final Review screen `+` button per category:_
---
## Ticket 12 — Staging / production runbook (operator checklist)
## Ticket 12 — Staging / production admin handoff (Cloudron at MEDLab)
**Depends on:** Tickets 18 complete enough to deploy a vertical slice.
**Server / admin:** **This is the main ticket where you need the admin.** You draft the runbook; **admin fills in real hostnames, DB endpoint, SMTP, backup tooling, and who runs `migrate deploy`.** Without their input, you cannot complete production-ready deploy steps.
**Server / admin:** **This is the handoff ticket.** Scope is **narrowed** vs. the original "full operator runbook" framing: the deliverable is the **admin-handoff sheet** — exactly what access, env vars, and platform decisions to ask MEDLab's Cloudron admin for. The full deploy runbook (build / push / install / migrate / smoke / rollback) is **split out into a follow-up ticket** so CR-83 isn't blocked on access we don't have yet.
**Goal:** Single doc for admin: env vars, TLS, DB backups, migrations, Docker, SMTP, health checks.
**Goal:** Single short doc the admin can read end-to-end and respond to in one round-trip: required access, Cloudron-injected vs. manually-set env vars, platform settings to confirm (`httpPort`, `healthCheckPath`, addons, memory, backups), and the open product decisions only admin can answer (subdomains, sender address, registry choice, cutover overlap, retention).
**Implementation:**
**Platform context:** Target is **Cloudron at MEDLab** (same host as the legacy [`CommunityRule/CommunityRuleBackend`](https://git.medlab.host/CommunityRule/CommunityRuleBackend), which is Express + MySQL with a 30-min `run.sh` watchdog). New app is a properly packaged Cloudron app (Docker image + `CloudronManifest.json`), uses the **postgresql + sendmail + localstorage** addons, and replaces the legacy service entirely — **no data migration**. Cloudron's container supervisor replaces the old watchdog.
1. Add `docs/ops-backend-deploy.md` (or similar) with numbered steps:
- Required env: `DATABASE_URL`, `SESSION_SECRET`, `SMTP_URL`, `SMTP_FROM`, optional `NEXT_PUBLIC_ENABLE_BACKEND_SYNC`.
- `docker compose` vs `Dockerfile` deploy; `prisma migrate deploy` before traffic.
- Reverse proxy: `GET /api/health` for LB health.
- Backups and restore drill for Postgres.
- SMTP DNS (SPF/DKIM).
2. Cross-link [docs/backend-roadmap.md](docs/backend-roadmap.md) §11 (environments) and §8 (migrations policy); note **never rewrite applied migrations** and where application logs go.
**Implementation (shipped):**
1. [`docs/guides/ops-backend-deploy.md`](ops-backend-deploy.md) — admin handoff sheet (~1 page):
- **§2 Access checklist** (Cloudron admin login, registry creds, DNS, `cloudron` CLI, log access, read of legacy app config).
- **§3 Env vars** split into Cloudron auto-injected (`CLOUDRON_POSTGRESQL_URL`, `CLOUDRON_MAIL_SMTP_*`) vs. manually-set (`SESSION_SECRET`, `SMTP_FROM`, `NEXT_PUBLIC_ENABLE_BACKEND_SYNC`).
- **§4 Platform settings** (`httpPort: 3000`, `healthCheckPath: /api/health`, memory, backups, TLS).
- **§5 Decisions** (subdomains, sender, registry, cutover, retention).
- **§7 Old vs new deltas** (addons, watchdog, OTP→magic link, sender, API surface — all reasons not to reuse legacy infra).
- **§8 Follow-up tickets** (the six tickets below).
2. Cross-links: [`docs/guides/backend-roadmap.md`](backend-roadmap.md) §11 (environments — names Cloudron at MEDLab) and §8 (migrations policy — never rewrite applied migrations).
**Acceptance criteria:**
- [ ] Someone who did not write the code can deploy and roll back migrations with only the doc.
- [x] Admin can grant the right access + answer the open decisions in one pass without further back-and-forth.
- [x] Doc is ~1 page and explicitly lists what is **not** in scope so admin doesn't expect a full deploy walkthrough.
- [x] Six follow-up tickets enumerated and linked (see below).
**Files:** new `docs/ops-backend-deploy.md`.
**Files:** [`docs/guides/ops-backend-deploy.md`](ops-backend-deploy.md), [`docs/guides/backend-roadmap.md`](backend-roadmap.md), [`docs/README.md`](../README.md), [`CONTRIBUTING.md`](../../CONTRIBUTING.md).
**Status:** [CR-83](https://linear.app/community-rule/issue/CR-83/backend-stagingproduction-runbook-admin-handoff-docsops-backend) **Done** (admin handoff scope). Deployment-pipeline implementation tracked in the follow-up tickets below.
### Follow-up tickets filed under CR-83
All six are titled `[Backend] …`, assigned to Vinod, in the **community-rule** team, **Backlog** state. IDs filled in once filed via Linear MCP.
| # | Linear | Title | Depends on |
| - | ------ | ----- | ---------- |
| 1 | [CR-96](https://linear.app/community-rule/issue/CR-96/backend-bridge-cloudron-env-vars-to-canonical-names) | `[Backend] Bridge CLOUDRON_* env vars to canonical names` | none — can ship now |
| 2 | [CR-97](https://linear.app/community-rule/issue/CR-97/backend-container-image-registry-choose-build-push) | `[Backend] Container image registry: choose, build, push` | registry decision (handoff §5) |
| 3 | [CR-98](https://linear.app/community-rule/issue/CR-98/backend-cloudron-staging-install-smoke) | `[Backend] Cloudron staging install + smoke` | CR-96 + CR-97 + Cloudron CLI access + staging DNS |
| 4 | [CR-99](https://linear.app/community-rule/issue/CR-99/backend-cloudron-production-install-dns-cutover) | `[Backend] Cloudron production install + DNS cutover` | CR-98 green for the agreed overlap window |
| 5 | [CR-100](https://linear.app/community-rule/issue/CR-100/backend-steady-state-operator-runbook) | `[Backend] Steady-state operator runbook` | CR-98 (write what we actually did) |
| 6 | [CR-101](https://linear.app/community-rule/issue/CR-101/backend-decommission-legacy-expressmysql-backend) | `[Backend] Decommission legacy Express/MySQL backend` | CR-99 + sign-off window |
**Per-ticket detail:**
1. **Bridge `CLOUDRON_*` env vars to canonical names.** Cloudron injects `CLOUDRON_POSTGRESQL_URL` and `CLOUDRON_MAIL_SMTP_SERVER/PORT/USERNAME/PASSWORD`; the app reads `DATABASE_URL` / `SMTP_URL`. Recommended approach: read both names in [`lib/server/env.ts`](../../lib/server/env.ts) and assemble `SMTP_URL` from the four parts in [`lib/server/mail.ts`](../../lib/server/mail.ts) when only the Cloudron names are present. Alternative: a `start.sh` shim in the image. Acceptance: with only `CLOUDRON_*` set, app connects to DB and sends mail; with only canonical names set (current behavior), unchanged; unit tests cover both.
2. **Container image registry: choose, build, push.** Acceptance: `docker pull <registry>/communityrule:<tag>` works from a Cloudron-reachable network. CI builds and pushes on merge to `main` (stretch).
3. **Cloudron staging install + smoke.** Acceptance: `curl https://<staging>/api/health` returns `{"ok":true,"database":"connected"}`; magic-link request → click link → `GET /api/auth/session` returns a user; publishing a rule succeeds.
4. **Cloudron production install + DNS cutover.** Acceptance: production subdomain resolves to the new app; old subdomain still works during overlap; sign-in + publish succeed against production; backups confirmed.
5. **Steady-state operator runbook.** Lives at `docs/guides/ops-runbook.md` (sibling to the handoff). Covers deploy a new version, rollback, restore drill cadence, multi-instance limitations from [`backend-roadmap.md`](backend-roadmap.md) §5/§7. Acceptance: a fresh reader can deploy + roll back using only this doc.
6. **Decommission legacy Express/MySQL backend.** Acceptance: old Cloudron app stopped + uninstalled; old MySQL addon backed up once and removed; legacy Gitea repo README updated to point at this app. Priority: Low.
---
@@ -661,7 +691,7 @@ _Section B — Final Review screen `+` button per category:_
| 9 | 9 | Web vitals persistence |
| 10 | 10 | Public rule detail (optional) |
| 11 | 11 | CI migrate smoke (optional) |
| 12 | 12 | Ops runbook |
| 12 | 12 | Ops admin handoff (Cloudron) **Done** |
| 13 | 13 | API errors + request-id logging |
| 14 | 14 | Session lifecycle + cleanup |
| 15 | 15 | Profile + account (Figma profile) |
@@ -678,7 +708,7 @@ Tickets **1011** can be deferred without blocking the core “auth + drafts +
## Linear (Community-rule team)
**Main chain (historical):** **CR-72 → CR-83** was the original **strict sequence**; **repo + Linear status today:** **CR-72CR-79**, **CR-88**, **CR-89** are **Done**; **CR-77** (publish) **Done**; **CR-80CR-83** remain **Backlog** (web vitals, public rule detail, CI migrate smoke, ops runbook — optional / ops tail). **Parallel:** **CR-84**, **CR-85** (**Backlog**); **CR-86** / Ticket 15 (**Backlog** — publish **not** a blocker); **CR-93** (**Backlog**); **CR-90** / Ticket 18 (stakeholder invites); **CR-91** / Ticket 19 (`Add` button behavior).
**Main chain (historical):** **CR-72 → CR-83** was the original **strict sequence**; **repo + Linear status today:** **CR-72CR-79**, **CR-83**, **CR-88**, **CR-89** are **Done**; **CR-77** (publish) **Done**; **CR-80CR-82** remain **Backlog** (web vitals, public rule detail, CI migrate smoke). **CR-83** (admin handoff) shipped as a narrow handoff sheet; the actual Cloudron deployment pipeline is split into the **`[Backend]` follow-up tickets** filed under it (env-var bridging → image registry → staging → production cutover → operator runbook → legacy decommission). **Parallel:** **CR-84**, **CR-85** (**Backlog**); **CR-86** / Ticket 15 (**Backlog** — publish **not** a blocker); **CR-93** (**Backlog**); **CR-90** / Ticket 18 (stakeholder invites); **CR-91** / Ticket 19 (`Add` button behavior).
| Doc ticket | Linear | Title (short) |
| ---------: | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
@@ -693,7 +723,13 @@ Tickets **1011** can be deferred without blocking the core “auth + drafts +
| 9 | [CR-80](https://linear.app/community-rule/issue/CR-80/backend-persist-web-vitals-outside-next-db-or-external-rum) | Web vitals (prefer external) |
| 10 | [CR-81](https://linear.app/community-rule/issue/CR-81/backend-public-rule-detail-page-get-apirulesid-optional) | Public rule detail (optional) |
| 11 | [CR-82](https://linear.app/community-rule/issue/CR-82/backend-ci-postgres-migration-smoke-optional) | CI migrate smoke (optional) |
| 12 | [CR-83](https://linear.app/community-rule/issue/CR-83/backend-stagingproduction-runbook-admin-handoff-docsops-backend) | Ops runbook / admin handoff |
| 12 | [CR-83](https://linear.app/community-rule/issue/CR-83/backend-stagingproduction-runbook-admin-handoff-docsops-backend) | Ops admin handoff (Cloudron) **Done** |
| 12.1 | [CR-96](https://linear.app/community-rule/issue/CR-96/backend-bridge-cloudron-env-vars-to-canonical-names) | `[Backend] Bridge CLOUDRON_* env vars to canonical names` |
| 12.2 | [CR-97](https://linear.app/community-rule/issue/CR-97/backend-container-image-registry-choose-build-push) | `[Backend] Container image registry: choose, build, push` |
| 12.3 | [CR-98](https://linear.app/community-rule/issue/CR-98/backend-cloudron-staging-install-smoke) | `[Backend] Cloudron staging install + smoke` |
| 12.4 | [CR-99](https://linear.app/community-rule/issue/CR-99/backend-cloudron-production-install-dns-cutover) | `[Backend] Cloudron production install + DNS cutover` |
| 12.5 | [CR-100](https://linear.app/community-rule/issue/CR-100/backend-steady-state-operator-runbook) | `[Backend] Steady-state operator runbook` |
| 12.6 | [CR-101](https://linear.app/community-rule/issue/CR-101/backend-decommission-legacy-expressmysql-backend) | `[Backend] Decommission legacy Express/MySQL backend` |
| 13 | [CR-84](https://linear.app/community-rule/issue/CR-84/backend-api-error-contract-request-id-logging) | API errors + request-id logging |
| 14 | [CR-85](https://linear.app/community-rule/issue/CR-85/backend-custom-session-lifecycle-cleanup-invalidation-policy) | Session lifecycle + cleanup |
| 15 | [CR-86](https://linear.app/community-rule/issue/CR-86/backend-profile-dashboard-account-figma-profile) | Profile + account (Figma 22143:900069) |
+7 -5
View File
@@ -206,13 +206,15 @@ npm run dev
**Optional QA:** Run automated tests against an **ephemeral** database in CI instead of maintaining a fourth long-lived server.
**Target platform:** **Cloudron at MEDLab** — same host as the legacy [`CommunityRule/CommunityRuleBackend`](https://git.medlab.host/CommunityRule/CommunityRuleBackend) (Express + MySQL). The new app is packaged as a proper Cloudron app (Docker image + `CloudronManifest.json`, **postgresql + sendmail + localstorage** addons). Cloudron's container supervisor replaces the legacy 30-min `run.sh` watchdog. Admin handoff (access, env vars, platform settings, open decisions): [`docs/guides/ops-backend-deploy.md`](ops-backend-deploy.md). Note: Cloudron injects `CLOUDRON_POSTGRESQL_URL` and `CLOUDRON_MAIL_SMTP_*`; the app reads `DATABASE_URL` / `SMTP_URL`, so a small env-var bridge in [`lib/server/env.ts`](../../lib/server/env.ts) / [`lib/server/mail.ts`](../../lib/server/mail.ts) is needed (tracked in [**CR-96**](https://linear.app/community-rule/issue/CR-96/backend-bridge-cloudron-env-vars-to-canonical-names), filed under CR-83 — see [backend-linear-tickets.md](backend-linear-tickets.md) Ticket 12 follow-ups).
**Admin / infra (coordinate with whoever runs the server):**
1. TLS certificates and hostnames.
2. PostgreSQL backups and restore drill.
3. SMTP DNS (SPF, DKIM).
4. Health check URL for reverse proxy (`/api/health`).
5. Log retention and alerts for 5xx errors.
1. TLS certificates and hostnames. _On Cloudron: handled by the platform per chosen subdomain._
2. PostgreSQL backups and restore drill. _On Cloudron: daily snapshots; configure retention in admin UI._
3. SMTP DNS (SPF, DKIM). _On Cloudron: handled for the platform-managed domain._
4. Health check URL for reverse proxy (`/api/health`). _On Cloudron: set `healthCheckPath` in `CloudronManifest.json`._
5. Log retention and alerts for 5xx errors. _On Cloudron: app log viewer; export off-platform if longer retention is needed._
---
+154
View File
@@ -0,0 +1,154 @@
# Backend deploy — admin handoff
This is the list of access, environment variables, and platform decisions
needed to deploy CommunityRule (the new Next.js + Postgres app in this
repo) onto MEDLab's Cloudron. Hand it to the Cloudron admin; once they
confirm what is checked below, the actual deploy happens in the
follow-up tickets listed in §8.
## 1. Context
- This app **replaces** the old Express + MySQL backend at
[`CommunityRule/CommunityRuleBackend`](https://git.medlab.host/CommunityRule/CommunityRuleBackend).
- It is packaged as a real Cloudron app (Docker image +
`CloudronManifest.json`). No `/app/data` drop-in. No `run.sh`
watchdog — Cloudron's container supervisor handles restarts.
- **Greenfield Postgres.** No data migration from the old MySQL addon.
Old auth (4-digit OTP in `email_otp`) is replaced by hashed
magic-link tokens; old API and `rules` / `version_history` tables do
not map to anything in the new app.
## 2. Access I need
Check off as granted:
- [ ] **Cloudron admin login** for the MEDLab instance, or "deploy
app" capability scoped to one app slot.
- [ ] **Container registry credentials** — read/write to wherever
images get pushed (Docker Hub, GHCR, MEDLab self-hosted registry —
admin's choice; see §6).
- [ ] **DNS** — ability to add/edit a subdomain record pointing at the
Cloudron host, or confirmation that admin will add the records I
specify.
- [ ] **`cloudron` CLI access** from my workstation (token-based; no
SSH needed for normal ops).
- [ ] **Cloudron app log access** — web UI is fine; SSH only if web
logs aren't enough.
- [ ] **Read access to the existing legacy app's Cloudron config** so
I can confirm domain / cert / SMTP setup before cutover.
## 3. Environment variables
### Cloudron auto-injects (admin: confirm the addons are enabled)
- `CLOUDRON_POSTGRESQL_URL` — from the **postgresql** addon. The app
reads `DATABASE_URL`; bridging is a small in-app code change (see
§8 ticket 1).
- `CLOUDRON_MAIL_SMTP_SERVER` / `_PORT` / `_USERNAME` / `_PASSWORD`
from the **sendmail** addon. The app reads `SMTP_URL`; bridged the
same way.
### I set manually via `cloudron configure --app <id> --set-env`
- `SESSION_SECRET` — long random (`openssl rand -hex 32`). Required,
≥ 16 chars. Rotating it logs everyone out.
- `SMTP_FROM` — visible "From:" address on sign-in emails. Cloudron
does **not** inject this. Recommend `hello@communityrule.info` (same
as the old service) unless admin wants a new address.
- `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` — turns on Postgres draft
persistence for signed-in users. Recommended for production.
## 4. Platform settings to confirm
- Container `httpPort`: **3000** (matches [`Dockerfile`](../../Dockerfile)
`ENV PORT=3000`).
- Health-check path: **`/api/health`**
([`app/api/health/route.ts`](../../app/api/health/route.ts) returns
`200 {"ok":true,"database":"connected"}` when healthy, `503`
otherwise).
- Memory limit: start at **512 MB**; raise if Next.js standalone OOMs
under load.
- Backups: confirm Cloudron's daily snapshot is on for both this app
and its postgresql addon.
- TLS, DNS, SPF/DKIM: handled by Cloudron for the chosen subdomain —
confirm.
## 5. Decisions I need from admin
1. **Subdomains** for staging and production. Default proposal:
`staging-app.communityrule.info` + `app.communityrule.info`.
2. **Sender address** — reuse legacy `hello@communityrule.info`, or
pick a new one?
3. **Container registry** — MEDLab self-hosted, Docker Hub (under
what org), or GHCR?
4. **Cutover overlap** — how many days should the old service keep
running in parallel before we uninstall it?
5. **Backup retention** beyond Cloudron defaults? (If "defaults are
fine," say so.)
## 6. What is intentionally NOT in this doc
So admin doesn't expect more than this scope:
- The deploy runbook itself (build / push / install / migrate / smoke
/ rollback) — written **after** access in §2 is granted; see §8
ticket 5.
- Code changes to bridge `CLOUDRON_*` env vars to `DATABASE_URL` /
`SMTP_URL` — I do those locally and ship the image with bridging
built in; see §8 ticket 1.
- Decommissioning the legacy Express/MySQL service — does not happen
until the new app is green in production; see §8 ticket 6.
## 7. Old vs new backend deltas
So nothing surprises admin or users at cutover:
- Old addons: **MySQL + sendmail**. New addons: **postgresql +
sendmail + localstorage**. Different DB addon entirely; do not
reuse the old one.
- Old service ran from `/app/data/public/communityRuleBackend` with a
30-min `lsof`-based `run.sh` watchdog — not a packaged Cloudron
app. New app is a proper Cloudron app; Cloudron supervises it.
- Old auth = plaintext 4-digit OTP. New auth = magic **link** in
email. If users report "I'm not getting a code," remind them to
look for a link.
- Old code hardcoded `from: 'hello@communityrule.info'` in
[`controllers/emailController.js`](https://git.medlab.host/CommunityRule/CommunityRuleBackend/raw/branch/master/controllers/emailController.js)
because Cloudron does not inject a `MAIL_FROM`. New app reads
`SMTP_FROM` — see §3.
- Old API surface (`/api/send_otp`, `/api/publish_rule`, etc.) and
schema (`rules` + `version_history` tables, soft-delete via
`deleted` column) **do not overlap**. No data migration.
## 8. Follow-up tickets
All filed in Linear, titled `[Backend] …`, assigned to me, in the
**Community-rule** team, **Backlog** state.
1. [**CR-96**](https://linear.app/community-rule/issue/CR-96/backend-bridge-cloudron-env-vars-to-canonical-names)
`[Backend] Bridge CLOUDRON_* env vars to canonical names`. No
admin dependency; can land now.
2. [**CR-97**](https://linear.app/community-rule/issue/CR-97/backend-container-image-registry-choose-build-push)
`[Backend] Container image registry: choose, build, push`.
Depends on registry decision (§5).
3. [**CR-98**](https://linear.app/community-rule/issue/CR-98/backend-cloudron-staging-install-smoke)
`[Backend] Cloudron staging install + smoke`. Blocked by CR-96
+ CR-97; needs Cloudron CLI access + staging DNS.
4. [**CR-99**](https://linear.app/community-rule/issue/CR-99/backend-cloudron-production-install-dns-cutover)
`[Backend] Cloudron production install + DNS cutover`. Blocked
by CR-98 green for the agreed overlap window.
5. [**CR-100**](https://linear.app/community-rule/issue/CR-100/backend-steady-state-operator-runbook)
`[Backend] Steady-state operator runbook`. Blocked by CR-98
(we write it after we've actually done it).
6. [**CR-101**](https://linear.app/community-rule/issue/CR-101/backend-decommission-legacy-expressmysql-backend)
`[Backend] Decommission legacy Express/MySQL backend`. Blocked
by CR-99 + sign-off window. Priority: Low.
## 9. Related docs
- [`docs/guides/backend-roadmap.md`](backend-roadmap.md) §11
(environments) and §8 (Prisma migrations policy).
- [`docs/guides/backend-linear-tickets.md`](backend-linear-tickets.md)
Ticket 12 / CR-83 — this doc satisfies it.
- [`CONTRIBUTING.md`](../../CONTRIBUTING.md) — local dev setup
(Postgres, magic-link, draft sync).