ObjectStackObjectStack

Permission Metadata

Define access control with permission sets — object CRUD, field security, tab visibility, and row-level security

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

PropertyTypeRequiredDescription
namestringMachine name (snake_case)
labelstringoptionalDisplay label
isDefaultbooleanoptional[ADR-0090 D5] Baseline for the everyone position: an app-level default set is auto-bound at boot (guarded, idempotent); a package-shipped set instead becomes an install-time suggestion an admin confirms
adminScopeAdminScopeoptional[ADR-0090 D12] Delegated-administration scope (BU subtree + assignable-set allowlist)
objectsRecord<string, ObjectPermission>Object-level permissions
fieldsRecord<string, FieldPermission>optionalField-level security, keyed by 'object.field'
systemPermissionsstring[]optionalSystem-level capabilities
tabPermissionsRecord<string, TabVisibility>optionalTab/app visibility
rowLevelSecurityRowLevelSecurityPolicy[]optionalRow-level security policies

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
    viewAllRecords: false,     // Bypass sharing rules for read
    modifyAllRecords: false,   // Bypass sharing rules for write
  },
}
PermissionDescription
allowCreateCreate new records
allowReadView records (subject to sharing rules)
allowEditUpdate records (subject to sharing rules)
allowDeleteSoft-delete records
allowTransferTransfer record ownership
viewAllRecordsView all records regardless of sharing rules
modifyAllRecordsEdit all records regardless of sharing rules

Retired: the former allowRestore / allowPurge keys were removed (#12497, ADR-0049) — the restore / purge operations they claimed to gate do not exist yet, so authoring them granted nothing. Authoring either key is now a loud publish-time error carrying this prescription. The keys return with the M2 lifecycle initiative (feature + RBAC in one batch, #1883); until then a dispatched restore / purge is denied unconditionally.

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
}
PermissionDescription
readableUser can see the field value
editableUser 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',           // Shown
  admin: 'hidden',          // Hidden — removed from nav
  reports: 'default_on',    // Shown today (per-user toggle default not yet implemented)
  analytics: 'default_off', // Shown today (per-user toggle default not yet implemented)
}
VisibilityDescription
visibleShown (also the effective behavior when a tab is left unset)
hiddenHidden — the app is removed from the user's navigation
default_onShown today; reserved for a future per-user toggle default
default_offShown today; reserved for a future per-user toggle default

Note: Only hidden currently affects runtime behavior — the app-visibility gate hides an app when its tab is set to hidden and treats every other value (including default_off) as visible. The per-user toggle / default-off semantics implied by default_on/default_off are not yet implemented.

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_organization',
    object: 'account',
    operation: 'select',
    using: "organization_id == current_user.organization_id",
  },
]

Context Variables

Note: A permission set cannot declare its own context values. The contextVariables field was removed in ADR-0105 D11 (enforce-or-remove) because the RLS compiler never read it. Conditions reference the built-in current_user.* variables described below, plus any set a registered membership resolver stages into ExecutionContext.rlsMembership (referenced as field IN (current_user.<key>)); a constant belongs in the policy itself as a literal (status = 'published').

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,
      viewAllRecords: true,
      modifyAllRecords: false,
    },
    opportunity: {
      allowCreate: true,
      allowRead: true,
      allowEdit: true,
      allowDelete: true,
      allowTransfer: true,
      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: 'org_isolation',
      object: 'account',
      operation: 'select',
      using: "organization_id == current_user.organization_id",
    },
  ],
};

On this page