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] Install-time suggestion to bind this set to the everyone position (admin confirms; never auto-bound)
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
contextVariablesRecord<string, any>optionalRLS 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
  },
}
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',         // Always shown
  admin: 'hidden',         // Never shown
  reports: 'default_on',   // Shown by default, user can hide
  analytics: 'default_off', // Hidden by default, user can show
}
VisibilityDescription
visibleAlways visible, user cannot hide
hiddenAlways hidden, user cannot show
default_onVisible by default, user can toggle off
default_offHidden 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",
    },
  ],
};

On this page