Docs / Security and data
Roles and permissions
How the API decides: permission keys, the staff-editable role matrix, per-person lists, own or organization scope, the staff flag.
Authorization needs an explicit role and an organization membership. Company names, email text and workspace choices never grant either. Every action is a permission key in one registry (packages/contracts/src/permissions.ts, catalog in docs/permissions.md); which roles hold which key is a table staff edit live at /platform/permissions (see Roles and permissions matrix). The code is apps/services/src/permissions.
How a request is decided#
- The session guard resolves the caller (see Session API). No session is a 401.
- The guard asks
PermissionService.resolve(userId, organizationId)for the caller's effective permissions: the role's cells in the live matrix (cached in Redis, a few seconds at most behind an edit), with the person's override list on top. Nothing is read from token claims, so a staff edit or an administrator's change applies to the next request. - A service calls
can(user, "applications.decide")(403 when missing) orrequireStaff(user, "staff.tenants.decide"). Scoped reads use theownororgscope the permission carries. - Tenant scope: vendor-side callers may only touch applications of their own organization (
isApplicationVendorScoped). A resource outside the scope is usually a 404, a missing permission a 403. - Cross-tenant access needs
isStaffOperator(a platform role and the sessionstaffflag from the staff sign-in) and, for each staff action, the matchingstaff.*key.
Roles#
The built-in roles and their default grants are listed in docs/permissions.md. In short: supplier roles Owner, Administrator (ORG_ADMIN), Credit manager, Salesman (SALES_MANAGER), Account admin, Vendor user; customer roles Owner, Administrator, Buyer manager, Buyer (all APPLICANT in the database, told apart by the owner link and a role: entry or the legacy permission set); the reference role; staff roles; and the AGENT_MCP ceiling for AI agents. Staff can add custom roles for an audience, edit any built-in role and reset it to its defaults; built-in roles cannot be deleted.
A per-person list (permissions on the user or membership) replaces the role's cells for the keys it names (old names such as view_pipeline expand to the keys they used to grant); role:<KEY> selects a matrix role instead.
Permissions that matter most#
| Permission | Gates |
|---|---|
applications.decide | recording a credit decision (high risk) |
references.remind | sending and reminding references for an application |
applications.view, references.view | reading applications and references; scope own (a Buyer) or org |
users.invite, users.edit | inviting teammates and changing their access (high risk) |
audit.view, staff.audit.view | the organization's audit trail, the platform audit log |
staff.permissions.edit | editing this matrix |
Public registration never creates staff and never joins an existing organization without an invitation. Platform staff use internal email codes; the password option needs an explicit
INTERNAL_STAFF_PASSWORD_EMAILSentry.