Better Enrollment

Quick Start

Set up Better Enrollment step by step, for a closed app or open sign-up.

Better Enrollment runs in one of two modes, and the setup flow is the same for both; only your auth config differs. Pick your path once and the examples on this page follow it:

  • Invite-only (closed app): sign-up is disabled and invitations are the only way in.
  • Open sign-up: anyone can sign up, and redeeming an invite grants roles and organization membership.

Add the plugin to your auth config

Import betterEnrollment and add it to your betterAuth plugins.

Sign-up stays disabled; the plugin sees the fully closed config and auto-detects invite-only mode. Users can only come in through an invitation.

auth.ts
import { betterAuth } from "better-auth";
import { admin } from "better-auth/plugins";
import { betterEnrollment } from "@octopi-ai/better-enrollment";

export const auth = betterAuth({
  emailAndPassword: {
    enabled: true,
    disableSignUp: true // closed: invitations are the only way in
  },
  plugins: [
    admin(),
    betterEnrollment({
      async sendPrivateInvitation({ email, url }) {
        await sendInvitationEmail(email, url);
      }
    })
  ]
});

mode: "auto" (the default) inspects your config at startup and picks the right mode; a mixed config throws and asks you to set mode explicitly. See The two modes for the details and its warning about sign-up paths added by other plugins.

Migrate the database

npx @better-auth/cli migrate
# or, to review the SQL first
npx @better-auth/cli generate

This creates the invite and inviteUse tables; see Database for every column.

Role field required

Invited roles are stored in a string role field on your user model, and the default admin gate reads it. The admin() plugin above provides one; if you skip that plugin, add the field via user.additionalFields or gate management with adminUserIds or canManageInvites instead.

Add the client plugin

Import betterEnrollmentClient and add it to your auth client:

auth-client.ts
import { createAuthClient } from "better-auth/react";
import { betterEnrollmentClient } from "@octopi-ai/better-enrollment/client";

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

Create your first invite

Invites are created by an admin (or an org member with permission). Create them from your server, or from an admin dashboard with the client:

See Invites for org invites (org-join, org-create) and every option.

Build the invite page

Every invite link points at one page in your app, carrying only ?token=. The page calls invite.get, renders what the response says, and submits everything to invite.redeem; it never needs to know the invite kind or the mode.

app/invite/page.tsx
// 1. Look up the token from the URL
const { data: invite } = await authClient.invite.get({ token });

// 2. Render by invite.nextAction:
//    "SIGN_UP"  -> a form with the fields in invite.requiredFields
//    "SIGN_IN"  -> your sign-in form; keep the token in the URL
//    "CONFIRM"  -> a single confirm button
//    null       -> expired, revoked, or consumed; show a terminal message

// 3. Submit to one endpoint
await authClient.invite.redeem({ token, password, name, email });
// -> { status: "ACCEPTED", user, organization? }

The full rendering table and requiredFields reference are on The invite page.

What redemption does

The invitee has no account, so invite.get returns nextAction: "SIGN_UP". Redeeming creates the user, their credential account, and a session in one atomic step: they land signed in with the invited role.

Where to go next

Last updated on

On this page