Waitlist & subscribers
Gate signups behind an invite-only waitlist with referrals, and manage newsletter subscribers with one-click unsubscribe.
Waitlist & subscribers
Valcraven can run in waitlist mode: instead of open signups, visitors join a waitlist (with referral tracking), an admin invites them in batches, and each invite carries a code that unlocks signup. The same area of the app also handles newsletter subscribers — the list your email campaigns send to — with a compliant subscribe/unsubscribe flow.
Where it lives
| Piece | Path |
|---|---|
| Join / status / invite validation | apps/web/app/api/waitlist/* |
| Newsletter subscribe | apps/web/app/api/subscribe/route.ts |
| One-click unsubscribe | apps/web/app/api/unsubscribe/route.ts |
| Landing forms | apps/web/components/landing/waitlist-signup.tsx, .../newsletter.tsx |
| Admin invites | apps/web/app/api/admin/waitlist/* (/admin/waitlist) |
| Tables | waitlist, invite_codes, newsletter_subscribers |
Waitlist mode
Waitlist mode is a presentation flag. Set it and the landing page shows a "join the waitlist" form instead of a signup CTA, and the auth page requires an invite code to register:
WAITLIST_MODE=true
NEXT_PUBLIC_WAITLIST_MODE=trueNEXT_PUBLIC_WAITLIST_MODE is what the client reads to switch the landing page (app/page.tsx) and the auth page (app/auth/page.tsx) into waitlist behavior.
The invite-code check on signup is enforced in the auth page UI: it validates the code via
/api/waitlist/validate-invitebefore calling sign-up, then redeems it via/api/waitlist/redeem-inviteafter. Treat waitlist mode as an onboarding funnel, not a hard server-side security gate.
Joining the waitlist
POST /api/waitlist/join with { email, referralCode? }:
- Validates and normalizes the email (trim + lowercase).
- If already on the list, returns the existing position and referral code.
- Otherwise inserts the entry, generates a unique referral code for the new joiner, and — if a valid
referralCodewas supplied — credits the referrer (referral_count += 1). - Returns the joiner's position (count of
waitingentries up to theirs) and their referral code (201on a fresh join).
The endpoint is rate-limited. Share ?ref=<referralCode> links to climb the list — referrals are counted per entry.
GET /api/waitlist/status?email= returns whether an email is on the list, its position (if still waiting), the total waiting, status, and join time. It's rate-limited too, since it's an unauthenticated email lookup.
A waitlist entry's status flows waiting → invited → registered as it moves through the funnel.
Inviting users (admin)
From /admin/waitlist, an admin invites people in batches. POST /api/admin/waitlist/invite with { emails: string[] } (max 100 per batch, requires the ADMIN_WAITLIST permission):
- For each email, generates a random
invite_codesrow (8 random bytes, hex) created by the admin. - Flips the matching
waitlistentry fromwaitingtoinvited. - Sends the invite email via
sendWaitlistInviteEmail(email, code)(fire-and-forget).
The response summarizes how many were sent vs. failed. An invite code can be locked to a specific email at creation.
Redeeming an invite
Two endpoints back the signup flow:
| Method + path | Auth | Purpose |
|---|---|---|
GET /api/waitlist/validate-invite?code= | public | Check a code — returns { valid, reason?, lockedToEmail? } |
POST /api/waitlist/redeem-invite | session | Consume the code for the signed-in user |
Validation rejects unknown, already-used, and expired codes. Redemption additionally checks that a code locked to an email matches the user's email, marks the code used (stamping used_by / used_at), and updates the user's waitlist entry to registered.
Newsletter subscribers
Separately from the waitlist, the landing page has a newsletter form (components/landing/newsletter.tsx) that posts to POST /api/subscribe with { email }. It's rate-limited, validates the email, and inserts an active row into newsletter_subscribers with a generated unsubscribe_token. A duplicate email is treated as success (to avoid leaking who's subscribed). This is the list your email campaigns target; admins manage subscribers and their tags at /admin/crm.
Unsubscribe (one-click, compliant)
/api/unsubscribe?token=<token> supports both directions of RFC 8058 one-click unsubscribe:
GET— flips the subscriber tounsubscribed, stampsunsubscribed_at, notifies any configured marketing provider (removeContact), and redirects to/unsubscribed.POST— same effect, for theList-Unsubscribe-Postone-click header; returns JSON.
Marketing emails include List-Unsubscribe / List-Unsubscribe-Post headers automatically when a send is given an unsubscribeToken (see Transactional email). An invalid or unknown token returns 404. Once unsubscribed, an address is status = 'unsubscribed' and is therefore never included in a resolved campaign audience.
Data model
waitlist—email(unique),referral_code(unique),referred_by,referral_count,status(waitingdefault), timestamps.invite_codes—code(unique), optionalemaillock,created_by,used_by,used_at,expires_at.newsletter_subscribers—email(unique),status(activedefault),tags(JSON array),unsubscribe_token(unique),unsubscribed_at.
Related
- Email campaigns — send to subscribers; audiences are subscriber tags
- Transactional email — the invite email and unsubscribe headers
- Authentication — the signup flow waitlist mode gates
Webhooks
Outbound webhooks — let your users register HTTPS endpoints and deliver HMAC-signed, retried event payloads to them, with SSRF protection built in.
Feature Flags
A feature flag system with a three-tier transport — Cloudflare Flagship on Workers, an OpenFeature fallback, and local env/JSON flags for dev — plus built-in maintenance mode.