Core concepts
Warrant splits authorization into three separate things. Keeping them separate is the whole idea.
The three pieces
Section titled “The three pieces”| 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.
The flow
Section titled “The flow” 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:
- Can this user act on this row? — a Laravel Policy handles this.
- 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.
- 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.
“They” is the current user
Section titled ““They” is the current user”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.”
Deny-overrides
Section titled “Deny-overrides”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
cannotis an absolute veto. Nocanrule can bring back an ability acannotdenies. - An ability with no
canrule 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.
Warrant stores nothing
Section titled “Warrant stores nothing”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.
