Skip to content

Define policies

A policy is the “…but only this one” rule that RBAC can’t express. Register one per permission, and keep each policy in the module that owns the resource.

Put policies in the owning module’s policies/ folder — an order policy lives with the orders module:

src/app/orders/policies/index.ts
import { definePolicy } from "@warlock.js/access";
definePolicy("orders.update", (user, order, ctx) =>
order.get("organization_id") === ctx.tenant &&
(order.get("customer_id") === user.id || ctx.hasRole("manager")),
);

Load it once from that module’s main.ts so it’s registered before any request (the module root stays just routes.ts + main.ts):

src/app/orders/main.ts
import "./policies";
await authorize(user, "orders.update"); // policy SKIPPED (grant only)
await authorize(user, "orders.update", { resource: order }); // policy runs

So a route gate (gate, no resource) verifies the grant, and the per-record condition runs in your service after you load the order. Class-level and instance-level stay cleanly separated.

  • No grant → denied; the policy never runs.
  • Grant, no policy registered → denied by default (5.13+) — strictPolicies defaults to true. The engine logs a one-time warning naming the permission: a typo’d permission name, or a forgotten import "./policies", is the usual cause. Register the missing definePolicy(...), or set strictPolicies: false in your access config to restore the pre-5.13 RBAC-only fallback (allowed, not recommended) — see configuration.
  • Grant + policy → the policy decides.

Policies deny further — they can never let a user past a permission they don’t hold.

(user, resource, ctx). ctx carries the resolved tenant, the engine helpers hasRole(role) / hasPermission(perm), and any extra keys you passed on the check:

await authorize(user, "orders.refund", { resource: order, amount: 5000 });
definePolicy("orders.refund", (user, order, ctx) =>
ctx.hasRole("manager") || (ctx.amount as number) <= 1000,
);
  • A policy that throws is treated as a denial (fail-closed) and logged.
  • Policies are keyed by permission name — define each once.
  • “Own resource” at graph scale (deep relationship chains) is ReBAC, which is out of scope; a policy covers the everyday ownership case.