Skip to content

Waitlist

The waitlist module provides a pre-launch sign-up gate. When enabled, users must receive an invite token before they can create an account. The module is fully independent — it has its own database table, API surface, and admin UI, connecting to auth only through a single BetterAuth plugin that validates tokens at sign-up time.

  1. User joins the waitlist — submits their email via a public endpoint
  2. Admin sends an invite — generates a unique token and emails a sign-up link
  3. User signs up with token — the waitlistGate plugin validates the token before allowing registration

Set waitlist.enabled to true in the admin config panel. When active, the sign-up form displays the waitlist join UI instead of the standard registration form.

Config keyDefaultDescription
waitlist.enabledfalseToggle waitlist mode
waitlist.sendConfirmationEmailtrueEmail confirmation when a user joins
waitlist.resendSegmentIdOptional Resend audience segment for marketing sync

Users submit their email to join. Duplicate emails are handled gracefully.

import { api } from "@red/backend/api"
// From the frontend
await convex.mutation(api.modules.core.waitlist.waitlist_api.join, {
email: "user@example.com",
})
// Returns: { success: true, alreadyOnList: false }

Used by the sign-in page to pre-fill the email field when a user arrives via an invite link.

const result = await convex.query(
api.modules.core.waitlist.waitlist_api.validateToken,
{ token: "uuid-token" },
)
// Returns: { valid: true, email: "user@example.com" }

All admin endpoints require the global admin role.

FunctionDescription
listPaginated list with optional search and status filter
inviteSend invite to a single pending entry
inviteBulkBatch invite multiple pending entries
removeDelete a waitlist entry
statsCount of pending, invited, and registered entries

The waitlistGate BetterAuth plugin intercepts the /sign-up/email endpoint:

  • Before sign-up: validates the x-waitlist-token header matches a token in invited status for the given email
  • After sign-up: marks the waitlist entry as registered with a timestamp

The invite link format is /sign-in?invite={token}, which the frontend reads to pass the token header during registration.

waitlist
├── email: string
├── status: "pending" | "invited" | "registered"
├── inviteToken?: string
├── invitedAt?: number
├── registeredAt?: number
└── resendContactId?: string
Indexes: by_email, by_status, by_invite_token
FilePurpose
packages/backend/src/convex/modules/core/waitlist/waitlist_api.tsPublic and admin endpoints
packages/backend/src/convex/modules/core/waitlist/waitlist_internal.tsEmail sending and Resend segment sync
packages/backend/src/convex/modules/core/waitlist/waitlist_table.tsDatabase schema
packages/backend/src/convex/lib/core/auth/waitlist_gate.tsBetterAuth sign-up gate plugin