Personas & Access
What this covers / who it's for. The two user populations (client users vs central operators), how each signs in, the MFA and step-up model, and how a tenant's team works — invitations, seats, member management, and per-tenant roles. For anyone reasoning about "who can do what, and what stands between them and a sensitive action".
Two populations, fully separate
| Client user | Central operator | |
|---|---|---|
| Who | A person inside one tenant workspace (owner, admin, approver, member, or a custom role) | Stables staff: support, compliance, platform admin |
| Surface | The tenant SPA — every request carries the workspace identity | The operator backoffice |
| Scope | Sees only their own tenant's data (database-enforced isolation) | Central — works across tenants |
| Roles | Per-tenant roles (defaults + custom) | Global operator roles |
| Provisioned by | Self-service registration (the first owner) or a team invitation | A super-admin bootstrap command or an operator invitation |
| Account states | active / suspended / deactivated — only active can sign in | Same three states, same rule |
They are distinct account records on distinct sign-in systems. The same person (even the same
email) holding both has two separate identities — separate passwords, separate MFA
enrolments, separate sessions. Record specifics:
identity README; flow specifics:
authentication README.
The client sign-in journey (email-first)
A client user never types a workspace identifier. Sign-in is email-first:
- Discovery resolves which tenant(s) an email belongs to, so one email can hold memberships in several workspaces.
- Login happens inside one identified workspace. If the user has MFA enrolled, credentials alone do not sign them in — the session is deferred until a second factor is verified. Five failed challenge attempts void the pending login.
- Registration, discovery, login, and password-reset endpoints are bot-protected (Cloudflare Turnstile) and rate-limited.
Supporting flows:
| Flow | Behavior |
|---|---|
| Self-service registration | Creates a brand-new workspace plus its first user (the owner) in one step. See Tenant lifecycle. |
| Email verification | Code-based (short-lived, attempt-limited codes); resending is throttled. Verification advances the new workspace's lifecycle. |
| Password reset | Code-based, scoped to the identified workspace. Authenticated password change requires the current password. |
| Session identity | GET /me returns who you are plus a live abilities snapshot (permissions + entitlements + step-up freshness) so the app can show/hide UI truthfully. |
MFA — enrolment, challenge, and recovery
Both populations use the same MFA surface. Three factor kinds are supported:
| Method | What it is |
|---|---|
| Authenticator app (TOTP) | Time-based one-time codes |
| Platform authenticator (WebAuthn) | Face ID / Touch ID / Windows Hello on the user's device |
| Security key | A roaming hardware key (e.g. YubiKey) |
- Confirming a first factor issues a set of recovery codes — shown exactly once — and marks the account MFA-enrolled; from then on every login includes an MFA challenge (a recovery code can substitute for a lost factor).
- For a new workspace's first user, confirming the first factor also advances the workspace lifecycle (see Tenant lifecycle).
- Users self-manage their credentials: list, add, and remove factors, and regenerate recovery codes.
Step-up (sudo) for sensitive actions
Being signed in is not enough for dangerous operations. Actions marked as sensitive —
changing a member's roles, deactivating a member, creating/editing/deleting roles, and (for
operators) compliance actions like freezing an account — are refused with a
STEP_UP_REQUIRED signal until the user re-confirms a factor, which opens a short
freshness window (default 5 minutes). When the window lapses, the next sensitive action
prompts again; the abilities feed reflects the lapse immediately so the UI never pretends a
sudo window is still open.
Central operators
Operators sign in on the central backoffice surface — no workspace context. Differences from clients:
- Provisioning is never self-service: the first operator comes from the
stables:create-super-adminbootstrap command; further operators are invited by existing staff (the backoffice invitation flow). - MFA enrolment is forced — an operator cannot settle into the backoffice without a factor.
- Passkey passwordless login: operators can additionally sign in with a passkey alone (no password), on top of the shared password + MFA-challenge path.
- Operator roles are global (platform-wide), managed in the backoffice; see Operator backoffice.
Teams — membership inside a tenant
The first user of a workspace is its owner. Everyone else arrives by invitation. Full
specifics: team README.
Invitations & seats
- Inviting requires an available seat — seats are a hard per-tier cap (a limit, not a resetting quota; see Plans & entitlements). Today's caps: starter 3 / growth 10 / elite 50 (placeholder magnitudes, not committed pricing).
- The seat is checked at invite time but consumed at accept time. There is no reservation:
an owner near the cap can over-issue invites, and the last joiner to accept is the one
refused (
LIMIT_REACHED, with guidance to ask an administrator to free a seat or upgrade). This is a deliberate trade-off. - Accepting is public — the single-use invitation token is the credential. The new member is created active with the invited role, then runs their own email-verification + MFA journey.
- Pending invitations can be listed and revoked.
Member management
- Role changes, deactivation, reactivation all require step-up MFA.
- Deactivating a member frees their seat and revokes their sessions immediately; reactivating re-consumes a seat (and can itself hit the cap).
- The last-owner guard: the only remaining owner can never be demoted or deactivated
(refused with
LAST_OWNER) — a workspace cannot orphan itself.
Roles — per-tenant vocabulary of a global language
- The permission vocabulary is global (the same catalog of permission keys platform-wide), but roles are per-tenant: each workspace composes its own bundles of permissions.
- Every workspace is provisioned with four protected defaults —
owner,admin,approver,member— which can be assigned but never edited or deleted. - Custom roles (create / edit / delete, all step-up-gated) are a premium capability —
gated on the
team.advanced-rolesfeature, granted on the top tier only today. - No privilege escalation: a member can only grant permissions they themselves hold — a narrow role-manager cannot mint a stronger role and assign it to themselves.
- Deleting a role that members still hold is refused unless the deletion names a reassignment target role, in which case holders are moved over first.
- Some permissions depend on others (you cannot invite to a team you cannot see); dependencies are auto-completed when a role is composed, so an impossible role cannot exist.
What stands between a user and an action (the gate stack)
For any client action, in order: a live session in the right workspace → account active →
workspace not compliance-frozen/closed (see Tenant lifecycle) → the
required permission (via a role) → the required entitlement (jurisdiction + tier, see
Plans & entitlements) → for sensitive actions, a fresh
step-up. Every layer fails closed.