Guides
Multi-role system and app feature permissions
This guide explains multi-role in MUDBASE: configuring roles for your project, how end-user JWTs interact with feature permissions, how to simulate access before you ship UI, and how presets and the permissions matrix fit together. Use the REST API (see multiRole tag), OpenAPI, or SDKs to automate the same operations.
1. Two audiences (do not confuse them)
| Audience | Auth | What they use |
|---|---|---|
| Project admins (dashboard, integrations) | Dashboard JWT, API keys | Multi-role admin APIs under /api/projects/{projectId}/… — configure roles, featurePermissions, collection rules. Not subject to app feature gates. |
| App end users (your customers) | Project-scoped JWT with customRole (role slug) | Product-facing APIs (messaging, data, storage, chat, …). May be checked by feature gates when the token clearly identifies an app user for that projectId. |
Feature gates apply only for a recognized app end-user context:
- The JWT includes
customRole(slug, e.g.customer). - The JWT
projectIdmatches theprojectIdin the request path.
If that is not true (console user, API key, or no project id on the token), feature gates do not add an extra denial; normal project access and role checks still apply.
2. Stored model (high level)
Each project has one MultiRoleFeature document (project is unique).
2.1 Top-level fields
| Field | Purpose |
|---|---|
isEnabled | Whether multi-role behavior is active for the project. |
defaultRole | Slug assigned when signup does not pick a role (often customer). |
settings | e.g. allowMultipleRoles, requireRoleSelection, autoAssignDefault, dataOwnerField (default owner field for row scope). |
roles[] | Role templates: slug, name, signup, collectionPermissions, featurePermissions, etc. |
2.2 Per-role featurePermissions (app-facing toggles)
featurePermissions is a mixed object. It is not the same as dashboard RBAC for the builder.
Semantics (important):
- Missing bucket or empty
featurePermissions→ no extra denial (legacy-friendly): the gate does not block for that resource. - If a resource bucket exists and is a non-empty object, then for the evaluated action, if any relevant key is explicitly
false, the action is denied.
For messaging, the platform accepts legacy keys (email, sms, …) and new keys (send_email, read_history, …) as synonyms when evaluating the gate.
Other resources (for example integration, storage, functions) use the action names documented for that resource (e.g. execute, upload).
2.3 featurePermissions keys and the API contract
When you add (POST .../multi-role/roles) or update (PATCH .../multi-role/roles/:roleSlug) a role, use resource and action names that match what the API expects. Otherwise some flags may never affect a real endpoint (harmless), or you may omit keys that do gate traffic.
| Source | Purpose |
|---|---|
OpenAPI → AppRoleFeaturePermissions | Documented JSON shape and vocabulary for role bodies. |
Simulate API (POST .../multi-role/simulate-permissions) | Dry-run whether a role + HTTP method/path (or operationId) would be allowed. |
| API reference | Operation-level detail for each multi-role route. |
Resource → actions (boolean keys under each resource object):
| Resource | Actions |
|---|---|
messaging | send_email, send_sms, send_push, read_history, read_stats — or legacy email, sms, push, history, stats |
integration | read, create, update, delete, execute, test, export, read_usage |
functions | create, read, update, delete, execute, simulate |
data | create, read, update, delete |
search | query, suggestions, read_analytics |
usage | read |
storage | read, create, update, delete, upload |
chat | read, create, update, delete |
realtime | read_analytics, read_active_users, presence, read_throughput, read_history |
roleElevation | request, status, documents |
webhooks | config_read, config_update, test_transformation |
Built-in admin / user / viewer presets use this same vocabulary for featurePermissions bundles.
2.4 Per-collection permissions and dataScope (separate from feature gates)
collectionPermissions on each role, plus collection-level permissions, control which collections a role can touch and whether list/read/update are scoped to “own” rows (dataScope: "own") using the configured owner field (e.g. createdBy). That logic is separate from feature gates: see §2.3 for feature vs collection CRUD.
3. Runtime: how a gated request is decided
- The client calls a project-scoped endpoint with a valid token.
- The platform confirms project access (and normal API-key or JWT rules).
- If the caller is not an app end-user in the sense of §1 (no
customRole/projectIdmatch), no extra feature check is applied for gates. - If the HTTP method and path match a mapped feature rule, the platform resolves
{ resource, action }. Unmapped routes are not denied by feature gates. - The platform loads
featurePermissionsfor(projectId, customRole)(results may be cached briefly). - If the role’s permissions deny that resource/action, the API returns 403 with the stable body below. Otherwise the request continues to the handler.
3.1 Feature denied response (403)
{
"success": false,
"error": "feature_not_allowed",
"resource": "messaging",
"action": "send_email",
"message": "Your role does not have permission to use messaging (send_email) for this project."
}
Use POST .../multi-role/simulate-permissions with the same method, path, and role slug to verify behavior before your users hit the endpoint.
4. Admin APIs (project management)
Base path: /api/projects/{projectId} (all paths below are appended to that prefix).
Auth: Dashboard JWT (builder / admin session) or API key with permission to manage the project (see Authentication and the multiRole operations in the API reference).
| Method | Path | Purpose |
|---|---|---|
GET | /permissions-matrix | Matrix of collections × role actions + featurePermissions snapshot per role. |
GET | /multi-role | Full multi-role config. |
PATCH | /multi-role/settings | Update isEnabled, defaultRole, settings. |
POST | /multi-role/simulate-permissions | Dry-run feature gate for a role + path or operationId. |
POST | /multi-role/roles/:roleSlug/apply-preset | Apply admin / user / viewer featurePermissions preset only. |
PATCH | /multi-role/roles/:roleSlug/toggle | Enable/disable role. |
PATCH | /multi-role/roles/:roleSlug | Update role (name, featurePermissions, collection permissions, etc.). |
POST | /multi-role/roles | Add role. |
Full URL example: GET /api/projects/685ad30be129932fbb7a1047/permissions-matrix.
Additional routes (per-collection role permissions, signup role lists, etc.) are documented under the multiRole tag in OpenAPI.
5. Full request/response examples
All examples use projectId in the URL. Send Authorization: Bearer <admin or API key token> (and any other headers your deployment requires). No request body for GET routes below.
| § | Endpoint |
|---|---|
| 5.1 | GET .../multi-role |
| 5.2 | PATCH .../multi-role/settings |
| 5.3 | GET .../permissions-matrix |
| 5.4 | PATCH .../multi-role/roles/:roleSlug/toggle |
| 5.5 | PATCH .../multi-role/roles/:roleSlug |
| 5.6 | POST .../multi-role/roles |
| 5.7 | POST .../multi-role/simulate-permissions |
| 5.8 | POST .../multi-role/roles/:roleSlug/apply-preset |
5.1 GET /api/projects/{projectId}/multi-role
HTTP
GET /api/projects/685ad30be129932fbb7a1047/multi-role HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Response (200)
{
"success": true,
"data": {
"isEnabled": true,
"defaultRole": "customer",
"settings": {
"allowMultipleRoles": false,
"requireRoleSelection": true,
"autoAssignDefault": true,
"dataOwnerField": "createdBy"
},
"roles": [
{
"slug": "customer",
"name": "Customer",
"description": "Default app user role.",
"isEnabled": true,
"isCustom": true,
"signupEndpoint": "customer",
"requiresApproval": false,
"requiresPayment": false,
"requiresKYC": false,
"defaultPermissions": [],
"collectionPermissions": [],
"featurePermissions": {
"messaging": {
"email": true,
"sms": true,
"push": false,
"history": true,
"stats": true
},
"integration": {
"read": true,
"execute": true
}
}
}
]
}
}
Error (500)
{
"error": "Failed to get multi-role configuration"
}
5.2 PATCH /api/projects/{projectId}/multi-role/settings
HTTP
PATCH /api/projects/685ad30be129932fbb7a1047/multi-role/settings HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/json
Request body
{
"isEnabled": true,
"defaultRole": "customer",
"settings": {
"allowMultipleRoles": false,
"requireRoleSelection": true,
"autoAssignDefault": true,
"dataOwnerField": "createdBy"
}
}
All top-level fields are optional; send only what you want to change.
Response (200)
{
"success": true,
"message": "Multi-role settings updated",
"data": {
"isEnabled": true,
"defaultRole": "customer",
"settings": {
"allowMultipleRoles": false,
"requireRoleSelection": true,
"autoAssignDefault": true,
"dataOwnerField": "createdBy"
},
"roles": [
{
"slug": "customer",
"name": "Customer",
"description": "Default app user role.",
"isEnabled": true,
"isCustom": true,
"signupEndpoint": "customer",
"requiresApproval": false,
"requiresPayment": false,
"requiresKYC": false,
"defaultPermissions": [],
"collectionPermissions": [],
"featurePermissions": {}
}
]
}
}
Error (500)
{
"error": "Failed to update multi-role settings"
}
5.3 GET /api/projects/{projectId}/permissions-matrix
HTTP
GET /api/projects/685ad30be129932fbb7a1047/permissions-matrix HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Response (200)
{
"success": true,
"data": {
"collections": [
{
"id": "507f1f77bcf86cd799439011",
"name": "Orders",
"slug": "orders",
"permissions": [
{
"role": "customer",
"actions": ["read", "create"],
"hasConditions": false,
"dataScope": "own",
"ownerField": "createdBy"
}
],
"stateMachine": null
}
],
"roles": [
{
"slug": "customer",
"name": "Customer"
}
],
"features": [
{
"slug": "customer",
"featurePermissions": {
"messaging": {
"email": true,
"sms": true
}
}
}
]
}
}
Error (500)
{
"error": "Failed to get permissions matrix"
}
5.4 PATCH /api/projects/{projectId}/multi-role/roles/{roleSlug}/toggle
HTTP
PATCH /api/projects/685ad30be129932fbb7a1047/multi-role/roles/customer/toggle HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/json
Request body
{
"isEnabled": false
}
Response (200)
{
"success": true,
"message": "Role disabled",
"data": {
"slug": "customer",
"name": "Customer",
"description": "Default app user role.",
"isEnabled": false,
"isCustom": true,
"signupEndpoint": "customer",
"requiresApproval": false,
"requiresPayment": false,
"requiresKYC": false,
"defaultPermissions": [],
"collectionPermissions": [],
"featurePermissions": {}
}
}
If isEnabled is true, message is "Role enabled".
Error (500)
{
"error": "Failed to toggle role"
}
5.5 PATCH /api/projects/{projectId}/multi-role/roles/{roleSlug}
Updates a single role. You may send partial fields (e.g. only name or only featurePermissions). defaultPermissions / collectionPermissions are normalized together when either is present.
featurePermissions: use resource/action keys from §2.3 and components/schemas/AppRoleFeaturePermissions in openapi.yaml (same shape as POST .../roles).
HTTP
PATCH /api/projects/685ad30be129932fbb7a1047/multi-role/roles/customer HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/json
Request body (example — partial update)
{
"name": "Customer (updated)",
"description": "B2C buyers",
"featurePermissions": {
"messaging": {
"email": true,
"sms": false,
"push": false,
"history": true,
"stats": true
}
}
}
Request body (example — collection permissions + dataScope)
{
"collectionPermissions": [
{
"collectionId": "507f1f77bcf86cd799439011",
"collectionSlug": "orders",
"actions": ["read", "create", "update"],
"dataScope": "own",
"ownerField": "createdBy",
"conditions": {}
}
]
}
Response (200)
{
"success": true,
"message": "Role updated",
"data": {
"slug": "customer",
"name": "Customer (updated)",
"description": "B2C buyers",
"isEnabled": true,
"isCustom": true,
"signupEndpoint": "customer",
"requiresApproval": false,
"requiresPayment": false,
"requiresKYC": false,
"defaultPermissions": [],
"collectionPermissions": [
{
"collectionId": "507f1f77bcf86cd799439011",
"collectionSlug": "orders",
"actions": ["read", "create", "update"],
"conditions": {},
"dataScope": "own",
"ownerField": "createdBy"
}
],
"featurePermissions": {
"messaging": {
"email": true,
"sms": false,
"push": false,
"history": true,
"stats": true
}
}
}
}
Error (404)
{
"error": "Role not found"
}
Error (500)
{
"error": "Failed to update role"
}
5.6 POST /api/projects/{projectId}/multi-role/roles
Creates a new app role. Required: slug, name, signupEndpoint.
featurePermissions: optional; if set, align with §2.3 and the OpenAPI schema AppRoleFeaturePermissions (spec).
HTTP
POST /api/projects/685ad30be129932fbb7a1047/multi-role/roles HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/json
Request body
{
"slug": "vendor",
"name": "Vendor",
"description": "Seller role",
"signupEndpoint": "vendor",
"requiresApproval": true,
"requiresPayment": false,
"requiresKYC": false,
"defaultPermissions": [],
"collectionPermissions": [],
"featurePermissions": {
"messaging": {
"email": true,
"sms": true,
"push": true,
"history": true,
"stats": true
},
"integration": {
"read": true,
"execute": true,
"create": false
},
"storage": {
"read": true,
"upload": true,
"delete": false
}
}
}
Response (201)
{
"success": true,
"message": "Custom role added",
"data": {
"slug": "vendor",
"name": "Vendor",
"description": "Seller role",
"isEnabled": true,
"isCustom": true,
"signupEndpoint": "vendor",
"requiresApproval": true,
"requiresPayment": false,
"requiresKYC": false,
"defaultPermissions": [],
"collectionPermissions": [],
"featurePermissions": {
"messaging": {
"email": true,
"sms": true,
"push": true,
"history": true,
"stats": true
},
"integration": {
"read": true,
"execute": true,
"create": false
},
"storage": {
"read": true,
"upload": true,
"delete": false
}
}
}
}
Error (400) — missing slug, name, or signupEndpoint
{
"error": "Missing required fields",
"required": ["slug", "name", "signupEndpoint"]
}
Error (500)
{
"error": "Failed to add custom role"
}
5.7 POST /api/projects/{projectId}/multi-role/simulate-permissions
Simulates what the app feature gate would do for a given role slug and either:
operationId(fromopenapi.yaml), ormethod+pathname(orpathas alias forpathname).
HTTP
POST /api/projects/685ad30be129932fbb7a1047/multi-role/simulate-permissions HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/json
Request (by path)
{
"role": "customer",
"method": "POST",
"pathname": "/api/messaging/projects/685ad30be129932fbb7a1047/messaging/email"
}
Request (by OpenAPI operationId)
{
"role": "customer",
"operationId": "sendEmail"
}
Response — mapped path, allowed
{
"success": true,
"allowed": true,
"reason": "allowed",
"evaluated": {
"role": "customer",
"pathname": "/api/messaging/projects/685ad30be129932fbb7a1047/messaging/email",
"method": "POST",
"resource": "messaging",
"action": "send_email"
}
}
Response — mapped path, denied by featurePermissions
{
"success": true,
"allowed": false,
"reason": "feature_not_allowed",
"evaluated": {
"role": "viewer",
"pathname": "/api/messaging/projects/685ad30be129932fbb7a1047/messaging/email",
"method": "POST",
"resource": "messaging",
"action": "send_email"
}
}
Response — no gate for this path (unmapped)
{
"success": true,
"allowed": true,
"reason": "no_feature_gate_for_path",
"evaluated": {
"role": "customer",
"pathname": "/api/projects/685ad30be129932fbb7a1047/some-unmapped-route",
"method": "GET"
}
}
Response — operationId not in server map
{
"success": true,
"allowed": true,
"reason": "no_feature_gate_for_operation_id",
"evaluated": {
"role": "customer",
"operationId": "registerUser"
}
}
Error (400) — missing role or missing both path and operationId:
{
"error": "role (or roleSlug) is required, plus either operationId or both method and pathname (or path)"
}
Error (404) — role slug not found on project:
{
"error": "Role not found for this project"
}
Error (500)
{
"error": "Simulation failed"
}
5.8 POST /api/projects/{projectId}/multi-role/roles/{roleSlug}/apply-preset
Applies only featurePermissions from a named bundle; does not change collection CRUD or dataScope.
HTTP
POST /api/projects/685ad30be129932fbb7a1047/multi-role/roles/customer/apply-preset HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/json
Request body
{
"preset": "viewer"
}
Valid preset values: admin, user, viewer.
Response (200)
{
"success": true,
"message": "Applied Viewer preset",
"data": {
"slug": "customer",
"name": "Customer",
"description": "Default app user role.",
"isEnabled": true,
"isCustom": true,
"signupEndpoint": "customer",
"requiresApproval": false,
"requiresPayment": false,
"requiresKYC": false,
"defaultPermissions": [],
"collectionPermissions": [],
"featurePermissions": {
"messaging": {
"email": false,
"sms": false,
"push": false,
"history": true,
"stats": true
},
"integration": {
"create": false,
"read": true,
"update": false,
"delete": false,
"execute": false,
"test": false,
"export": true,
"read_usage": true
}
}
}
}
Error (400) — bad preset:
{
"error": "Invalid preset",
"valid": ["admin", "user", "viewer"]
}
Error (404) — role not found:
{
"error": "Role not found"
}
Error (500)
{
"error": "Failed to apply preset"
}
6. End-to-end workflow (simulation)
6.1 Narrative
- Provision multi-role for the project (
GET /multi-roleinitializes if needed). - Define roles (
POST/PATCHroles) and collection permissions for CRUD and row scope. - Set feature bundles either manually on
featurePermissionsor viaapply-preset. - Before changing the mobile/web app, call
simulate-permissionswith the samemethod+pathname(oroperationId) your client will use, for each role slug you care about. - Ship the app: end users authenticate and receive a JWT with
projectId+customRole. - On gated routes, the server enforces
featurePermissions; if denied, the client receivesfeature_not_allowed(403).
6.2 Sequence (mermaid)
sequenceDiagram
participant Admin as Dashboard (admin JWT)
participant API as Admin API /api/projects/:id
participant Store as Role configuration
participant App as App user (project JWT)
participant Endpoint as Product API
Admin->>API: PATCH role featurePermissions / apply-preset
API->>Store: Save roles[].featurePermissions
Admin->>API: POST simulate-permissions (method+pathname)
API->>Store: Load role by slug
API-->>Admin: { allowed, reason, evaluated }
App->>Endpoint: POST .../messaging/email (Bearer: project JWT)
Endpoint->>Endpoint: Project access check
Endpoint->>Endpoint: Feature permission check
Endpoint->>Store: featurePermissions for (projectId, customRole)
alt allowed
Endpoint-->>App: 201 + body
else denied
Endpoint-->>App: 403 feature_not_allowed
end
6.3 Concrete simulation walkthrough
-
Goal: Confirm whether role
viewermay callPOST /api/messaging/projects/{projectId}/messaging/email. -
Configure
viewer.featurePermissionssomessaging.emailisfalse(or apply viewer preset). -
Call simulate:
POST /api/projects/685ad30be129932fbb7a1047/multi-role/simulate-permissions Content-Type: application/json{ "role": "viewer", "method": "POST", "pathname": "/api/messaging/projects/685ad30be129932fbb7a1047/messaging/email" } -
Expect
allowed: false,reason: "feature_not_allowed",evaluated.resource:"messaging",evaluated.action:"send_email". -
Optional: Same check via
operationId(from OpenAPI):{ "role": "viewer", "operationId": "sendEmail" } -
Runtime: An app user JWT with
customRole: "viewer"calling that route receives 403 witherror: "feature_not_allowed"when the role’sfeaturePermissionsdeny the action.
7. OpenAPI, SDKs, and tools
- Contract: Download OpenAPI — see
POST /api/projects/{projectId}/multi-role/simulate-permissionsfor examples andGET .../permissions-matrixfor the matrix. - SDKs: Use generated clients from SDKs for typed calls to the same endpoints.
featurePermissionson add/update role: schemaAppRoleFeaturePermissionsin OpenAPI — align keys with §2.3 so gates behave as you expect.
POST /api/projects/{projectId}/multi-role/roles includes a fullTesting example: optional fields plus all featurePermissions resource buckets (messaging through webhooks) so you can copy from Swagger UI / Redoc and exercise permissions end-to-end. The same shape appears under 201 as the illustrative success body.