ObjectStackObjectStack

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

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
    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
  },
}
PermissionDescription
allowCreateCreate new records
allowReadView records (subject to sharing rules)
allowEditUpdate records (subject to sharing rules)
allowDeleteSoft-delete records
allowTransferTransfer record ownership
allowRestoreRestore records from trash
allowPurgePermanently delete records (GDPR compliance)
viewAllRecordsView all records regardless of sharing rules
modifyAllRecordsEdit 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
}
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,
      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: 'org_isolation',
      object: 'account',
      operation: 'select',
      using: "organization_id == current_user.organization_id",
    },
  ],
};

On this page