Supercommerce API Docs
Admin API

Admin RBAC Module

HTTP surface for managing platform-admin roles (custom role definitions with a permission map) and reading the catalog of available permissions. Also exports the PermissionsGuard…

HTTP surface for managing platform-admin roles (custom role definitions with a permission map) and reading the catalog of available permissions. Also exports the PermissionsGuard and @RequirePermissions(...) decorator that every admin endpoint across the platform uses to gate access.

Source: api-modules/admin-rbac (registered via AdminRbacModule.forRoot() in apps/api/src/app.module.ts).

RBAC works on platform admins only — vendor-side scoping uses resolveActiveVendorId(session) (active organization). Built-in admin roles (superAdmin, admin) live in code; this module's role table holds dynamic roles administrators create at runtime.


Conventions

Authentication

All endpoints require a Better-Auth session and a role with the matching role:* permission.

Endpoint groupPermission
GET /admin/rbac/roles, GET /admin/rbac/roles/:idrole: read
POST /admin/rbac/rolesrole: create
PUT /admin/rbac/roles/:idrole: update
DELETE /admin/rbac/roles/:idrole: delete
GET /admin/rbac/permissionsany platform admin (no per-resource permission — listing the catalog is read-only metadata)

Response envelope

Successful responses are wrapped by ResponseInterceptor:

{
  "data": <payload>,
  "message": "Success",
  "statusCode": 200,
  "metadata": { /* optional, e.g. pagination */ }
}

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR
401UNAUTHORIZED
403FORBIDDEN
404NOT_FOUND
409UNIQUE_VIOLATION (role name taken)
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

How permissions work

A permission requirement is a record from resource → array of allowed actions. A user satisfies it if any of their assigned roles grants every listed action on every listed resource.

Controller-side, the requirement is declared via @RequirePermissions(...):

import { PermissionsGuard, RequirePermissions } from "@sc/admin-rbac";

@Controller("admin/orders")
@UseGuards(BetterAuthGuard, PermissionsGuard)
export class AdminOrdersController {
  @Get()
  @RequirePermissions({ order: ["view"] })            // single resource
  list() { /* ... */ }

  @Post(":id/cancel")
  @RequirePermissions({ order: ["cancel"] })          // distinct action
  cancel() { /* ... */ }
}

Multi-resource requirements are an AND — the user needs all of them:

@RequirePermissions({ order: ["view"], user: ["list"] })
// requires: order:view AND user:list

Multiple actions on one resource is also AND:

@RequirePermissions({ vendor: ["view", "update"] })
// requires: vendor:view AND vendor:update

When the guard rejects, it responds 403 Forbidden (FORBIDDEN).


Permission catalog

The full platform permissions map lives in @sc/auth/access-control and is the source of truth for what's grantable to a dynamic role.

ResourceActions
usercreate, update, list, set-role, ban, impersonate, impersonate-admins, delete, set-password
sessionlist, revoke, delete
vendorapprove, suspend, view, update
orderview, update, refund, cancel
productview, update, delete, approve
productAttributecreate, read, update, delete
categorycreate, read, update, delete, approve
tagcreate, read, update, delete, approve
brandcreate, read, update, delete, approve
ingredientcreate, read, update, delete, approve
bannercreate, read, update, delete
globalScriptcreate, read, update, delete
adminSettingread, update
storeSettingread, update
platformVendorSettingread, update
rolecreate, read, update, delete
discountcreate, read, update, archive, delete
freeGiftcreate, read, update, archive, delete
dynamicLinkGroupcreate, read, update, delete
dynamicLinkcreate, read, update, delete
cartview, manage
searchreindex
reviewread, create, update, delete, approve, reject, mark-spam
notificationsview, broadcast
payoutview, create, mark_paid, cancel, adjust
fbtview, rebuild
rewardsmanage
affiliateApplicationread, review
affiliatePayoutread, create, process
affiliateProfileread, manage, suspend
affiliateOverrideread, manage
googleMerchantview, manage
metaCatalogview, manage
klaviyoview, manage

Built-in roles (not in this table — defined in code):

  • superAdmin — every action on every resource. Cannot be edited or deleted via this API.
  • admin — every action except destructive user/role management (no user:impersonate-admins, user:set-password, role:create).

Use the dynamic-roles API below to build narrower roles (e.g. "Support" with order:view + user:list + review:read).


Domain types

RoleResponse

type RoleResponse = {
  id: string;                                  // UUID
  name: string;                                // 1..255 chars; unique
  description: string | null;
  permissions: Record<string, string[]>;       // resourceactions
  createdAt: string;                           // ISO
  updatedAt: string;
};

PermissionRequirement / StoredPermissions

Type-narrowed equivalents used inside the codebase. Each resource key is constrained to the literal union from platformPermissions, and the action array to the actions that resource declares:

type PermissionRequirement = {
  user?:           ("create" | "update" | "list" | ...)[];
  order?:          ("view" | "update" | "refund" | "cancel")[];
  // ... one optional key per resource in the catalog
};

The wire format (above) is the looser Record<string, string[]> because zod can't enforce the per-resource action constraint cleanly — the service validates each (resource, action) pair against the catalog before persisting.


Roles

Base path: /admin/rbac/roles. Permissions on the role:* resource.

GET /admin/rbac/roles — List dynamic roles

Required permission: role: read. Standard QueryDto (page / limit / search / filters[]).

Response 200 — paginated envelope of RoleResponse[].


GET /admin/rbac/roles/:id — Get a role

Required permission: role: read.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

POST /admin/rbac/roles — Create a role

Required permission: role: create. The service validates every (resource, action) pair against platformPermissions — unknown pairs are rejected with 400 VALIDATION_ERROR (service-layer, not zod).

Body

{
  "name": "Support",
  "description": "Read-only support staff",
  "permissions": {
    "order":  ["view"],
    "user":   ["list"],
    "review": ["read", "mark-spam"]
  }
}
FieldTypeConstraints
namestring1..255 chars. Unique among non-deleted roles
descriptionstring?Free-form
permissionsRecord<string, string[]>At least one resource. Each (resource, action) must exist in platformPermissions

Response 201RoleResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod; unknown resource/action pair
409UNIQUE_VIOLATIONRole with this name already exists

PUT /admin/rbac/roles/:id — Update a role

Required permission: role: update. Partial update — every field is optional, but permissions (when sent) replaces the entire map (whole-set semantics; not deep-merge).

Changes take effect on the next request a user makes — there is no session invalidation; the next call evaluates against the new map.

Body

{
  "name": "Support Lead",
  "description": "...",
  "permissions": { "order": ["view", "update"], "user": ["list"] }
}

Response 200 — updated RoleResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

DELETE /admin/rbac/roles/:id — Delete a role

Required permission: role: delete. Users currently holding this role lose its grants on their next request; their session itself is not affected.

Response 200 — the deleted RoleResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

Permissions catalog endpoint

GET /admin/rbac/permissions — List all permissions

Returns the full platformPermissions map verbatim. The role-builder UI uses this to render a checklist that's always in sync with the codebase.

Response 200

{
  "data": {
    "user":   ["create", "update", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password"],
    "order":  ["view", "update", "refund", "cancel"],
    "review": ["read", "create", "update", "delete", "approve", "reject", "mark-spam"]
    /* ...one entry per resource */
  },
  "message": "Success",
  "statusCode": 200
}

Programmatic API

Other modules consume admin-rbac via the exported PermissionsGuard + RequirePermissions decorator. To gate a new endpoint:

import { PermissionsGuard, RequirePermissions } from "@sc/admin-rbac";
import { BetterAuthGuard } from "@sc/auth";

@Controller("admin/widgets")
@UseGuards(BetterAuthGuard, PermissionsGuard)
export class AdminWidgetController {
  @Get()
  @RequirePermissions({ widget: ["read"] })   // requires `widget:read`
  list() { /* ... */ }
}

For new permissions to exist, add the resource + actions to platformPermissions in api-modules/auth/src/access-control/permissions.ts. The catalog endpoint, role builder, and @RequirePermissions type checker all pick them up at compile time.


  • auth — owns platformPermissions, the user/role tables, and the BetterAuthGuard that runs before PermissionsGuard. Without the session, PermissionsGuard has nothing to check against.
  • vendor — uses RequirePermissions({ vendor: ["approve"] }) to gate the vendor-application approval flow. See vendor.md.
  • Every other admin-facing module (order, catalog, reviews, discount, banner, …) gates its admin controllers via this module.

On this page