Skip to content

How evaluation works ​

← Documentation home

Understanding the evaluation pipeline prevents surprises when combining priority, deny rules, conditions, and tenancy. This page walks through exactly what happens when you call engine.evaluate().


Entry point ​

typescript
const decision = engine.evaluate(
  subject,
  action,
  resource,
  resourceContext?, // default {}
  tenantId?,          // optional
);

The same pipeline powers explain() and permitted() — they differ only in output shape.


Step 1: Resolve subject roles ​

Input: subject.roles, tenantId (optional), roleHierarchy (optional)

1. If tenantId provided:
     Keep assignments where assignment.tenantId === tenantId OR assignment has no tenantId (global)
   Else:
     Use all assignments (unless strictTenancy throws — see multitenancy doc)

2. Collect role names from filtered assignments

3. If roleHierarchy configured:
     Expand each role to include inherited roles (e.g. admin → manager → member)

Output: Set of effective role names

If the effective set is empty, only rules with anyRole() or roles: "*" can match on the role axis.


Step 2: Find candidate rules ​

For each compiled rule, check:

CheckPass when
RoleRule roles is "*" OR intersects effective roles
ActionRule actions is "*" OR exact match OR wildcard regex matches
ResourceRule resources is "*" OR includes requested resource

Rules that fail any check are not candidates.

Wildcard patterns like invoice:* were compiled to RegExp at addRule() — no per-request regex compilation cost.


Step 3: Sort candidates ​

Candidates are sorted before evaluation:

  1. Higher priority first (default priority is 0)
  2. At equal priority, deny before allow

This ordering implements "deny wins at the same priority level."


Step 4: First match wins ​

Candidates are evaluated in sorted order. The first rule that fully matches determines the outcome.

For each candidate:

  1. If the rule has no conditions → it matches immediately; stop.
  2. If the rule has conditions → evaluate each in order:
    • Sync: must return true
    • Async: not allowed in sync evaluate() — use evaluateAsync()
    • Thrown error → treated as false, optional onConditionError callback
    • All must pass → rule matches; stop
    • Any fails → try next candidate

Step 5: Default effect ​

If no candidate matched:

  • defaultEffect: "deny" (default) → allowed: false, effect: "default-deny"
  • defaultEffect: "allow" → allowed: true

Most applications leave deny as default.


Step 6: Build the Decision ​

The returned Decision includes:

  • allowed, effect, matchedRule, reason
  • durationMs, timestamp
  • Echo of subject, action, resource, resourceContext, tenantId

onDecision listeners fire for every evaluation.


Cache interaction ​

If cacheSize > 0, unconditional results may be served from LRU cache keyed by subject, action, resource, tenant. Conditional paths bypass cache. Rule changes clear the cache.

→ Evaluation cache


Mental model diagram ​

evaluate(subject, action, resource, ctx, tenantId)
        │
        ▼
  Resolve roles (+ hierarchy, tenant filter)
        │
        ▼
  Filter rules → candidates (role ∧ action ∧ resource)
        │
        ▼
  Sort by priority ↓, deny before allow at tie
        │
        ▼
  For each candidate in order:
        ├─ conditions pass? ──yes──► return allow/deny from rule
        └─ no ──► next candidate
        │
        ▼
  No match → default deny (or allow)

Released under the MIT License.