# Launch Readiness **Status as of 2026-05-18. Cutover has not happened yet.** Code is on local `main`; deploy steps below still need to execute. Pre-cutover deploy checklist is the live content on this page. Everything else (post-launch work, bylaws decoupling, deferred features, simplify follow-ups, a11y) lives in [`BACKLOG.md`](./BACKLOG.md). Completed launch-blocker items are archived — see `~/.claude/projects/-Users-jennie-Sites-ghostguild-org/memory/project_launch_readiness_archive.md`. --- ## Launch shape (2026-05-18) The launch decision: **site live with events ASAP, applications open immediately, Slack invitations sent later in waves.** - Anyone can hit the site, see events, buy a ticket (members and guests both supported on `main`). - Anyone can join — `/join` (anonymous) and `/accept-invite` (waitlist pre-registrants) both render the same `SignupFlowOverlay` and call the same Helcim signup path. New members become `active` immediately on payment; `slackInvited=false` until an admin marks them in a wave. - The entire waitlist is invited to apply at launch via the pre-registrant invitation tool. They go through the same flow as anonymous signups, just with email pre-filled and a token-bound pre-reg. Open decisions that gate the launch comms — see [Open decisions](#open-decisions-before-launch-comms) below. --- ## Current state - All launch code is on local `main`: Helcim plan consolidation, contribution-amount redesign + migration script, cadence UX unification, receipts Phase 1, and `feature/guest-event-accounts` (merged in `e96d493`). Not pushed — site is not deployed yet. - Helcim plan consolidation migration **ran against prod 2026-04-18** (Monthly plan id `50302`, Annual plan id `50303`). - Contribution-amount migration has **NOT** yet been run against prod. - Receipts Phase 1 code is shipped; remaining work is deploy-time only (see Deploy checklist). - `cancel-subscription` correctly keeps status `active` per ratified bylaws (Fix #9 in this doc; the stale B1 entry in BACKLOG was marked done 2026-05-18). --- ## P0 — Must fix before launch None outstanding. --- ## P1 — Strongly preferred before launch None outstanding. --- ## Deploy checklist 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 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 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. ### Activation (after Cutover passes) The site is deployed but not yet public. These are the steps that flip the switch. - [ ] **Disable the coming-soon gate.** Set `NUXT_PUBLIC_COMING_SOON=false` (or remove the var) in Dokploy and redeploy. The gate lives in `app/middleware/coming-soon.global.js:4` and is purely env-driven. Verify `/`, `/about`, `/events`, `/board` all render without a redirect when logged out. - [ ] **Publish first event(s).** Confirm at least one event or series is live and visible publicly. Walk through the guest ticket-purchase flow end-to-end (anonymous → buy ticket → registered → confirmation email). - [ ] **Pre-flight real-money signup test on prod.** Have one trusted person (ideally outside the immediate build team) go through `/join` from scratch: choose a small contribution, pay, receive welcome email, land on dashboard, see "Slack coming" note. This catches end-to-end issues that no internal test reproduces. - [ ] **Send waitlist invitation batch** via the pre-registrant admin tool. Decide cadence first (see [Open decisions](#open-decisions-before-launch-comms)). Smoke-test by inviting yourself or one friend first; only fan out once that round-trip is clean. ### Open decisions before launch comms These do not block deploy but need answers before the waitlist invite goes out. Each carries a small amount of work depending on the answer. - [ ] **Apply-framing decision.** Today's CTAs say "Join Ghost Guild" / "Become a member"; there is no "Apply" copy in the codebase. Both `/join` and `/accept-invite` use the same `SignupFlowOverlay`, so the mechanical flow is single-source. Pick one: - **A (no code work).** Keep "Join" everywhere on-site; use "apply" only in external comms (waitlist email, social, etc.). - **B (small code work).** Rename to "Apply" across CTAs + page copy. Touches `app/pages/index.vue:11`, `app/pages/about.vue:86`, `app/pages/join.vue:5,109,111,301`, `app/components/LoginModal.vue:66`, and at least the waitlist invite + welcome email copy. Likely ~30 min of search-and-replace + screenshot review. - [ ] **First Slack wave date.** A publicly-stated date or cadence rule (e.g. "end of each month"). Used in three places: waitlist invite email, welcome email, dashboard "Slack coming" note. Without this, every new member emails support asking when Slack is coming. - [ ] **Non-member event CTA — ticket-first or membership-first?** Event pages render to anonymous visitors with both paths viable. Pick which one is primary: "Buy ticket" lowers friction, "Apply for membership" protects the funnel. Write the CTA copy once and use consistently across events. - [ ] **Receipts for guest ticket purchases.** Phase 1 receipts cover membership payments only. Guest ticket buyers will get no CRA-compliant receipt at launch. Options: (a) ship a basic transactional receipt for tickets pre-launch, (b) accept the gap until Phase 2 (build June–Oct 2026, live Jan 2027). - [ ] **Waitlist invite cadence.** Single blast vs staggered (e.g., 50/day over 4 days). Trade-off is Day-1 support load — a stagger gives you time to catch real issues from early batches before the rest of the list hits. ### Pre-launch code cleanup (recommended, not blocking) Items from [`BACKLOG.md`](./BACKLOG.md) that materially affect the launch-window experience. None are deploy blockers, but each shows up to real users: - [ ] **`/api/auth/member` returns `slackInvited`.** Without this, the dashboard "Slack coming" note shows for every active member regardless of state. Highest-priority of the wave-Slack bugs because every new member sees the broken case. - [ ] **Admin members-list row reactivity** on "Mark as Slack invited" — admin has to manually reload after clicking. Hits operators, not members, but operators are us. - [ ] **`/board` color-contrast fix** (`.block-label`, `.slack-handle` — `#746a58` on `#e8dfc8` → 4.01:1, needs ≥4.5:1). Single CSS-var change, currently the only red item in `e2e/a11y.spec.js`. - [ ] **Spec vs UI mismatch on wave language.** `docs/specs/wave-based-slack-onboarding-tests.md` §7.5 says "no wave/cohort/batch language" but shipped copy uses "monthly onboarding waves." Pick a side and align before launch comms go out. **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=50302` - `NUXT_HELCIM_ANNUAL_PLAN_ID=50303` - `NUXT_PUBLIC_HELCIM_PORTAL_URL` - `NUXT_RECONCILE_TOKEN` (32+ char random string) - `SLACK_BOT_TOKEN` - `OIDC_COOKIE_SECRET` --- ## Fixed 2026-04-25 Day-of-launch security and correctness audit. All commit shas TBD until Phase 5. ### CRITICAL (security) - **Fix #1** — `HELCIM_API_TOKEN` removed from public runtime config + dead `useHelcim.js` deleted. **Token must be rotated post-deploy** (was previously exposed via `window.__NUXT__`). - **Fix #2** — `/api/helcim/customer` gated with origin check + per-IP/email rate limit + magic-link email verification (replaces unauthenticated `setAuthCookie`). - **Fix #3** — `/api/events/[id]/payment` deleted (dead code with auth bypass). `processHelcimPayment` stub + `eventPaymentSchema` removed. - **Fix #4** — `/api/helcim/initialize-payment` re-derives ticket amount server-side via `calculateTicketPrice`; new `series_ticket` metadata type. - **Fix #5** — `/api/helcim/customer` upgrades existing `status:guest` members in place rather than rejecting with 409. ### HIGH (correctness) - **Fix #6** — Recurring reconciliation: Netlify scheduled function calls `/api/internal/reconcile-payments` daily. Requires `NUXT_RECONCILE_TOKEN` env var. - **Fix #7** — `validateBeforeSave: false` added to event subdoc saves (waitlist endpoints) to dodge legacy location validators. - **Fix #8** — Series-pass purchase always creates a guest Member when caller is unauthenticated, mirroring event-ticket flow. - **Fix #9** — `cancel-subscription` leaves status `active` (per ratified bylaws); adds `lastCancelledAt` audit field. - **Fix #10** — `/api/auth/verify` uses `validateBody` with `.strict()` Zod schema. - **Fix #11** — Added 8 vitest cases for `cancel-subscription.post.js` (was uncovered). ### Side-quests - Visual audit Phase 4 changes (events/series surface) - Per-fix branch verification: see `docs/superpowers/specs/2026-04-25-fix-*.md` --- ## Manual browser tests still needed None outstanding. All launch-blocking flows verified via local dev or cloudflared tunnel with real Helcim test card + real email (see archive for the full log). The one remaining browser verification is the staging test charge bundled into the Deploy checklist above. --- ## Post-launch & deferred work Bylaws decoupling, post-launch a11y, ASVS Phase 4, deferred features, simplify-pass follow-ups, known gotchas, wave-Slack pilot follow-ups — **everything that isn't a deploy step has moved to [`BACKLOG.md`](./BACKLOG.md).**