13 KiB
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. 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 sameSignupFlowOverlayand call the same Helcim signup path. New members becomeactiveimmediately on payment;slackInvited=falseuntil 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 below.
Current state
- All launch code is on local
main: Helcim plan consolidation, contribution-amount redesign + migration script, cadence UX unification, receipts Phase 1, andfeature/guest-event-accounts(merged ine96d493). Not pushed — site is not deployed yet. - Helcim plan consolidation migration ran against prod 2026-04-18 (Monthly plan id
50302, Annual plan id50303). - 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-subscriptioncorrectly keeps statusactiveper 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.jsNitro plugin fails fast at boot ifMONGODB_URI/JWT_SECRET/RESEND_API_KEY/HELCIM_API_TOKENare missing — container refuses to start, so misconfig surfaces immediately in logs. BASE_URLmust exactly match the public origin (e.g.https://ghostguild.org, no trailing slash). The/api/helcim/customerorigin check atserver/api/helcim/customer.post.js:11-15does exact-match comparison against theOriginheader — ifBASE_URLis wrong or unset, signup 403s.NODE_ENV=productionmust be set. Without it:Securecookie flag, HSTS, and CSP all silently no-op.- Add a Dokploy Scheduled Task for daily reconciliation. Command:
Schedule:curl -fsS -X POST "$BASE_URL/api/internal/reconcile-payments" -H "X-Reconcile-Token: $NUXT_RECONCILE_TOKEN"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
maintoorigin/main. - Run
node scripts/migrate-contribution-amount.cjs --applyagainst prod Mongo BEFORE the new code serves traffic. Idempotent; dry-run on local counted 34 members. RequiresMONGODB_URIin env. The script writescontributionAmount(Number) derived from existingcontributionTier(String) on every Member doc; the old field is left intact for a window. - Set
NUXT_HELCIM_MONTHLY_PLAN_ID=50302in Dokploy env. - Set
NUXT_HELCIM_ANNUAL_PLAN_ID=50303in Dokploy env. - Set
NUXT_RECONCILE_TOKENto 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 --applyagainst prod Mongo AFTER the new code serves traffic to backfill Payment records for pre-existing members. Idempotent (uniquehelcimTransactionId); 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-eventregistrationswhere the registrant is not in the parent series' pass-holder list, filter toregisteredAt < 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 fromupsertPaymentFromHelcim; 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 commit208638e. - 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 inapp/middleware/coming-soon.global.js:4and is purely env-driven. Verify/,/about,/events,/boardall 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
/joinfrom 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). 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
/joinand/accept-inviteuse the sameSignupFlowOverlay, 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 that materially affect the launch-window experience. None are deploy blockers, but each shows up to real users:
/api/auth/memberreturnsslackInvited. 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.
/boardcolor-contrast fix (.block-label,.slack-handle—#746a58on#e8dfc8→ 4.01:1, needs ≥4.5:1). Single CSS-var change, currently the only red item ine2e/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=productionBASE_URL(exact public origin, no trailing slash)MONGODB_URIJWT_SECRET(orNUXT_JWT_SECRET— theNUXT_variant wins)RESEND_API_KEYHELCIM_API_TOKENNUXT_HELCIM_MONTHLY_PLAN_ID=50302NUXT_HELCIM_ANNUAL_PLAN_ID=50303NUXT_PUBLIC_HELCIM_PORTAL_URLNUXT_RECONCILE_TOKEN(32+ char random string)SLACK_BOT_TOKENOIDC_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_TOKENremoved from public runtime config + deaduseHelcim.jsdeleted. Token must be rotated post-deploy (was previously exposed viawindow.__NUXT__). - Fix #2 —
/api/helcim/customergated with origin check + per-IP/email rate limit + magic-link email verification (replaces unauthenticatedsetAuthCookie). - Fix #3 —
/api/events/[id]/paymentdeleted (dead code with auth bypass).processHelcimPaymentstub +eventPaymentSchemaremoved. - Fix #4 —
/api/helcim/initialize-paymentre-derives ticket amount server-side viacalculateTicketPrice; newseries_ticketmetadata type. - Fix #5 —
/api/helcim/customerupgrades existingstatus:guestmembers in place rather than rejecting with 409.
HIGH (correctness)
- Fix #6 — Recurring reconciliation: Netlify scheduled function calls
/api/internal/reconcile-paymentsdaily. RequiresNUXT_RECONCILE_TOKENenv var. - Fix #7 —
validateBeforeSave: falseadded 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-subscriptionleaves statusactive(per ratified bylaws); addslastCancelledAtaudit field. - Fix #10 —
/api/auth/verifyusesvalidateBodywith.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.