Recipes
Short patterns for common setups.
Custom invite link URL
buildInviteUrl: ({ token }) => `${process.env.APP_URL}/invite?token=${encodeURIComponent(token)}`,Captcha on redemption endpoints
captcha({ endpoints: ["/invite/redeem", "/invite/accept", "/invite/activate"] });Breached password checks for invited users
haveIBeenPwned({
paths: ["/sign-up/email", "/reset-password", "/invite/redeem", "/invite/accept"]
});Gating invited users with validateUserInfo
On Better Auth 1.7, user.validateUserInfo sees every user Better Enrollment creates: the pre-created account behind a private invite, and the account made when someone redeems a public invite. The plugin passes source.method === "invite", so you can apply a rule to invite sign-ups only, or tell them apart from your other sign-up paths:
betterAuth({
user: {
validateUserInfo: ({ user, source }) => {
if (source.method !== "invite") return;
if (!String(user.email).endsWith("@acme.com")) {
return { error: "DOMAIN_NOT_ALLOWED", errorDescription: "Use your work email." };
}
}
}
});A private invite to a rejected address fails at /invite/create, so the inviter sees it straight away and nothing is stored. A rejected public redemption fails at redeem, and the use goes back to the invite. Both return your code with status 403.
Gating org creation by role
An org-create invite bypasses this gate by design; the invitation is itself the authorization:
organization({ allowUserToCreateOrganization: roleGate(["admin", "org-creator"]) });Changing roles without losing invite-granted ones
Better Auth stores multiple roles as one comma-separated string, and the admin plugin's setRole
replaces the whole field with whatever you send. Invite redemption grants roles by merging into
that field, so a bare setRole({ role: "admin" }) from your admin UI silently strips anything an
invite granted earlier (such as org-creator). Always compute the union first:
const roles = (user.role || "user")
.split(",")
.map((r) => r.trim())
.filter(Boolean);
if (!roles.includes("admin")) {
await authClient.admin.setRole({ userId: user.id, role: [...roles, "admin"] });
}Demoting is the same in reverse: filter the role out and send what remains, falling back to your default role when the list would be empty.
Last updated on