Skip to main content

primitives

Core data types — Subject, Resource, Action, Scope, Environment, AccessRequest, Decision. The shapes every check is built on.

The request side

Every authorization check assembles an AccessRequest:

Loading diagram...


Subject

The entity making the request — usually a user or service account.

interface Subject {
  id: string
  roles: readonly string[]
  scopedRoles?: readonly ScopedRole[]
  attributes: Record<string, AttributeValue>
}
interface Subject {
  id: string
  roles: readonly string[]
  scopedRoles?: readonly ScopedRole[]
  attributes: Record<string, AttributeValue>
}

A subject carries an id, assigned roles, and arbitrary attributes — department, plan tier, clearance level, or whatever the domain requires.

The engine resolves a subject from subjectId on every check (cached after the first resolution). You don't construct Subject objects directly — pass the ID and the engine handles it.


Resource

The thing being accessed — a post, document, settings page.

interface Resource {
  type: string // e.g. "post", "comment", "dashboard.settings"
  id?: string // optional instance ID
  attributes: Record<string, AttributeValue>
}
interface Resource {
  type: string // e.g. "post", "comment", "dashboard.settings"
  id?: string // optional instance ID
  attributes: Record<string, AttributeValue>
}

Resource types use dot-separated hierarchical matching. A rule targeting "dashboard" also matches "dashboard.users" and "dashboard.users.settings".

attributes carries record-level data: ownerId, status, tenantId, tags, etc. This is what enables ABAC checks like isOwner() or resourceAttr('status', 'eq', 'published').


Action

A string describing the operation: "read", "create", "update", "delete", or any custom action. Actions support wildcards:

  • "*" — matches everything
  • Custom prefixes — define your own (e.g. "posts:read", "posts:write")

There's nothing magical about the four CRUD verbs — they're convention, not built-in. Pick action names that fit your domain.


Scope

An optional namespace for multi-tenant isolation. With scope: "org-1", only roles and rules matching that scope apply. Any string works — org IDs, workspace slugs, project keys.

Loading diagram...

See scoped roles for the three scoping mechanisms.


Environment

Request-time context: IP, user agent, timestamp, plus any custom fields conditions need:

interface Environment {
  ip?: string
  userAgent?: string
  timestamp?: number
  [key: string]: AttributeValue | undefined // custom fields
}
interface Environment {
  ip?: string
  userAgent?: string
  timestamp?: number
  [key: string]: AttributeValue | undefined // custom fields
}

The server middleware integrations (Express, Hono, Nest, Next) build a default Environment from request headers. Add custom fields like region, dayOfWeek, maintenanceMode, etc. for time/geo/feature-flag rules.


AccessRequest

The full context for an authorization check:

interface AccessRequest {
  subject: Subject
  action: string
  resource: Resource
  scope?: string
  environment?: Environment
}
interface AccessRequest {
  subject: Subject
  action: string
  resource: Resource
  scope?: string
  environment?: Environment
}

engine.can() and engine.check() build this object internally — you supply the parts. engine.authorize() accepts a pre-built AccessRequest directly for advanced use.


Decision

The output of an authorization check (in development mode):

interface Decision {
  allowed: boolean // the boolean you need
  effect: 'allow' | 'deny'
  rule?: Rule // which rule decided
  policy?: string // which policy it came from
  reason: string // human-readable explanation
  duration: number // evaluation time in ms
  timestamp: number // when the check happened
}
interface Decision {
  allowed: boolean // the boolean you need
  effect: 'allow' | 'deny'
  rule?: Rule // which rule decided
  policy?: string // which policy it came from
  reason: string // human-readable explanation
  duration: number // evaluation time in ms
  timestamp: number // when the check happened
}

In production mode, engine.authorize() returns a plain boolean — no Decision allocation. engine.can() always returns boolean regardless of mode (it's the simple-API method).


Policy

A named collection of rules with a combining algorithm:

interface Policy {
  id: string
  name: string
  description?: string
  version?: number
  algorithm: CombiningAlgorithm // 'deny-overrides' | 'allow-overrides' | 'first-match' | 'highest-priority'
  rules: readonly Rule[]
  targets?: {
    actions?: readonly string[]
    resources?: readonly string[]
    roles?: readonly string[]
  }
}
interface Policy {
  id: string
  name: string
  description?: string
  version?: number
  algorithm: CombiningAlgorithm // 'deny-overrides' | 'allow-overrides' | 'first-match' | 'highest-priority'
  rules: readonly Rule[]
  targets?: {
    actions?: readonly string[]
    resources?: readonly string[]
    roles?: readonly string[]
  }
}

See policies for the full builder API and combining algorithm details.


Rule

A single authorization statement inside a policy:

interface Rule {
  id: string
  effect: 'allow' | 'deny'
  description?: string
  priority: number
  actions: readonly string[]
  resources: readonly string[]
  conditions: ConditionGroup
}
interface Rule {
  id: string
  effect: 'allow' | 'deny'
  description?: string
  priority: number
  actions: readonly string[]
  resources: readonly string[]
  conditions: ConditionGroup
}

A rule fires when the action matches, the resource matches, and all conditions pass. See rule matching for the match flow.


Condition and ConditionGroup

A single condition checks one field against one value:

interface Condition {
  field: string // e.g. "subject.attributes.department"
  operator: Operator // e.g. "eq", "in", "contains"
  value?: AttributeValue
}
interface Condition {
  field: string // e.g. "subject.attributes.department"
  operator: Operator // e.g. "eq", "in", "contains"
  value?: AttributeValue
}

Conditions are grouped using logical operators:

type ConditionGroup =
  | { all: Array<Condition | ConditionGroup> } // AND
  | { any: Array<Condition | ConditionGroup> } // OR
  | { none: Array<Condition | ConditionGroup> } // NOT (none must be true)
type ConditionGroup =
  | { all: Array<Condition | ConditionGroup> } // AND
  | { any: Array<Condition | ConditionGroup> } // OR
  | { none: Array<Condition | ConditionGroup> } // NOT (none must be true)

Groups nest up to 10 levels. Past that, evaluation returns false (fail closed).

See conditions and nesting for the full builder.