Docs

Docs

App Permissions

When your app sends X-App-Id header, cPod enforces per-domain permissions — only domains you've granted are accessible. Without the header, no permission check runs.

You can do everything from the portal UI at Settings → Groups, or via API calls shown below.


Step 1 — Register your app#

Via portal: Settings → Groups → "+ New group" → enter name + optional appId.

Via API:

bash
POST /api/v1/apps/register
Authorization: Bearer <your_token>
Content-Type: application/json
 
{ "name": "My CRM App", "app_code": "my_crm" }

Response:

json
{
  "app_id": "app-my-crm-a1b2c3d4",
  "client_secret": "cs_...",
  "name": "My CRM App",
  "tenant_id": "tnt_global",
  "install_id": "app_install_tnt_global_my_crm"
}

Save client_secret — it's shown only once.

What happens automatically#

When you register an app, the backend creates:

WhatWhereWhy
App identitycore_appsApp registration (app_id, client_id, secret)
Default groupcore_scim_groupsNamed after your app, scoped to your app_id
Install entryplatform_appsSo X-App-Id is recognized by the middleware

You can see the default group at Settings → Groups or via GET /api/v1/groups.


Step 2 — Set permissions FIRST#

Before adding users or testing, decide which EDM domains your app can access.

Via portal: Settings → Groups → click your group → toggle read/write per domain.

Via API:

bash
PUT /api/v1/groups/{group_id}/permissions
Authorization: Bearer <your_token>
Content-Type: application/json
 
{
  "permissions": {
    "people": ["read", "write"],
    "contracts": ["read"],
    "helpdesk": ["read", "write"]
  }
}

How permissions work#

OperationWhat it covers
"read"List and get (GET requests)
"write"Create, update, delete (POST, PATCH, DELETE)

Available domains#

DomainWhat it covers
peoplePersons, contacts
customerCRM accounts, deals, activities
projectsProjects, tasks, sprints
contractsContracts, vendors, obligations
financeInvoices, expenses, budgets
helpdeskTickets, SLA policies
knowledgeDocuments, templates, SOPs
grcFrameworks, controls, risks
socAlerts, investigations
okrObjectives, key results
legalContracts, NDAs, IP
operationsAccess requests, equipment
orgLocations
procurementSuppliers
vendorScorecards, certifications
analyticsDashboards, metrics
storageFiles, KV, SQLite
secretsSecret management

Verify permissions are set:

bash
GET /api/v1/groups/{group_id}/permissions
Authorization: Bearer <your_token>
 
# → { "permissions": { "people": ["read", "write"], "contracts": ["read"], ... } }

Step 3 — Add users to the group#

Only users in the group get the permissions you set in Step 2.

bash
POST /api/v1/groups/{group_id}/members
Authorization: Bearer <your_token>
Content-Type: application/json
 
{ "user_id": "usr_venky_123" }

Step 4 — Test permissions#

Now that permissions are set and users are added, test enforcement:

Test 1 — ALLOWED (has people:write):

bash
POST /api/v1/people/persons
Authorization: Bearer <user_token>
X-App-Id: app-my-crm-a1b2c3d4
Content-Type: application/json
 
{ "display_name": "John Doe", "email": "john@example.com" }
 
# → 201 Created ✅

Test 2 — DENIED (does NOT have finance:write):

bash
POST /api/v1/finance/invoices
Authorization: Bearer <user_token>
X-App-Id: app-my-crm-a1b2c3d4
Content-Type: application/json
 
{ "amount": 100 }
 
# → 403 Forbidden: "app-scoped caller lacks finance:write permission" ❌

Test 3 — BYPASSED (no X-App-Id header):

bash
POST /api/v1/finance/invoices
Authorization: Bearer <user_token>
 
# → 201 Created (no permission check — normal user call) ⏭️

X-App-Id is what activates permission enforcement. Without it, calls bypass the check entirely (owner scope applies).


How enforcement works#

code
1. App sends request with: X-App-Id: app-my-crm-a1b2c3d4
                    ↓
2. Backend validates: is this app_id registered?
                    ↓
3. Backend checks: is this user in a group for this app?
                    ↓
4. Backend resolves: merge all group permissions for this user+app
                    ↓
5. Backend checks: does the user have <domain>:<op> permission?
                    ↓
   YES → allow          NO → 403 Forbidden

Rules:

  • Permissions are per-domainpeople:write does NOT grant contracts:write
  • Multiple groups for the same app merge — user gets the union
  • No X-App-Id = no enforcement (normal user call)
  • App must be registered — unregistered IDs are silently stripped

Multi-role: different access levels#

The default group gives everyone the same permissions. For different roles, create additional groups with the same app_id:

bash
POST /api/v1/groups
Authorization: Bearer <your_token>
Content-Type: application/json
 
{ "display_name": "CRM Editors", "app_id": "app-my-crm-a1b2c3d4" }

Set different permissions per group:

code
App: "My CRM App" (app_id: app-my-crm-a1b2c3d4)

Default group (auto-created) → { people: ["read"] }     ← basic users
Group: "CRM Editors"         → { people: ["read", "write"] }
Group: "CRM Admins"          → { people: ["read", "write"], contracts: ["read", "write"] }

A user in "Editors" gets { people: ["read", "write"] }. A user in both "Editors" + "Admins" gets the union of both.


API reference#

MethodEndpointPurpose
POST/api/v1/apps/registerRegister app (auto-creates default group + install)
GET/api/v1/groupsList your groups
POST/api/v1/groupsCreate additional group (with app_id)
GET/api/v1/groups/{id}Get group details
PATCH/api/v1/groups/{id}Update group name/description
DELETE/api/v1/groups/{id}Delete group
POST/api/v1/groups/{id}/membersAdd user to group
DELETE/api/v1/groups/{id}/members/{user_id}Remove user from group
GET/api/v1/groups/{id}/permissionsGet group permissions
PUT/api/v1/groups/{id}/permissionsSet group permissions
DELETE/api/v1/groups/{id}/permissionsRemove all permissions
GET/api/v1/users/{id}/permissionsGet user's merged permissions
GET/api/v1/permissionsList permission catalog

App-Scoped File Upload#

Apps can upload files with appId — these are hidden from workspace listings and only visible when filtered by appId.

bash
# Upload
POST /api/v1/workspaces/files/upload
Authorization: Bearer <token>
Content-Type: multipart/form-data
 
files: <file bytes>
appId: app-my-crm-a1b2c3d4
 
# List app files only
GET /api/v1/workspaces/files?appId=app-my-crm-a1b2c3d4
 
# Search app files via MCP
POST /api/v1/mcp/tools/agentic_search/invoke
Authorization: Bearer <token>
 
{ "query": "customer feedback", "app_ids": ["app-my-crm-a1b2c3d4"] }

ARAI auto-indexes uploaded files via MongoDB change stream — no manual indexing needed.