Roles & Permissions

lockedWho can do what in setldhub — the role and permission model· 2026-05-28
0

Executive Summary

SetldHub implements a deny-by-default RBAC model with 36 fine-grained permissions organized across 12 domains and bundled into 14 roles (6 internal, 5 external, 3 machine). The approach is hybrid: Frontegg assigns coarse roles via the JWT roles[] claim, while a code-owned shared catalog (@setldpay/rbac) maps those roles to granular <entity>:<action> permissions. Both the frontend and backend resolve permissions from the same catalog at runtime, ensuring consistency by construction.

Data scope is orthogonal to permissions. A principal's clientIds and programIds — already carried in the token — determine which data they can see (row-level isolation), while permissions determine which actions they can perform (feature-level gating). The two compose: an action check (403) runs before a scope check (404), so an out-of-scope target never discloses existence.

Every unresolved product question was shipped as a denied cell (no permissive defaults). All 14 numbered decisions are now resolved. The model supports multi-role composition (permissions union across roles), purpose-built machine integration roles for API-key principals, and a phased migration path that avoids locking anyone out during rollout.

1

Capability Inventory

The platform surface spans 12 domains. Audience indicates whether the capability is offered to external client users (always composed with their own-client / own-program scope).

DomainMutating actionsExternal audience
programs program config, group association read: yes. config: internal-only. associate: yes (own client)
merchants create/edit, set-status, membership read/write: yes. set-status (incl. go-live): admin, PM, support, onboarder
merchant-groups create/edit, delete, set-status, membership read: yes. write: yes (client-admin). delete: no
terminals create, set-status, confirm read/write/confirm: yes. set-status: support + onboarder (in-field ops)
merchant-onboarding attach/start/confirm, sign, go-live read/write: yes. sign: yes. go-live: client-admin + program-manager
transactions (read only) export button (hidden, no API) read: yes. export: internal-only (FE-reserved)
cards (read only) read: yes (PII split: balance vs identity)
agreements author template read: yes. write: no (internal-only)
agreement-authorizations record signing read: yes (PM). write: yes (client-scoped by design)
mccs (read only) read: yes (global reference)
mcc-imports upload/delete/publish no (internal-only, super-admin)
audit-events (read only) read: client-admin only (not PM/CV)
2

Permission Catalog

Conventions: lowercase snake_case entity, lowercase verb, : separator. R = read, W = mutate.

platform
dashboard:readRAccess the hub UI. Granted to every human role; excluded from machine roles.
programs
program:readRList and detail for all programs in scope.
program:writeWChange program fraud posture (restriction mode). Internal-only.
program:link_merchant_groupWAssociate or disassociate a program with a merchant group.
merchants
merchant:readRList and detail for merchants in scope.
merchant:writeWCreate a merchant or edit merchant details.
merchant:set_statusWActivate or deactivate a merchant (including go-live via onboarding).
merchant-groups
merchant_group:readRList, detail, members, and linked programs.
merchant_group:writeWCreate or edit a merchant group.
merchant_group:deleteWHard-delete a group. Destructive, internal-only.
merchant_group:set_statusWActivate or deactivate a group (cascades to member merchants).
merchant_group:manage_membersWAdd or remove merchants from a group.
terminals
terminal:readRList terminals for a merchant.
terminal:writeWCreate a terminal.
terminal:set_statusWActivate or deactivate a terminal (incl. external support/onboarder for in-field troubleshooting).
terminal:confirmWConfirm onboarding tap-to-provision.
onboarding
onboarding:readRView progress, worklists, and the onboarding wizard.
onboarding:writeWAttach merchant, start/resume, and confirm detection steps.
onboarding:sign_agreementWIn-wizard signing (legally binding, captures signatory PII).
onboarding_card:readRView verification cards and poll detections (scope varies by role).
transactions
transaction:readRView transactions across all sub-scoped views (program, merchant, card).
transaction:exportR(bulk)Bulk data export (FE-reserved, no backend route yet). Internal-only.
cards
card:readRCard existence and metadata.
card:read_balanceRFunding and balance data (confidential financial data).
card:read_piiRCardholder identity (name, address, DOB). Actual PII.
agreements
agreement:readRList agreement versions and view current template body.
agreement:writeWAuthor or version a binding agreement template. Internal-only.
agreement_authorization:readRRead signed authorizations (includes signatory PII).
agreement_authorization:writeWRecord a merchant signing. Client-scoped by design.
mccs
mcc:readRGlobal MCC code lookup. Safe for any authenticated user.
mcc_import:readRView MCC import batches. Internal reference-data admin.
mcc_import:writeWUpload or delete pending MCC batches. Internal-only.
mcc_import:publishWPublish an MCC batch (rebuilds global table). Most restricted.
audit
audit_event:readRClient-scoped audit log (no program filter). Internal-actor rows hidden from external roles.
access
api_key:readRInspect API keys. Internal key-management surface.
api_key:writeWCreate, rotate, or revoke API keys. Internal ops only.
3

Role Catalog

Coarse roles assigned in Frontegg (delivered as roles[]). Scope (clientIds/programIds) is assigned separately on the Frontegg user / API key and is orthogonal. Permission counts are derived from the shipped catalog.

Internal (SetldPay staff)

sp-super-admin36/36
Break-glass platform owner. Everything.
Cross-client.
sp-ops-admin32/36
Day-to-day operations on behalf of any client: full merchant/group/program/terminal/onboarding lifecycle.
Cross-client.
sp-onboarding22/36
Field-ops onboarding: full onboarding workflow + cross-tenant verification-card pool visibility.
Cross-client card-pool widener; own-client otherwise.
sp-risk-compliance20/36
Read-everything + risk-control mutations (status, restriction mode), agreement authoring, full audit.
Cross-client.
sp-support22/36
Tier-1/2 support: broad read + low-risk metadata writes + membership management. No lifecycle/destructive/approve.
Cross-client.
sp-analyst13/36
Read-only internal analyst across operational/financial domains (excludes signatory PII and audit). Zero writes.
Cross-client, read-only.

External (client-org users)

client-admin26/36
Senior client user: full self-serve over their own client — merchants, groups, programs, onboarding, audit.
Own client only (all their programs).
program-manager21/36
Operates assigned programs (merchant/terminal/onboarding) and cardholder support, scoped to their programIds.
Own client + own programIds only.
client-viewer12/36
Read-only client user. No writes, no audit, no PII-heavy reads.
Own client only, read-only.
client-support13/36
External card desk / cardholder support: cards including balance + identity PII, transactions, full terminal ops for in-field troubleshooting.
Own client only.
client-onboarder17/36
External onboarding-only rep: merchant/terminal/onboarding management + sign agreements. No cardholder PII, no structural admin.
Own client + rep-scoped onboarding.

Machine (non-interactive)

sp-service0/36
Internal service-to-service principal (HS256 internal tokens). Empty by default — concrete grants land with the Unit C inventory.
Per-token scope.
client-integration15/36
External client API-key role: read (incl. balances) + safe external writes. No go-live, signings, or PII.
Per-key clientIds/programIds. External-only.
client-integration-ro11/36
External client API-key role: read-only programmatic access for reporting/reconciliation. No identity PII.
Per-key clientIds/programIds. Reads only.

Bounded legacy aliases (Phase 0 migration): onboarding-adminsp-onboarding , RANUIAdministratorsp-ops-admin

Role composition (multi-role)

A user may hold multiple roles; effective permissions are the union across them (the roles[] claim is an array; permissionsForRoles unions). Additive-only — there is no deny role, so combining roles only ever grants more.

  • Compose, don't duplicate. Keep roles coarse and non-overlapping; assign combinations for "two-hats" users.
  • Union can't narrow. To grant someone less than an existing role, define a dedicated narrow role.
  • Scope is per-principal. Effective data scope is the token's single clientIds/programIds regardless of role count. The lone exception is the role-driven cross-tenant widener (sp-onboarding / sp-super-admin over the onboarding-card pool).
  • Tier-mixing guard (load-bearing). Never assign an internal sp-* role and an external client-* role to the same user. Enforce via assignment governance and by having the cross-tenant widener also gate on internal tokenType (defense in depth).
4

Role x Permission Matrix

Interactive matrix showing all 14 roles and 36 permissions. Click a role header to see its full grant set, or click a permission row to see which roles hold it. Use the toolbar to filter by tier, domain, or external-facing permissions.

generated from @setldpay/rbac Every grant in this matrix is derived at build time from the vendored catalog.ts — a byte-identical copy of platform/packages/shared/rbac, the same catalog both the frontend and backend enforce against. It cannot drift from the shipped model. Refresh with mise run docs:rbac-sync.
Roles
Domain
36 rows
Permission internal external machine
SA OA ONB RC SUP AN CA PM CV CS CO SVC CI CIR
platform
dashboard:readR
programs
program:readR
program:writeW
program:link_merchant_groupW
merchants
merchant:readR
merchant:writeW
merchant:set_statusW
merchant-groups
merchant_group:readR
merchant_group:writeW
merchant_group:deleteW
merchant_group:set_statusW
merchant_group:manage_membersW
terminals
terminal:readR
terminal:writeW
terminal:set_statusW
terminal:confirmW
onboarding
onboarding:readR
onboarding:writeW
onboarding:sign_agreementW
onboarding_card:readR
transactions
transaction:readR
transaction:exportR(bulk)
cards
card:readR
card:read_balanceR
card:read_piiR
agreements
agreement:readR
agreement:writeW
agreement_authorization:readR
agreement_authorization:writeW
mccs
mcc:readR
mcc_import:readR
mcc_import:writeW
mcc_import:publishW
audit
audit_event:readR
access
api_key:readR
api_key:writeW
5

Enforcement Architecture

Every request takes the same path, regardless of how the caller authenticated. Two gates compose, in a fixed order: the action gate answers "may this principal do this at all?" and the scope gate answers "is this row theirs?" — so an out-of-scope target is never revealed to a caller who couldn't act on it anyway.

UI enforcement (advisory)

The frontend derives permissions client-side from the JWT roles[] claim via the shared @setldpay/rbac catalog. A rewritten PermissionsService exposes can(permission) signals that drive route guards, sidebar visibility (requires keys per nav item), and a *spCan structural directive for inline UI elements (buttons, menus, tabs). This layer is advisory — it shapes the user experience but is not a security boundary. A stale catalog at worst shows a button that returns 403; it never grants access.

Backend enforcement (security gate)

A requirePermission preHandler on each route is the real security gate. It resolves effective permissions once per request from the token's roles[] via the same shared catalog, then asserts the route's required permission before the handler runs (403 on denial). This composes with the existing per-service data-scope isolation: permissions answer "may this principal do this action at all?" while scope answers "is this row theirs?" (404). Scoped endpoints return 403 then 404; global-resource endpoints (MCC imports) use the permission gate as their sole control.

Machine-to-machine access (API keys)

Client API keys (spk_ tokens) are first-class authorization principals resolved through the same catalog. Keys carry a roles[] column mapped to purpose-built integration roles (client-integration for read + safe writes, client-integration-ro for read-only reporting). All token types — Frontegg, internal, and API key — resolve permissions identically via permissionsForRoles(). Scope (clientIds/programIds) on the key drives row-level isolation, composing with permissions the same way it does for human principals.