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:
POST /api/v1/apps/register
Authorization: Bearer <your_token>
Content-Type: application/json
{ "name": "My CRM App", "app_code": "my_crm" }Response:
{
"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:
| What | Where | Why |
|---|---|---|
| App identity | core_apps | App registration (app_id, client_id, secret) |
| Default group | core_scim_groups | Named after your app, scoped to your app_id |
| Install entry | platform_apps | So 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:
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#
| Operation | What it covers |
|---|---|
"read" | List and get (GET requests) |
"write" | Create, update, delete (POST, PATCH, DELETE) |
Available domains#
| Domain | What it covers |
|---|---|
people | Persons, contacts |
customer | CRM accounts, deals, activities |
projects | Projects, tasks, sprints |
contracts | Contracts, vendors, obligations |
finance | Invoices, expenses, budgets |
helpdesk | Tickets, SLA policies |
knowledge | Documents, templates, SOPs |
grc | Frameworks, controls, risks |
soc | Alerts, investigations |
okr | Objectives, key results |
legal | Contracts, NDAs, IP |
operations | Access requests, equipment |
org | Locations |
procurement | Suppliers |
vendor | Scorecards, certifications |
analytics | Dashboards, metrics |
storage | Files, KV, SQLite |
secrets | Secret management |
Verify permissions are set:
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.
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):
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):
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):
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#
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-domain —
people:writedoes NOT grantcontracts: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:
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:
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#
| Method | Endpoint | Purpose |
|---|---|---|
POST | /api/v1/apps/register | Register app (auto-creates default group + install) |
GET | /api/v1/groups | List your groups |
POST | /api/v1/groups | Create 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}/members | Add user to group |
DELETE | /api/v1/groups/{id}/members/{user_id} | Remove user from group |
GET | /api/v1/groups/{id}/permissions | Get group permissions |
PUT | /api/v1/groups/{id}/permissions | Set group permissions |
DELETE | /api/v1/groups/{id}/permissions | Remove all permissions |
GET | /api/v1/users/{id}/permissions | Get user's merged permissions |
GET | /api/v1/permissions | List 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.
# 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.