43 KiB
Pre-Launch Signup System — Design & Roadmap
Service:
platform-service(port 4003) Module:src/modules/waitlist/Cosmos Container:waitlist(partition key:
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 condition — getNextPosition() 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 email — GET /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:
emailNormalized+productIdwithin the same partition.
Position assignment: Cosmos DB has no
AUTO_INCREMENT. Options:
- Stored procedure for atomic counter (strongest guarantee)
SELECT VALUE MAX(c.position)+ 1 (acceptable with low concurrency)- 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? |
|---|---|---|
| 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 (
referredByfield, priority boost) are self-contained within thewaitlist/module. The existingreferrals/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.statusenum from'active' | 'disabled'to'draft' | 'pre_launch' | 'beta' | 'active' | 'sunset' | 'disabled'—66e657a- File:
services/platform-service/src/modules/products/types.ts
- File:
- 1.2 Update
CreateProductSchemaandUpdateProductSchemawith new status values —66e657a- File:
services/platform-service/src/modules/products/types.ts
- File:
- 1.3 Add optional
prelaunchConfigfield toProductDoc—66e657a- Include:
signupEnabled,launchDate,customFields[],confirmationMessage,redirectUrl,maxSignups,captchaEnabled,captchaProvider,tagline,logoUrl
- Include:
- 1.4 Update
getRequestProductId()status gating logic —66e657a- File:
services/platform-service/src/lib/request-context.ts - Blocks:
draft,sunset,disabled - Added
getRequestProductIdForPublic()that permitspre_launch
- File:
- 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)
- Tests in:
- 1.7 Add product status transition validation —
66e657aisValidStatusTransition(current, next)helper inproducts/types.ts- Validation enforced in
PUT /products/:idroute - 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)
- Existing
Phase 2 — Waitlist Module (Core)
- 2.1 Create
src/modules/waitlist/types.ts—2692c91WaitlistEntryDocinterface (includingemailNormalized,invitedAt,convertedAt,ipHash)JoinWaitlistSchema,CheckStatusSchema,UnsubscribeSchema,UpdateWaitlistEntrySchema,WaitlistQuerySchema,BatchInviteSchema
- 2.2 Create
src/modules/waitlist/repository.ts—2692c91- 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)
- All functions:
- 2.3 Create
src/modules/waitlist/routes.ts—2692c91- 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/resequencedeferred to Phase 7
- 2.4 Register waitlist container in
src/lib/cosmos-init.ts—2692c91- Container:
waitlist, partition key:/email - Also added 13 previously missing containers to
CONTAINER_DEFS
- Container:
- 2.5 Register routes in
src/server.ts—2692c91 - 2.6 Write unit tests (45 tests passing) —
6a996cc- File:
src/modules/waitlist/waitlist.test.ts - Schema validation, email normalization, status transitions, route exports
- File:
- 2.7 Add admin role guard to all admin endpoints —
2692c91requireAdmin(req)helper throwsForbiddenErrorifrole !== 'admin'
- 2.8 Add
waitlist.joinedwebhook event —2692c91dispatchWaitlistJoined()insrc/lib/webhooks.ts- Env var
WEBHOOK_WAITLIST_JOINED_URLadded to.env.example
- 2.9 Add public product config endpoint —
2692c91GET /public/waitlist/:productId/config— stripsmaxSignups+captchaProviderfrom 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 thewaitlist/module.
- 3.1 Add
refquery param support toPOST /public/waitlist/:productId—2692c91- Lookup referrer by waitlist entry ID
- Set
referredByon new entry, setsource: 'referral' - Bump referrer's
priorityscore 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)
- Returns
- 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/inviteadmin endpoint —2692c91- Input:
{ count: number, strategy: 'fifo' | 'priority' | 'random' } - Selects only
status === 'pending'entries (idempotent) - Marks entries
invited+ setsinvitedAt - Returns:
{ invited, failed, total } - TODO-3: Auto-generate invitation codes via
invitations/module not yet wired - TODO-2: Audit log entry not yet wired
- Input:
- 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', setconvertedAt - Hook into existing
dispatchInvitationRedeemedwebhook callback - Guard: only update if current status is
invited(prevent double-conversion)
- When user redeems invite code → lookup waitlist entry by
- 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
- Path:
- 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
prelaunchConfigon product settings page - Custom field builder (add/remove/reorder fields)
- Toggle signup enabled/disabled
- Set estimated launch date
- Edit
- 5.4 Add API routes in admin dashboard for waitlist
- Proxy to platform-service
/api/waitlist/*endpoints
- Proxy to platform-service
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
- Route:
- 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
ipHashandunsubscribeTokenfrom export (security)
- 7.3 Add feature flag:
pre_launch_signup_enabledper 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
emailNormalizedfor 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)
- Generated via
- 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
- Wire into existing
- 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-limitpattern from existingpublic/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
tagfield for sub-lists.
- Current design: one waitlist per productId. Could add optional
Do we want webhook/callback support when someone joins?Resolved: Yes — addedwaitlist.joinedwebhook (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/mewith email + unsubscribeToken (deletes entry entirely).
- Should referral priority boost be configurable per product?
- Current design: hardcoded boost value. Could add
referralBoost: numbertoPrelaunchConfig.
- Current design: hardcoded boost value. Could add
- 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.1–5.4 | Admin dashboard UI | Not started | Waitlist management page, stats widget, config editor |
| 6.1–6.4 | Public signup page frontend | Not started | Responsive signup form, confirmation, status check |
| 7.1–7.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 |