Skip to content

Tenant management ​

The plugin exposes CRUD endpoints for tenants and platform-user memberships.

Authorization ​

Management access is resolved in this order:

  1. canManageTenants returns true — global admin (e.g. operator API key). Can create and manage every tenant.
  2. Authenticated platform user — a session whose user.tenantId is null. Access is scoped by membership role.
  3. Otherwise — denied.

Tenant end-users (user.tenantId set) cannot create or manage tenants.

Roles ​

RoleUpdate tenantDelete tenantOAuth configsManage members
owneryesyesyesyes (any role)
adminyesnoyesadd/remove member only
membernononolist members only

Creating a tenant attaches the platform user as owner (ownerId + tenantMember row).

Create a tenant ​

ts
const { data: tenant } = await authClient.$fetch("/tenant/create", {
  method: "POST",
  body: {
    name: "Acme Corp",
    slug: "acme",
    metadata: { plan: "pro" },
  },
});
// tenant.ownerId === current platform user id

Slugs must be 2–63 lowercase letters, digits, or hyphens (no leading/trailing hyphen), and cannot use a reserved value like admin, api, or www. See reservedSlugs.

Members ​

ts
// Add by user id or email (platform users only)
await auth.api.addTenantMember({
  body: { tenantId: tenant.id, email: "partner@platform.com", role: "admin" },
  headers,
});

When adding by email, the plugin resolves platform users (tenantId null) deterministically. If multiple platform users share the same email, the request fails with PLATFORM_USER_AMBIGUOUS. Add a partial unique index on user.email where tenantId is null in your database to prevent duplicates — see Schema.

ts
await auth.api.listTenantMembers({
  query: { tenantId: tenant.id },
  headers,
});

await auth.api.updateTenantMember({
  body: { tenantId: tenant.id, userId: "...", role: "member" },
  headers,
});

await auth.api.removeTenantMember({
  body: { tenantId: tenant.id, userId: "..." },
  headers,
});

The last owner cannot be removed or demoted.

Get / list / update / delete ​

  • Get by id or slug is public, but unauthorized callers receive only id, name, slug, and createdAt — ownerId and metadata are only returned to tenant members and global admins (see exposeTenantDetailsPublicly).
  • List returns all tenants for global admin; otherwise tenants the caller is a member of. Pass limit and/or offset for paginated results ({ data, total, nextOffset? }); omit both for a plain array.
  • Update requires admin or higher. Pass metadata: null to clear metadata.
  • Delete requires owner (or global admin). Cascades related users, sessions, accounts, members, and OAuth configs.
ts
await authClient.$fetch("/tenant/update", {
  method: "POST",
  body: {
    id: "<tenant-id>",
    data: { name: "Acme Inc", slug: "acme-inc" },
  },
});

await authClient.$fetch("/tenant/delete", {
  method: "POST",
  body: { id: "<tenant-id>" },
});

Tenant model ​

FieldTypeDescription
idstringUnique identifier
namestringDisplay name
slugstringURL-friendly unique identifier
ownerIdstringPrimary platform owner (null if none)
metadatastringOptional JSON-serialized metadata
createdAtDateCreation timestamp
updatedAtDateLast update timestamp

Member model ​

FieldTypeDescription
idstringUnique identifier
tenantIdstringTenant
userIdstringPlatform user
rolestringowner | admin | member
createdAtDateCreation timestamp

See Endpoints for full request/response details.

Released under the MIT License.