Valcraven Docs
Features

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

PiecePath
Join / status / invite validationapps/web/app/api/waitlist/*
Newsletter subscribeapps/web/app/api/subscribe/route.ts
One-click unsubscribeapps/web/app/api/unsubscribe/route.ts
Landing formsapps/web/components/landing/waitlist-signup.tsx, .../newsletter.tsx
Admin invitesapps/web/app/api/admin/waitlist/* (/admin/waitlist)
Tableswaitlist, 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=true

NEXT_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-invite before calling sign-up, then redeems it via /api/waitlist/redeem-invite after. 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 referralCode was supplied — credits the referrer (referral_count += 1).
  • Returns the joiner's position (count of waiting entries up to theirs) and their referral code (201 on 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):

  1. For each email, generates a random invite_codes row (8 random bytes, hex) created by the admin.
  2. Flips the matching waitlist entry from waiting to invited.
  3. 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 + pathAuthPurpose
GET /api/waitlist/validate-invite?code=publicCheck a code — returns { valid, reason?, lockedToEmail? }
POST /api/waitlist/redeem-invitesessionConsume 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 to unsubscribed, stamps unsubscribed_at, notifies any configured marketing provider (removeContact), and redirects to /unsubscribed.
  • POST — same effect, for the List-Unsubscribe-Post one-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

  • waitlistemail (unique), referral_code (unique), referred_by, referral_count, status (waiting default), timestamps.
  • invite_codescode (unique), optional email lock, created_by, used_by, used_at, expires_at.
  • newsletter_subscribersemail (unique), status (active default), tags (JSON array), unsubscribe_token (unique), unsubscribed_at.

On this page