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/65f1a2b3c4d5e6f7a8b9c0d1/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/65f1a2b3c4d5e6f7a8b9c0d1/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,
"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/65f1a2b3c4d5e6f7a8b9c0d1/multi-role/settings HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/jsonRequest 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,
"defaultPermissions": [],
"collectionPermissions": [],
"featurePermissions": {}
}
]
}
}Error (500)
{
"error": "Failed to update multi-role settings"
}5.3 GET /api/projects/{projectId}/permissions-matrix
HTTP
GET /api/projects/65f1a2b3c4d5e6f7a8b9c0d1/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/65f1a2b3c4d5e6f7a8b9c0d1/multi-role/roles/customer/toggle HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/jsonRequest 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,
"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/65f1a2b3c4d5e6f7a8b9c0d1/multi-role/roles/customer HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/jsonRequest 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,
"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/65f1a2b3c4d5e6f7a8b9c0d1/multi-role/roles HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/jsonRequest body
{
"slug": "vendor",
"name": "Vendor",
"description": "Seller role",
"signupEndpoint": "vendor",
"requiresApproval": true,
"requiresPayment": 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,
"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/65f1a2b3c4d5e6f7a8b9c0d1/multi-role/simulate-permissions HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/jsonRequest (by path)
{
"role": "customer",
"method": "POST",
"pathname": "/api/messaging/projects/65f1a2b3c4d5e6f7a8b9c0d1/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/65f1a2b3c4d5e6f7a8b9c0d1/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/65f1a2b3c4d5e6f7a8b9c0d1/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/65f1a2b3c4d5e6f7a8b9c0d1/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/65f1a2b3c4d5e6f7a8b9c0d1/multi-role/roles/customer/apply-preset HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/jsonRequest 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,
"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
end6.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/65f1a2b3c4d5e6f7a8b9c0d1/multi-role/simulate-permissions Content-Type: application/json{ "role": "viewer", "method": "POST", "pathname": "/api/messaging/projects/65f1a2b3c4d5e6f7a8b9c0d1/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. Collection permissions and owner-scoped records
This section covers collection-level role permissions (which roles can create, read, update, or delete records in a collection) and owner scoping (filtering so each user only sees their own records). Both are configured independently of feature gates (§2.2 to §3).
7.1 Role permissions on collections
Each role carries a collectionPermissions list. Every entry maps one collection (by slug) to a set of allowed actions:
{
"collectionPermissions": {
"posts": ["create", "read", "update", "delete"],
"comments": ["create", "read"],
"admin_logs": []
}
}Pass this as collectionPermissions when creating or updating a role. The backend normalizes the value so you can use either the object form above or an array form:
{
"collectionPermissions": [
{ "collection": "posts", "actions": ["create", "read", "update", "delete"] },
{ "collection": "comments", "actions": ["create", "read"] }
]
}Access modes and the viewer fallback. In addition to explicit role grants, each collection has an accessMode that controls the default posture for end-user tokens:
accessMode | Behavior |
|---|---|
open (default) | The signed-in viewer role has an implicit read fallback. Useful for public-readable data. |
restricted | No implicit fallback. An end user needs an explicit grant for their role. |
serverOnly | Only server API keys and project team members (owner/admin/developer). End users receive 403. |
Set accessMode per collection:
curl -X PATCH \
"https://cloud.mudbase.dev/api/schemas/projects/PROJECT_ID/collections/COLLECTION_ID" \
-H "X-API-Key: YOUR_SERVER_KEY" \
-H "Content-Type: application/json" \
-d '{"settings":{"accessMode":"restricted"}}'7.2 Owner-scoped records: each user sees only their own rows
Owner scoping restricts list, read, update, and delete operations so each end user only accesses documents where a specific field matches their user ID. It is configured per role per collection and takes effect automatically for every data operation that role performs.
Three-step setup:
Step 1 - Create or update the role with collection CRUD grants.
curl -X PATCH \
"https://cloud.mudbase.dev/api/projects/PROJECT_ID/multi-role/roles/customer" \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"collectionPermissions": {
"orders": ["create", "read", "update", "delete"]
}
}'Step 2 - Apply owner scoping to that role on that collection.
curl -X PATCH \
"https://cloud.mudbase.dev/api/projects/PROJECT_ID/multi-role/roles/customer/collections/COLLECTION_ID/permissions" \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"dataScope": "own",
"ownerField": "createdBy"
}'ownerField is the document field that holds the owner's user ID. The platform stamps this field automatically on every create, and folds { createdBy: callerUserId } into every list, get, update, and delete query so a user can never reach another user's document.
Step 3 - Verify with the permissions matrix.
curl "https://cloud.mudbase.dev/api/projects/PROJECT_ID/permissions-matrix" \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN"The response shows dataScope: "own" and ownerField: "createdBy" for the role/collection pair, confirming the scoping is active.
7.3 What owner scoping enforces
Once dataScope: "own" is active for a role on a collection, the platform enforces the ownership constraint at every data access point:
| Operation | Enforcement |
|---|---|
List (GET /data) | Mongo query folds { [ownerField]: callerUserId } into the filter - only the caller's rows. |
Get by ID (GET /data/:id) | Same filter, so another user's document ID returns 404. |
Create (POST /data) | ownerField is auto-stamped on write. A caller-supplied mismatch is rejected with 400. |
Update (PATCH /data/:id) | Ownership is verified before the write runs. A non-owner receives 403 not_owner. |
Delete (DELETE /data/:id) | Ownership is verified before the delete runs. A non-owner receives 403 not_owner. |
| Realtime subscribe | Subscribe:document checks the caller's permission (including owner conditions) before joining the room. Subscribe:collection filters each emitted document per subscriber. |
| Transactions | Same ownership pipeline runs per operation inside the transaction. |
| Search and aggregate | Owner conditions are folded into every per-collection query. |
| Populated references | Referenced-collection lookups apply that collection's own ownership conditions. |
API key callers bypass owner scoping by design. A server-side API key has no user identity, so conditions: {} is used (no owner filter). If you use an API key as a proxy for end users, apply your own user-level filter server-side before forwarding results.
7.4 JavaScript SDK example
The platform's JavaScript SDK uses Bearer tokens for end-user sessions. Owner scoping is transparent to the SDK client: the server applies the filter automatically.
import { Configuration, DataApi } from "mudbase-sdk";
// Use the end user's Bearer token (obtained after login)
const config = new Configuration({
basePath: "https://cloud.mudbase.dev",
baseOptions: {
headers: { "Authorization": `Bearer ${userToken}` },
},
});
const data = new DataApi(config);
const projectId = "YOUR_PROJECT_ID";
const collectionId = "orders";
// List: only this user's orders are returned (owner scoping applied server-side)
const { data: listRes } = await data.listData(projectId, collectionId);
console.log(listRes.data); // Only rows where createdBy === callerUserId
// Create: createdBy is auto-stamped with the caller's user ID
const { data: createRes } = await data.createData(projectId, collectionId, {
item: "Widget",
quantity: 3,
});
console.log(createRes.data._id); // New order ID
// Get by ID: returns 404 if the document belongs to a different user
const { data: getRes } = await data.getDataById(projectId, collectionId, createRes.data._id);
console.log(getRes.data.item); // "Widget"
// Update: atomic - fails if another user owns the document (403)
await data.updateData(projectId, collectionId, createRes.data._id, { quantity: 5 });
// Delete: same atomic ownership check
await data.deleteData(projectId, collectionId, createRes.data._id);7.5 REST-only example (no SDK)
# As an end user with customRole "customer" and dataScope "own" on "orders"
# List own orders
curl "https://cloud.mudbase.dev/api/data/projects/PROJECT_ID/collections/orders/data" \
-H "Authorization: Bearer USER_TOKEN"
# Response only contains rows where createdBy === USER_ID
# Create an order (createdBy stamped automatically)
curl -X POST \
"https://cloud.mudbase.dev/api/data/projects/PROJECT_ID/collections/orders/data" \
-H "Authorization: Bearer USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"item":"Widget","quantity":3}'
# Try to get another user's order - server returns 404 (not 403, to avoid leaking existence)
curl "https://cloud.mudbase.dev/api/data/projects/PROJECT_ID/collections/orders/data/OTHER_USER_DOC_ID" \
-H "Authorization: Bearer USER_TOKEN"
# {"error":"Document not found"}
# Try to update another user's order - server returns 403
curl -X PATCH \
"https://cloud.mudbase.dev/api/data/projects/PROJECT_ID/collections/orders/data/OTHER_USER_DOC_ID" \
-H "Authorization: Bearer USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"quantity":10}'
# {"success":false,"error":"not_owner","message":"You can only modify your own data"}7.6 Console UI (Roles page)
In Console > Roles, open any custom role and click Edit. In the collection permission rows, each row now shows a Row scope dropdown:
- All rows (default): the role accesses all records in the collection, subject to the permission actions (create/read/update/delete).
- Own rows only: the platform folds
{ [ownerField]: callerUserId }into every operation. An Owner field text input appears (leave blank to use the project default,createdBy).
Save the role. The CRUD actions are saved first via the standard update endpoint; owner-scope settings are saved immediately after via the per-collection permissions endpoint.
8. 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.