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.
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 generateThis 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:
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.
// 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