better-auth-waitlist
Version:
A lightweight Better Auth plugin for waitlist management with admin approval workflows, domain restrictions, and customizable validation.
799 lines (792 loc) • 30.2 kB
text/typescript
import * as better_auth from 'better-auth';
import { InferOptionSchema, User } from 'better-auth';
import { z } from 'zod/v3';
import { FieldAttribute } from 'better-auth/db';
declare const WAITLIST_MODEL_NAME = "waitlist";
declare const WAITLIST_STATUS: {
PENDING: string;
APPROVED: string;
REJECTED: string;
};
declare const schema: {
waitlist: {
fields: {
email: {
type: "string";
required: true;
unique: true;
};
status: {
type: "string";
required: true;
input: false;
defaultValue: string;
};
requestedAt: {
type: "date";
required: false;
input: false;
defaultValue: () => Date;
};
processedAt: {
type: "date";
required: false;
input: false;
};
processedBy: {
type: "string";
references: {
model: string;
field: string;
onDelete: "no action";
};
required: false;
input: false;
};
};
};
};
interface WaitlistOptions {
/**
* Allow users to join the waitlist
* @default true
*/
enabled?: boolean;
/**
* Restrict waitlist to specific email domains
* @example ["@company.com", "@organization.org"]
*/
allowedDomains?: string[];
/**
* Maximum number of waitlist entries
* @default undefined (no limit)
*/
maximumWaitlistParticipants?: number;
/**
* schema for the waitlist plugin. Use this to rename fields.
*/
schema?: InferOptionSchema<typeof schema>;
/**
* Extend the `waitlist` schema with additional fields.
*/
additionalFields?: {
[key: string]: FieldAttribute;
};
/**
* Wether to disable sign in & sign ups while the waitlist is active.
*
* @default false
*/
disableSignInAndSignUp?: boolean;
/**
* Auto-approve waitlist entries based on criteria
* @param email - The email of the user
* @returns true if the entry should be auto-approved, false otherwise
* @default false
*/
autoApprove?: boolean | ((email: string) => boolean);
/**
* Custom validation for waitlist entries
* @param data - The data of the entry including email and additional fields
* @returns true if the entry should be validated, false otherwise
*/
validateEntry?: (data: {
email: string;
[key: string]: any;
}) => Promise<boolean> | boolean;
/**
* Webhook/callback when entry is processed
*
* @example
* ```ts
* onStatusChange: (entry) => {
* console.log(entry);
* }
* ```
* @param entry - The entry that has been processed
* @returns void
*/
onStatusChange?: (entry: {
id: string;
email: string;
status: "pending" | "accepted" | "rejected";
}) => Promise<void> | void;
/**
* Webhook/callback when entry is created
*
* @example
* ```ts
* onJoinRequest: (params) => {
* console.log(params);
* }
* ```
* @param params - The params that has been created
* @returns void
*/
onJoinRequest?: (params: {
request: WaitlistEntry & {
[key: string]: any;
};
}) => Promise<void> | void;
/**
* Custom email notification settings
*/
notifications?: {
enabled: boolean;
onJoin?: boolean;
onAccept?: boolean;
onReject?: boolean;
};
/**
* Rate limiting for waitlist joins
*/
rateLimit?: {
maxAttempts: number;
windowMs: number;
};
/**
* Custom access control function for admin endpoints (list, findOne, approve, reject)
* @param user - The authenticated user object from session
* @returns Promise<boolean> - true for access granted, false for access denied
*
* @example
* ```ts
* canManageWaitlist: async (user) => {
* // Simple role check
* return user.role === "admin" || user.role === "moderator";
* }
*
* // Or with Better Auth's permission system
* canManageWaitlist: async (user) => {
* try {
* const hasPermission = await auth.api.userHasPermission({
* body: {
* userId: user.id,
* permissions: { waitlist: ["list", "read", "update"] }
* }
* });
* return hasPermission;
* } catch {
* return user.role === "admin";
* }
* }
* ```
*/
canManageWaitlist?: (user: User) => Promise<boolean>;
}
interface WaitlistEntry {
id: string;
email: string;
status: (typeof WAITLIST_STATUS)[keyof typeof WAITLIST_STATUS];
requestedAt: Date;
processedAt: Date | null | undefined;
processedBy: string | null | undefined;
}
interface WaitlistClientOptions {
additionalFields?: {
[key: string]: FieldAttribute;
};
}
declare const waitlistClient: <CO extends WaitlistClientOptions>(options?: CO) => {
id: "waitlist";
$InferServerPlugin: ReturnType<typeof waitlist<{
additionalFields: CO["additionalFields"] | undefined;
}>>;
};
declare const HTTP_STATUS_CODES: {
readonly OK: 200;
readonly CREATED: 201;
readonly UNAUTHORIZED: 401;
readonly FORBIDDEN: 403;
readonly NOT_FOUND: 404;
readonly UNPROCESSABLE_ENTITY: 422;
readonly TOO_MANY_REQUESTS: 429;
};
declare const HTTP_STATUS_CODE_MESSAGES: {
readonly 200: "OK";
readonly 201: "Created";
readonly 401: "Unauthorized";
readonly 403: "Forbidden";
readonly 404: "Not Found";
readonly 422: "Unprocessable Entity";
readonly 429: "Too Many Requests";
};
declare const WAITLIST_ERROR_CODES: {
readonly EMAIL_ALREADY_IN_WAITLIST: "email_already_in_waitlist";
readonly DOMAIN_NOT_ALLOWED: "domain_not_allowed";
readonly INVALID_ENTRY: "invalid_entry";
readonly WAITLIST_FULL: "waitlist_full";
readonly RATE_LIMIT_EXCEEDED: "rate_limit_exceeded";
readonly WAITLIST_NOT_ENABLED: "waitlist_not_enabled";
readonly UNAUTHORIZED: "unauthorized";
readonly FORBIDDEN: "permission_denied";
readonly WAITLIST_ENTRY_NOT_FOUND: "waitlist_entry_not_found";
};
declare const WAITLIST_ERROR_MESSAGES: {
readonly email_already_in_waitlist: "Email already in waitlist";
readonly domain_not_allowed: "Email domain not allowed";
readonly invalid_entry: "Invalid entry data";
readonly waitlist_full: "Waitlist is full";
readonly rate_limit_exceeded: "Too many requests, please try again later";
readonly waitlist_not_enabled: "Waitlist is not enabled";
readonly unauthorized: "You are not authorized to perform this action";
readonly permission_denied: "Not enough permissions to perform this action";
readonly waitlist_entry_not_found: "Waitlist entry not found";
};
type WaitlistErrorCode = keyof typeof WAITLIST_ERROR_CODES;
declare const waitlist: <O extends WaitlistOptions>(options?: O) => {
id: "waitlist";
schema: {
waitlist: {
fields: {
email: {
type: "string";
required: true;
unique: true;
};
status: {
type: "string";
required: true;
input: false;
defaultValue: string;
};
requestedAt: {
type: "date";
required: false;
input: false;
defaultValue: () => Date;
};
processedAt: {
type: "date";
required: false;
input: false;
};
processedBy: {
type: "string";
references: {
model: string;
field: string;
onDelete: "no action";
};
required: false;
input: false;
};
};
};
};
$ERROR_CODES: {
readonly EMAIL_ALREADY_IN_WAITLIST: "email_already_in_waitlist";
readonly DOMAIN_NOT_ALLOWED: "domain_not_allowed";
readonly INVALID_ENTRY: "invalid_entry";
readonly WAITLIST_FULL: "waitlist_full";
readonly RATE_LIMIT_EXCEEDED: "rate_limit_exceeded";
readonly WAITLIST_NOT_ENABLED: "waitlist_not_enabled";
readonly UNAUTHORIZED: "unauthorized";
readonly FORBIDDEN: "permission_denied";
readonly WAITLIST_ENTRY_NOT_FOUND: "waitlist_entry_not_found";
};
endpoints: {
join: {
<AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: {
body: Omit<WaitlistEntry, "status" | "requestedAt" | "processedAt" | "processedBy" | "id">;
} & {
method?: "POST" | undefined;
} & {
query?: Record<string, any> | undefined;
} & {
params?: Record<string, any>;
} & {
request?: Request;
} & {
headers?: HeadersInit;
} & {
asResponse?: boolean;
returnHeaders?: boolean;
use?: better_auth.Middleware[];
path?: string;
} & {
asResponse?: AsResponse | undefined;
returnHeaders?: ReturnHeaders | undefined;
}): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
headers: Headers;
response: {
id: string;
email: string;
requestedAt: Date;
};
} : {
id: string;
email: string;
requestedAt: Date;
}>;
options: {
method: "POST";
body: z.ZodType<Omit<WaitlistEntry, "id" | "status" | "requestedAt" | "processedAt" | "processedBy" | "requestedAt">>;
} & {
use: any[];
};
path: "/waitlist/join";
};
list: {
<AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: {
body?: undefined;
} & {
method?: "GET" | undefined;
} & {
query: {
status?: string | undefined;
page?: string | number | undefined;
limit?: string | number | undefined;
sortBy?: "status" | "requestedAt" | undefined;
direction?: "asc" | "desc" | undefined;
};
} & {
params?: Record<string, any>;
} & {
request?: Request;
} & {
headers?: HeadersInit;
} & {
asResponse?: boolean;
returnHeaders?: boolean;
use?: better_auth.Middleware[];
path?: string;
} & {
asResponse?: AsResponse | undefined;
returnHeaders?: ReturnHeaders | undefined;
}): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
headers: Headers;
response: {
data: (WaitlistEntry & {
[x: string]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>);
[x: number]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>);
} & {
[x: string]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>) | null | undefined;
[x: number]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>) | null | undefined;
})[];
page: string | number;
limit: number;
total: number;
};
} : {
data: (WaitlistEntry & {
[x: string]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>);
[x: number]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>);
} & {
[x: string]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>) | null | undefined;
[x: number]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>) | null | undefined;
})[];
page: string | number;
limit: number;
total: number;
}>;
options: {
method: "GET";
use: ((inputContext: better_auth.MiddlewareInputContext<better_auth.MiddlewareOptions>) => Promise<{
session: {
session: Record<string, any> & {
id: string;
userId: string;
expiresAt: Date;
createdAt: Date;
updatedAt: Date;
token: string;
ipAddress?: string | null | undefined;
userAgent?: string | null | undefined;
};
user: Record<string, any> & {
id: string;
email: string;
emailVerified: boolean;
name: string;
createdAt: Date;
updatedAt: Date;
image?: string | null | undefined;
};
};
}>)[];
query: z.ZodObject<{
page: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
limit: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
status: z.ZodOptional<z.ZodEnum<[string, string, string]>>;
sortBy: z.ZodOptional<z.ZodEnum<["requestedAt", "status"]>>;
direction: z.ZodOptional<z.ZodEnum<["asc", "desc"]>>;
}, "strip", z.ZodTypeAny, {
status?: string | undefined;
page?: string | number | undefined;
limit?: string | number | undefined;
sortBy?: "status" | "requestedAt" | undefined;
direction?: "asc" | "desc" | undefined;
}, {
status?: string | undefined;
page?: string | number | undefined;
limit?: string | number | undefined;
sortBy?: "status" | "requestedAt" | undefined;
direction?: "asc" | "desc" | undefined;
}>;
metadata: {
openapi: {
responses: {
200: {
description: string;
content: {
"application/json": {
schema: {
type: "object";
properties: {
waitlist: {
type: string;
items: {
$ref: string;
};
};
total: {
type: string;
};
limit: {
type: string[];
};
page: {
type: string[];
};
};
};
};
};
};
};
};
};
} & {
use: any[];
};
path: "/waitlist/list";
};
findOne: {
<AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: {
body?: undefined;
} & {
method?: "GET" | undefined;
} & {
query: {
id: string;
};
} & {
params?: Record<string, any>;
} & {
request?: Request;
} & {
headers?: HeadersInit;
} & {
asResponse?: boolean;
returnHeaders?: boolean;
use?: better_auth.Middleware[];
path?: string;
} & {
asResponse?: AsResponse | undefined;
returnHeaders?: ReturnHeaders | undefined;
}): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
headers: Headers;
response: WaitlistEntry & {
[x: string]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>);
[x: number]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>);
} & {
[x: string]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>) | null | undefined;
[x: number]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>) | null | undefined;
};
} : WaitlistEntry & {
[x: string]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>);
[x: number]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>);
} & {
[x: string]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>) | null | undefined;
[x: number]: string | number | boolean | Date | string[] | number[] | (string & Record<never, never>) | null | undefined;
}>;
options: {
method: "GET";
query: z.ZodObject<{
id: z.ZodString;
}, "strip", z.ZodTypeAny, {
id: string;
}, {
id: string;
}>;
use: ((inputContext: better_auth.MiddlewareInputContext<better_auth.MiddlewareOptions>) => Promise<{
session: {
session: Record<string, any> & {
id: string;
userId: string;
expiresAt: Date;
createdAt: Date;
updatedAt: Date;
token: string;
ipAddress?: string | null | undefined;
userAgent?: string | null | undefined;
};
user: Record<string, any> & {
id: string;
email: string;
emailVerified: boolean;
name: string;
createdAt: Date;
updatedAt: Date;
image?: string | null | undefined;
};
};
}>)[];
metadata: {
openapi: {
responses: {
200: {
description: string;
};
401: {
description: string;
};
403: {
description: string;
};
404: {
description: string;
};
};
};
};
} & {
use: any[];
};
path: "/waitlist/request/find";
};
checkRequestStatus: {
<AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: {
body?: undefined;
} & {
method?: "GET" | undefined;
} & {
query: {
email: string;
};
} & {
params?: Record<string, any>;
} & {
request?: Request;
} & {
headers?: HeadersInit;
} & {
asResponse?: boolean;
returnHeaders?: boolean;
use?: better_auth.Middleware[];
path?: string;
} & {
asResponse?: AsResponse | undefined;
returnHeaders?: ReturnHeaders | undefined;
}): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
headers: Headers;
response: {
status: string;
requestedAt: Date;
};
} : {
status: string;
requestedAt: Date;
}>;
options: {
method: "GET";
query: z.ZodObject<{
email: z.ZodString;
}, "strip", z.ZodTypeAny, {
email: string;
}, {
email: string;
}>;
metadata: {
openapi: {
responses: {
200: {
description: string;
};
404: {
description: string;
};
};
};
};
} & {
use: any[];
};
path: "/waitlist/request/check-status";
};
approveRequest: {
<AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: {
body: {
id: string;
};
} & {
method?: "POST" | undefined;
} & {
query?: Record<string, any> | undefined;
} & {
params?: Record<string, any>;
} & {
request?: Request;
} & {
headers?: HeadersInit;
} & {
asResponse?: boolean;
returnHeaders?: boolean;
use?: better_auth.Middleware[];
path?: string;
} & {
asResponse?: AsResponse | undefined;
returnHeaders?: ReturnHeaders | undefined;
}): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
headers: Headers;
response: {
message: string;
};
} : {
message: string;
}>;
options: {
method: "POST";
body: z.ZodObject<{
id: z.ZodString;
}, "strip", z.ZodTypeAny, {
id: string;
}, {
id: string;
}>;
use: ((inputContext: better_auth.MiddlewareInputContext<better_auth.MiddlewareOptions>) => Promise<{
session: {
session: Record<string, any> & {
id: string;
userId: string;
expiresAt: Date;
createdAt: Date;
updatedAt: Date;
token: string;
ipAddress?: string | null | undefined;
userAgent?: string | null | undefined;
};
user: Record<string, any> & {
id: string;
email: string;
emailVerified: boolean;
name: string;
createdAt: Date;
updatedAt: Date;
image?: string | null | undefined;
};
};
}>)[];
metadata: {
openapi: {
responses: {
200: {
description: string;
};
401: {
description: string;
};
403: {
description: string;
};
404: {
description: string;
};
};
};
};
} & {
use: any[];
};
path: "/waitlist/request/approve";
};
rejectRequest: {
<AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: {
body: {
id: string;
};
} & {
method?: "POST" | undefined;
} & {
query?: Record<string, any> | undefined;
} & {
params?: Record<string, any>;
} & {
request?: Request;
} & {
headers?: HeadersInit;
} & {
asResponse?: boolean;
returnHeaders?: boolean;
use?: better_auth.Middleware[];
path?: string;
} & {
asResponse?: AsResponse | undefined;
returnHeaders?: ReturnHeaders | undefined;
}): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
headers: Headers;
response: {
message: string;
};
} : {
message: string;
}>;
options: {
method: "POST";
body: z.ZodObject<{
id: z.ZodString;
}, "strip", z.ZodTypeAny, {
id: string;
}, {
id: string;
}>;
use: ((inputContext: better_auth.MiddlewareInputContext<better_auth.MiddlewareOptions>) => Promise<{
session: {
session: Record<string, any> & {
id: string;
userId: string;
expiresAt: Date;
createdAt: Date;
updatedAt: Date;
token: string;
ipAddress?: string | null | undefined;
userAgent?: string | null | undefined;
};
user: Record<string, any> & {
id: string;
email: string;
emailVerified: boolean;
name: string;
createdAt: Date;
updatedAt: Date;
image?: string | null | undefined;
};
};
}>)[];
metadata: {
openapi: {
responses: {
200: {
description: string;
};
401: {
description: string;
};
403: {
description: string;
};
404: {
description: string;
};
};
};
};
} & {
use: any[];
};
path: "/waitlist/request/reject";
};
};
};
export { HTTP_STATUS_CODES, HTTP_STATUS_CODE_MESSAGES, WAITLIST_ERROR_CODES, WAITLIST_ERROR_MESSAGES, WAITLIST_MODEL_NAME, WAITLIST_STATUS, type WaitlistEntry, type WaitlistErrorCode, type WaitlistOptions, schema, waitlist, waitlistClient };