A workspace is a password-protected collection of accounts. At rest it is locked, and its contents are unreadable without the password.
The password is held to a strength policy at creation: shorter than 12
characters is 422 with weak_password, checked before anything is
provisioned. There is no character-class requirement.
A token is the unlock
There is no separate unlock endpoint. POST /v1/auth/token takes the workspace
password and returns a bearer token, and that token’s lifetime is the unlock
session. While a valid token exists, the workspace is open. Revoke it and the
workspace locks again.
This has three consequences worth internalising:
- The workspace password is presented exactly once per session, at mint time. That call is authenticated by tenant credentials, because it is the only workspace-tier call that has no token yet.
DELETE /v1/auth/tokenrevokes the lease of the token that sent it, and only that one. Other tokens on the same workspace keep the session open; it locks when the last lease goes. Minting twice for one workspace gives you two independently revocable tokens, not two copies of one.- No workspace-token route takes the workspace in its path. It comes from the token’s claim, so a path and a token can never disagree about which workspace is in play. The tenant-tier routes do use a path slug — they carry no token to disagree with.
sequenceDiagram autonumber participant C as Client participant S as TEE Docker C->>S: POST /v1/auth/token — tenant creds + workspace password Note over S: workspace unsealed inside the enclave S-->>C: 201 — bearer token with scopes; the session is open C->>S: authenticated calls with Authorization: Bearer C->>S: POST /v1/auth/token/refresh S-->>C: new token, idle clock pushed out C->>S: DELETE /v1/auth/token Note over S: last lease gone, so the workspace locks S-->>C: 204
Two clocks
A session is bounded by two independent timers, both configured per tenant.
| Clock | Config | Default | Meaning |
|---|---|---|---|
| Idle | ttl.workspaceIdleSec |
15 minutes | Time since the last activity on the session. Any authenticated request resets it, not only a refresh. Account unlock is no exception: an unlock that is admitted slides the clock even when the password turns out to be wrong. |
| Absolute | ttl.workspaceAbsoluteSec |
8 hours | Total session lifetime from mint. Refresh does not extend it. |
Your operator can set both per tenant, so treat the defaults as the shape of the thing rather than a contract — but they are what applies when nothing is configured.
POST /v1/auth/token/refresh issues a new token and pushes the idle clock out.
Its response reports both, so you can see the ceiling coming:
{
"token": "eyJhbGciOi...",
"expiresAt": "2026-08-18T12:49:56.000Z",
"sessionExpiresAt": "2026-08-18T20:15:00.000Z",
"workspace": "demo",
"scopes": ["read", "write", "sign"]
}When sessionExpiresAt arrives, refreshing stops working. Mint a new token with
the workspace password instead. A refreshed token carries the same scopes
its predecessor did — refresh renews a lease, it never re-negotiates one.
GET /v1/workspace also reports the idle deadline as expiresAt, alongside
locked, the account count, and a damagedAccounts count, so a running
integration can watch the near clock without minting anything.
Accounts have their own lock
Locking is two-tiered. Opening the workspace does not necessarily open everything inside it.
- An account using the shared workspace password unlocks transparently the
first time you touch it — but only for as long as its custody window lasts.
Once that window closes, the transparent unlock does not happen again:
the account is marked locked for the rest of the session and reopening it
means calling
POST /v1/accounts/{slug}/unlockwith the workspace password. Creating an account also starts that window, so a newly created account is already on the clock. - An account created with
hasOwnPassword: truehas its own password and must be unlocked explicitly viaPOST /v1/accounts/{slug}/unlock. Until then it answers423with codeaccount_locked.
Account unlocks have their own absolute timer (ttl.accountAbsoluteSec,
five minutes by default — much shorter than the session) and their own rate
limit: repeated failed attempts return 429 with
account_unlock_rate_limited, backing off 1, 2, 4, then 8 seconds. Only a wrong
password counts as a failure, and a success clears the record.
A successful unlock of an account that is still inside a live window does not extend it. The window runs from when custody began, not from the last unlock, so re-sending the password is not a way to buy more time.
The fifth consecutive failure ends the session — or fewer, if your operator runs a mint budget below five, since the threshold is the lower of the two. The streak is also forgotten after 60 seconds of no attempts, so failures spread out never accumulate. Not just that unlock — the
whole workspace session is destroyed, and every other token holding it open
starts answering 401 with session_expired. Treat a wrong account password
as a stop condition, not something to retry into.
Capacity
Four limits can refuse a mint even when your credentials are perfect.
Three of them share the code session_capacity_exceeded, and error.details
names which one you hit:
details.scope |
Ceiling | Default |
|---|---|---|
workspace |
Live tokens on this one workspace | 64 |
tenant |
Workspaces your tenant holds open at once (limits.maxUnlockedWorkspaces) |
8 |
process |
Workspaces open across the whole service, every tenant | 32 |
The process leg is worth knowing about because you cannot fix it: it is not
your tenant’s activity that filled it, and revoking your own tokens may not
clear it. The workspace leg surprises clients that mint per request instead
of reusing a token — sixty-four live leases on one workspace is enough, with no
second workspace open at all.
The fourth is the mint rate limit — 429 with mint_rate_limited, per tenant,
10 mints per rolling minute by default. The charge applies once the request
is well formed and names a known workspace, so a tight retry loop against a bad
password is counted. A rejected attempt costs nothing and does not extend the
window, so a burst does not turn into a lockout.
The rate-limited 429s carry a retry-after header, with the same figure in
error.details.retryAfterSec. Honour it rather than backing off blind. The
capacity 429s above are different: they report scope and limit instead,
because there is no deadline to wait for — something has to be released.
Creating workspaces has its own budget
Workspace creation is limited separately from minting, so exhausting one leaves
the other alone. Two ceilings apply, both 429:
| Code | Default | Meaning |
|---|---|---|
workspace_creation_rate_limited |
10 per rolling minute, per tenant | Too many creates too quickly |
workspace_recreation_cooldown |
60 seconds | This exact slug was deleted recently |
The cooldown is per slug and is persisted, so it survives a restart: a delete-and-recreate loop over one name stalls where a loop over fresh names does not.
Related
- Authenticate — the token lifecycle as a task
- Scopes and permissions — what a token is allowed to do
- Auth endpoints — exact request and response shapes