---
title: Tenants and Users
path: concepts/tenants-and-users
status: published
---

# Tenants and Users

ScaiDNS is multi-tenant from the ground up. Every zone belongs to a tenant, every user belongs to a tenant, and most API calls operate within a tenant context.

Identities are owned by [ScaiKey](https://scaikey.scailabs.ai), the ScaiLabs identity platform. ScaiDNS is a subscriber to ScaiKey events and maintains a local cache for authorization and audit.

## The hierarchy

Three levels, mirrored from ScaiKey:

```mermaid
flowchart TB
    P["Partner<br/>(e.g., ScaiLabs, Acme MSP)"]
    T["Tenant<br/>(e.g., BBinfra NL, customer-42)"]
    U["User<br/>(e.g., mbi@bbinfra.net)"]
    P --> T
    T --> U
```

**Partner.** The top-level org. In single-org deployments, there's one partner. In MSP scenarios, a partner represents an operator serving many customers.

**Tenant.** A customer, department, or project inside a partner. Zones, API keys, role assignments, and audit logs scope to a tenant.

**User.** An individual human identity. A user belongs to one primary tenant but can be granted access to others (see [Permissions and Access](./permissions-and-access.md)).

**Group.** Collections of users within a tenant. Groups can carry role assignments and own API keys, making them the primary tool for "give this team access to these zones."

## Where identities live

Tenants, users, and groups are **created in ScaiKey**. Not in ScaiDNS. If you need to add a user, you do it in ScaiKey's admin UI (or ScaiDNS proxies to ScaiKey via `POST /api/v1/admin/users`).

ScaiDNS maintains a read-mostly local cache with:

- User profile (email, name, status)
- Group membership
- Tenant slug, name, and status
- Mapping from ScaiKey IDs (`usr_...`, `tnt_...`) to internal UUIDs

This cache is updated in two ways:

### 1. Webhooks

ScaiKey sends HMAC-signed events on every identity change:

- `user.created`, `user.updated`, `user.deleted`
- `group.created`, `group.updated`, `group.deleted`, `group.member_added`, `group.member_removed`
- `tenant.created`, `tenant.updated`, `tenant.deleted`
- `partner.*`
- `application.user_assigned`, `application.user_unassigned`

The webhook endpoint is `POST /api/v1/webhooks/scaikey`. See [Webhooks Deep Dive](../tutorials/webhook-integration.md) for signature verification and event shapes.

Webhooks need to be configured on both sides:

- ScaiKey's admin UI: point the webhook URL at your ScaiDNS instance and set the signing secret.
- ScaiDNS's `.env`: `SCAIKEY_WEBHOOK_SECRET` — the same secret, used for signature verification.

### 2. Periodic sync

A CLI command pulls the full state from ScaiKey:

```bash
scaidns sync                  # Full sync (partners, tenants, users, groups)
scaidns sync --users-only     # Only users assigned to the ScaiDNS application
```

Run on initial setup and if webhooks were missed for some reason. A scheduled daily sync is a reasonable belt-and-braces practice.

## Application assignment

ScaiDNS is registered in ScaiKey as an application (with an ID like `app_abc123`). Only users and groups **assigned to the ScaiDNS application** receive `user.*` and `group.*` webhooks, and are returned by `client.applications.get_effective_users(app_id)`.

To give a user access to ScaiDNS:

1. Assign them (or a group they belong to) to the ScaiDNS application in ScaiKey.
2. ScaiKey sends `application.user_assigned` → ScaiDNS upserts the user locally.
3. Assign them a role or access grant in ScaiDNS (see [Permissions and Access](./permissions-and-access.md)).

## Tenant context

Every API request resolves to a tenant. ScaiDNS determines it in this order:

1. **API keys** are bound to the tenant of their owning user or group at creation time.
2. **JWTs** carry `tenant_id` (a ScaiKey tenant ID) in their claims. ScaiDNS resolves this to the internal tenant UUID on each request via tenant-resolution middleware.

The resolved tenant ID is available on the `CurrentUser` context object as `internal_tenant_id`. It's what scopes all list/read/write operations.

A user can belong to multiple tenants in ScaiKey; which one they act as is determined by the JWT claim they authenticated with. Tenant switching is a ScaiKey concern, not a ScaiDNS one.

## Platform admins

Some users transcend tenants. A **platform admin** can read and write across every tenant, manage the partner/tenant list, edit platform configuration, and see the global audit log.

Platform admin status comes from one of:

- A claim in the JWT (`roles: ["platform_admin"]` or `scopes: ["platform:admin"]`), set by ScaiKey.
- A local role assignment in ScaiDNS: `role_name = "platform_admin"` at `scope = "platform"` in the `user_roles` table.

Normal users and tenant admins never bypass tenant scoping.

## Common operations

| I want to... | Do this |
|-----------|---------|
| Add a user to ScaiDNS | Create in ScaiKey, assign to the ScaiDNS application |
| Give a user access to zones | Assign a role via `POST /api/v1/roles/users/{user_id}` |
| Give a team access to specific zones only | Create an access grant via `POST /api/v1/domains/{id}/access-grants` |
| Check what a user can do | `GET /api/v1/roles/users/{user_id}/permissions` |
| See who changed what | `GET /api/v1/admin/audit-logs` (tenant-scoped for tenant admins) |

## What's next

- [Permissions and Access](./permissions-and-access.md) — role hierarchy and access grants.
- [Webhooks Deep Dive](../tutorials/webhook-integration.md) — syncing identity changes.
- [Users, Groups, and Roles](../reference/users-and-access.md) — the endpoint reference.
