UNDER DEVELOPMENT — ALPHA
- Invites don't depend on the inviter knowing the invitee's email — invite links can be shared over 3rd-party platforms (instant messaging, SMS) or live (with a QR code).
- Choice of email to sign up with is up to the invitee.
- Users without invite can sign up with reduced access (useful for waitlist functionality).
- Meant to work with all authentication methods. Currently tested: email and password; email OTP; social.
- Customizable code duration, code generation, and invite creation and acceptance criteria (e.g. to limit upgrade to users from a given domain).
- Keeps track of who created and who accepted the invite.
better-auth-invite
works in concert with the admin
plugin and its access control capabilities.
When a user redeems a valid invite, the invite code gets stored into a signed, http-only cookie in the user's browser.
If a user without an active invite signs up, he or she receives the default role, as defined by the admin
plugin. That is "user"
by default, but you might want to reserve that for invited users, and set the default role to "guest"
for clarity.
If a user with an active invite signs up or signs in, his or her role gets upgraded.
Import the invite
plugin and add it to your betterAuth
configuration.
Important: make sure defaultRole
in the admin
plugin options and roleForSignupWithoutInvite
in the invite
plugin options match.
import { betterAuth } from "better-auth";
import { admin as adminPlugin } from "better-auth/plugins";
import { createAccessControl } from "better-auth/plugins/access";
import { invite } from "better-auth-invite";
// Define your access control statements and roles
const statement = { ...defaultStatements } as const;
const ac = createAccessControl(statement);
const guest = ac.newRole({ ...userAc.statements });
const user = ac.newRole({ ...userAc.statements });
const admin = ac.newRole({ ...adminAc.statements });
const auth = betterAuth({
database, // Your database adapter
plugins: [
adminPlugin({
ac,
roles: { guest, user, admin },
defaultRole: "guest",
}),
invite({
inviteDurationSeconds: 3600, // Invites valid for 1 hour
roleForSignupWithoutInvite: "guest", // Role for users signing up without an invite
roleForSignupWithInvite: "user", // Role for users signing up with a valid invite
// Optional:
// generateCode: () => generateRandomString(8),
// canCreateInvite: (user) => user.role === 'manager',
// canAcceptInvite: (user) => user.email.endsWith('@acme.com'),
}),
],
emailAndPassword: { enabled: true }, // Or other auth strategies
// ... other betterAuth options
});
InviteOptions
:
-
inviteDurationSeconds
(number, required): The duration in seconds for which an invite code is valid after its creation. -
roleForSignupWithoutInvite
(string, required): The role assigned by theadmin
plugin to users who sign up without an active invite. -
roleForSignupWithInvite
(string, required): The role assigned to users who sign up with a valid, active invite. -
canCreateInvite
(function, optional): A function(user: UserWithRole) => boolean
that determines if a given user can create invites. If not provided, any authenticated user who is not in theroleForSignupWithoutInvite
can create invites. - [TODO]
canAcceptInvite
(function, optional): A function(user: UserWithRole) => boolean
that determines if a given user can accept/redeem an invite. If not provided, any user can accept an invite. -
generateCode
(function, optional): A function() => string
that returns a string to be used as the invite code. Defaults to a cryptographically strong random string generator (6 characters, 0-9, A-Z). -
getDate
(function, optional): A function() => Date
that returns the currentDate
. Defaults to() => new Date()
. Useful for testing time-sensitive features.
Import the inviteClient
plugin and add it to your betterAuth
client configuration.
import { createClient } from "better-auth/client"; // Or your client creation utility
// Adjust the import path based on your project setup
import { inviteClient } from "./client.js";
const client = createClient({
// ... other client options
plugins: [inviteClient()],
});
Authenticated users can create invite codes. The client plugin (client.invite.create
) provides a method for this:
import { client } from "@/lib/auth-client";
const { data, error } = await client.invite.create({
_: true, // better-call seems to require a body to be always defined for POST
});
if (error) {
console.error("Failed to create invite:", error);
return;
}
if (data) {
console.log("Invite code created:", data.code);
// Example response: { data: { code: "invite-123" }, error: null }
return data.code;
}
The server will handle storing this invite code, associating it with the creating user, and setting its expiry based on inviteDurationSeconds
.
When a user receives an invite code, he or she needs to redeem it. This is typically done by visiting a specific link or entering the code on a page. The client plugin (client.invite.redeem
) provides a method to handle this.
// Assuming 'client' is your configured better-auth client instance
async function redeemInvite(code: string) {
const { data, error } = await client.invite.redeem({ code });
if (error) {
console.error("Failed to redeem invite:", error);
// Handle error (e.g., code invalid, expired, already used)
return false;
}
// On successful redemption, a cookie named 'better-auth.invite-code'
// is set in the user's browser. This cookie will be used during sign-up.
console.log("Invite redeemed successfully. User can now sign up.");
return true;
}
The invite system integrates with the standard sign-up process. The behavior depends on whether a user has an active, redeemed invite.
Scenario 1: Signing Up or Signing In with an Active Invite
-
Redeem Invite: The user first redeems an invite code (see "Redeeming Invites"). This sets a
better-auth.invite-code
cookie. - Sign Up: The user proceeds to sign up (e.g., using email and password).
-
Role Upgrade: If a valid
better-auth.invite-code
cookie is present, the invite is validated. Additionally, if acanAcceptInvite
function is configured, it is evaluated for the user. If all conditions are met and the user's initial role (as defined byroleForSignupWithoutInvite
) is appropriate for an upgrade:- The user's role is upgraded to
roleForSignupWithInvite
. - The invite code is marked as used in the database.
- The
better-auth.invite-code
cookie is cleared.
- The user's role is upgraded to
// Assuming 'client' is your configured better-auth client instance
// and the user has already redeemed an invite code (the 'better-auth.invite-code' cookie is set).
async function signUpNewUserWithInvite(email, password, name) {
const { data, error } = await client.signUp.email({
email,
password,
name,
});
if (error) {
console.error("Sign-up failed:", error);
return;
}
if (data) {
console.log(
"Sign-up successful, user should have roleForSignupWithInvite:",
data.user,
);
// data.user contains the new user object, whose role should now be roleForSignupWithInvite.
// data.token contains the session token.
}
}
Scenario 2: Signing Up Or Signing In Without an Invite (or with an Invalid/Expired Invite)
Users can also sign up without an invite code. This is useful to implement waiting lists.
- Sign Up: The user signs up directly without redeeming an invite, or if their redeemed invite is invalid, expired, or already used.
-
Default Role:
- The user is created with the default role as defined in the
admin
plugin. - The
invite
plugin does not run.
- The user is created with the default role as defined in the
This allows the system to capture user interest even if invites are limited. Their role can be upgraded later, potentially by issuing them an invite or through other administrative actions.
// Assuming 'client' is your configured better-auth client instance
async function signUpNewUserWithoutInvite(email, password, name) {
const { data, error } = await client.signUp.email({
email,
password,
name,
});
if (error) {
console.error("Sign-up failed:", error);
// Standard sign-up errors.
return;
}
if (data) {
console.log(
"Sign-up successful, user should have roleForSignupWithoutInvite:",
data.user,
);
// data.user contains the new user object with the roleForSignupWithoutInvite.
// data.token contains the session token.
}
}
The invite plugin adds an invite
table to your database. Key fields include:
-
code
(string, unique): The invite code. -
invitedByUserId
(string, referencesuser.id
): The ID of the user who created the invite. -
usedByUserId
(string, optional, referencesuser.id
): The ID of the user who used the invite. -
used
(boolean): Whether the invite has been used. -
createdAt
(date): Timestamp of creation. -
expiresAt
(date): Timestamp when the invite expires. -
usedAt
(date, optional): Timestamp when the invite was used.
This schema is managed by better-auth
migrations when the plugin is active and shouldRunMigrations: true
is set during initialization.