Documentation

Roles and permissions

The RBAC model — RoleType, permission verbs, decorators, and how to define custom roles.

Once a request authenticates, the next gate is authorization. AppEngine's RBAC layer combines a small enum of system roles, a set of permission verbs, and per-collection (or per-record) override rules. Decorators on controller methods enforce the matrix.

RoleType

Fourteen system roles ship out of the box, defined in @jaclight/dbsdk (dist/types/role-type.d.ts):

enum RoleType {
  Guest         = 'Guest',
  User          = 'User',
  Owner         = 'Owner',
  Publisher     = 'Publisher',
  Reviewer      = 'Reviewer',
  PowerUser     = 'PowerUser',
  System        = 'System',
  ContentAdmin  = 'ContentAdmin',
  ConfigAdmin   = 'ConfigAdmin',
  RootAdmin     = 'RootAdmin',
  RootSystem    = 'RootSystem',
  RootUser      = 'RootUser',
  RootPowerUser = 'RootPowerUser',
  Customer      = 'Customer',
}

The roles fall into a few intent groups:

FieldTypeDescription
Guestrole

Anonymous, unauthenticated visitor. Read-only access to public content. The default principal type before signin.

Userrole

Authenticated staff member with no special privileges. The baseline for any signed-in employee.

Ownerrole

Owns specific records or business entities (e.g. an account owner). Used by per-record requiredRole checks; rarely a global role.

Publisherrole

Can publish content (pages, posts, products). Sits between Reviewer and ContentAdmin in the editorial workflow.

Reviewerrole

Can approve/reject content but not publish. Used for review-and-approve workflows.

PowerUserrole

Elevated User. Can perform staff actions beyond the User baseline (broader create/update on more collections), without being a full admin.

Systemrole

Service principals — automation runners, scheduled jobs, internal integrations. Not intended for human staff.

ContentAdminrole

CMS administrator. Manages pages, marketing content, and the editorial pipeline. Org-scoped.

ConfigAdminrole

Tenant configuration and billing. Manages org settings, integrations, API keys, billing details. Org-scoped.

RootAdminrole

Cross-org platform administrator. Used by AppMint platform support. The only role that can act across org boundaries on staff endpoints.

RootSystemrole

Platform-level service principal. Internal automation that crosses org boundaries.

RootUserrole

Platform-level staff with read/write across orgs (less privileged than RootAdmin).

RootPowerUserrole

Platform-level PowerUser — elevated staff with cross-org reach.

Customerrole

End-user (purchaser, member, customer-portal account). Distinct from staff; signs in via /profile/customer/signin. Sees only customer-scoped endpoints.

A principal can hold multiple roles. The most-privileged role applicable to a given endpoint wins.

Custom roles are stored in the userrole collection — create them like any other record. They can be referenced by name in requiredRole configurations on collections and individual records.

Permission verbs

Permissions express what a principal can do with a resource:

enum PermissionTypeContent {
  read,    create,    update,    delete,
  review,  approve
}

Plus a smaller set for component-level UI permissions (add, view, remove, configure). The content verbs are what you'll see on data endpoints.

Where do permissions come from? Two sources, combined at request time:

  • Role defaults — each role carries a default permission set. ContentAdmin gets create, read, update, delete on content-flavored collections by default.
  • Per-collection overrides — collections can declare requiredRole: { read: [...], create: [...], update: [...], ... } to require specific roles for each verb.
  • Per-record overrides — individual BaseModel records can carry their own requiredRole field, overriding the collection default for that record.

The decorators

Controllers stack two decorators on top of the global JWT guard:

@Roles(...roles)

Restricts the endpoint to principals who hold at least one of the listed roles.

import { Roles } from '../users/decorators/roles.decorator';
import { RoleType } from '@jaclight/dbsdk';

@Post('billing/payouts/process')
@Roles(RoleType.ConfigAdmin, RoleType.RootAdmin)
async processPayouts() { ... }

Source: src/users/decorators/roles.decorator.ts. The RolesGuard reads the metadata, compares against request.user.roles, and rejects with 403 on mismatch.

@RequirePermissions(...verbs)

Asserts that the principal can perform the listed verbs on the affected resource.

import { RequirePermissions } from '../users/decorators/permission.decorator';
import { PermissionTypeContent } from '@jaclight/dbsdk';

@Post('contact/:id/update')
@RequirePermissions(PermissionTypeContent.update)
async updateContact(@Param('id') id: string, @Body() body: any) { ... }

Source: src/users/decorators/permission.decorator.ts. The PermissionsGuard consults the resource's requiredRole matrix (collection-level + record-level) plus the user's role defaults to decide.

You can also combine them — the most restrictive wins:

@Post('settings/feature-flags')
@Roles(RoleType.ConfigAdmin)
@RequirePermissions(PermissionTypeContent.update)
async updateFeatureFlag() { ... }

Defining a custom role

Custom roles live in the userrole collection. Create one with a POST /repository/create:

curl -X POST https://appengine.appmint.io/repository/create \
  -H "orgid: my-org" \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "datatype": "userrole",
    "data": {
      "name": "support-agent",
      "title": "Support Agent",
      "description": "Read-only contact access plus ticket update",
      "permissions": {
        "contact":     ["read"],
        "crm-ticket":  ["read", "update"],
        "crm-message": ["read", "create"]
      }
    }
  }'

The role name (support-agent) is what you'll reference in @Roles('support-agent') and in collection requiredRole lists. Role names are case-sensitive.

Assigning roles to a User

POST/profile/user/role/addJWT
POST/profile/user/role/removeJWT
curl -X POST https://appengine.appmint.io/profile/user/role/add \
  -H "orgid: my-org" -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{ "userId": "user-...", "role": "support-agent" }'

Both endpoints require RoleType.ConfigAdmin (or higher). Equivalent group endpoints (/profile/user/group/add, /profile/user/group/remove) manage user groups rather than roles — groups bundle multiple roles for easier assignment.

Per-collection role overrides

When defining a custom collection, set requiredRole to lock specific verbs to specific roles:

{
  "datatype": "collection",
  "data": {
    "name": "internal-memo",
    "schema": { "...": "..." },
    "requiredRole": {
      "read":   ["RootAdmin", "ConfigAdmin", "ContentAdmin"],
      "create": ["ContentAdmin"],
      "update": ["ContentAdmin"],
      "delete": ["RootAdmin"]
    }
  }
}

The repository service consults this on every operation. Mixing role names with custom roles is fine — ["ContentAdmin", "support-agent"] works.

Per-record overrides

Individual records can override the collection-level matrix by setting their own requiredRole:

{
  "datatype": "internal-memo",
  "data": { "title": "Confidential Q4 plan" },
  "requiredRole": {
    "read": ["RootAdmin", "ConfigAdmin"]
  }
}

The record-level matrix supersedes the collection's. Use this for ad-hoc gating ("only finance leadership can read this one record").

Checking permissions in client code

Two patterns:

  • Optimistic UI — render every action button, let the server reject. Show errors clearly. Simpler.
  • Permission probe — fetch the user's effective permissions on signin and gate the UI. The signin response includes data.user.roles. Map each role to its capabilities client-side, or call GET /profile/whoami to refresh.

There's no dedicated "can I do X?" probe endpoint — the source of truth is the server-side guard. If you need to know in advance, mirror the role/permission matrix in your client config.

const me = activeSession.getUser();
const isAdmin = me.roles?.some(r =>
  ['RootAdmin', 'ConfigAdmin'].includes(r)
);

if (isAdmin) {
  // show admin menu
}

Audit

Every authorization rejection is logged to the org's audit trail along with the principal id, the endpoint, and the missing role/permission. Review via the monitoring endpoints or Studio Manager → Activity.

For high-stakes actions (delete data, change billing, transfer ownership), the platform additionally requires a recent successful 2FA challenge regardless of the user's role. This is enforced separately from @Roles / @RequirePermissions.