Skip to content
arter.dev

Marque

Laravel authorization with scoped roles, deny rules, permission boundaries, and JSON policy documents

Published

A letter of marque was a government document that granted pirates scoped permission to plunder specific waters. Marque does roughly the same thing for Laravel authorization, minus the plundering. A user is admin in one team and viewer in another, and every permission check accepts a scope.

Scoped roles

Spatie’s laravel-permission covers flat RBAC well. Once you need “admin in Acme, viewer in Widget Co,” you either glue team scoping on top or outgrow the package. Marque takes scope as a first-class argument:

Marque::createRole('admin', 'Admin')
    ->grant(['members.*', 'posts.*'])
    ->assignTo($user, scope: $acmeTeam);

$user->can('members.remove', $acmeTeam);   // true
$user->can('members.remove', $widgetTeam); // false

A scope can be any model that implements Scopeable: teams, orgs, projects, plan tiers.

Deny rules

Prefix a permission with ! and that denial overrides every role that grants it. An editor with posts.* and !posts.delete has full post access except delete:

Marque::createRole('editor', 'Editor')
    ->grant(['posts.*'])
    ->deny(['posts.delete']);

Boundaries

A boundary caps what any role can do inside a scope, admins included. Plan tiers live in the authorization layer instead of scattered feature flag checks:

Marque::boundary($freeOrg)->permits(['posts.read']);
Marque::boundary($proOrg)->permits(['posts.*', 'analytics.*']);

$user->assignRole('admin', $freeOrg);
$user->can('analytics.view', $freeOrg);  // false, boundary blocks it
$user->can('analytics.view', $proOrg);   // true

JSON policy documents

Roles, boundaries, and deny rules can live in version-controlled JSON and import at deploy time:

{
  "roles": [{ "id": "editor", "permissions": ["posts.*", "!posts.delete"] }],
  "boundaries": [{ "scope": "plan::free", "max_permissions": ["posts.read"] }]
}
php artisan marque:import policies/production.json

Wired into the Gate

$user->can(), @can, $this->authorize(), and the can: route middleware work with no extra wiring. Existing code that already uses Laravel’s Gate keeps working; Marque is the driver underneath.

Install

composer require dynamik-dev/marque

GitHub

View Next

Bully

A lint pipeline for Claude Code that fails the tool call when an edit violates your rules

Cloak PHP

PHP library that redacts sensitive values from strings, then puts them back when you need them