RAMSIO

Docs

Authentication & RBAC

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.

Two independent role systems

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.

Global roles

SUPER_ADMIN

Every permission, including user deletion and full audit export.

ADMIN

Broad management access — users, billing, teams, workflows, audit reads, security incidents, notifications — without user deletion.

USER

Read-only access on users, billing, teams, team members, workflows, and notifications, scoped to what the guards let a non-admin caller reach.

Team roles (PRO tier)

OWNER

Full team control — update, delete, manage members, run/manage workflows.

ADMIN

Manage team and members, run workflows — cannot delete the team.

MEMBER

Read team and members, execute workflows.

GUEST

Read-only — team and members, no workflow execution.

The one permission with a scoping gotcha

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.

Brute-force & credential protection

  • bcrypt cost factor 12 for every stored password.
  • 5 failed login attempts (AUTH_MAX_LOGIN_ATTEMPTS) trigger a 15-minute lockout (AUTH_LOCKOUT_MINUTES) — both configurable, both with sane defaults if left unset.
  • Registration and password changes are checked against a known-breach password corpus (Have I Been Pwned’s k-anonymity API) — only a hashed prefix ever leaves the server, never the plaintext password. The check fails open on any lookup error (timeout, HIBP outage) rather than blocking registration on a third-party dependency being briefly unreachable.
  • A “step-up” action — changing your password, deleting your account — requires having actually logged in recently (not just holding a refreshed token), enforced by a dedicated guard with its own configurable window.

OAuth account linking (PRO tier)

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.

MFA & SSO (ENTERPRISE tier)

  • TOTP — Google Authenticator-compatible, 6-digit codes, 2-window tolerance for clock skew, with hashed one-time backup codes.
  • SMS — Twilio-delivered 6-digit codes, usable alongside TOTP rather than instead of it.
  • WebAuthn / passkeys — hardware keys (YubiKey) and platform authenticators (Face ID, Touch ID, Windows Hello), multiple credentials per user, with device naming and management. Requires an exact 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.
  • SSO/SAML — SAML 2.0 with JIT user provisioning, replay protection via a unique assertion ID, signature and timestamp validation, and audience restriction checks. Sessions default to an 8-hour expiry with automatic cleanup.

Enforcement, not just documentation

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.