Organizations
Org invites, seat limits, and platform controls.
Org features switch on when the organization plugin is detected. Pass the same ac and roles objects you gave the org plugin:
import { organization } from "better-auth/plugins";
import { betterEnrollment, roleGate } from "@octopi-ai/better-enrollment";
plugins: [
admin(),
organization({
ac, roles,
allowUserToCreateOrganization: roleGate(["admin", "org-creator"]),
}),
betterEnrollment({
organization: {
ac, roles,
defaultOrganizationRole: "member",
defaultSeatLimit: 10,
},
}),
],Redeeming an org invite writes the member row (and team membership when teamId is set) in the same flow that creates the account. Redemption re-validates everything, since state drifts between create and accept.
Active organization is a product decision
After an org-join or org-create invite is redeemed, the plugin does not select an active
organization. Whether to switch the user into the new org automatically at sign-in or let them
pick through an org switcher is your app's call: set it with the org plugin's setActive where it
fits your flow.
Seat limits
Seat limits resolve in order: resolveSeatLimit(org) callback, the org's seatLimit column, defaultSeatLimit, then unlimited. Seats used = members + pending invite reservations (a private invite reserves one seat, a public one reserves maxUses - useCount). Enforcement happens at creation and again inside a guarded write at redemption, so parallel accepts never overshoot.
await authClient.invite.org.setSeatLimit({ organizationId, seatLimit: 25 });
await authClient.invite.org.usage({ organizationId });
// -> { seatLimit, members, pendingReserved, remaining }Platform controls
App-admin only: disable, enable, and delete organizations. A disabled org refuses invite creation, redemption, and org plugin mutations. Member accounts are never touched by default; banMembers: true additionally bans every member app-wide, for fraud takedowns.
await authClient.invite.org.disable({ organizationId });
await authClient.invite.org.enable({ organizationId });
await authClient.invite.org.delete({ organizationId });Who can do what
| Action | App admin | Org owner or admin | Member with invitation:create | Plain member |
|---|---|---|---|---|
Create app / org-create invite | Yes | No | No | No |
Create org-join invite | No | Yes | Yes | No |
| List invites | All | Own org | Own org | No |
| Revoke or delete | Moderation backstop | Own org | Own org (invitation:cancel) | No |
| Seat limits, disable, delete org | Yes | No | No | No |
App admins come from the admin plugin's AC file (ac and roles passed to the plugin) or, without one, from adminRoles (default ["admin"]), adminUserIds, or your own canManageInvites(user) callback; see Permissions. Org permissions are the org plugin's own access control. Cross-org calls return FORBIDDEN, and unknown org ids look identical so they cannot be enumerated.
One actor sits outside this table: trusted server code. The server-only createSystemInvite endpoint can create any invite kind, including org-join, without a session or org membership; seat limits, disabled-org checks, and the allowOwnerInvites switch still apply. System invites store createdByUserId as null and a self-declared inviter, so they remain distinguishable from member-created invites in the audit trail, but their attribution is only as trustworthy as the code that called the endpoint.
Where to go next
Last updated on