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:
| Field | Type | Description |
|---|---|---|
| Guest | role | Anonymous, unauthenticated visitor. Read-only access to public content. The default principal type before signin. |
| User | role | Authenticated staff member with no special privileges. The baseline for any signed-in employee. |
| Owner | role | Owns specific records or business entities (e.g. an account owner). Used by per-record |
| Publisher | role | Can publish content (pages, posts, products). Sits between Reviewer and ContentAdmin in the editorial workflow. |
| Reviewer | role | Can approve/reject content but not publish. Used for review-and-approve workflows. |
| PowerUser | role | Elevated User. Can perform staff actions beyond the User baseline (broader create/update on more collections), without being a full admin. |
| System | role | Service principals — automation runners, scheduled jobs, internal integrations. Not intended for human staff. |
| ContentAdmin | role | CMS administrator. Manages pages, marketing content, and the editorial pipeline. Org-scoped. |
| ConfigAdmin | role | Tenant configuration and billing. Manages org settings, integrations, API keys, billing details. Org-scoped. |
| RootAdmin | role | Cross-org platform administrator. Used by AppMint platform support. The only role that can act across org boundaries on staff endpoints. |
| RootSystem | role | Platform-level service principal. Internal automation that crosses org boundaries. |
| RootUser | role | Platform-level staff with read/write across orgs (less privileged than RootAdmin). |
| RootPowerUser | role | Platform-level PowerUser — elevated staff with cross-org reach. |
| Customer | role | End-user (purchaser, member, customer-portal account). Distinct from staff; signs in via |
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.
ContentAdmingetscreate,read,update,deleteon 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
BaseModelrecords can carry their ownrequiredRolefield, 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
/profile/user/role/addJWT/profile/user/role/removeJWTcurl -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 callGET /profile/whoamito 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.