Concepts

Organizations

Multi-tenant organization support in auth-service — the org_id claim, member management, and admin operations.

Auth Service includes first-class support for organizations (companies / tenants) powered by BetterAuth's organization() plugin. Organizations are designed for a two-tier distribution model:

TierActorTypical role
1Reseller / distributorCreates and owns organizations representing their end-clients
2IntegratorMember of one or more organizations; manages IT resources scoped to those organizations

Organization schema

FieldTypeDescription
idstringPrimary key
namestringDisplay name (max 100 chars)
slugstringUnique URL-safe identifier (lowercase alphanumeric + hyphens, max 50 chars)
logostringOptional logo URL
metadataJSON objectArbitrary key-value metadata
createdAttimestampCreation date

Member roles

Every member of an organization holds one of three roles. They are BetterAuth's default organization roles and govern the native /api/auth/organization/* endpoints; the admin API below is gated by the global admin / superadmin role instead.

RoleBetterAuth default permissions
ownerUpdate and delete the organization, create, update and delete members, create and cancel invitations
adminUpdate the organization, create, update and delete members, create and cancel invitations
memberNo organization, member or invitation permissions

The org_id claim

When a client requests the org scope during an OAuth 2.0 authorization flow, the access token (a JWT, so the request needs a resource) includes an org_id claim set to the ID of the organization the authorization is bound to:

{
  "org_id": "org_01hwxyz..."
}

Which organization is used:

  • A user who belongs to exactly one organization is bound to it implicitly.
  • A user who belongs to several is redirected to /select-org during authorization and picks one; the page sets it as the session's active organization (/api/auth/organization/set-active) and the authorization resumes with that organization.
  • A user who belongs to none gets no org_id.

The claim is not added to the ID token or to userinfo.

Backend services can use this claim to:

  • Scope database queries to the organization's data.
  • Enforce CASL abilities with { org_id } conditions.
  • Isolate resources between tenants without additional requests to auth-service.

Access control

Only users with the global role admin or superadmin can create organizations (BetterAuth's allowUserToCreateOrganization is restricted to them), and every admin API route below requires one of these roles. Regular user-role accounts can belong to organizations as members but cannot create them.

Admin API endpoints

List organizations

GET /api/admin/organizations

Supports pagination and filtering:

Query parameterDefaultDescription
page1Page number
limit20Results per page (max 100)
search—Filter by name or slug (case-insensitive, partial match)
sortBynameSort field: name, slug, or createdAt
sortOrderascasc or desc

Response includes a memberCount for each organization, plus total, page and limit.

Create an organization

POST /api/admin/organizations
{
  "name": "Acme Corp",
  "slug": "acme-corp",
  "logo": "https://example.com/logo.png",
  "metadata": { "region": "eu-west" }
}

logo, when given, must be a URL. Answers 201 { organization }; a taken slug is refused with ORG_002.

Get an organization

GET /api/admin/organizations/:id

Returns the organization with its full member list.

Update an organization

PATCH /api/admin/organizations/:id

Accepts name, slug, logo and metadata (at least one).

Delete an organization

DELETE /api/admin/organizations/:id

List members

GET /api/admin/organizations/:id/members

Query parameters: limit (default 50, max 100), offset and search (user name or email). Returns { members, total }.

Add a member

POST /api/admin/organizations/:id/members
{
  "userId": "...",
  "role": "member"
}

role is owner, admin or member (default member). A user who is already a member is refused with ORG_005.

Update a member's role

PATCH /api/admin/organizations/:id/members/:memberId/role
{
  "role": "admin"
}

:memberId is the membership id (the id of the member entry), not the user id. Answers 204.

Remove a member

DELETE /api/admin/organizations/:id/members/:userId

A user who is not a member is refused with ORG_004. Answers 204.

List invitations

GET /api/admin/organizations/:id/invitations

Send an invitation

POST /api/admin/organizations/:id/invitations
{
  "email": "newmember@example.com",
  "role": "member"
}

Sends an invitation email via BetterAuth's invitation flow. Requires SMTP to be configured for the email to be delivered.

Cancel an invitation

DELETE /api/admin/organizations/:id/invitations/:invitationId

Invitation acceptance

Organization invitations are created through the BetterAuth organization plugin and emailed as a link to:

https://<auth-host>/accept-invitation?id=<invitation-id>

The page is always server-rendered from the template system (built-in fallback), because it has no equivalent in the Vue SPA. It supports three paths:

SituationBehaviour
The invitee has no accountEmail/password registration is allowed even when registration is closed. The account is created verified, added to the invited organization and the invitation is marked accepted in one transaction. Social providers configured globally are also offered.
The invitee has an accountThe page asks them to sign in. After sign-in, the invitation is accepted if the session email matches the invited address (AUTH_014 otherwise) and is verified (AUTH_015 otherwise).
A social provider callback returns a sessionThe provider account is accepted only when its verified email equals the invited address. The user.create.before hook aborts account creation otherwise.

Acceptance never depends on a default organization: the organization is the one carried by the invitation row. Application invitations are separate and grant application access only; they do not create or assign an organization.

Social profile company and domain auto-join

auth-service does not use a social profile's company (or any equivalent provider field) to assign an organization. Social profile fields are self-declared and untrusted: a user can type any company name in a provider profile, so treating it as membership would let them claim access to a tenant.

Membership is granted only by an explicit action:

  • an organization invitation accepted by the matching email address;
  • an administrator adding the member through the admin API or UI.

No email-domain rule exists either. An organization has no email_domain field, so a user is never auto-joined because their email ends in a given suffix.

Copyright © 2026