13 KiB
Launch Readiness
Status as of 2026-04-17 (audited). Target launch: before 2026-05-01.
Single source of truth for the work that must happen before cutover to production. Anything marked P0 blocks launch. P1 is strongly preferred but survivable. Post-launch backlog lives in docs/TODO.md.
Current state
- Vitest: 577/577 passing.
- Working tree clean on
main. - Infra in place:
/api/health, CSRF + security headers + rate-limit middleware,.env.example,Dockerfile,.dockerignore, OWASP ASVS L1 phases 0–3.
P0 — Must fix before launch
1. Write Privacy Policy and Terms of Service
/policies/privacyand/policies/termscurrently render a "being finalized" placeholder viaapp/pages/policies/[slug].vue.- Community Guidelines links to both as part of the agreement members accept at signup, so they need real text before we can claim the agreement is complete.
- Fix: author both; policies live on THIS site (not Obsidian — unlike Code of Conduct and Conflict Resolution). Implementation path once copy exists: extend the
POLICIESmap in[slug].vuewith a body field, or split into dedicatedprivacy.vue/terms.vuepages. Remove the "being finalized" placeholder either way./policies/by-lawscan stay as a placeholder until launch; by-laws are referenced but not user-visible at signup.
2. get-or-create-customer creates duplicate Helcim customers ✅ 2026-04-17
server/api/helcim/get-or-create-customer.post.js- Fixed: short-circuits on
member.helcimCustomerIdviagetHelcimCustomer(), only falling through on 404 (other errors rethrow — silent recreate was the root cause). Email-search fallback now case-insensitive. Bothmember.save()calls converted tofindByIdAndUpdate(..., { runValidators: false }). - Requires a manual browser retest of the tier-upgrade flow before launch (already in the Manual browser tests list).
3. Run pre-deploy migration scripts against production ✅ 2026-04-17
- Imported 97 Baby Ghosts pre-registrants into
preregistrations(2 more than the original 95 — new signups between doc authoring and run). - Stripped stale
privacysubdocs from members via mongo shell (script skipped; target data was already empty on real users). - Dropped legacy
communityEcology/onboarding.ecologyPageVisitedfields and renamedactivitylogs.action: community_ecology_updated→board_updatedvia mongo shell (rename script skipped to avoid$renametarget-collision edge case). test@example.comremoved frompreregistrationspost-import.
P1 — Strongly preferred before launch
4. helcim/subscription.post.js Slack helper still uses member.save() ✅ 2026-04-17
server/api/helcim/subscription.post.js- Fixed: all three
member.save()calls in theinviteToSlackhelper converted toMember.findByIdAndUpdate(member._id, { $set: { ... } }, { runValidators: false }), scoped to the Slack fields each branch actually mutates. Matches the pattern applied tologin.post.js,create.post.js,verify.post.js,logout.post.js,cancel-subscription.post.js,update-contribution.post.js, andget-or-create-customer.post.js.
5. Series pass pricing mismatch ✅ 2026-04-17
server/api/series/[id]/tickets/purchase.post.js:38-45triesrequireAuththen falls back to email lookup; line 67 rejects submittedticketTypethat doesn't match the entitlement. Gap: if the client omitsticketType, no mismatch is raised, so the backend can still charge a paid cart while recording a member-tier registration.- Fixed:
seriesTicketPurchaseSchemainserver/utils/schemas.jsnow requiresticketTypeasz.enum(['member', 'public', 'guest'])(no.optional()), sovalidateBody400s any purchase that omits it — the existing mismatch check atpurchase.post.js:67now runs on every request.SeriesPassPurchase.vueupdated to sendticketType: passInfo.ticket.type(server still re-derives entitlement from the authenticated member / email lookup; the submitted value is only used for the consistency check). Schema coverage added intests/server/api/validation.test.js.
6. Set NUXT_PUBLIC_HELCIM_PORTAL_URL in production ✅ 2026-04-18
- Wired in
nuxt.config.ts:102. Without it,account.vuedoesn't render the "Manage billing in Helcim" link for members withhelcimCustomerId. - Done: set in production env 2026-04-18.
Pre-deploy runbook
Run in this order before cutover. All scripts require MONGODB_URI (production) in env:
# 1. Import pre-registrants from Baby Ghosts
BG_MONGODB_URI="<bg-uri>" MONGODB_URI="<prod-uri>" \
node server/migrations/import-babyghosts-preregistrations.js
# Verify: 95 records in `preregistrations` with status:'pending'
# 2. Remove stale privacy subdocs (per-field privacy was deleted 2026-04-15)
MONGODB_URI="<prod-uri>" node scripts/unset-member-privacy.js
# 3. Rename ecology collections → board
MONGODB_URI="<prod-uri>" node scripts/migrate-ecology-to-board.cjs
# 4. Consolidate Helcim plans (delete per-tier plans, create Monthly + Annual plans, backfill Mongo)
HELCIM_API_TOKEN="<prod-token>" MONGODB_URI="<prod-uri>" \
node scripts/helcim-plan-consolidation.js # dry-run first, inspect output
HELCIM_API_TOKEN="<prod-token>" MONGODB_URI="<prod-uri>" \
node scripts/helcim-plan-consolidation.js --confirm # destructive; copy the printed plan ids into env
Env vars required in production:
MONGODB_URIJWT_SECRET(orNUXT_JWT_SECRET— theNUXT_variant wins)RESEND_API_KEYHELCIM_API_TOKENNUXT_HELCIM_MONTHLY_PLAN_ID(set after runningscripts/helcim-plan-consolidation.js --confirm)NUXT_HELCIM_ANNUAL_PLAN_ID(set after runningscripts/helcim-plan-consolidation.js --confirm)SLACK_BOT_TOKENBASE_URLOIDC_COOKIE_SECRETNUXT_PUBLIC_HELCIM_PORTAL_URL(P1 above)
Manual browser tests still needed
These cannot be verified by the Vitest suite — all require a real browser + real Helcim test card + real email.
- Paid-tier join flow end-to-end (blocked on localhost by HelcimPay.js iframe
X-Frame-Options: sameorigin; use cloudflared tunnel or ngrok HTTPS). ✅ 2026-04-18 — verified via tunnel; two bugs found and fixed along the way (missingtvin JWT payload, missing welcome email). - Event ticket purchase with payment (same iframe limitation).
- Tier upgrade free → paid. Account → tier change →
/member/payment-setup?tier=&circle=→ Helcim $0 verify → subscription created. ✅ 2026-04-18 — verified via tunnel; one bug found and fixed (ColumnsLayout cols="1"silently dropped default slot onpayment-setup.vue). - Paid → free downgrade. Confirms Helcim subscription is cancelled.
- Paid → paid tier swap. Confirms existing subscription is updated, not recreated.
- Annual join end-to-end. New paid signup picks annual cadence at $15 tier, completes payment, Mongo record has
billingCadence: 'annual'and Helcim sub is underAnnual MembershipwithrecurringAmount: 150. - Annual tier swap within-cadence. Annual $15 member changes to $50 annual,
updateHelcimSubscriptioncalled with{ recurringAmount: 500 }, MongocontributionTierupdated,billingCadenceunchanged. - Pre-registrant invite → accept flow with paid tier (exercises Helcim customer creation during acceptance).
- Magic-link login including 15-min expiry and jti burn on reuse.
- Guest event signup — four branches: new email + consent, new email without consent, existing guest, existing active member. Confirms cookie only sets for new/guest, and confirmation email appends
/loginlink for real members. - Mobile responsive layout — sidebar hides ≤1024px, nav works on phone.
--text-dim/--text-faintWCAG AA contrast check.
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). 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_paymentand tier to'0'. Under the new model, cancelling a payment plan moves the member to the $0 tier — status should stayactive. - Fix: change
status: 'pending_payment'→status: 'active'in both thefindByIdAndUpdatepayload (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.jsif it doesn't already exist.
B2. Server doesn't gate member-only events by member.status ✅ 2026-04-18
- Fixed: introduced
hasMemberAccess(member)helper inserver/utils/tickets.js— returns true only forstatus∈ {active,pending_payment}. Used at every server-side enforcement surface:server/utils/tickets.js:calculateTicketPrice,calculateSeriesTicketPrice, andvalidateTicketPurchase(membersOnly gate) all route the member throughhasMemberAccessbefore granting member-tier pricing or access.server/api/events/[id]/register.post.js: replaces the bareif (!member)gate withhasMemberAccess(member)for both the membersOnly check and the payment-required check;isMember/membershipLevelfollow suit.server/api/events/[id]/tickets/purchase.post.js: replaces localisRealMember(which only excluded guests) with sharedhasMemberAccess.server/api/series/[id]/tickets/purchase.post.js: derivesaccessMemberand uses it for validation, registrationisMember, andmembershipLevel.
- Test coverage: 12 new cases in
tests/server/utils/tickets.test.jscovering pricing and membersOnly gating across all four inactive statuses (guest, suspended, cancelled, plus pending_payment as the access-conferring counterpart).tests/server/api/event-registration.test.jsupdated to assert the shared helper.baseMember()test factory now defaults tostatus: 'active'so the existing happy-path coverage remains valid. - Net behavior:
pending_paymentkeeps member access (payment is decoupled per bylaws).guest,suspended,cancelledare uniformly rejected from member-only events at the API and pay public pricing on open events.tickets/available.get.jsdid not need direct changes — it callscalculateTicketPrice(event, member)which now applieshasMemberAccessinternally, so a suspended/cancelled lookup automatically returns public pricing. The cosmeticmemberSavingsblock atavailable.get.js:115will report$0 savedfor inactive members — harmless, but worth a follow-up to suppress the comparison block when!hasMemberAccess(member)if it ever surfaces in UI.
B3. Vestigial pending_payment status
- Once payment is fully decoupled,
pending_paymentno longer gates anything and is functionally equivalent toactive. Consider removing it from the enum (server/models/member.js:38,server/utils/schemas.js:299) and treating new signups asactivefrom 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 existingpending_paymentrows toactive. - Larger refactor — break out into its own ticket once B1/B2 land.
B4. Admin "Pending Payment" filter label (cosmetic)
app/pages/admin/members/index.vue:45,499,[id].vue:69showpending_paymentas "Pending Payment". If B3 removes the status entirely, this disappears too. If we keeppending_paymentfor now, rename in admin UI to "Payment setup incomplete" so admins also stop conflating it with membership state.
Stale TODOs in source (non-blocking)
server/api/events/[id]/tickets/purchase.post.jsasymmetry vs. series endpoint confirmed safe 2026-04-17. No P1-equivalent pricing bypass:ticketPurchaseSchemaaccepts noticketType, so the server is authoritative —calculateTicketPrice(event, member)derives the tier from theMemberdoc looked up by submitted email, withisRealMemberexcludingstatus:"guest". Submitting another member's email registers the ticket under their email (not the attacker's), sends the confirmation to them, and the auto-login guard at lines 141-148 explicitly refuses to issue a cookie for a real member, so the attacker gains no ticket, no cookie, and no email. Residual griefing concern (forcing an unsolicited registration onto another member's email) is not a pricing bypass and is out of scope.
Post-launch backlog
See docs/TODO.md for:
- Button minimum target size (WCAG AAA 2.5.5).
/oidc/interaction/[uid]routing quirk.- Admin layout migration from
guild-*tokens to zine spec. - Admin dashboard quick-action button contrast.
- Members table NAME column clipping.
- OWASP ASVS L1 Phase 4 (file-upload validation pipeline, granular RBAC, credential encryption).