Skip to content

Organizations & Roles

Organizations provide multi-tenant isolation. Each organization is a separate tenant with its own members, roles, and billing. Users can belong to multiple organizations and switch between them.

Server Configuration

The organization plugin is configured in apps/api/lib/auth.ts:

ts
organization({
  allowUserToCreateOrganization: true,
  organizationLimit: 5,
  creatorRole: "owner",
}),
SettingValueDescription
allowUserToCreateOrganizationtrueAny user can create organizations
organizationLimit5Max organizations per user
creatorRole"owner"Creator automatically gets the owner role

Database Tables

organization

Defined in db/schema/organization.ts:

ColumnTypeDescription
idtextPrefixed CUID2 (org_cm...)
nametextDisplay name
slugtextURL-safe unique identifier
logotextLogo URL (optional)
metadatatextJSON string for custom data
stripeCustomerIdtextStripe customer for org-level billing

member

Links users to organizations with a role:

ColumnTypeDescription
idtextPrefixed CUID2 (mem_cm...)
userIdtextReferences user.id
organizationIdtextReferences organization.id
roletext"owner", "admin", or "member"

A unique constraint on (userId, organizationId) prevents duplicate memberships.

invitation

Manages pending invitations, defined in db/schema/invitation.ts:

ColumnTypeDescription
idtextPrefixed CUID2 (inv_cm...)
emailtextInvitee's email address
inviterIdtextReferences user.id
organizationIdtextReferences organization.id
roletextRole assigned upon acceptance
statustext"pending", "accepted", "rejected", or "canceled"
expiresAttimestampInvitation expiration
acceptedAttimestampWhen the invite was accepted
rejectedAttimestampWhen the invite was rejected or canceled

A unique constraint on (organizationId, email) prevents duplicate invitations to the same person.

Roles

Three built-in roles with hierarchical permissions:

RoleCan manage membersCan manage settingsCan delete org
ownerYesYesYes
adminYesYesNo
memberNoNoNo

Role Checks in API Procedures

Use the session's activeOrganizationId with a membership query to check roles:

ts
// apps/api/routers/organization.ts
const [row] = await ctx.db
  .select({ role: Db.member.role })
  .from(Db.member)
  .where(
    and(
      eq(Db.member.organizationId, referenceId),
      eq(Db.member.userId, user.id),
    ),
  );

const isAdmin = row?.role === "owner" || row?.role === "admin";

Active Organization

The session tracks which organization is currently active via activeOrganizationId:

ts
export type AuthSession = SessionResponse["session"] & {
  activeOrganizationId?: string;
};

This field is stored in the session table and persists across requests. When the user switches organizations, Better Auth updates this field.

Billing Integration

Subscriptions scope to the active organization. The billing router uses activeOrganizationId as the billing reference, falling back to the user's own ID for personal billing:

ts
// apps/api/routers/billing.ts
const referenceId = ctx.session.activeOrganizationId ?? ctx.user.id;

The Stripe plugin's authorizeReference hook enforces that only owners and admins can manage an organization's subscription:

ts
authorizeReference: async ({ user, referenceId }) => {
  if (referenceId === user.id) return true; // Personal billing
  const [row] = await db
    .select({ role: Db.member.role })
    .from(Db.member)
    .where(
      and(
        eq(Db.member.organizationId, referenceId),
        eq(Db.member.userId, user.id),
      ),
    );
  return row?.role === "owner" || row?.role === "admin";
},

Invitation Lifecycle

  1. Owner/admin invites – sends invitation to email with assigned role
  2. Invitation pending – stored in invitation table with status: "pending" and an expiration
  3. Invitee accepts – Better Auth creates a member record and updates invitation status
  4. Or invitee rejects / invitation expires – invitation status is updated, no member created

Each organization can only have one pending invitation per email address.

Client API

The organizationClient() plugin adds organization methods to the auth client:

ts
// Create an organization
await auth.organization.create({ name: "Acme Inc", slug: "acme" });

// List user's organizations
const { data } = await auth.organization.list();

// Set active organization
await auth.organization.setActive({ organizationId: "org_cm..." });

// Invite a member
await auth.organization.inviteMember({
  email: "[email protected]",
  role: "member",
  organizationId: "org_cm...",
});

See the Better Auth organization plugin docs for the complete client API.