API Key Scopes
TL;DR
- Global keys belong to the tenant. They carry exactly the API scopes you chose at mint time. Use them for service accounts.
- User-bound keys belong to a specific user. Their effective scope is the intersection of the key's stored scopes and the owner's currently-granted permissions, evaluated live on every request. Use them when the automation should track a real person's authority.
- An owner deactivation, a permission revoke, or a group-membership change propagates to user-bound keys within 60 seconds.
Why Two Scope Types?
The original design treated every key as tenant-wide. That works fine for monitoring agents and CI jobs, but it creates two problems for keys that represent a person's automation:
- Ghost permissions. A user leaves the company, the admin disables the user account — but their personal API key keeps working until someone notices and revokes it manually.
- Stale snapshots. The key was minted while the user had
assets:write. Months later the user was demoted to read-only — but the key still grants write access.
User-bound keys fix both issues by binding the key to the owner's user account and re-evaluating permissions live.
Permission Intersection (User-Bound Keys)
A user-bound key carries a stored scope set chosen at mint time (e.g. ["assets:read", "assets:write"]). On every request, the auth middleware:
- Checks whether the owner's user account is active. If not → access denied.
- Resolves the owner's current permissions (updated within 60 seconds).
- Maps named permissions to API scopes:
admin→ all eight API scopes (wildcard)assets:write→assets:read+assets:writeassets:use→assets:readusers:manage→users:read+users:writeprocesses:manage→processes:read+processes:writeprocesses:use→processes:readtickets:manage/tickets:admin→tickets:read+tickets:writetickets:create/tickets:close→tickets:read
- The effective scope set is the intersection of the key's stored scopes and the owner's live API scopes.
A key can never grant more than the owner currently holds. A request whose required scope is missing from the intersection returns 403 INSUFFICIENT_SCOPE.
Update Behaviour
Owner permissions are cached for up to 60 seconds. On group-membership or group-permission changes the cache is refreshed immediately, so a revoked permission takes effect on the next request.
Ownership Rules at Mint Time
| Caller | scope_type | user_id | Result |
|---|---|---|---|
| any | (missing) | — | 400 SCOPE_REQUIRED |
| admin | global | null | 201 |
| admin | global | non-null | 400 VALIDATION_ERROR |
| non-admin | global | — | 403 GLOBAL_KEY_ADMIN_ONLY |
| admin | user | tenant member | 201 |
| admin | user | cross-tenant | 400 INVALID_USER |
| non-admin | user | self | 201 |
| non-admin | user | someone else | 403 FORBIDDEN |
scope_type has no default. Callers must always pick explicitly.
Owner Lifecycle Effects
| Event | Effect on user-bound keys |
|---|---|
| Owner removed from permission group | Effective scope shrinks within 60s |
| Owner deactivated | Next request is denied |
| Owner deleted | Key is automatically removed |
| Admin revokes the key explicitly | Key is deactivated |
When to Use Which
Pick Global when:
- The key represents a service, not a person (CI, backups, monitoring).
- You need a key that survives any individual employee leaving.
- The integration needs broad scope that is intentionally not tied to anyone's role.
Pick User-Bound when:
- The key represents a person's automation (personal CLI, browser extension, IFTTT).
- You want the key to follow the user's role changes automatically.
- Off-boarding should disable the key as a side effect of disabling the user.
When in doubt: prefer user-bound. The "ghost permission" failure mode is far more common than the cases where global is genuinely required.