App Role Groups
App role groups let a tenant admin control which users can access which app.
When an app is registered, a SCIM group is auto-created for it. Admins add
users to that group; those users see it in their /me response.
The flow at a glance#
① Register app → app_id + credentials
② Auto-created: a SCIM group with app_id (the app's "role group")
③ Tenant admin adds users to the group
④ User calls /me → groups array lists their group IDs
1. Register an app#
POST /api/v1/apps/register
Authorization: Bearer <admin_token>
Content-Type: application/json
{
"name": "CRM Sync",
"app_code": "crm_sync"
}The response includes app_id, client_id, and client_secret (shown once —
store it securely).
Side effect: a SCIM group is auto-created in core_scim_groups:
{
"_id": "grp_019f458b-...",
"display_name": "CRM Sync",
"app_id": "app-crm-sync-58d93289",
"tenant_id": "tnt_global"
}The app_id field links this group to the app. Ordinary user groups have
app_id: null.
2. Add users to the group#
Via the SCIM API (requires control-plane admin access):
PATCH /scim/v2/Groups/{group_id}
Authorization: Bearer <cp_token>
Content-Type: application/json
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "add",
"path": "members",
"value": [{"value": "<user_id>"}]
}
]
}Or directly in the database:
// core_group_members
{ "group_id": "grp_019f458b-...", "user_id": "usr_711484e0-..." }3. User's /me shows their groups#
When a user logs in, the control plane includes their group IDs in the JWT.
The backend's /me endpoint surfaces them:
GET /api/v1/auth/me
Authorization: Bearer <user_token>Response:
{
"sub": "usr_711484e0-...",
"email": "tej.gangineni@gmail.com",
"roles": ["user"],
"groups": ["grp_019f458b-9c5c-70a3-b53b-0154a5de9dd8"],
"tenantId": "tnt_global",
"displayName": "Tej G"
}The groups array contains the group IDs the user belongs to. To check if a
user has access to a specific app, see if the app's role group ID is in their
groups.
4. Get full group details by ID#
To resolve a group ID to its full details (name, app_id, members), call:
GET /api/v1/groups/{group_id}
Authorization: Bearer <admin_token>Access rules:
tenant_admin/global_admin— can see any group in their tenant- Regular users (
userrole) — can only see groups they are a member of - Non-members get
403 Forbidden
Response:
{
"id": "grp_019f458b-9c5c-70a3-b53b-0154a5de9dd8",
"display_name": "CRM Sync",
"app_id": "app-crm-sync-58d93289",
"tenant_id": "tnt_global",
"members": ["usr_711484e0-...", "usr_92ab..."],
"member_details": [
{
"user_id": "usr_711484e0-...",
"email": "tej.gangineni@gmail.com",
"display_name": "Tej G",
"roles": ["user"]
},
{
"user_id": "usr_92ab...",
"email": "anita@company.com",
"display_name": "Anita K",
"roles": ["tenant_admin"]
}
]
}| Field | Description |
|---|---|
id | The group's unique ID |
display_name | Human-readable name |
app_id | The app this group belongs to (null for ordinary user groups) |
tenant_id | The tenant this group belongs to |
members | List of user IDs in this group |
member_details | Full details per member — user_id, email, display_name, roles |
List all groups#
GET /api/v1/groups
Authorization: Bearer <admin_token>Returns all groups in the caller's tenant, each with the same shape as above.
5. How the portal uses this#
The portal reads groups from /me to determine which app role-groups a user
belongs to. This drives:
- App access gating — show/hide app sections based on group membership
- Role-based UI — different views per group
Data model#
| Collection | Purpose |
|---|---|
core_scim_groups | Groups (with optional app_id for app-scoped groups) |
core_group_members | User → group memberships |
core_apps | App registry (app_id, client_id, etc.) |
core_scim_groups fields#
| Field | Type | Notes |
|---|---|---|
_id | string | grp_<ulid> |
tenant_id | string | tenant scope |
display_name | string | group/role label |
app_id | string (nullable) | the app this group belongs to; null for ordinary user groups |
created_at | int | epoch seconds |
updated_at | int | epoch seconds |
Key points#
app_id is optional (nullable). Ordinary user groups have app_id: null.
Only app-scoped role groups carry the app's id. This is backward compatible —
existing groups are unchanged.
- Multi-role per app = multiple groups with the same
app_id. A user in multiple groups gets the union of permissions. groupsin/mecomes from the JWT (issued by the control plane at login, populated fromcore_group_members).- Auto-creation is best-effort — if group creation fails during app registration, the registration still succeeds (logged as a warning).