Tenant management
The plugin exposes CRUD endpoints for tenants and platform-user memberships.
Authorization
Management access is resolved in this order:
canManageTenantsreturnstrue— global admin (e.g. operator API key). Can create and manage every tenant.- Authenticated platform user — a session whose
user.tenantIdis null. Access is scoped by membership role. - Otherwise — denied.
Tenant end-users (user.tenantId set) cannot create or manage tenants.
Roles
| Role | Update tenant | Delete tenant | OAuth configs | Manage members |
|---|---|---|---|---|
owner | yes | yes | yes | yes (any role) |
admin | yes | no | yes | add/remove member only |
member | no | no | no | list members only |
Creating a tenant attaches the platform user as owner (ownerId + tenantMember row).
Create a tenant
const { data: tenant } = await authClient.$fetch("/tenant/create", {
method: "POST",
body: {
name: "Acme Corp",
slug: "acme",
metadata: { plan: "pro" },
},
});
// tenant.ownerId === current platform user idSlugs 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
// 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.
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, andcreatedAt—ownerIdandmetadataare only returned to tenant members and global admins (seeexposeTenantDetailsPublicly). - List returns all tenants for global admin; otherwise tenants the caller is a member of. Pass
limitand/oroffsetfor paginated results ({ data, total, nextOffset? }); omit both for a plain array. - Update requires
adminor higher. Passmetadata: nullto clear metadata. - Delete requires
owner(or global admin). Cascades related users, sessions, accounts, members, and OAuth configs.
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
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier |
name | string | Display name |
slug | string | URL-friendly unique identifier |
ownerId | string | Primary platform owner (null if none) |
metadata | string | Optional JSON-serialized metadata |
createdAt | Date | Creation timestamp |
updatedAt | Date | Last update timestamp |
Member model
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier |
tenantId | string | Tenant |
userId | string | Platform user |
role | string | owner | admin | member |
createdAt | Date | Creation timestamp |
See Endpoints for full request/response details.