learning_ai_common_plat/docs/PRE_LAUNCH_SIGNUP_SYSTEM.md

43 KiB
Raw Blame History

Pre-Launch Signup System — Design & Roadmap

Service: platform-service (port 4003) Module: src/modules/waitlist/ Cosmos Container: waitlist (partition key: /email) Status: Planning


0. Review Findings & Gaps Addressed

This section documents bugs, gaps, and inconsistencies found during systematic review of this design against the actual platform-service codebase.

0.1 Data Model Bugs

# Issue Severity Resolution
D1 Cosmos partition key mismatch — doc says waitlist container uses /productId, but WaitlistEntryDoc lookups are primarily by email+productId. High-cardinality partition key /productId means all entries for a product land in one logical partition → hot partition under load. High Changed to /email partition key. Cross-partition queries (admin list-all) use Cosmos cross-partition fan-out which is acceptable for admin reads.
D2 Missing emailNormalized field — emails are case-insensitive but Cosmos queries are case-sensitive. Without normalization, User@Gmail.com and user@gmail.com create duplicate entries. High Added emailNormalized field (lowercase, trimmed). Dedupe queries use this field.
D3 position race conditiongetNextPosition() is not atomic in Cosmos DB. Two concurrent signups can get the same position number. Cosmos has no AUTO_INCREMENT. Medium Documented: use Cosmos stored procedure for atomic increment, or accept approximate positions and re-sequence periodically via admin endpoint.
D4 customData: Record<string, unknown> — no validation against prelaunchConfig.customFields. Malicious clients can submit arbitrary keys/huge payloads. Medium Added: route must validate customData keys against product's customFields schema, reject unknown keys, enforce max payload size.
D5 Missing invitedAt field — when entry transitions pending → invited, there's no timestamp for when the invite was sent. Needed for invite expiry tracking. Low Added invitedAt?: string to WaitlistEntryDoc.
D6 Missing convertedAt field — same gap for invited → converted transition. Low Added convertedAt?: string to WaitlistEntryDoc.

0.2 API / Security Gaps

# Issue Severity Resolution
A1 Status check leaks position by emailGET /public/waitlist/:productId/status?email=x lets anyone check if an email is on the waitlist (enumeration attack). High Changed: require both email + unsubscribeToken to check status. Alternatively, return only a success/not-found boolean without position details for unauthenticated requests.
A2 No admin auth enforcement documented — admin endpoints say "JWT Auth Required" but the doc doesn't specify role check. Any authenticated user (including regular users) could hit admin endpoints. High Added: admin endpoints must check req.jwtPayload.role === 'admin'. Added guard task to roadmap.
A3 Unsubscribe via DELETE with token in query param — tokens in URLs get logged in server access logs, CDN logs, browser history. Medium Changed to POST /public/waitlist/unsubscribe with token in request body.
A4 No email format validation mentioned — public signup endpoint must validate email format before storing. Medium Added: Zod .email() validation in JoinWaitlistSchema.
A5 No CAPTCHA / bot protection — rate limiting alone isn't enough for public signup. Bots can rotate IPs. Medium Added task: integrate turnstile/hCaptcha/reCAPTCHA token validation on signup endpoint.
A6 ipAddress storage without consent — storing IP addresses has GDPR implications. Low Added: note IP is hashed (SHA-256) before storage, not stored raw.

0.3 Integration Gaps

# Issue Severity Resolution
I1 getRequestProductId() blocks draft and sunset but not pre_launch — current code at request-context.ts:59 only blocks status === 'disabled'. After adding new statuses, draft products would still accept API calls (including creating subscriptions, etc.). High Task 1.4 updated: getRequestProductId() must block draft, sunset, disabled. Allow pre_launch only for waitlist routes, beta only for invited users, active for all.
I2 Missing containers in cosmos-init.ts — the existing CONTAINER_DEFS doesn't include containers used by other modules: invitation_codes, referrals, promo_codes, subscriptions, payments, licenses, plans, usage_daily, api_tokens, tracker_items, comments, votes, themes. These are created on-demand by getContainer() but won't be auto-initialized. Medium Added task: audit and add all missing containers to cosmos-init.ts CONTAINER_DEFS (separate from waitlist work but prerequisite).
I3 No webhook for waitlist.joined — existing webhook system supports invitation.redeemed and referral.status_changed but nothing for waitlist events. Admins may want Slack/email notifications when someone joins. Medium Added dispatchWaitlistJoined webhook + env var WEBHOOK_WAITLIST_JOINED_URL to roadmap.
I4 Batch invite has no idempotency — if the admin clicks "invite 50" twice, 100 invites go out. No guard against re-inviting already-invited entries. Medium Added: batch invite must filter status === 'pending' only, and the endpoint should return how many were already invited (skipped).
I5 Referral integration uses referrals/ module but waitlist referrals are different — existing referrals/ tracks userId→userId referrals with reward tokens. Waitlist referrals are email→email with position boost. Overloading the same module creates confusion. Medium Clarified: waitlist referrals are self-contained within the waitlist/ module (referredBy field). The existing referrals/ module is for post-signup user referrals. Phase 3 updated to reflect this.

0.4 Roadmap Task Gaps

# Issue Resolution
R1 No task for adding waitlist to cosmos-init.ts CONTAINER_DEFS Was in 2.4 text but partition key path was wrong. Fixed.
R2 No task for adding admin role guard middleware Added task 2.7.
R3 No task for email normalization logic Added to task 2.2.
R4 No task for CAPTCHA/bot protection Added task 7.7.
R5 No task for product status transition validation (e.g. can't go draft → active skipping pre_launch) Added task 1.7.
R6 No task for backward compatibility — existing products have status: 'active' | 'disabled'. A migration or default mapping is needed. Added task 1.8.
R7 Phase 3 conflates waitlist referrals with existing referrals/ module Rewritten to be self-contained in waitlist module.
R8 No task for webhook waitlist.joined event Added task 2.8.
R9 No task for prelaunchConfig public endpoint — frontend needs to fetch custom fields to render the form, but no public API exposes it. Added task 2.9.
R10 Effort table section numbered "7" collides with Phase 7 in section 6 Renumbered remaining sections: 7→8→9→10.

1. Problem Statement

When a new product (e.g. LysnrAI, MindLyst, or any future ByteLyst product) is being built, we need a way to:

  • Collect interest signups (email + custom data) before launch
  • Show a public-facing signup page scoped to a specific productId
  • Support custom form fields per product (use case, role, platform preference, etc.)
  • Track referral chains for viral waitlist growth
  • Let admins manage, prioritize, and batch-invite waitlist entries
  • Transition smoothly from pre-launch → beta → general availability

The system must be product-agnostic — the same module serves every product in the ByteLyst ecosystem.


2. Product Status Lifecycle (Extended)

Current ProductDoc.status only supports active | disabled. This needs expansion:

draft → pre_launch → beta → active → sunset → disabled
Status Public Signup? Product Usable? Description
draft No No Product created, not publicly visible
pre_launch Yes No Waitlist page is live, product not ready
beta Invite-only Yes (limited) Early access via invitation codes
active Open Yes Generally available
sunset No Yes (existing) No new signups, existing users continue
disabled No No Fully offline

3. Data Model

3.1 WaitlistEntryDoc

interface WaitlistEntryDoc {
  id: string; // "wl_<uuid>"
  productId: string; // scoped to product
  email: string; // original casing as entered
  emailNormalized: string; // lowercase + trimmed (used for dedupe queries)
  name?: string; // optional display name
  source: 'organic' | 'referral' | 'social' | 'ad' | 'api';
  referredBy?: string; // waitlist entry ID of referrer
  status: 'pending' | 'invited' | 'converted' | 'unsubscribed';
  position: number; // queue position (auto-assigned, see §0.1 D3)
  priority: number; // boost score (referrals bump this)
  customData: Record<string, unknown>; // validated against product's customFields schema
  invitationCodeId?: string; // links to invitations/ module when invited
  invitedAt?: string; // timestamp when invite was sent
  convertedAt?: string; // timestamp when invite was redeemed
  ipHash?: string; // SHA-256 of IP (not raw IP, GDPR-safe)
  utmSource?: string; // auto-captured from query params
  utmMedium?: string;
  utmCampaign?: string;
  unsubscribeToken: string; // for one-click unsubscribe (in POST body, not URL)
  createdAt: string;
  updatedAt: string;
}

Partition key rationale: /email distributes entries across logical partitions evenly. Admin list-all queries are cross-partition (acceptable for low-frequency admin reads). Deduplication is a point-read by emailNormalized + productId within the same partition.

Position assignment: Cosmos DB has no AUTO_INCREMENT. Options:

  1. Stored procedure for atomic counter (strongest guarantee)
  2. SELECT VALUE MAX(c.position) + 1 (acceptable with low concurrency)
  3. Accept approximate positions, admin re-sequences periodically

3.2 PrelaunchConfig (on ProductDoc)

interface PrelaunchConfig {
  signupEnabled: boolean; // master toggle
  launchDate?: string; // estimated launch date (shown on page)
  tagline?: string; // short description shown on signup page
  logoUrl?: string; // product logo for signup page
  customFields: CustomField[]; // dynamic form schema
  confirmationMessage?: string; // e.g. "You're #{{position}} on the waitlist!"
  redirectUrl?: string; // redirect after signup
  maxSignups?: number; // cap waitlist size (null = unlimited)
  captchaEnabled: boolean; // require CAPTCHA on public signup
  captchaProvider?: 'turnstile' | 'hcaptcha' | 'recaptcha';
}

interface CustomField {
  key: string; // e.g. "company_size", "use_case"
  label: string; // "What's your company size?"
  type: 'text' | 'email' | 'select' | 'multiselect' | 'textarea' | 'number' | 'checkbox';
  options?: string[]; // for select/multiselect types
  required: boolean;
  placeholder?: string;
  maxLength?: number; // for text/textarea (default: 500)
}

3.3 Pre-Launch Data Collection Checklist

Data Point Why Required?
Email Primary contact, launch notification Always
Name Personalized communications Optional
UTM params Track which channels drive signups Auto
Referral source Viral loop tracking Auto
Use case (custom) Prioritize features, segment beta invites Per-product
Role / title (custom) B2B targeting Per-product
Company size (custom) Enterprise vs. indie segmentation Per-product
Platform preference (custom) iOS / Android / Web / Desktop priority Per-product
Willing to beta test? (custom) Beta cohort selection Per-product
How did you hear about us? Attribution beyond UTM Per-product

4. API Endpoints

4.1 Public Endpoints (No Auth, Rate-Limited)

Method Path Rate Limit Description
POST /public/waitlist/:productId 10/min/IP Join waitlist (email + custom fields + optional CAPTCHA token)
POST /public/waitlist/:productId/status 30/min/IP Check position (email + unsubscribeToken in body, not query param — prevents enumeration)
GET /public/waitlist/:productId/count 60/min/IP Total signups (social proof number)
GET /public/waitlist/:productId/config 60/min/IP Get prelaunchConfig (custom fields schema, tagline, launch date — needed by frontend to render form)
POST /public/waitlist/unsubscribe 10/min/IP Unsubscribe (token in POST body, not URL — avoids token leaking to logs/CDN)

4.2 Admin Endpoints (JWT Auth Required, role === 'admin')

All admin endpoints must verify req.jwtPayload.role === 'admin' — not just valid JWT.

Method Path Description
GET /api/waitlist List entries (paginated, filterable by status/source/date)
GET /api/waitlist/stats Signup stats (by day, source, custom field breakdown)
GET /api/waitlist/:id Get single entry
PUT /api/waitlist/:id Update entry (change priority, status)
DELETE /api/waitlist/:id Delete entry (creates audit log entry)
POST /api/waitlist/invite Batch-invite top N (idempotent: skips already-invited, returns skip count)
POST /api/waitlist/export Export as CSV (creates audit log entry)
POST /api/waitlist/resequence Re-calculate position numbers (fix gaps from deletes/unsubscribes)

5. Integration with Existing Modules

User visits pre-launch page
    │
    ├─► GET /public/waitlist/:productId/config
    │     └─► products/ ── verify status is `pre_launch` + signupEnabled
    │     └─► Return customFields schema, tagline, launchDate (frontend renders form)
    │
    ├─► POST /public/waitlist/:productId (submit form)
    │     ├─► flags/ ── check `pre_launch_signup_enabled` flag
    │     ├─► products/ ── verify product status is `pre_launch`
    │     ├─► CAPTCHA ── validate turnstile/hcaptcha token (if captchaEnabled)
    │     ├─► Validate customData keys against product's customFields schema
    │     ├─► Dedupe ── check emailNormalized + productId (return existing if found)
    │     ├─► waitlist/ ── store entry, assign position, capture UTM + custom data
    │     ├─► waitlist/ ── if ?ref=<entryId>, set referredBy, bump referrer priority
    │     ├─► webhooks/ ── fire `waitlist.joined` event (async, non-blocking)
    │     └─► Return { position, unsubscribeToken, referralLink }
    │
    └─► (Later) Admin triggers batch invite from admin dashboard
            │
            ├─► POST /api/waitlist/invite { count, strategy }
            │     ├─► Filter only `status === 'pending'` entries (idempotent)
            │     ├─► invitations/ ── auto-generate invite codes for selected entries
            │     ├─► waitlist/ ── mark entries `invited`, set invitationCodeId + invitedAt
            │     ├─► notifications/ ── queue email with invite code + instructions
            │     ├─► audit/ ── log admin action
            │     └─► Return { invited: N, skipped: N (already invited) }
            │
            └─► (User redeems invite code)
                  ├─► invitations/ ── redeem code (existing flow)
                  ├─► webhooks/ ── existing `invitation.redeemed` fires
                  └─► waitlist/ ── callback marks entry `converted`, sets convertedAt

Note on referrals: Waitlist referrals (referredBy field, priority boost) are self-contained within the waitlist/ module. The existing referrals/ module is for post-signup user-to-user referrals with reward tokens — different lifecycle.


6. Implementation Roadmap

Phase 1 — Product Status Lifecycle Extension

  • 1.1 Extend ProductDoc.status enum from 'active' | 'disabled' to 'draft' | 'pre_launch' | 'beta' | 'active' | 'sunset' | 'disabled'66e657a
    • File: services/platform-service/src/modules/products/types.ts
  • 1.2 Update CreateProductSchema and UpdateProductSchema with new status values — 66e657a
    • File: services/platform-service/src/modules/products/types.ts
  • 1.3 Add optional prelaunchConfig field to ProductDoc66e657a
    • Include: signupEnabled, launchDate, customFields[], confirmationMessage, redirectUrl, maxSignups, captchaEnabled, captchaProvider, tagline, logoUrl
  • 1.4 Update getRequestProductId() status gating logic — 66e657a
    • File: services/platform-service/src/lib/request-context.ts
    • Blocks: draft, sunset, disabled
    • Added getRequestProductIdForPublic() that permits pre_launch
  • 1.5 Update product cache to include new fields (prelaunchConfig) — 66e657a
    • Cache auto-includes all ProductDoc fields (no changes needed to cache.ts)
  • 1.6 Write tests for product status transitions — 2692c91
    • Tests in: services/platform-service/src/modules/waitlist/waitlist.test.ts (isValidStatusTransition tests)
  • 1.7 Add product status transition validation — 66e657a
    • isValidStatusTransition(current, next) helper in products/types.ts
    • Validation enforced in PUT /products/:id route
    • Transitions: draft→pre_launch→beta→active→sunset→disabled, disabled→draft
  • 1.8 Backward compatibility migration for existing products — 66e657a
    • Existing 'active' | 'disabled' values are valid members of the new enum
    • No data migration needed (new statuses are additive)

Phase 2 — Waitlist Module (Core)

  • 2.1 Create src/modules/waitlist/types.ts2692c91
    • WaitlistEntryDoc interface (including emailNormalized, invitedAt, convertedAt, ipHash)
    • JoinWaitlistSchema, CheckStatusSchema, UnsubscribeSchema, UpdateWaitlistEntrySchema, WaitlistQuerySchema, BatchInviteSchema
  • 2.2 Create src/modules/waitlist/repository.ts2692c91
    • All functions: create, getById, getByEmail, getByUnsubscribeToken, list, update, remove, count, getNextPosition, getByStatus, unsubscribe, stats, normalizeEmail
    • Note: resequence() deferred to Phase 7 (not critical for MVP)
  • 2.3 Create src/modules/waitlist/routes.ts2692c91
    • 5 public endpoints + 7 admin endpoints implemented
    • CustomData validation against product's customFields schema
    • IP hashing (SHA-256, truncated), email normalization, dedup
    • Note: POST /api/waitlist/resequence deferred to Phase 7
  • 2.4 Register waitlist container in src/lib/cosmos-init.ts2692c91
    • Container: waitlist, partition key: /email
    • Also added 13 previously missing containers to CONTAINER_DEFS
  • 2.5 Register routes in src/server.ts2692c91
  • 2.6 Write unit tests (45 tests passing) — 6a996cc
    • File: src/modules/waitlist/waitlist.test.ts
    • Schema validation, email normalization, status transitions, route exports
  • 2.7 Add admin role guard to all admin endpoints — 2692c91
    • requireAdmin(req) helper throws ForbiddenError if role !== 'admin'
  • 2.8 Add waitlist.joined webhook event — 2692c91
    • dispatchWaitlistJoined() in src/lib/webhooks.ts
    • Env var WEBHOOK_WAITLIST_JOINED_URL added to .env.example
  • 2.9 Add public product config endpoint — 2692c91
    • GET /public/waitlist/:productId/config — strips maxSignups + captchaProvider from response

Phase 3 — Waitlist Referral Loop (Self-Contained)

Note: This is separate from the existing referrals/ module which handles post-signup user-to-user referrals with reward tokens. Waitlist referrals are simpler: email→email with priority boost, entirely within the waitlist/ module.

  • 3.1 Add ref query param support to POST /public/waitlist/:productId2692c91
    • Lookup referrer by waitlist entry ID
    • Set referredBy on new entry, set source: 'referral'
    • Bump referrer's priority score by +1
    • Guard: referrer must exist, same productId, and status === 'pending'
  • 3.2 Add referral stats to waitlist stats endpoint
    • Track: signups via referral, top referrers (by count), referral chain depth
  • 3.3 Generate shareable referral link per waitlist entry — 2692c91
    • Returns referralLink: '?ref=<entryId>' in join response
    • Full URL construction left to frontend (product-specific domain)
  • 3.4 Add referral leaderboard endpoint (optional)
    • GET /public/waitlist/:productId/top-referrers — top 10 referrers (anonymized: first name + initial)
  • 3.5 Write referral integration tests
    • Test: circular referral prevention (A refers B, B can't refer A)
    • Test: self-referral prevention
    • Test: referrer priority increment

Phase 4 — Batch Invite Flow

  • 4.1 Implement POST /api/waitlist/invite admin endpoint — 2692c91
    • Input: { count: number, strategy: 'fifo' | 'priority' | 'random' }
    • Selects only status === 'pending' entries (idempotent)
    • Marks entries invited + sets invitedAt
    • Returns: { invited, failed, total }
    • TODO-3: Auto-generate invitation codes via invitations/ module not yet wired
    • TODO-2: Audit log entry not yet wired
  • 4.2 Wire into notifications module for email delivery
    • Send invite email with: code, instructions, product info
    • Use notification preferences (respect email opt-out)
  • 4.3 Handle invitation redemption → waitlist conversion
    • When user redeems invite code → lookup waitlist entry by invitationCodeId
    • Update waitlist entry: status → 'converted', set convertedAt
    • Hook into existing dispatchInvitationRedeemed webhook callback
    • Guard: only update if current status is invited (prevent double-conversion)
  • 4.4 Write batch invite + conversion tests

Phase 5 — Admin Dashboard UI

  • 5.1 Add waitlist management page to admin dashboard
    • Path: admin-dashboard-web/src/app/(dashboard)/waitlist/page.tsx
    • Table view: email, name, position, priority, status, source, date
    • Filters: status, source, date range, search by email
    • Bulk actions: invite selected, delete, export
  • 5.2 Add waitlist stats widget to admin dashboard home
    • Total signups, signups today, conversion rate, top referrers
  • 5.3 Add product pre-launch config editor
    • Edit prelaunchConfig on product settings page
    • Custom field builder (add/remove/reorder fields)
    • Toggle signup enabled/disabled
    • Set estimated launch date
  • 5.4 Add API routes in admin dashboard for waitlist
    • Proxy to platform-service /api/waitlist/* endpoints

Phase 6 — Public Signup Page (Frontend)

  • 6.1 Design public waitlist signup page template
    • Responsive, dark-themed (ByteLyst design tokens)
    • Product logo, name, tagline, launch date countdown
    • Email field + dynamic custom fields (from prelaunchConfig)
    • Social proof counter ("Join 1,234 others on the waitlist")
    • Referral share link after signup
  • 6.2 Build as reusable component in tracker-dashboard or standalone
    • Route: /waitlist/:productId
    • Fetches product config + custom fields from public API
    • Posts signup to POST /public/waitlist/:productId
  • 6.3 Add confirmation/thank-you state
    • Show position number, estimated wait, referral link
    • "Share to move up the waitlist" CTA
  • 6.4 Add status check page
    • Input email → shows current position, status
    • Link to unsubscribe

Phase 7 — Analytics & Polish

  • 7.1 Add waitlist analytics to GET /api/waitlist/stats
    • Signups per day (time series)
    • Signups by source breakdown
    • Custom field aggregations (e.g. "60% want iOS first")
    • Referral chain depth stats
    • Conversion funnel: pending → invited → converted → (with drop-off rates)
  • 7.2 Add CSV/JSON export for admin
    • All fields, filterable by status/date/source
    • Exclude ipHash and unsubscribeToken from export (security)
  • 7.3 Add feature flag: pre_launch_signup_enabled per product
    • Quick kill switch without changing product status
    • Overrides prelaunchConfig.signupEnabled (flag takes precedence)
  • 7.4 Duplicate detection is core (moved to Phase 2)
    • Same email + productId → return existing position (don't create duplicate)
    • Different productId → allow (user can be on multiple waitlists)
    • Uses emailNormalized for case-insensitive matching
  • 7.5 Unsubscribe token is core (moved to Phase 2)
    • Generated via crypto.randomUUID() at signup time
    • Used for status check + unsubscribe (both via POST body)
  • 7.6 Add audit log entries for admin actions (invite, delete, export, resequence)
    • Wire into existing audit/ module
    • Category: waitlist, actions: waitlist.invite, waitlist.delete, waitlist.export, waitlist.resequence
  • 7.7 Add CAPTCHA / bot protection for public signup
    • Support Cloudflare Turnstile (preferred), hCaptcha, or reCAPTCHA
    • Configurable per product via prelaunchConfig.captchaEnabled + captchaProvider
    • Server-side token verification in signup route
  • 7.8 Add email delivery integration (optional)
    • Confirmation email on signup (with position, referral link)
    • Invite email on batch invite (with code, instructions)
    • Options: SendGrid, AWS SES, or Azure Communication Services
    • Out of scope for core module — can be wired via webhook consumers

8. Estimated Effort

Phase Description ~LOC Priority
1 Product status lifecycle ~150 P0
2 Waitlist module (core) ~900 P0
3 Waitlist referral loop ~200 P1
4 Batch invite flow ~250 P1
5 Admin dashboard UI ~800 P1
6 Public signup page ~600 P1
7 Analytics & polish ~400 P2
Total ~3,300

9. Decision: New Module in platform-service (Not a New Service)

Why not a separate service?

  • Product-agnostic — already scoped by productId, same pattern as all other modules
  • Same Cosmos DB instance — no new infrastructure
  • Same auth pattern — public routes for signup, JWT-admin routes for management
  • Direct integration with invitations/, referrals/, notifications/, flags/ — same process, no network hops
  • Rate limiting — reuses @fastify/rate-limit pattern from existing public/ routes
  • Estimated ~650 LOC for core module — far too small to justify a separate service

Where it lives:

services/platform-service/src/modules/waitlist/
├── types.ts          # Zod schemas + TypeScript interfaces
├── repository.ts     # Cosmos DB CRUD operations
├── routes.ts         # Public + admin REST endpoints
└── waitlist.test.ts  # Unit tests

10. Open Questions

  • Should we support multiple waitlists per product (e.g. "iOS waitlist" vs "Android waitlist")?
    • Current design: one waitlist per productId. Could add optional tag field for sub-lists.
  • Do we want webhook/callback support when someone joins? Resolved: Yes — added waitlist.joined webhook (task 2.8).
  • Should the public signup page be a standalone static site or part of an existing dashboard?
    • Option A: Static site (fastest, cheapest, CDN-deployed)
    • Option B: Route in tracker-dashboard (reuses existing infra)
    • Option C: New standalone Next.js micro-site
  • Do we need GDPR-style data export/deletion for waitlist entries?
    • IP is already hashed (not raw). Email is PII — may need right-to-erasure endpoint.
    • Consider: DELETE /public/waitlist/me with email + unsubscribeToken (deletes entry entirely).
  • Should referral priority boost be configurable per product?
    • Current design: hardcoded boost value. Could add referralBoost: number to PrelaunchConfig.
  • Do we need email verification (double opt-in) before counting someone on the waitlist?
    • Pro: prevents fake signups, ensures deliverable email
    • Con: adds friction, requires email delivery infrastructure
  • Should the product status transition be strictly sequential or allow admin override?
    • Current design: suggest validation but allow admin override. Document both options.

11. Implementation TODOs (Deferred — Needs Review)

These TODOs were marked during implementation. They are in the code as comments and need your decision before wiring up.

# Location Description Blocker?
TODO-1 routes.ts — join endpoint CAPTCHA validation: When prelaunchConfig.captchaEnabled is true, we need to verify captchaToken against the configured provider (Turnstile/hCaptcha/reCAPTCHA). Requires: provider API keys, HTTP call to verify endpoint. Currently skipped — signup works without CAPTCHA. No — rate limiting is in place as interim protection
TODO-2 routes.ts — delete, invite, export endpoints Audit log wiring: Admin actions (delete entry, batch invite, CSV export) should create audit log entries via the existing audit/ module. Currently commented out — needs auditRepo.create() calls. No — actions work, just not logged
TODO-3 routes.ts — batch invite endpoint Invitation code auto-generation: Batch invite currently marks entries as invited but does NOT auto-generate invitation codes via the invitations/ module. The invitationCodeId field stays empty. Need to call invitations/repository.ts create() for each entry. Yes — invited users won't have a code to redeem until this is wired

Remaining Unchecked Roadmap Items

Task Phase Status Notes
3.2 Referral stats in stats endpoint Not started Add referral-specific aggregations to GET /api/waitlist/stats
3.4 Referral leaderboard endpoint Not started Optional — GET /public/waitlist/:productId/top-referrers
3.5 Referral integration tests Not started Circular/self-referral prevention tests
4.2 Notifications email delivery Not started Depends on email provider (SendGrid/SES/Azure Comms)
4.3 Invitation redemption → conversion Not started Hook dispatchInvitationRedeemed → update waitlist entry
4.4 Batch invite + conversion tests Not started Depends on TODO-3
5.15.4 Admin dashboard UI Not started Waitlist management page, stats widget, config editor
6.16.4 Public signup page frontend Not started Responsive signup form, confirmation, status check
7.17.8 Analytics & polish Not started Time series, CSV export, CAPTCHA, email delivery

Summary of Commits

Commit Description
66e657a Phase 1: Product status lifecycle + prelaunchConfig types
2692c91 Phase 2: Waitlist module (types, repo, routes, webhooks, cosmos-init, server registration)
6a996cc Tests (45 passing) + doc checkboxes + .env.example webhook vars