chore(deploy): retarget launch checklist from Netlify to Dokploy on Hetzner
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:
parent
8e76ce9366
commit
c149fba13a
3 changed files with 31 additions and 68 deletions
|
|
@ -30,32 +30,48 @@ None outstanding.
|
||||||
|
|
||||||
## Deploy checklist
|
## 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`.
|
- [ ] 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.
|
- [ ] **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_MONTHLY_PLAN_ID=50302` in Dokploy env.
|
||||||
- [ ] Set `NUXT_HELCIM_ANNUAL_PLAN_ID=50303` in production env.
|
- [ ] Set `NUXT_HELCIM_ANNUAL_PLAN_ID=50303` in Dokploy 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_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.
|
- [ ] **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.
|
- [ ] **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).
|
- [ ] **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 prod env var. The token was previously exposed in `window.__NUXT__` payload until today's deploy.
|
- [ ] **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`.
|
||||||
- [ ] **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.
|
- [ ] **Trigger the daily reconcile task once manually** in Dokploy to confirm scheduled task + token are wired correctly. Expect a `[reconcile] done {...}` log line.
|
||||||
- [ ] **Verify Netlify scheduled function `reconcile-payments` is enabled** in the Netlify dashboard. Schedule: daily.
|
|
||||||
|
|
||||||
**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`
|
- `MONGODB_URI`
|
||||||
- `JWT_SECRET` (or `NUXT_JWT_SECRET` — the `NUXT_` variant wins)
|
- `JWT_SECRET` (or `NUXT_JWT_SECRET` — the `NUXT_` variant wins)
|
||||||
- `RESEND_API_KEY`
|
- `RESEND_API_KEY`
|
||||||
- `HELCIM_API_TOKEN`
|
- `HELCIM_API_TOKEN`
|
||||||
- `NUXT_HELCIM_MONTHLY_PLAN_ID`
|
- `NUXT_HELCIM_MONTHLY_PLAN_ID=50302`
|
||||||
- `NUXT_HELCIM_ANNUAL_PLAN_ID`
|
- `NUXT_HELCIM_ANNUAL_PLAN_ID=50303`
|
||||||
- `SLACK_BOT_TOKEN`
|
|
||||||
- `BASE_URL`
|
|
||||||
- `OIDC_COOKIE_SECRET`
|
|
||||||
- `NUXT_PUBLIC_HELCIM_PORTAL_URL`
|
- `NUXT_PUBLIC_HELCIM_PORTAL_URL`
|
||||||
- `NUXT_RECONCILE_TOKEN`
|
- `NUXT_RECONCILE_TOKEN` (32+ char random string)
|
||||||
|
- `SLACK_BOT_TOKEN`
|
||||||
|
- `OIDC_COOKIE_SECRET`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
13
netlify.toml
13
netlify.toml
|
|
@ -1,13 +0,0 @@
|
||||||
[build]
|
|
||||||
command = "npm run build"
|
|
||||||
publish = "dist"
|
|
||||||
|
|
||||||
[functions]
|
|
||||||
directory = "netlify/functions"
|
|
||||||
|
|
||||||
# Daily reconciliation cron — invokes the protected Nitro route to upsert
|
|
||||||
# Payment docs from Helcim transactions. Schedule is also pinned inline in
|
|
||||||
# netlify/functions/reconcile-payments.mjs (Netlify accepts either; both is
|
|
||||||
# harmless).
|
|
||||||
[functions."reconcile-payments"]
|
|
||||||
schedule = "@daily"
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
/**
|
|
||||||
* Netlify scheduled function — daily reconciliation of Helcim payments.
|
|
||||||
*
|
|
||||||
* Calls the protected Nitro route `/api/internal/reconcile-payments` with the
|
|
||||||
* shared-secret header. Heavy lifting (Mongo queries, Helcim API calls, retry
|
|
||||||
* logic) lives in the Nitro handler so it can use auto-imported utils.
|
|
||||||
*
|
|
||||||
* Required env (set in Netlify dashboard):
|
|
||||||
* - URL (set automatically by Netlify)
|
|
||||||
* - RECONCILE_TOKEN (must match NUXT_RECONCILE_TOKEN in Nitro runtime config)
|
|
||||||
*
|
|
||||||
* Schedule: @daily (00:00 UTC). Also pinned in netlify.toml.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default async () => {
|
|
||||||
const url = `${process.env.URL}/api/internal/reconcile-payments`
|
|
||||||
const token = process.env.RECONCILE_TOKEN
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
const msg = '[reconcile] RECONCILE_TOKEN not configured; aborting'
|
|
||||||
console.error(msg)
|
|
||||||
return new Response(msg, { status: 500 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'X-Reconcile-Token': token }
|
|
||||||
})
|
|
||||||
const body = await res.text()
|
|
||||||
if (!res.ok) {
|
|
||||||
console.error('[reconcile] route failed', res.status, body)
|
|
||||||
return new Response(body, { status: res.status })
|
|
||||||
}
|
|
||||||
console.log('[reconcile] ok', body)
|
|
||||||
return new Response(body, { status: 200 })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
schedule: '@daily'
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue