- Setup
- Add
- organization()
- plugin to server config
- Add
- organizationClient()
- plugin to client config
- Run
- npx @better-auth/cli migrate
- Verify: check that organization, member, invitation tables exist in your database
- import
- {
- betterAuth
- }
- from
- "better-auth"
- ;
- import
- {
- organization
- }
- from
- "better-auth/plugins"
- ;
- export
- const
- auth
- =
- betterAuth
- (
- {
- plugins
- :
- [
- organization
- (
- {
- allowUserToCreateOrganization
- :
- true
- ,
- organizationLimit
- :
- 5
- ,
- // Max orgs per user
- membershipLimit
- :
- 100
- ,
- // Max members per org
- }
- )
- ,
- ]
- ,
- }
- )
- ;
- Client-Side Setup
- import
- {
- createAuthClient
- }
- from
- "better-auth/client"
- ;
- import
- {
- organizationClient
- }
- from
- "better-auth/client/plugins"
- ;
- export
- const
- authClient
- =
- createAuthClient
- (
- {
- plugins
- :
- [
- organizationClient
- (
- )
- ]
- ,
- }
- )
- ;
- Creating Organizations
- The creator is automatically assigned the
- owner
- role.
- const
- createOrg
- =
- async
- (
- )
- =>
- {
- const
- {
- data
- ,
- error
- }
- =
- await
- authClient
- .
- organization
- .
- create
- (
- {
- name
- :
- "My Company"
- ,
- slug
- :
- "my-company"
- ,
- logo
- :
- "https://example.com/logo.png"
- ,
- metadata
- :
- {
- plan
- :
- "pro"
- }
- ,
- }
- )
- ;
- }
- ;
- Controlling Organization Creation
- Restrict who can create organizations based on user attributes:
- organization
- (
- {
- allowUserToCreateOrganization
- :
- async
- (
- user
- )
- =>
- {
- return
- user
- .
- emailVerified
- ===
- true
- ;
- }
- ,
- organizationLimit
- :
- async
- (
- user
- )
- =>
- {
- // Premium users get more organizations
- return
- user
- .
- plan
- ===
- "premium"
- ?
- 20
- :
- 3
- ;
- }
- ,
- }
- )
- ;
- Creating Organizations on Behalf of Users
- Administrators can create organizations for other users (server-side only):
- await
- auth
- .
- api
- .
- createOrganization
- (
- {
- body
- :
- {
- name
- :
- "Client Organization"
- ,
- slug
- :
- "client-org"
- ,
- userId
- :
- "user-id-who-will-be-owner"
- ,
- //
userIdis required - }
- ,
- }
- )
- ;
- Note
- The
userId
parameter cannot be used alongside session headers.
Active Organizations
Stored in the session and scopes subsequent API calls. Set after user selects one.
const
setActive
=
async
(
organizationId
:
string
)
=>
{
const
{
data
,
error
}
=
await
authClient
.
organization
.
setActive
(
{
organizationId
,
}
)
;
}
;
Many endpoints use the active organization when
organizationId
is not provided (
listMembers
,
listInvitations
,
inviteMember
, etc.).
Use
getFullOrganization()
to retrieve the active org with all members, invitations, and teams.
Members
Adding Members (Server-Side)
await
auth
.
api
.
addMember
(
{
body
:
{
userId
:
"user-id"
,
role
:
"member"
,
organizationId
:
"org-id"
,
}
,
}
)
;
For client-side member additions, use the invitation system instead.
Assigning Multiple Roles
await
auth
.
api
.
addMember
(
{
body
:
{
userId
:
"user-id"
,
role
:
[
"admin"
,
"moderator"
]
,
organizationId
:
"org-id"
,
}
,
}
)
;
Removing Members
Use
removeMember({ memberIdOrEmail })
. The last owner cannot be removed — assign ownership to another member first.
Updating Member Roles
Use
updateMemberRole({ memberId, role })
.
Membership Limits
organization
(
{
membershipLimit
:
async
(
user
,
organization
)
=>
{
if
(
organization
.
metadata
?.
plan
===
"enterprise"
)
{
return
1000
;
}
return
50
;
}
,
}
)
;
Invitations
Setting Up Invitation Emails
import
{
betterAuth
}
from
"better-auth"
;
import
{
organization
}
from
"better-auth/plugins"
;
import
{
sendEmail
}
from
"./email"
;
export
const
auth
=
betterAuth
(
{
plugins
:
[
organization
(
{
sendInvitationEmail
:
async
(
data
)
=>
{
const
{
email
,
organization
,
inviter
,
invitation
}
=
data
;
await
sendEmail
(
{
to
:
email
,
subject
:
Join ${ organization . name }, html : `
${ inviter . user . name } invited you to join ${ organization . name }
Accept Invitation
,
}
)
;
}
,
}
)
,
]
,
}
)
;
Sending Invitations
await
authClient
.
organization
.
inviteMember
(
{
email
:
"newuser@example.com"
,
role
:
"member"
,
}
)
;
Shareable Invitation URLs
const
{
data
}
=
await
authClient
.
organization
.
getInvitationURL
(
{
email
:
"newuser@example.com"
,
role
:
"member"
,
callbackURL
:
"https://yourapp.com/dashboard"
,
}
)
;
// Share data.url via any channel
This endpoint does not call
sendInvitationEmail
— handle delivery yourself.
Invitation Configuration
organization
(
{
invitationExpiresIn
:
60
*
60
*
24
*
7
,
// 7 days (default: 48 hours)
invitationLimit
:
100
,
// Max pending invitations per org
cancelPendingInvitationsOnReInvite
:
true
,
// Cancel old invites when re-inviting
}
)
;
Roles & Permissions
Default roles:
owner
(full access),
admin
(manage members/invitations/settings),
member
(basic access).
Checking Permissions
const
{
data
}
=
await
authClient
.
organization
.
hasPermission
(
{
permission
:
"member:write"
,
}
)
;
if
(
data
?.
hasPermission
)
{
// User can manage members
}
Use
checkRolePermission({ role, permissions })
for client-side UI rendering (static only). For dynamic access control, use the
hasPermission
endpoint.
Teams
Enabling Teams
import
{
organization
}
from
"better-auth/plugins"
;
export
const
auth
=
betterAuth
(
{
plugins
:
[
organization
(
{
teams
:
{
enabled
:
true
}
}
)
,
]
,
}
)
;
Creating Teams
const
{
data
}
=
await
authClient
.
organization
.
createTeam
(
{
name
:
"Engineering"
,
}
)
;
Managing Team Members
Use
addTeamMember({ teamId, userId })
(member must be in org first) and
removeTeamMember({ teamId, userId })
(stays in org).
Set active team with
setActiveTeam({ teamId })
.
Team Limits
organization
(
{
teams
:
{
maximumTeams
:
20
,
// Max teams per org
maximumMembersPerTeam
:
50
,
// Max members per team
allowRemovingAllTeams
:
false
,
// Prevent removing last team
}
}
)
;
Dynamic Access Control
Enabling Dynamic Access Control
import
{
organization
}
from
"better-auth/plugins"
;
import
{
dynamicAccessControl
}
from
"@better-auth/organization/addons"
;
export
const
auth
=
betterAuth
(
{
plugins
:
[
organization
(
{
dynamicAccessControl
:
{
enabled
:
true
}
}
)
,
]
,
}
)
;
Creating Custom Roles
await
authClient
.
organization
.
createRole
(
{
role
:
"moderator"
,
permission
:
{
member
:
[
"read"
]
,
invitation
:
[
"read"
]
,
}
,
}
)
;
Use
updateRole({ roleId, permission })
and
deleteRole({ roleId })
. Pre-defined roles (owner, admin, member) cannot be deleted. Roles assigned to members cannot be deleted until reassigned.
Lifecycle Hooks
Execute custom logic at various points in the organization lifecycle:
organization
(
{
hooks
:
{
organization
:
{
beforeCreate
:
async
(
{
data
,
user
}
)
=>
{
// Validate or modify data before creation
return
{
data
:
{
...
data
,
metadata
:
{
...
data
.
metadata
,
createdBy
:
user
.
id
}
,
}
,
}
;
}
,
afterCreate
:
async
(
{
organization
,
member
}
)
=>
{
// Post-creation logic (e.g., send welcome email, create default resources)
await
createDefaultResources
(
organization
.
id
)
;
}
,
beforeDelete
:
async
(
{
organization
}
)
=>
{
// Cleanup before deletion
await
archiveOrganizationData
(
organization
.
id
)
;
}
,
}
,
member
:
{
afterCreate
:
async
(
{
member
,
organization
}
)
=>
{
await
notifyAdmins
(
organization
.
id
,
New member joined
)
;
}
,
}
,
invitation
:
{
afterCreate
:
async
(
{
invitation
,
organization
,
inviter
}
)
=>
{
await
logInvitation
(
invitation
)
;
}
,
}
,
}
,
}
)
;
Schema Customization
Customize table names, field names, and add additional fields:
organization
(
{
schema
:
{
organization
:
{
modelName
:
"workspace"
,
// Rename table
fields
:
{
name
:
"workspaceName"
,
// Rename fields
}
,
additionalFields
:
{
billingId
:
{
type
:
"string"
,
required
:
false
,
}
,
}
,
}
,
member
:
{
additionalFields
:
{
department
:
{
type
:
"string"
,
required
:
false
,
}
,
title
:
{
type
:
"string"
,
required
:
false
,
}
,
}
,
}
,
}
,
}
)
;
Security Considerations
Owner Protection
The last owner cannot be removed from an organization
The last owner cannot leave the organization
The owner role cannot be removed from the last owner
Always ensure ownership transfer before removing the current owner:
// Transfer ownership first
await
authClient
.
organization
.
updateMemberRole
(
{
memberId
:
"new-owner-member-id"
,
role
:
"owner"
,
}
)
;
// Then the previous owner can be demoted or removed
Organization Deletion
Deleting an organization removes all associated data (members, invitations, teams). Prevent accidental deletion:
organization
(
{
disableOrganizationDeletion
:
true
,
// Disable via config
}
)
;
Or implement soft delete via hooks:
organization
(
{
hooks
:
{
organization
:
{
beforeDelete
:
async
(
{
organization
}
)
=>
{
// Archive instead of delete
await
archiveOrganization
(
organization
.
id
)
;
throw
new
Error
(
"Organization archived, not deleted"
)
;
}
,
}
,
}
,
}
)
;
Invitation Security
Invitations expire after 48 hours by default
Only the invited email address can accept an invitation
Pending invitations can be cancelled by organization admins
Complete Configuration Example
import
{
betterAuth
}
from
"better-auth"
;
import
{
organization
}
from
"better-auth/plugins"
;
import
{
sendEmail
}
from
"./email"
;
export
const
auth
=
betterAuth
(
{
plugins
:
[
organization
(
{
// Organization limits
allowUserToCreateOrganization
:
true
,
organizationLimit
:
10
,
membershipLimit
:
100
,
creatorRole
:
"owner"
,
// Slugs
defaultOrganizationIdField
:
"slug"
,
// Invitations
invitationExpiresIn
:
60
*
60
*
24
*
7
,
// 7 days
invitationLimit
:
50
,
sendInvitationEmail
:
async
(
data
)
=>
{
await
sendEmail
(
{
to
:
data
.
email
,
subject
:
Join
${
data
.
organization
.
name
}
,
html
:
Accept
,
}
)
;
}
,
// Hooks
hooks
:
{
organization
:
{
afterCreate
:
async
(
{
organization
}
)
=>
{
console
.
log
(
Organization
${
organization
.
name
}
created
`
)
;
}
,
}
,
}
,
}
)
,
]
,
}
)
;