ghostguild-org/docs/LAUNCH_READINESS.md

15 KiB

Launch Readiness

Status as of 2026-04-20. Target launch: before 2026-05-01.

Single source of truth for work remaining before cutover. P0 blocks launch; P1 is strongly preferred but survivable. Completed items are archived — see ~/.claude/projects/-Users-jennie-Sites-ghostguild-org/memory/project_launch_readiness_archive.md. Post-launch backlog lives in docs/TODO.md.


Current state

  • Vitest snapshot 2026-04-25 ~18:23 local: 703 passing / 8 failing / 2 skipped (713 total). The previously-flagged 6 helcim-payment failures are now green. The 8 current failures are in tests/server/api/auth-verify.test.js and tests/server/api/cancel-subscription.smoke.test.js, both belonging to in-flight Phase 5 fixes (#10 and #9) being landed by parallel impl subagents — they will resolve as those branches merge.
  • All launch code is on local main: Helcim plan consolidation, contribution-amount redesign, cadence UX unification, and receipts Phase 1. Not pushed — site is not on Netlify 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).

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.

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 #1HELCIM_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 #7validateBeforeSave: 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 #9cancel-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.


Bylaws decoupling — follow-ups (added 2026-04-18)

Context: bylaws are being amended to remove automatic termination for nonpayment. Membership status will be fully decoupled from payment status; failed payments trigger committee outreach, not status change. Copy + UI access gates already aligned in useMemberStatus.js and account.vue (2026-04-18). Server-side status gating shipped as B2 (see archive). The behavioral changes below remain.

Not blocking launch — the amendment hasn't passed yet, and the user-visible copy/UI is already consistent. Pick up once the amendment is ratified.

B1. cancel-subscription flips status to pending_payment

  • server/api/members/cancel-subscription.post.js:31,48
  • When a member cancels their paid subscription, status is set to pending_payment and contribution amount to 0. Under the new model, cancelling a payment plan moves the member to the $0 contribution — status should stay active.
  • Fix: change status: 'pending_payment'status: 'active' in both the findByIdAndUpdate payload (line 31) and the response (line 48). Comment at line 26 also needs updating ("(not cancelled) so member can re-subscribe" → reflect new framing).
  • Add coverage in tests/server/api/cancel-subscription.test.js if it doesn't already exist.

B3. Vestigial pending_payment status

  • Once payment is fully decoupled, pending_payment no longer gates anything and is functionally equivalent to active. Consider removing it from the enum (server/models/member.js:38, server/utils/schemas.js:299) and treating new signups as active from the moment of account creation.
  • Touches: signup flow (helcim/customer.post.js:34, invite/accept.post.js:48), admin filter UI (app/pages/admin/members/index.vue:45,382,499,1145, [id].vue:69,286), admin alerts (server/utils/adminAlerts.js:22,100-116, server/models/adminAlertDismissal.js:6), and a data migration to flip existing pending_payment rows to active.
  • Larger refactor — break out into its own ticket once B1 lands.

B4. Admin "Pending Payment" filter label (cosmetic)

  • app/pages/admin/members/index.vue:45,499, [id].vue:69 show pending_payment as "Pending Payment". If B3 removes the status entirely, this disappears too. If we keep pending_payment for now, rename in admin UI to "Payment setup incomplete" so admins also stop conflating it with membership state.

Post-launch backlog

See docs/TODO.md for:

  • Button minimum target size (WCAG AAA 2.5.5).
  • /oidc/interaction/[uid] routing quirk — fixed 2026-04-29 (commit 23154ff); root cause was oidc-provider's devInteractions overriding our custom interactions.url.
  • Admin layout migration from guild-* tokens to zine spec — verified clean 2026-04-29; grep for guild-[0-9]|candlelight-[0-9]|ember-[0-9] across app/layouts/, app/pages/admin/, app/components/admin/ returns zero matches. All tokens already converted.
  • Admin dashboard quick-action button contrast — verified stale 2026-04-29.
  • Members table NAME column clipping — verified stale 2026-04-29.
  • OWASP ASVS L1 Phase 4 (file-upload validation pipeline, granular RBAC, credential encryption).
  • tickets/available.get.js:115 memberSavings block reports $0 saved for inactive members — fixed 2026-04-29 (commit f66455e); memberSavings now gated on hasMemberAccess(member).
  • Simplify-pass follow-ups (2026-04-25): SHIPPED 2026-04-27 on branch chore/simplify-pass-follow-ups (pending merge). See ~/.claude/projects/-Users-jennie-Sites-ghostguild-org/memory/project_simplify_pass_2026_04_25.md.
  • Reconcile customerCode bug — fixed on main in commit 3c38333 ("pass customerCode (not helcimCustomerId) to Helcim transactions API"). Verified in server/api/internal/reconcile-payments.post.js:97.
  • Drive-by from 2026-04-29 phantom-Tailwind sweep: app/components/EventSeriesBadge.vue has zero usages — deleted 2026-04-29 (commit f85f284); 81 lines removed.
  • Simplify-pass follow-ups (2026-04-29): smallest wins shipped in commit 26791cc; deferred items (rename setPaymentBridgeCookie, dedup admin STATUS_LABELS, extract .tint-candle/.tint-ember utilities, audit member && truthy checks in sibling routes, restore ImageUpload alt-text input focus styling) tracked in docs/TODO.md § Simplify-pass follow-ups — 2026-04-29.

Known gotchas worth addressing post-launch

  • Admin edit does not sync Helcim recurringAmount. /admin/members/[id] PUT writes contributionAmount direct to Mongo by design. Admins must PATCH Helcim manually. Worth surfacing in admin UI or docs.
  • Cadence switch rejected on active subscriptions. update-contribution.post.js:184-189 refuses cadence changes mid-subscription; no UI toggle exists on /member/account. Adding cadence switch would require a Helcim subscription replacement flow, not a plain update.
  • S2 test fixture id/slug inconsistency. (Local dev only.) Seeded S2 series has id: 'test-s2-drop-in-allowed' but slug: 'test-s2-drop-in-allowed-series'. Doesn't affect prod — fix the seed script if anyone re-runs fixtures and is confused why id-based Mongo queries return empty.

Events-surface visual audit — deferred items (2026-04-21)

Context: Phase 4 audit against docs/specs/events-visual-audit-findings.md fixed all critical phantom-palette, rounded-corner, CTA-mismatch, and input-styling issues across EventTicketCard, EventTicketPurchase, EventSeriesTicketCard, SeriesPassPurchase. Items below were explicitly deferred or out of reach.

  • Success-state color convention (4 instances). Resolved 2026-04-29: gold (--candle) chosen as zine-consistent. Phantom-Tailwind cleanup shipped in dc2becf (EventSeriesTicketCard.vue + SeriesPassPurchase.vue member-benefit notice).
  • Sidebar breakpoint unverified. Verified clean 2026-04-29 — .events-mini hides at ≤1024px cleanly across 1023/1024/1025/1100. Actual rule lives in EventsMiniSidebar.vue:129 + ColumnsLayout.vue:83 (audit doc cited the wrong line).
  • EventTicketPurchase.vue:469 magic padding. Fixed 2026-04-29 (commit 7e44809); consent block now uses a grid approach.
  • .section-label extraction candidate. Verified 2026-04-29 — utility already exists at main.css:128 and is used in 30+ places. Two scoped overrides intentionally diverge.
  • Past-events toggle component. Audited 2026-04-29 — consistent with the design system (dashed-border button, gold active state, valid aria-pressed toggle). Added missing :focus-visible outline in commit dadec1a; no other changes warranted.

Contribution-amount redesign — cosmetic cleanup (naming only, not behavior)

SHIPPED 2026-04-29 in commit 955217a (admin column header, dropdown labels, handler rename, log message).