npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@guilhermejansen/better-auth-waitlist

v1.0.0

Published

Waitlist / early-access plugin for Better Auth. Intercepts all registration paths and gates sign-ups behind an invite-based waitlist.

Readme

@guilhermejansen/better-auth-waitlist

A Better Auth community plugin that adds waitlist and early-access gating to your authentication system. Intercepts all registration paths and gates sign-ups behind an invite-based waitlist.

Features

  • Intercepts all registration paths -- email/password, OAuth, magic link, OTP, phone, anonymous, one-tap, and SIWE are all gated automatically
  • Dual-layer protection -- hooks intercept requests before processing and database hooks block user creation as a safety net
  • Admin dashboard endpoints -- approve, reject, bulk approve, list entries, and view statistics
  • Invite code system -- unique codes with configurable expiration (default 48 hours)
  • Auto-approve mode -- pass true to approve everyone, or a function for conditional logic
  • Bulk approve -- approve specific emails or the next N entries in the queue
  • Referral tracking -- track referrals and attach arbitrary JSON metadata to entries
  • Lifecycle callbacks -- onJoinWaitlist, onApproved, onRejected, and sendInviteEmail for email notifications
  • Full TypeScript support -- type-safe client and server APIs with inference
  • Works with any Better Auth adapter -- Prisma 5/6/7, Drizzle, MongoDB, SQLite, MySQL, PostgreSQL, and more
  • Framework agnostic -- Next.js 14-16, Nuxt, SvelteKit, Solid, Remix, Hono, Express, and any other framework Better Auth supports

Requirements

  • better-auth >= 1.0.0
  • Node.js >= 18 (or Bun, Deno, etc.)

Installation

npm install @guilhermejansen/better-auth-waitlist
pnpm add @guilhermejansen/better-auth-waitlist
bun add @guilhermejansen/better-auth-waitlist
yarn add @guilhermejansen/better-auth-waitlist

Quick Start

Server Setup

import { betterAuth } from "better-auth";
import { admin } from "better-auth/plugins/admin";
import { waitlist } from "@guilhermejansen/better-auth-waitlist";

export const auth = betterAuth({
  // ... your config
  plugins: [
    admin(), // Required for admin role checking
    waitlist({
      requireInviteCode: true,
      sendInviteEmail: async ({ email, inviteCode, expiresAt }) => {
        await sendEmail({
          to: email,
          subject: "You're invited!",
          body: `Use code: ${inviteCode}`,
        });
      },
    }),
  ],
});

Client Setup

import { createAuthClient } from "better-auth/client";
import { waitlistClient } from "@guilhermejansen/better-auth-waitlist/client";

export const authClient = createAuthClient({
  plugins: [waitlistClient()],
});

API Reference

Public Endpoints

These endpoints are available without authentication.

Join the Waitlist

const { data, error } = await authClient.waitlist.join({
  email: "[email protected]",
  referredBy: "friend-id", // optional
  metadata: { source: "landing-page" }, // optional
});
// data: { id, email, status, position, createdAt }

Check Waitlist Status

const { data } = await authClient.waitlist.status({
  email: "[email protected]",
});
// data: { status: "pending" | "approved" | "rejected" | "registered", position: number }

Verify Invite Code

const { data } = await authClient.waitlist.verifyInvite({
  inviteCode: "abc-123-def",
});
// data: { valid: boolean, email: string | null }

Register with Invite Code

When requireInviteCode is enabled, pass the invite code during sign-up:

const { data } = await authClient.signUp.email({
  email: "[email protected]",
  password: "securepassword",
  name: "User",
  inviteCode: "abc-123-def", // Required when requireInviteCode is true
});

Or via header:

const { data } = await authClient.signUp.email(
  { email: "[email protected]", password: "securepassword", name: "User" },
  { headers: { "x-invite-code": "abc-123-def" } },
);

Admin Endpoints

All admin endpoints require an authenticated session with an admin role.

Approve Entry

await auth.api.approveEntry({
  body: { email: "[email protected]" },
});

Reject Entry

await auth.api.rejectEntry({
  body: { email: "[email protected]", reason: "Not qualified" },
});

Bulk Approve

// Approve specific emails
await auth.api.bulkApprove({
  body: { emails: ["[email protected]", "[email protected]"] },
});

// Approve next N entries in the queue (ordered by position)
await auth.api.bulkApprove({
  body: { count: 10 },
});

List Entries

const data = await auth.api.listWaitlist({
  query: {
    status: "pending", // optional: filter by status
    page: 1,
    limit: 20,
    sortBy: "createdAt", // "createdAt" | "position" | "email" | "status"
    sortDirection: "desc", // "asc" | "desc"
  },
});
// data: { entries: WaitlistEntry[], total: number, page: number, totalPages: number }

Get Statistics

const stats = await auth.api.getWaitlistStats();
// stats: { total, pending, approved, rejected, registered }

Configuration Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | enabled | boolean | true | Enable or disable the waitlist gate | | requireInviteCode | boolean | false | Require an invite code during registration | | inviteCodeExpiration | number | 172800 | Invite code TTL in seconds (48 hours) | | maxWaitlistSize | number | undefined | Maximum number of entries allowed on the waitlist | | skipAnonymous | boolean | false | Skip waitlist checks for anonymous sign-ins | | autoApprove | boolean \| (email: string) => boolean \| Promise<boolean> | undefined | Auto-approve entries on join. Pass true for all, or a function for conditional logic | | interceptPaths | string[] | All registration paths | Override which Better Auth paths are intercepted | | adminRoles | string[] | ["admin"] | Roles that are allowed to perform admin actions | | onJoinWaitlist | (entry: WaitlistEntry) => void \| Promise<void> | undefined | Called after an entry joins the waitlist | | onApproved | (entry: WaitlistEntry) => void \| Promise<void> | undefined | Called after an entry is approved | | onRejected | (entry: WaitlistEntry) => void \| Promise<void> | undefined | Called after an entry is rejected | | sendInviteEmail | (data: { email, inviteCode, expiresAt }) => void \| Promise<void> | undefined | Called on approval to deliver the invite code | | schema | object | undefined | Customize table and field names |

Default Intercepted Paths

When interceptPaths is not set, these registration paths are intercepted:

  • /sign-up/email
  • /callback/ (OAuth)
  • /oauth2/callback/ (OAuth2)
  • /magic-link/verify
  • /sign-in/email-otp
  • /email-otp/verify-email
  • /phone-number/verify
  • /sign-in/anonymous
  • /one-tap/callback
  • /siwe/verify

Database Schema

The plugin creates a waitlist table with the following fields:

| Field | Type | Description | |-------|------|-------------| | id | string | Primary key | | email | string | Email address (unique, indexed) | | status | string | pending / approved / rejected / registered | | inviteCode | string? | Unique invite code (generated on approval) | | inviteExpiresAt | date? | Invite code expiration timestamp | | position | number? | Queue position (assigned on join) | | referredBy | string? | Referral identifier | | metadata | string? | JSON-serialized metadata | | approvedAt | date? | Approval timestamp | | rejectedAt | date? | Rejection timestamp | | registeredAt | date? | Registration timestamp | | createdAt | date | Created timestamp | | updatedAt | date | Updated timestamp |

How It Works

The plugin uses a dual-layer interception strategy to ensure no unapproved user can register, regardless of which authentication method they use:

  1. Hooks Layer -- hooks.before intercepts registration endpoints and validates waitlist status before the request is processed. This catches email/password sign-ups, OTP, magic links, and any path that includes the email in the request body.

  2. Database Hooks Layer -- databaseHooks.user.create.before acts as a safety net, blocking user creation at the database level if the email does not have an approved waitlist entry. This catches OAuth callbacks and any other flow where the email is not available in the request body.

  3. Post-Registration -- databaseHooks.user.create.after automatically marks the waitlist entry as registered after successful sign-up, preventing the invite code from being reused.

Schema Customization

You can customize the table and field names to match your existing database conventions:

waitlist({
  schema: {
    waitlist: {
      modelName: "WaitlistEntry", // Custom table name
      fields: {
        email: "emailAddress", // Custom field names
      },
    },
  },
});

Error Codes

The plugin exports WAITLIST_ERROR_CODES for programmatic error handling:

| Code | Message | |------|---------| | EMAIL_ALREADY_IN_WAITLIST | This email is already on the waitlist | | WAITLIST_ENTRY_NOT_FOUND | Waitlist entry not found | | NOT_APPROVED | You must be approved from the waitlist to register | | INVALID_INVITE_CODE | Invalid or expired invite code | | INVITE_CODE_REQUIRED | An invite code is required to register | | ALREADY_REGISTERED | This waitlist entry has already been used for registration | | WAITLIST_FULL | The waitlist is currently full | | UNAUTHORIZED_ADMIN_ACTION | You are not authorized to perform this action |

import { WAITLIST_ERROR_CODES } from "@guilhermejansen/better-auth-waitlist";

if (error.message === WAITLIST_ERROR_CODES.NOT_APPROVED) {
  // Handle not approved
}

Contributing

See CONTRIBUTING.md for guidelines on how to contribute to this project.

License

MIT -- Guilherme Jansen