Skip to main content
The Better Auth Invite Plugin supports two distinct types of invitations: private invites (email-based) and public invites (shareable codes). Each type serves different use cases and has unique characteristics.

Private invites

Private invites are sent directly to a specific email address. They are ideal for inviting specific individuals to your application.

Creating a private invite

To create a private invite, include an email field:
const result = await client.invite.create({
  email: "[email protected]",
  role: "member",
});

// Returns: { status: true, message: "The invitation was sent" }
The plugin automatically sends an email to the specified address with the invitation link. Source: src/routes/create-invite.ts:59-170

Private invite characteristics

Email validation

Only the user with the matching email can accept the invitation

Single use default

Private invites default to maxUses: 1

Email required

Must configure sendUserInvitation in plugin options

New account detection

Automatically detects if user exists or needs to sign up

Email validation enforcement

When a user attempts to accept a private invite, the plugin validates their email:
if (invitation.email && invitation.email !== invitedUser.email) {
  throw error("INVALID_EMAIL", "This token is for a specific email");
}
Source: src/utils.ts:114-116

New account vs role upgrade

Private invites automatically determine whether the recipient needs to create a new account:
// Plugin checks if user exists
const invitedUser = await adapter.findUserByEmail(email);
const newAccount = !invitedUser;

// Determines redirect URL
const callbackURL = invitedUser 
  ? redirectToSignIn   // Existing user: sign in to upgrade
  : redirectToSignUp;  // New user: sign up with invite
This information is:
  • Stored in the newAccount field of the invitation
  • Passed to the sendUserInvitation function for email templates
  • Available in permission checks via canAcceptInvite
Source: src/routes/create-invite.ts:100-111

Email configuration

Private invites require an email function:
import { betterAuth } from "better-auth";
import { invite } from "better-auth-invite";

export const auth = betterAuth({
  plugins: [
    invite({
      sendUserInvitation: async (data, request) => {
        const { email, name, role, url, token, newAccount } = data;
        
        const subject = newAccount
          ? `You've been invited to join as ${role}`
          : `Your role has been upgraded to ${role}`;
        
        await sendEmail({
          to: email,
          subject,
          html: `
            <p>Hello ${name || 'there'},</p>
            <p>${subject}</p>
            <a href="${url}">Accept invitation</a>
          `,
        });
      },
    }),
  ],
});
If you attempt to create a private invite without configuring sendUserInvitation, the plugin will throw an error:
Invitation email is not enabled. Pass `sendUserInvitation` to the plugin options.
Source: src/routes/create-invite.ts:61-72

Rejecting private invites

Only the recipient can reject a private invite:
// Only works if logged-in user's email matches invite email
await client.invite.reject({
  token: "invitation-token",
});
Public invites cannot be rejected:
const inviteType = invitation.email ? "private" : "public";

if (inviteType === "public" || invitation.email !== inviteeUser.email) {
  throw error("CANT_REJECT_INVITE");
}
Source: src/routes/reject-invite.ts:82-89

Public invites

Public invites are shareable codes or links that anyone can use. They’re perfect for open registration, beta access codes, or community invitations.

Creating a public invite

To create a public invite, omit the email field:
const result = await client.invite.create({
  role: "member",
  senderResponse: "token",  // or "url"
});

// Returns: { status: true, message: "abc123def456..." }
// The message contains either the token or full URL
Source: src/routes/create-invite.ts:172-190

Public invite characteristics

No email validation

Anyone with the token can use the invitation

Multi-use default

Public invites default to unlimited uses

No email sent

You receive the token/URL and distribute it yourself

Cannot be rejected

Public invites can only be canceled by the creator

Response format options

You can control what the API returns when creating public invites:

Token response

const result = await client.invite.create({
  role: "member",
  senderResponse: "token",
});

console.log(result.message);
// Output: "abc123def456ghi789jkl012"

URL response

const result = await client.invite.create({
  role: "member",
  senderResponse: "url",
});

console.log(result.message);
// Output: "https://yourapp.com/invite/abc123def456?callbackURL=/auth/sign-up"
Set a default in your plugin configuration:
invite({
  defaultSenderResponse: "url",  // "token" | "url"
});

Redirect destination

For public invites, you control whether the URL redirects to sign-up or sign-in:
const result = await client.invite.create({
  role: "member",
  senderResponse: "url",
  senderResponseRedirect: "signUp",  // "signUp" | "signIn"
});
This determines the callbackURL parameter in the generated URL:
const redirectTo = senderResponseRedirect === "signUp"
  ? redirectToSignUp   // Default: /auth/sign-up
  : redirectToSignIn;  // Default: /auth/sign-in
Source: src/routes/create-invite.ts:172-180

Usage limits

Public invites default to unlimited uses, but you can restrict them:
const result = await client.invite.create({
  role: "beta-tester",
  maxUses: 100,  // Limit to 100 uses
});
The plugin tracks each use in the inviteUse table:
const timesUsed = await adapter.countInvitationUses(invitation.id);

if (timesUsed >= invitation.maxUses) {
  throw error("NO_USES_LEFT_FOR_INVITE");
}
Source: src/adapter.ts:90-99

Public invite use cases

// Create shareable beta code
const { message } = await client.invite.create({
  role: "beta-user",
  tokenType: "code",        // 6-character code
  maxUses: 1000,
  expiresIn: 30 * 24 * 60 * 60,  // 30 days
});

// Share code: "A1B2C3"
// Create time-limited premium access
const { message } = await client.invite.create({
  role: "premium",
  maxUses: 50,
  expiresIn: 7 * 24 * 60 * 60,  // 7 days
});
// Create reusable team invite link
const { message } = await client.invite.create({
  role: "team-member",
  senderResponse: "url",
  redirectToAfterUpgrade: "/team/welcome",
});

// Share URL with new team members
// Create single-use event codes
const codes = await Promise.all(
  Array.from({ length: 100 }, async () => {
    const { message } = await client.invite.create({
      role: "attendee",
      tokenType: "code",
      maxUses: 1,
    });
    return message;
  })
);

Configuration differences

Here’s how configuration options differ between invite types:
OptionPrivate InvitesPublic Invites
emailRequiredOmit
maxUsesDefault: 1Default: Infinity
sendUserInvitationRequiredNot used
senderResponseIgnoredReturns token or URL
senderResponseRedirectIgnoredControls callback URL
newAccountAuto-detectedNot set
Can be rejectedYes (by recipient)No
Email validationYesNo
Source: src/routes/create-invite.ts:59 and src/adapter.ts:33-36

Hybrid approach

You can create private invites with multiple uses:
const result = await client.invite.create({
  email: "[email protected]",
  role: "member",
  maxUses: 3,  // Can be used 3 times
});
This allows the recipient to:
  • Accept the invite on multiple devices
  • Recover if the first attempt fails
  • Share with the same email on different accounts (if your auth system allows)
Email validation still applies - only users with the matching email can use the invitation, but they can use it up to maxUses times.

Type determination

The plugin determines invite type based on the presence of an email:
const inviteType = email ? "private" : "public";

if (inviteType === "private") {
  // Send email
  await options.sendUserInvitation({ email, role, url, token, newAccount });
  return { status: true, message: "The invitation was sent" };
}

if (inviteType === "public") {
  // Return token or URL
  const returnToken = senderResponse === "token" ? token : url;
  return { status: true, message: returnToken };
}
Source: src/routes/create-invite.ts:59-190

Getting invite information

The behavior of the getInvite endpoint differs by type:

Private invites

// Must be logged in with matching email
await client.invite.get({ token });

// Validates:
if (invitation.email && invitation.email !== sessionUser.email) {
  throw error("INVALID_TOKEN");
}

Public invites

// Can be accessed by anyone (no session required)
await client.invite.get({ token });

// No email validation
Source: src/routes/get-invite.ts:99-104

Build docs developers (and LLMs) love