Skip to content

Core concepts

Warrant splits authorization into three separate things. Keeping them separate is the whole idea.

Piece What it is Who writes it
Schema The vocabulary for one resource: the abilities that exist (view, approve, …) and the conditions a rule may test (is_self, is_manager, …). Conditions know how to emit SQL. You, in a PHP class
Rules The policy itself, written in Warrant’s rule language as a plain string (e.g. if is_self they can view). Rules reference the schema’s vocabulary. Stored as data — a DB table, config, JWT claims, wherever
Resolver The glue that, at request time, produces the rules that apply to this user for this resource. You, one small class

A schema is not a policy. It doesn’t decide anything — it only declares what words the language may use. The actual decisions live in the rules, which your resolver supplies. Warrant compiles those rules, validated against the schema, into SQL.

your data (roles, grants) request-time
│ │
▼ ▼
RuleResolver ──▶ WarrantRuleSet ──▶ RuleSetCompiler ──▶ SQL WHERE / column
▲ │
│ │ validated against
WarrantSchema ──────────────┘
(abilities + conditions)

Why “one rule, three questions” matters

Section titled “Why “one rule, three questions” matters”

Every real list screen asks three authorization questions, and traditional tools answer only the first:

  1. Can this user act on this row? — a Laravel Policy handles this.
  2. Which rows can they act on? — the policy can’t; you rewrite it as a query scope. Now the rule lives in two places and drifts.
  3. What can they do to each row on the page? — 50 rows × 4 abilities = 200 policy calls, each possibly hitting the database.

Because Warrant compiles one rule into one predicate, all three questions are answered by the same compiled SQL. The list filter and the edit button cannot contradict each other, because there is only one source of truth.

Warrant vs. spatie/laravel-permission walks through this failure mode and when to reach for each.

Throughout the rule language, “they” is the current user — the one your resolver was asked about for this request. A rule set never describes what everyone can do; it describes what this user can do with the resource it’s scoped to. So they can approve means “this user may approve every row of this resource,” not “approval is open to all users.”

Warrant combines grants and denials with deny-overrides. For a given ability, the compiled predicate is:

( any `can` rule for it matches ) AND ( no `cannot` rule for it matches )

Three consequences worth internalising early:

  • A cannot is an absolute veto. No can rule can bring back an ability a cannot denies.
  • An ability with no can rule is denied. Silence is not permission.
  • Rule order does not matter. The combination is commutative, so implicit rules, resolver rules, and multiple clauses combine the same way regardless of order.

The deny-overrides guide covers the corner cases.

Unlike laravel-permission or Bouncer, Warrant owns no tables and has no opinion about where rules live. It only asks that your resolver hand back a WarrantRuleSet for the current request. Fetch it from a table, build it from config, compose it per tenant, read it off a JWT claim — Warrant picks up wherever your resolver leaves off and compiles the result to SQL.