Policy enforcement checkpoints
Where policies fire in the pipeline, and why the placement matters.
A policy is a predicate that gates an action. Policies are evaluated at exactly one point in the pipeline: after the ActionInvocation row is persisted, before the handler runs. This page is about why — not how (see policy model for the contract).
The placement
Policies fire after persistence but before any handler logic. This placement is deliberate.
Why after persistence?
Because an attempted action is itself an audit fact. If the policy decision happened first and a block discarded the request, you would lose evidence that the actor tried. Compliance frequently cares about attempts, not just successes — repeated denied attempts to pull credit, for example, are a signal worth recording.
The ActionInvocation row exists with status: blocked_by_policy and a reference to the failing policy. The attempt is durable.
Why before the handler?
Because the handler is the only place domain mutations happen. By the time the handler is reached, every gate has already cleared:
- Module entitlement ✓
- Caller permission ✓
- Policy evaluation ✓
- State-machine transition ✓ (for atomic actions)
Inside the handler, business logic does not need to re-check authorization. This dramatically reduces the surface for "I forgot to call assertCanX here" bugs.
Policy outcomes
Three results, defined in packages/platform/policies/index.ts:
| Result | Effect |
|---|---|
pass | Continue to the next stage. |
warn | Continue, but record the warning in dispatch evidence. Surfaced in the UI / audit trail. |
block | Halt invocation. Status set to blocked_by_policy. Reason and dispatch evidence are persisted. |
aggregatePolicyOutcomes runs across all evaluated policies: any block halts; otherwise the first warn is surfaced; otherwise pass.
Dispatch evidence
Every policy outcome carries a PolicyDispatchEvidence object recording:
- Whether the policy was
code,data, orhybrid. - The dispatch path (
["data"],["code"], or["data", "fallback", "code"]). - For data policies — which conditions evaluated and how.
- For code policies — which evaluator was registered and resolved.
- For hybrid policies — whether and why the fallback was triggered.
This means a regulator asking "why did you allow this action?" gets a structured, machine-readable answer, not a hand-wave.
What policies do not do
Policies are gates, not orchestrators:
- Policies don't write domain state. They read inputs and return a result.
- Policies don't enqueue follow-up work. That's the saga's job.
- Policies don't replace input validation. The handler's schema is the source of truth for shape; policies are the source of truth for should this be allowed.
See also
- Policy model — the evaluator contract.
- Enforcement — code/data/hybrid dispatch in detail.
- Mutation pipeline — where this stage sits in the whole.