Skip to content

Organization ​

Organizations, members, invitations, teams, and dynamic access control (the largest plugin in the set). Mirrors the TS organization() plugin.

Enable ​

python
from better_auth import BetterAuth
from better_auth.access_control import create_access_control
from better_auth.plugins_ext import OrganizationPlugin

ac = create_access_control({"project": ["create", "share", "update", "delete"]})

auth = BetterAuth(
    secret="a-strong-32-character-minimum-secret",
    plugins=[
        OrganizationPlugin(
            ac=ac,
            roles={"admin": ac.new_role({"project": ["create", "update", "delete"]})},
            creator_role="owner",
        )
    ],
)

Options ​

OptionTypeDefaultDescription
allow_user_to_create_organizationbool | callableTrueGate organization creation, statically or per user.
organization_limitint | callable | NoneNoneMax organizations a user may create.
creator_rolestr"owner"Role given to the creating member.
membership_limitint | callable100Max members per organization.
acAccessControl | NoneNoneAccess-control statement set backing roles.
rolesdict[str, Role] | NoneNoneCustom role definitions.
dynamic_access_controldict | NoneNoneEnable per-organization roles stored in the database.
disable_organization_deletionboolFalseReject /organization/delete.
invitation_expires_inint172800Invitation lifetime in seconds (2 days).
invitation_limitint | callable | None100Max pending invitations per inviter.
cancel_pending_invitations_on_re_inviteboolFalseCancel a previous pending invitation when re-inviting the same email.
require_email_verification_on_invitationbool | NoneNoneRequire a verified email before accepting an invitation.
send_invitation_emailcallable | NoneNone(data) -> None, called when an invitation is created.
teamsdict | NoneNoneTeam support, gated on {"enabled": True} (plus team options).
organization_hooksdict[str, callable] | NoneNoneLifecycle hooks, snake_case keys (before_create_organization, ...); before_* hooks may return {"data": {...}} to merge.
additional_fieldsdict | NoneNoneExtra schema fields per organization table.

Endpoints ​

21 core routes under /organization/:

MethodPath
POST/organization/create
POST/organization/update
POST/organization/delete
POST/organization/set-active
GET/organization/get-organization
GET/organization/get-full-organization
GET/organization/list
POST/organization/check-slug
POST/organization/leave
GET/organization/list-members
POST/organization/remove-member
POST/organization/update-member-role
GET/organization/get-active-member
POST/organization/has-permission
POST/organization/invite-member
POST/organization/accept-invitation
POST/organization/reject-invitation
POST/organization/cancel-invitation
GET/organization/get-invitation
GET/organization/list-invitations
GET/organization/list-user-invitations

With teams={"enabled": True}, 9 more:

MethodPath
POST/organization/create-team
POST/organization/update-team
POST/organization/remove-team
GET/organization/list-teams
POST/organization/set-active-team
GET/organization/list-user-teams
GET/organization/list-team-members
POST/organization/add-team-member
POST/organization/remove-team-member

GET /organization/get-organization returns the organization row only, without members or invitations. It takes organizationId or organizationSlug and falls back to the active organization (a null body when there is none).

GET /organization/list-user-teams accepts userId and organizationId. A caller with the member:update permission in that organization can list another member's teams. Without parameters it lists the caller's own teams.

Schema ​

TableColumns
organizationid, name, slug, logo, metadata, createdAt
memberid, organizationId, userId, role, createdAt
invitationid, organizationId, email, role, status, expiresAt, createdAt, inviterId
sessionadds activeOrganizationId

With teams enabled: team, teamMember tables, invitation.teamId and session.activeTeamId. team.memberCount and teamMember.membershipKey back atomic team capacity checks. Both are internal: never accepted as input and never returned.

Changed in 1.1

team.memberCount and teamMember.membershipKey are new columns. Run your migrations. Existing teams resync their count on the next join. See Upgrade from 1.0.

Notes ​

  • metadata is stored as a JSON string column, identical to TS, so a Python and a TS app can share the database.
  • Deleting an organization cascades to members, invitations and teams; the cascade is not wrapped in a transaction here (a deliberate simplification: the MemoryAdapter has no transactions; TS wraps it in one).
  • Accepting an invitation is a guarded compare-and-set on its pending status: of two concurrent accepts, the loser gets INVITATION_NOT_FOUND instead of a success. A failed accept rolls the status back the same way.
  • Team member limits reserve a seat on team.memberCount before adding the member, so a full team refuses the request. The limit on the number of teams is still checked with count-then-create, which races under heavy concurrency, the same known FIXME as TS.
  • For instance-wide (non-organization) roles, see Admin.

MIT licensed · API-compatible with better-auth