chore(deploy): retarget launch checklist from Netlify to Dokploy on Hetzner
Some checks failed
Test / vitest (push) Successful in 11m42s
Test / playwright (push) Failing after 9m53s
Test / visual (push) Failing after 9m28s
Test / Notify on failure (push) Successful in 2s

Production hosting is Dokploy on Hetzner, not Netlify. The Nitro app
itself is host-agnostic — Dockerfile + Traefik-aware OIDC + xff-aware
rate limiting were already in place — but the Deploy checklist and the
daily reconcile cron were Netlify-specific.

- LAUNCH_READINESS.md: split Deploy checklist into one-time host setup
  + cutover; replace "Netlify scheduled function" with a Dokploy
  Scheduled Task (curl + X-Reconcile-Token); call out the BASE_URL
  exact-match origin gotcha at customer.post.js:11-15 and the
  NODE_ENV=production requirement.
- Delete netlify.toml and netlify/functions/reconcile-payments.mjs.
  The Nitro route at server/api/internal/reconcile-payments.post.js
  stays — it's host-agnostic; only the trigger moves into Dokploy.

No code changes. validate-env.js still hard-fails on missing
MONGODB_URI / JWT_SECRET / RESEND_API_KEY / HELCIM_API_TOKEN at boot.
Tests: 758 passing, 2 skipped, 0 failing.
This commit is contained in:
Jennie Robinson Faber 2026-04-26 12:29:48 +01:00
parent 8e76ce9366
commit c149fba13a
3 changed files with 31 additions and 68 deletions

View file

@ -30,32 +30,48 @@ None outstanding.
## Deploy checklist
Applies when the site is connected to Netlify / production hosting. Nothing here is actionable until that connection exists; kept here so nothing gets forgotten at cutover.
Applies when the app is deployed to **Dokploy on Hetzner**. Build is via the in-repo `Dockerfile` (`node:20-alpine`, runs `node .output/server/index.mjs` on port 3000); Dokploy autodetects it. Traefik (Dokploy's reverse proxy) handles SSL; `oidc-provider.ts:194` and the rate-limit middleware already trust `X-Forwarded-Proto` / `X-Forwarded-For`.
### One-time host setup
- [ ] **Provision the Dokploy app** pointing at this repo. Build context: repo root. Default Dockerfile. Container port: `3000`.
- [ ] **Set env vars in the Dokploy UI** (full list below). The `validate-env.js` Nitro plugin fails fast at boot if `MONGODB_URI` / `JWT_SECRET` / `RESEND_API_KEY` / `HELCIM_API_TOKEN` are missing — container refuses to start, so misconfig surfaces immediately in logs.
- [ ] **`BASE_URL` must exactly match the public origin** (e.g. `https://ghostguild.org`, no trailing slash). The `/api/helcim/customer` origin check at `server/api/helcim/customer.post.js:11-15` does exact-match comparison against the `Origin` header — if `BASE_URL` is wrong or unset, signup 403s.
- [ ] **`NODE_ENV=production`** must be set. Without it: `Secure` cookie flag, HSTS, and CSP all silently no-op.
- [ ] **Add a Dokploy Scheduled Task** for daily reconciliation. Command:
```
curl -fsS -X POST "$BASE_URL/api/internal/reconcile-payments" -H "X-Reconcile-Token: $NUXT_RECONCILE_TOKEN"
```
Schedule: `0 4 * * *` (or any time of day). The Nitro route does the heavy lifting (Mongo iteration, Helcim API, retries) — the scheduler just wakes it up.
### Cutover
- [ ] Push local `main` to `origin/main`.
- [ ] **Run `node scripts/migrate-contribution-amount.cjs --apply` against prod Mongo BEFORE the new code serves traffic.** Idempotent; dry-run on local counted 34 members. Requires `MONGODB_URI` in env. The script writes `contributionAmount` (Number) derived from existing `contributionTier` (String) on every Member doc; the old field is left intact for a window.
- [ ] Set `NUXT_HELCIM_MONTHLY_PLAN_ID=50302` in production env.
- [ ] Set `NUXT_HELCIM_ANNUAL_PLAN_ID=50303` in production env.
- [ ] **Run `node scripts/reconcile-helcim-payments.mjs --apply` against prod Mongo AFTER the new code serves traffic** to backfill Payment records for pre-existing members. Idempotent (unique `helcimTransactionId`); safe to re-run as a nightly reconciliation job post-launch.
- [ ] Set `NUXT_HELCIM_MONTHLY_PLAN_ID=50302` in Dokploy env.
- [ ] Set `NUXT_HELCIM_ANNUAL_PLAN_ID=50303` in Dokploy env.
- [ ] **Set `NUXT_RECONCILE_TOKEN`** to any 32+ char random string. Shared secret between the Dokploy scheduled task and `/api/internal/reconcile-payments`.
- [ ] Deploy.
- [ ] **Run `node scripts/reconcile-helcim-payments.mjs --apply` against prod Mongo AFTER the new code serves traffic** to backfill Payment records for pre-existing members. Idempotent (unique `helcimTransactionId`); the daily Dokploy cron picks it up from there.
- [ ] **Prod audit for pre-fix series-pass bypass registrations.** Fixed in `f34b062` + `4e1888a` (2026-04-20). Before that, child events of pass-only series (`tickets.requiresSeriesTicket=true && tickets.allowIndividualEventTickets=false`) accepted drop-in registrations from non-pass-holders. For every such series, list its child-event `registrations` where the registrant is not in the parent series' pass-holder list, filter to `registeredAt < 2026-04-20`, and decide per-case: grandfather (keep + notify), refund + unregister, or silently unregister. Local Mongo was scrubbed of 2 such rows on 2026-04-20; prod was intentionally untouched.
- [ ] **Helcim dashboard: disable the default payment-confirmation email for plans 50302 + 50303.** We send our own CRA-safe confirmation via Resend (`server/emails/paymentConfirmation.js`) triggered from `upsertPaymentFromHelcim`; leaving Helcim's default on = duplicate emails.
- [ ] **Run one real test charge on staging** via the cloudflared tunnel and verify (a) a Payment doc in Mongo with `amount`, `paymentType`, `status: 'success'`, and (b) exactly one CRA-compliant confirmation email (charity name + "not an official donation receipt" disclaimer; no banned assertive phrasing).
- [ ] **Rotate HELCIM_API_TOKEN** in the Helcim merchant portal and update the prod env var. The token was previously exposed in `window.__NUXT__` payload until today's deploy.
- [ ] **Set NUXT_RECONCILE_TOKEN** in production env (any 32+ char random string). Used as shared secret between Netlify scheduled function and the internal reconcile route.
- [ ] **Verify Netlify scheduled function `reconcile-payments` is enabled** in the Netlify dashboard. Schedule: daily.
- [ ] **Run one real test charge against the deployed app** and verify (a) a Payment doc in Mongo with `amount`, `paymentType`, `status: 'success'`, and (b) exactly one CRA-compliant confirmation email (charity name + "not an official donation receipt" disclaimer; no banned assertive phrasing).
- [ ] **Rotate HELCIM_API_TOKEN** in the Helcim merchant portal and update the Dokploy env var. The token was previously exposed in `window.__NUXT__` payload until commit `208638e`.
- [ ] **Trigger the daily reconcile task once manually** in Dokploy to confirm scheduled task + token are wired correctly. Expect a `[reconcile] done {...}` log line.
**Env vars required in production (reference):**
**Env vars required in Dokploy (reference):**
- `NODE_ENV=production`
- `BASE_URL` (exact public origin, no trailing slash)
- `MONGODB_URI`
- `JWT_SECRET` (or `NUXT_JWT_SECRET` — the `NUXT_` variant wins)
- `RESEND_API_KEY`
- `HELCIM_API_TOKEN`
- `NUXT_HELCIM_MONTHLY_PLAN_ID`
- `NUXT_HELCIM_ANNUAL_PLAN_ID`
- `SLACK_BOT_TOKEN`
- `BASE_URL`
- `OIDC_COOKIE_SECRET`
- `NUXT_HELCIM_MONTHLY_PLAN_ID=50302`
- `NUXT_HELCIM_ANNUAL_PLAN_ID=50303`
- `NUXT_PUBLIC_HELCIM_PORTAL_URL`
- `NUXT_RECONCILE_TOKEN`
- `NUXT_RECONCILE_TOKEN` (32+ char random string)
- `SLACK_BOT_TOKEN`
- `OIDC_COOKIE_SECRET`
---