Veefore docs

ADR 0003: Policy engine

A TypeScript intermediate representation evaluated in-process, with Rego as an export — and why not OPA as the engine.

#Context

The gateway evaluates policy on every protected action. Four properties are non-negotiable:

Deterministic. The same input must always produce the same decision. A policy engine that can decide differently on a Tuesday is not something you put in front of production.

Total. No operator may throw. An exception mid-evaluation is an outcome nobody specified, and under fail-closed it becomes a refusal with no explanation.

In-process. The gateway cannot take a network hop to another service in order to decide whether to allow something. That service could be down, and fail-closed would then turn every evaluation-service blip into a refusal of all traffic. ADR 0002

Round-trippable with a visual builder. Most policies are authored by people who will not write a policy language. The stored artefact has to be something a UI can read, render, edit and write back without loss.

The obvious candidate is Open Policy Agent with Rego. It is the standard, it is mature, and this decision is the one most likely to be questioned.

#Decision

A TypeScript intermediate representation, stored as JSON in PostgreSQL, evaluated by an in-process evaluator.

json
{
  "schemaVersion": 1,
  "defaultDecision": "ALLOW",
  "defaultReason": "No rule matched.",
  "rules": [{
    "id": "rule-refund-ceiling",
    "name": "Approval above ₹1,000",
    "priority": 700,
    "enabled": true,
    "when": {
      "kind": "condition",
      "id": "c-amount",
      "field": "amount",
      "operator": "gt",
      "value": 100000
    },
    "then": { "decision": "REQUIRE_APPROVAL", "reason": "…" }
  }]
}

That JSON is the policy — not a compilation artefact, not a cache. It is what the visual builder reads and writes, and what the evaluator executes.

Three constraints make the properties hold:

A closed field set. 58 fields, enumerated. An unknown field is a validation error at authoring time, not an undefined at decision time.

Total operators. 20 operators, each defined for every input type. An unresolvable comparison evaluates to false rather than throwing.

Specified precedence. Every matching rule contributes a candidate decision; the strictest wins. BLOCK > REQUIRE_APPROVAL > STEP_UP_AUTH > LIMIT > MASK > ALLOW. Priority orders evaluation and the explanation; it does not override strictness.

Rego is an export, not the engine. compileRuleSetToRego renders an equivalent module so a team already running OPA can enforce the same semantics outside the Node process.

#Alternatives

OPA as the engine, Rego as the source. The standard choice, and it was taken seriously.

Why not: evaluating Rego in-process means embedding a WASM runtime and shipping a compiled policy bundle, which adds a build step between authoring and enforcement — and a build step is a place a policy can be stale. Running OPA as a sidecar reintroduces the network hop that fail-closed makes dangerous.

The round-trip problem is the harder one. Rego is a general-purpose language with comprehensions, partial evaluation and user-defined functions. A visual builder can emit a subset of Rego; it cannot read arbitrary Rego back. So you get either a builder that silently refuses to open hand-written policies, or a product where the UI and the real policy are different artefacts that drift.

And totality is not a Rego property. A Rego policy can fail to produce a decision, and you then have to specify what that means — which is the problem the closed field set and total operators were introduced to avoid.

Cedar. Closer in spirit: analysable, total, designed for authorization. The decision model is oriented around principal-action-resource, and Veefore needs to test risk scores, velocity counters, trust evidence, delegation depth and destination hosts. Expressible, awkwardly. Also a build step between authoring and enforcement.

CEL. Good expression language, total, embeddable. It evaluates expressions; it does not carry rules, priorities, effects or the merge semantics. Most of the work would still have to be built around it, and the IR would end up being the real artefact anyway.

A custom DSL with a parser. More readable than JSON for someone writing by hand. Adds a parser, a formatter, error recovery, and the same round-trip problem as Rego. The IR is already readable enough that hand-authoring works.

Database-stored rows rather than a JSON document. Queryable, and it makes a rule set a join rather than a value — so versioning and diffing become significantly harder, and the atomic thing you activate is no longer a single object.

#Consequences

Policy is not portable to another engine as-is. Mitigated by the Rego export, which is real and tested, and which means a team can run OPA alongside for review or for a second enforcement point. It is an export, so the semantics are equivalent rather than identical by construction.

Expressiveness is deliberately limited. No comprehensions, no user-defined functions, no recursion. Every policy is a flat list of rules over a closed field set. Some things people might want to express are not expressible.

That limit is what buys determinism, totality, sub-millisecond evaluation, and a visual builder that round-trips. It is the trade, stated plainly.

Adding a field is a code change. It goes in POLICY_FIELDS, gets a descriptor with its type and permitted operators, and flows automatically into the builder, the evaluator, the Rego export, and the generated reference page. Deliberate friction: a new field is new authority for policy authors.

The precedence rule surprises people. A BLOCK from a priority-10 rule beats an ALLOW from a priority-900 rule, and a deny-by-default policy bound broadly denies everything in that scope. Both are correct and both generate support questions. Documented prominently in Policy and in the FAQ.

Totality has to be tested for. tests/policy includes 31 cases asserting every operator against every input type, and specifically that no operator throws. A property you cannot see is a property you have to assert.

Identity, authority, policy and enforcement for autonomous AI agents.