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)

AudienceAuthWhat they use
Project admins (dashboard, integrations)Dashboard JWT, API keysMulti-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 projectId matches the projectId in 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

FieldPurpose
isEnabledWhether multi-role behavior is active for the project.
defaultRoleSlug assigned when signup does not pick a role (often customer).
settingse.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.

SourcePurpose
OpenAPI → AppRoleFeaturePermissionsDocumented 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 referenceOperation-level detail for each multi-role route.

Resource → actions (boolean keys under each resource object):

ResourceActions
messagingsend_email, send_sms, send_push, read_history, read_stats - or legacy email, sms, push, history, stats
integrationread, create, update, delete, execute, test, export, read_usage
functionscreate, read, update, delete, execute, simulate
datacreate, read, update, delete
searchquery, suggestions, read_analytics
usageread
storageread, create, update, delete, upload
chatread, create, update, delete
realtimeread_analytics, read_active_users, presence, read_throughput, read_history
roleElevationrequest, status, documents
webhooksconfig_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

  1. The client calls a project-scoped endpoint with a valid token.
  2. The platform confirms project access (and normal API-key or JWT rules).
  3. If the caller is not an app end-user in the sense of §1 (no customRole / projectId match), no extra feature check is applied for gates.
  4. If the HTTP method and path match a mapped feature rule, the platform resolves { resource, action }. Unmapped routes are not denied by feature gates.
  5. The platform loads featurePermissions for (projectId, customRole) (results may be cached briefly).
  6. 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)

JSON
{
  "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).

MethodPathPurpose
GET/permissions-matrixMatrix of collections × role actions + featurePermissions snapshot per role.
GET/multi-roleFull multi-role config.
PATCH/multi-role/settingsUpdate isEnabled, defaultRole, settings.
POST/multi-role/simulate-permissionsDry-run feature gate for a role + path or operationId.
POST/multi-role/roles/:roleSlug/apply-presetApply admin / user / viewer featurePermissions preset only.
PATCH/multi-role/roles/:roleSlug/toggleEnable/disable role.
PATCH/multi-role/roles/:roleSlugUpdate role (name, featurePermissions, collection permissions, etc.).
POST/multi-role/rolesAdd 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.1GET .../multi-role
5.2PATCH .../multi-role/settings
5.3GET .../permissions-matrix
5.4PATCH .../multi-role/roles/:roleSlug/toggle
5.5PATCH .../multi-role/roles/:roleSlug
5.6POST .../multi-role/roles
5.7POST .../multi-role/simulate-permissions
5.8POST .../multi-role/roles/:roleSlug/apply-preset

5.1 GET /api/projects/{projectId}/multi-role

HTTP

HTTP
GET /api/projects/65f1a2b3c4d5e6f7a8b9c0d1/multi-role HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>

Response (200)

JSON
{
  "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)

JSON
{
  "error": "Failed to get multi-role configuration"
}

5.2 PATCH /api/projects/{projectId}/multi-role/settings

HTTP

HTTP
PATCH /api/projects/65f1a2b3c4d5e6f7a8b9c0d1/multi-role/settings HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/json

Request body

JSON
{
  "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)

JSON
{
  "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)

JSON
{
  "error": "Failed to update multi-role settings"
}

5.3 GET /api/projects/{projectId}/permissions-matrix

HTTP

HTTP
GET /api/projects/65f1a2b3c4d5e6f7a8b9c0d1/permissions-matrix HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>

Response (200)

JSON
{
  "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)

JSON
{
  "error": "Failed to get permissions matrix"
}

5.4 PATCH /api/projects/{projectId}/multi-role/roles/{roleSlug}/toggle

HTTP

HTTP
PATCH /api/projects/65f1a2b3c4d5e6f7a8b9c0d1/multi-role/roles/customer/toggle HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/json

Request body

JSON
{
  "isEnabled": false
}

Response (200)

JSON
{
  "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)

JSON
{
  "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

HTTP
PATCH /api/projects/65f1a2b3c4d5e6f7a8b9c0d1/multi-role/roles/customer HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/json

Request body (example - partial update)

JSON
{
  "name": "Customer (updated)",
  "description": "B2C buyers",
  "featurePermissions": {
    "messaging": {
      "email": true,
      "sms": false,
      "push": false,
      "history": true,
      "stats": true
    }
  }
}

Request body (example - collection permissions + dataScope)

JSON
{
  "collectionPermissions": [
    {
      "collectionId": "507f1f77bcf86cd799439011",
      "collectionSlug": "orders",
      "actions": ["read", "create", "update"],
      "dataScope": "own",
      "ownerField": "createdBy",
      "conditions": {}
    }
  ]
}

Response (200)

JSON
{
  "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)

JSON
{
  "error": "Role not found"
}

Error (500)

JSON
{
  "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

HTTP
POST /api/projects/65f1a2b3c4d5e6f7a8b9c0d1/multi-role/roles HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/json

Request body

JSON
{
  "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)

JSON
{
  "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

JSON
{
  "error": "Missing required fields",
  "required": ["slug", "name", "signupEndpoint"]
}

Error (500)

JSON
{
  "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 (from openapi.yaml), or
  • method + pathname (or path as alias for pathname).

HTTP

HTTP
POST /api/projects/65f1a2b3c4d5e6f7a8b9c0d1/multi-role/simulate-permissions HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/json

Request (by path)

JSON
{
  "role": "customer",
  "method": "POST",
  "pathname": "/api/messaging/projects/65f1a2b3c4d5e6f7a8b9c0d1/messaging/email"
}

Request (by OpenAPI operationId)

JSON
{
  "role": "customer",
  "operationId": "sendEmail"
}

Response - mapped path, allowed

JSON
{
  "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

JSON
{
  "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)

JSON
{
  "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

JSON
{
  "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:

JSON
{
  "error": "role (or roleSlug) is required, plus either operationId or both method and pathname (or path)"
}

Error (404) - role slug not found on project:

JSON
{
  "error": "Role not found for this project"
}

Error (500)

JSON
{
  "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

HTTP
POST /api/projects/65f1a2b3c4d5e6f7a8b9c0d1/multi-role/roles/customer/apply-preset HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>
Content-Type: application/json

Request body

JSON
{
  "preset": "viewer"
}

Valid preset values: admin, user, viewer.

Response (200)

JSON
{
  "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:

JSON
{
  "error": "Invalid preset",
  "valid": ["admin", "user", "viewer"]
}

Error (404) - role not found:

JSON
{
  "error": "Role not found"
}

Error (500)

JSON
{
  "error": "Failed to apply preset"
}

6. End-to-end workflow (simulation)

6.1 Narrative

  1. Provision multi-role for the project (GET /multi-role initializes if needed).
  2. Define roles (POST/PATCH roles) and collection permissions for CRUD and row scope.
  3. Set feature bundles either manually on featurePermissions or via apply-preset.
  4. Before changing the mobile/web app, call simulate-permissions with the same method + pathname (or operationId) your client will use, for each role slug you care about.
  5. Ship the app: end users authenticate and receive a JWT with projectId + customRole.
  6. On gated routes, the server enforces featurePermissions; if denied, the client receives feature_not_allowed (403).

6.2 Sequence (mermaid)

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

  1. Goal: Confirm whether role viewer may call POST /api/messaging/projects/{projectId}/messaging/email.

  2. Configure viewer.featurePermissions so messaging.email is false (or apply viewer preset).

  3. 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"
    }
    
  4. Expect allowed: false, reason: "feature_not_allowed", evaluated.resource: "messaging", evaluated.action: "send_email".

  5. Optional: Same check via operationId (from OpenAPI):

    {
      "role": "viewer",
      "operationId": "sendEmail"
    }
    
  6. Runtime: An app user JWT with customRole: "viewer" calling that route receives 403 with error: "feature_not_allowed" when the role’s featurePermissions deny 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:

JSON
{
  "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:

JSON
{
  "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:

accessModeBehavior
open (default)The signed-in viewer role has an implicit read fallback. Useful for public-readable data.
restrictedNo implicit fallback. An end user needs an explicit grant for their role.
serverOnlyOnly server API keys and project team members (owner/admin/developer). End users receive 403.

Set accessMode per collection:

Shell
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.

Shell
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.

Shell
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.

Shell
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:

OperationEnforcement
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 subscribeSubscribe:document checks the caller's permission (including owner conditions) before joining the room. Subscribe:collection filters each emitted document per subscriber.
TransactionsSame ownership pipeline runs per operation inside the transaction.
Search and aggregateOwner conditions are folded into every per-collection query.
Populated referencesReferenced-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.

JavaScript
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)

Shell
# 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-permissions for examples and GET .../permissions-matrix for the matrix.
  • SDKs: Use generated clients from SDKs for typed calls to the same endpoints.
  • featurePermissions on add/update role: schema AppRoleFeaturePermissions in 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.

Chat with us