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.
How it works
Section titled “How it works”- User joins the waitlist — submits their email via a public endpoint
- Admin sends an invite — generates a unique token and emails a sign-up link
- User signs up with token — the
waitlistGateplugin validates the token before allowing registration
Enabling the waitlist
Section titled “Enabling the waitlist”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 key | Default | Description |
|---|---|---|
waitlist.enabled | false | Toggle waitlist mode |
waitlist.sendConfirmationEmail | true | Email confirmation when a user joins |
waitlist.resendSegmentId | — | Optional Resend audience segment for marketing sync |
Public API
Section titled “Public API”Join the waitlist
Section titled “Join the waitlist”Users submit their email to join. Duplicate emails are handled gracefully.
import { api } from "@red/backend/api"
// From the frontendawait convex.mutation(api.modules.core.waitlist.waitlist_api.join, { email: "user@example.com",})// Returns: { success: true, alreadyOnList: false }Validate an invite token
Section titled “Validate an invite token”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" }Admin API
Section titled “Admin API”All admin endpoints require the global admin role.
| Function | Description |
|---|---|
list | Paginated list with optional search and status filter |
invite | Send invite to a single pending entry |
inviteBulk | Batch invite multiple pending entries |
remove | Delete a waitlist entry |
stats | Count of pending, invited, and registered entries |
Sign-up gate
Section titled “Sign-up gate”The waitlistGate BetterAuth plugin intercepts the /sign-up/email endpoint:
- Before sign-up: validates the
x-waitlist-tokenheader matches a token ininvitedstatus for the given email - After sign-up: marks the waitlist entry as
registeredwith a timestamp
The invite link format is /sign-in?invite={token}, which the frontend reads to pass the token header during registration.
Database schema
Section titled “Database schema”waitlist├── email: string├── status: "pending" | "invited" | "registered"├── inviteToken?: string├── invitedAt?: number├── registeredAt?: number└── resendContactId?: string
Indexes: by_email, by_status, by_invite_tokenKey files
Section titled “Key files”| File | Purpose |
|---|---|
packages/backend/src/convex/modules/core/waitlist/waitlist_api.ts | Public and admin endpoints |
packages/backend/src/convex/modules/core/waitlist/waitlist_internal.ts | Email sending and Resend segment sync |
packages/backend/src/convex/modules/core/waitlist/waitlist_table.ts | Database schema |
packages/backend/src/convex/lib/core/auth/waitlist_gate.ts | BetterAuth sign-up gate plugin |