Skip to main content
The Better Auth Invite Plugin provides a flexible permission system that controls who can create, accept, cancel, and reject invitations. You can use simple boolean flags, custom functions, or integrate with the Better Auth admin plugin for role-based access control.

Permission options overview

The plugin provides four permission options:
type InviteOptions = {
  canCreateInvite?: boolean | Function | Permissions;
  canAcceptInvite?: boolean | Function | Permissions;
  canCancelInvite?: boolean | Function | Permissions;
  canRejectInvite?: boolean | Function | Permissions;
};
Source: src/types.ts:23-84

Boolean flags

The simplest permission configuration uses boolean values.

Allow all (default)

import { invite } from "better-auth-invite";

export const auth = betterAuth({
  plugins: [
    invite({
      canCreateInvite: true,  // Default
      canAcceptInvite: true,  // Default
      canCancelInvite: true,  // Default
      canRejectInvite: true,  // Default
    }),
  ],
});
All permissions default to true, allowing any authenticated user to perform the action (subject to other constraints like email matching for private invites).

Deny all

invite({
  canCreateInvite: false,  // Nobody can create invites
});
When set to false, the permission check fails with an error:
if (!canCreateInvite) {
  throw ctx.error("BAD_REQUEST", {
    message: "User does not have sufficient permissions to create invite",
    errorCode: "INSUFFICIENT_PERMISSIONS",
  });
}
Source: src/routes/create-invite.ts:89-93

Permission functions

For dynamic permission logic, provide a function that returns a boolean.

canCreateInvite function

invite({
  canCreateInvite: async ({ invitedUser, inviterUser, ctx }) => {
    // Only admins can create invites
    return inviterUser.role === "admin";
  },
});
Function signature:
canCreateInvite?: (data: {
  invitedUser: {
    email?: string;
    role: string;
  };
  inviterUser: UserWithRole;
  ctx: GenericEndpointContext;
}) => Promise<boolean> | boolean;
Source: src/types.ts:23-32

canAcceptInvite function

invite({
  canAcceptInvite: async ({ invitedUser, newAccount }) => {
    // Only new accounts can accept invites
    return newAccount === true;
  },
});
Function signature:
canAcceptInvite?: (data: {
  invitedUser: UserWithRole;
  newAccount: boolean;
}) => Promise<boolean> | boolean;
Source: src/types.ts:46-51

canCancelInvite function

invite({
  canCancelInvite: async ({ inviterUser, invitation, ctx }) => {
    // Can only cancel within 24 hours
    const hoursSinceCreated = 
      (Date.now() - invitation.createdAt.getTime()) / (1000 * 60 * 60);
    return hoursSinceCreated < 24;
  },
});
Function signature:
canCancelInvite?: (data: {
  inviterUser: UserWithRole;
  invitation: InviteTypeWithId;
  ctx: GenericEndpointContext;
}) => Promise<boolean> | boolean;
Source: src/types.ts:61-67
Regardless of this option, only the user who created the invite can cancel it. This is enforced before the permission function runs:
if (invitation.createdByUserId !== inviterUser.id) {
  throw error("INSUFFICIENT_PERMISSIONS");
}
Source: src/routes/cancel-invite.ts:82-86

canRejectInvite function

invite({
  canRejectInvite: async ({ inviteeUser, invitation, ctx }) => {
    // Can only reject if not yet expired
    return new Date() < invitation.expiresAt;
  },
});
Function signature:
canRejectInvite?: (data: {
  inviteeUser: UserWithRole;
  invitation: InviteTypeWithId;
  ctx: GenericEndpointContext;
}) => Promise<boolean> | boolean;
Source: src/types.ts:77-83
Regardless of this option, only the invitee can reject private invites. Public invites cannot be rejected at all:
if (inviteType === "public" || invitation.email !== inviteeUser.email) {
  throw error("CANT_REJECT_INVITE");
}
Source: src/routes/reject-invite.ts:84-88

Permission objects (RBAC)

For advanced role-based access control, integrate with the Better Auth admin plugin.

Permission object structure

type Permissions = {
  statement: string;      // Resource/action identifier
  permissions: string[];  // Required permission values
};
Source: src/types.ts:358-361

Using permission objects

import { invite } from "better-auth-invite";
import { admin } from "better-auth/plugins";

export const auth = betterAuth({
  plugins: [
    admin(),  // Required for permission objects
    invite({
      canCreateInvite: {
        statement: "invite",
        permissions: ["create"],
      },
      canAcceptInvite: {
        statement: "invite",
        permissions: ["accept"],
      },
    }),
  ],
});

Permission validation flow

When you use a permission object, the plugin:
  1. Checks if the admin plugin is installed
  2. Calls the admin plugin’s userHasPermission endpoint
  3. Returns the result
export const checkPermissions = async (
  ctx: GenericEndpointContext,
  permissions: Permissions,
) => {
  const session = ctx.context.session;
  if (!session?.session) {
    throw ctx.error("UNAUTHORIZED");
  }

  const adminPlugin = getPlugin("admin", ctx.context);
  if (!adminPlugin) {
    throw ctx.error("FAILED_DEPENDENCY", {
      message: "Admin plugin is not set-up.",
    });
  }

  try {
    return await adminPlugin.endpoints.userHasPermission({
      ...ctx,
      body: {
        userId: session.user.id,
        permissions: { 
          [permissions.statement]: permissions.permissions 
        },
      },
    });
  } catch {
    return false;
  }
};
Source: src/utils.ts:245-279
If you use permission objects without the admin plugin installed, you’ll get an error:
Admin plugin is not set-up.
Make sure to install the admin plugin first:
import { admin } from "better-auth/plugins";

plugins: [admin(), invite({ /* ... */ })];

Permission evaluation order

Permissions are evaluated in this order:

For canCreateInvite

// 1. Get permission option
const canCreateInviteOption = 
  typeof options.canCreateInvite === "function"
    ? await options.canCreateInvite({ invitedUser, inviterUser, ctx })
    : options.canCreateInvite;

// 2. Check if it's a permission object
const canCreateInvite = 
  typeof canCreateInviteOption === "object"
    ? await checkPermissions(ctx, canCreateInviteOption)
    : canCreateInviteOption;

// 3. Throw error if denied
if (!canCreateInvite) {
  throw error("INSUFFICIENT_PERMISSIONS");
}
Source: src/routes/create-invite.ts:76-93

For canAcceptInvite

// 1. Validate email (private invites only)
if (invitation.email && invitation.email !== invitedUser.email) {
  throw error("INVALID_EMAIL");
}

// 2. Validate status
if (invitation.status !== "pending") {
  throw error("INVALID_TOKEN");
}

// 3. Get permission option
const canAcceptInviteOptions = 
  typeof options.canAcceptInvite === "function"
    ? await options.canAcceptInvite({ invitedUser, newAccount })
    : options.canAcceptInvite;

// 4. Check if it's a permission object
const canAcceptInvite = 
  typeof canAcceptInviteOptions === "object"
    ? await checkPermissions(ctx, canAcceptInviteOptions)
    : canAcceptInviteOptions;

// 5. Throw error if denied
if (!canAcceptInvite) {
  throw error("CANT_ACCEPT_INVITE");
}
Source: src/utils.ts:114-137

Custom permission examples

invite({
  canCreateInvite: async ({ inviterUser, invitedUser }) => {
    // Admins can invite anyone
    if (inviterUser.role === "admin") return true;
    
    // Managers can only invite members
    if (inviterUser.role === "manager") {
      return invitedUser.role === "member";
    }
    
    // Others cannot invite
    return false;
  },
});
import { kv } from "./kv-store";

invite({
  canCreateInvite: async ({ inviterUser }) => {
    const key = `invite:ratelimit:${inviterUser.id}`;
    const count = await kv.get(key) || 0;
    
    // Max 5 invites per day
    if (count >= 5) return false;
    
    await kv.set(key, count + 1, { ex: 86400 });
    return true;
  },
});
invite({
  canCreateInvite: async ({ invitedUser, inviterUser }) => {
    // Can only invite users from same domain
    const inviterDomain = inviterUser.email.split("@")[1];
    const inviteeDomain = invitedUser.email?.split("@")[1];
    
    return inviterDomain === inviteeDomain;
  },
});
invite({
  canAcceptInvite: async ({ invitedUser, newAccount }) => {
    // Only new users can accept "member" role invites
    if (invitedUser.role === "member" && !newAccount) {
      return false;
    }
    
    return true;
  },
});
invite({
  canCreateInvite: async ({ ctx }) => {
    // Only allow invite creation during business hours
    const hour = new Date().getHours();
    const isBusinessHours = hour >= 9 && hour < 17;
    
    return isBusinessHours;
  },
});
invite({
  canCreateInvite: async ({ inviterUser, invitedUser, ctx }) => {
    const hasPermission = inviterUser.role === "admin";
    
    // Log permission check
    await logAudit({
      action: "invite.create.permission",
      userId: inviterUser.id,
      targetEmail: invitedUser.email,
      allowed: hasPermission,
      timestamp: new Date(),
    });
    
    return hasPermission;
  },
});

Combining with hooks

Permissions run before hooks, allowing you to layer validation:
invite({
  // Permission check runs first
  canCreateInvite: async ({ inviterUser }) => {
    return inviterUser.role === "admin";
  },
  
  inviteHooks: {
    // Hook runs after permission check passes
    beforeCreateInvite: async ({ ctx }) => {
      // Additional validation or side effects
      await notifyAdmins({
        message: `${ctx.context.session.user.email} is creating an invite`,
      });
    },
  },
});
Source: src/routes/create-invite.ts:98

Permission vs validation

Understand the difference between permissions and built-in validation:

Built-in validations (always enforced)

  • Email must be valid format
  • Token must not be expired
  • Invitation must not exceed max uses
  • Only creator can cancel an invite
  • Only recipient can reject a private invite
  • Public invites cannot be rejected

Permissions (customizable)

  • Who can create invites
  • Who can accept invites
  • Additional constraints on cancellation
  • Additional constraints on rejection
Use permissions for authorization (“Is this user allowed?”)
Built-in validations handle data integrity (“Is this request valid?”)

Error handling

Permission failures return specific error codes:
try {
  await client.invite.create({
    email: "[email protected]",
    role: "admin",
  });
} catch (error) {
  if (error.code === "INSUFFICIENT_PERMISSIONS") {
    console.log("You don't have permission to create invites");
  }
}
Error codes:
  • INSUFFICIENT_PERMISSIONS - Permission check failed for create/cancel
  • CANT_ACCEPT_INVITE - Permission check failed for accept
  • CANT_REJECT_INVITE - Permission check failed for reject
  • UNAUTHORIZED - No session when using permission objects
  • FAILED_DEPENDENCY - Admin plugin not installed when using permission objects
Source: src/constants.ts:3-16

Build docs developers (and LLMs) love