Permission Metadata
Define access control with permission sets — object CRUD, field security, tab visibility, and row-level security
Permission Metadata
A Permission Set defines what a user can do within the application. It controls object-level CRUD operations, field-level visibility, tab/app access, and row-level security policies.
Basic Structure
const salesUserPermission = {
name: 'sales_user',
label: 'Sales User',
objects: {
account: {
allowCreate: true,
allowRead: true,
allowEdit: true,
allowDelete: false,
viewAllRecords: false,
modifyAllRecords: false,
},
opportunity: {
allowCreate: true,
allowRead: true,
allowEdit: true,
allowDelete: true,
viewAllRecords: false,
modifyAllRecords: false,
},
report: {
allowCreate: false,
allowRead: true,
allowEdit: false,
allowDelete: false,
viewAllRecords: true,
modifyAllRecords: false,
},
},
fields: {
'account.annual_revenue': { readable: true, editable: false },
'account.internal_notes': { readable: false, editable: false },
},
tabPermissions: {
crm: 'visible',
admin: 'hidden',
reports: 'default_on',
},
};Permission Set Properties
| Property | Type | Required | Description |
|---|---|---|---|
name | string | ✅ | Machine name (snake_case) |
label | string | optional | Display label |
isDefault | boolean | optional | [ADR-0090 D5] Install-time suggestion to bind this set to the everyone position (admin confirms; never auto-bound) |
adminScope | AdminScope | optional | [ADR-0090 D12] Delegated-administration scope (BU subtree + assignable-set allowlist) |
objects | Record<string, ObjectPermission> | ✅ | Object-level permissions |
fields | Record<string, FieldPermission> | optional | Field-level security, keyed by 'object.field' |
systemPermissions | string[] | optional | System-level capabilities |
tabPermissions | Record<string, TabVisibility> | optional | Tab/app visibility |
rowLevelSecurity | RowLevelSecurityPolicy[] | optional | Row-level security policies |
contextVariables | Record<string, any> | optional | RLS context variables |
Object Permissions
Control CRUD operations on each object:
objects: {
account: {
allowCreate: true, // Can create records
allowRead: true, // Can view records
allowEdit: true, // Can update records
allowDelete: false, // Can delete records
allowTransfer: false, // Can change record ownership
allowRestore: false, // Can restore deleted records
allowPurge: false, // Can permanently delete (GDPR)
viewAllRecords: false, // Bypass sharing rules for read
modifyAllRecords: false, // Bypass sharing rules for write
},
}| Permission | Description |
|---|---|
allowCreate | Create new records |
allowRead | View records (subject to sharing rules) |
allowEdit | Update records (subject to sharing rules) |
allowDelete | Soft-delete records |
allowTransfer | Transfer record ownership |
allowRestore | Restore records from trash |
allowPurge | Permanently delete records (GDPR compliance) |
viewAllRecords | View all records regardless of sharing rules |
modifyAllRecords | Edit all records regardless of sharing rules |
Field Permissions
Control visibility and editability of individual fields:
fields: {
// Keyed by 'object.field' → Permission
'account.annual_revenue': { readable: true, editable: false }, // Read-only
'account.internal_notes': { readable: false, editable: false }, // Hidden
'account.name': { readable: true, editable: true }, // Full access
'contact.ssn': { readable: false, editable: false }, // Restricted
'contact.salary': { readable: true, editable: false }, // Read-only
}| Permission | Description |
|---|---|
readable | User can see the field value |
editable | User can modify the field value |
Note: readable defaults to true and editable to false. The schema does not force editable: true to be paired with readable: true, but a field with readable: false is stripped from every read regardless of editable — always declare them together, and treat { readable: false } as completely hiding the field.
Tab Permissions
Control which apps/tabs are visible to users:
tabPermissions: {
crm: 'visible', // Always shown
admin: 'hidden', // Never shown
reports: 'default_on', // Shown by default, user can hide
analytics: 'default_off', // Hidden by default, user can show
}| Visibility | Description |
|---|---|
visible | Always visible, user cannot hide |
hidden | Always hidden, user cannot show |
default_on | Visible by default, user can toggle off |
default_off | Hidden by default, user can toggle on |
System Permissions
Grant system-level capabilities:
systemPermissions: [
'manage_users',
'customize_application',
'view_all_data',
'modify_all_data',
'manage_sharing',
'export_data',
'api_access',
'manage_integrations',
]Row-Level Security
Define policies that restrict which records a user can access:
rowLevelSecurity: [
{
name: 'own_records_only',
object: 'opportunity',
operation: 'select',
using: "owner_id == current_user.id",
},
{
name: 'same_department',
object: 'account',
operation: 'select',
using: "department == current_user.department",
},
]Context Variables
Provide custom context values that RLS conditions can reference (in addition to
the built-in current_user.* variables):
contextVariables: {
allowed_regions: ['US', 'EU'],
access_level: 2,
custom_attribute: 'value',
}The built-in context variables available in RLS using/check clauses include
current_user.id, current_user.organization_id, and current_user.positions
(ADR-0068). positions is a string array — the canonical assignment field
(D3 reserves the word "role"; there is no current_user.roles) — so test
membership with CEL: 'org_admin' in current_user.positions or
current_user.positions.exists(p, p == 'sales_manager'). The framework-seeded
built-in position names are platform_admin, org_owner, org_admin, and
org_member; everyone/guest are the implicit audience anchors (ADR-0090 D9).
(current_user.isPlatformAdmin is a derived, deprecated alias of
'platform_admin' in current_user.positions.)
Union semantics — no Profile tier (ADR-0090 D2)
There is no Profile concept: permission sets are the only capability
container and a user may hold any number of them. A user's effective
permissions = the union of every set reached — via positions, direct
grants, the everyone anchor's bindings, and the additive baseline
(member_default). Baseline access is authored on the everyone anchor (or
suggested by a package via isDefault: true); job functions are ordinary
sets bound to positions.
Complete Example
const salesManagerPermission = {
name: 'sales_manager',
label: 'Sales Manager',
objects: {
account: {
allowCreate: true,
allowRead: true,
allowEdit: true,
allowDelete: true,
allowTransfer: true,
allowRestore: true,
allowPurge: false,
viewAllRecords: true,
modifyAllRecords: false,
},
opportunity: {
allowCreate: true,
allowRead: true,
allowEdit: true,
allowDelete: true,
allowTransfer: true,
allowRestore: true,
allowPurge: false,
viewAllRecords: true,
modifyAllRecords: true,
},
contact: {
allowCreate: true,
allowRead: true,
allowEdit: true,
allowDelete: false,
viewAllRecords: true,
modifyAllRecords: false,
},
},
fields: {
'account.annual_revenue': { readable: true, editable: true },
'account.internal_rating': { readable: true, editable: true },
'contact.salary': { readable: true, editable: false },
},
tabPermissions: {
crm: 'visible',
reports: 'visible',
admin: 'hidden',
},
systemPermissions: [
'export_data',
'api_access',
],
rowLevelSecurity: [
{
name: 'team_accounts',
object: 'account',
operation: 'select',
using: "team == current_user.team",
},
],
};Related
- Object Metadata — Objects that permissions control access to
- Field Metadata — Fields that field permissions control
- App Metadata — Apps that tab permissions control visibility for