Docs
Fully custom auth — no Clerk, Auth0, or Supabase Auth in the dependency tree. That’s a deliberate architecture decision, not an oversight: zero recurring per-user auth cost, and customers own every line of the flow. This page covers the two-layer RBAC model; see API Contract for the token/cookie mechanics.
A user has exactly one global role (organization-wide) and, separately, one team role per team they belong to (PRO tier and above). These are checked independently — a global ADMIN still needs an OWNER/ADMIN team role to manage a specific team’s members, and a team OWNER has no special access outside that one team.
Every permission, including user deletion and full audit export.
Broad management access — users, billing, teams, workflows, audit reads, security incidents, notifications — without user deletion.
Read-only access on users, billing, teams, team members, workflows, and notifications, scoped to what the guards let a non-admin caller reach.
Full team control — update, delete, manage members, run/manage workflows.
Manage team and members, run workflows — cannot delete the team.
Read team and members, execute workflows.
Read-only — team and members, no workflow execution.
Every permission is a plain resource:action string — billing:read, teams:write, workflows:execute — checked by a guard on each route. The one worth calling out: audit:read/audit:export are granted to the base USER role too, for a self-service “my activity” page — but the audit-logs controller force-scopes a non-admin caller’s effective userId to their own regardless of what they pass, and the whole controller sits behind a subscription-tier guard, so it only does anything on ENTERPRISE. Add a new endpoint that trusts the permission alone, without also re-checking scope and tier, and it silently becomes a way for any signed-in user to read anyone’s audit history.
AUTH_MAX_LOGIN_ATTEMPTS) trigger a 15-minute lockout (AUTH_LOCKOUT_MINUTES) — both configurable, both with sane defaults if left unset.Google, GitHub, and Microsoft. A user can link and unlink provider accounts from their own session — GET /auth/oauth/accounts lists them, DELETE /auth/oauth/accounts/:provider unlinks one.
WEBAUTHN_RP_ID/WEBAUTHN_ORIGIN match — a mismatch fails registration and login without an obvious error. Challenges are held for 5 minutes with counter-based replay protection.Route-level guards and decorators apply this model at the controller level in every module that needs it — RBAC itself has no dedicated schema or standalone module, it’s a guards/decorators library only, with roles living on the User record directly. The frontend mirrors it with a <RequirePermission> component, but the enforcement that actually matters happens on the API side.