edgrosvenor/tupleauth
TupleAuth adds tuple-based, relationship-driven authorization to Laravel. Define access as subject-relation-object tuples, then check permissions through familiar Gates and Blade directives without building complex role hierarchies.
TupleAuth models authorization as (subject, relation, object) triples — (user:1, owner, document:5), (user:1, member, team:3) — and then exposes those triples through Laravel's normal Gate/middleware surface so the rest of your app never sees the tuple machinery. Treat it as a policy backend, not a new API to learn.
1. Pin and install. The package is pre-1.0 and archived; a caret range is unsafe.
composer require "edgrosvenor/tupleauth:0.1.3"
Commit the lock file and diff it — 0.1.3's whole purpose was relaxing framework constraints, so watch for transitive illuminate/* movement.
2. Register the provider. 0.1.3 explicitly targets older Laravel apps. Older majors predate package auto-discovery, so expect to add the provider to config/app.php manually even if the package ships discovery metadata:
'providers' => [
// ...
Edgrosvenor\TupleAuth\TupleAuthServiceProvider::class,
],
Confirm the FQCN from the package's composer.json/src before you write it — there is no README to copy from. This is a five-minute task and the first thing you should do.
3. Publish and read the config. php artisan vendor:publish against the package's provider, then read the published file end to end. That file is the primary documentation for this package: it tells you where tuples are stored, how relations are named, and how hierarchies are declared.
4. Define one tuple and check it. Start with the smallest possible rule rather than migrating your whole permission model:
// in config/tupleauth.php, or wherever the package expects definitions
'owner' => ['editor', 'viewer'], // illustrative hierarchy shape
Then check it through the Gate you already use:
if (Gate::allows('view', $document)) {
// ...
}
First use case: a single resource where today you have an ad-hoc if ($user->id === $doc->user_id || $user->isAdmin()) sprinkled across three controllers. Replace that one check, verify it through the existing can: middleware on one route, and stop. You now know whether the abstraction earns its place before touching anything load-bearing.
Route middleware is the highest-leverage integration. The package pitches "familiar Gate and middleware APIs," which means your existing can: middleware should keep working once tuples are registered. That is the cheapest possible migration path: leave controllers untouched, swap out the authorization source, not the authorization call sites.
Route::put('/documents/{document}', [DocumentController::class, 'update'])
->middleware('can:update,document');
If your app is on a Laravel version old enough to need manual middleware registration, register the package's alias in Kernel::$routeMiddleware first.
Register tuples at the boundary, check them in the domain. Load tuples once per request (in a service provider, a middleware, or a Gate::before callback) rather than querying inside every check. If the config/code path is the source of truth, load it eagerly at boot; if tuples come from a table, cache them per-user for the request and bust the cache on write. A tuple store queried inside a policy is a per-check N+1 waiting to happen on any index page.
Model hierarchical roles as relation edges, not booleans. The package supports role and relationship hierarchies. The idiomatic use is chained implication — owner implies editor implies viewer — so a check for viewer succeeds when the user holds owner. This is what lets you keep your check sites stable as you refine the model: you add a new relation and an edge, not a new if. Resist the urge to encode "is admin" as a special case outside the hierarchy; admins are just a tuple that resolves broadly.
Keep it behind your existing abstractions. Wrap the package in a thin internal AuthorizationService or a set of policies, rather than calling it from controllers directly. Given the archive status and 0.x versioning, this is your replacement insurance: when the package stops working on a future Laravel upgrade, you rewrite one class instead of grepping 200 call sites.
Pilot in the oldest target app first. 0.1.3's stated claim is older-Laravel compatibility, and that is precisely the path least likely to be exercised in anyone's local environment. If you're standardizing across a mixed-version estate, install it in the oldest app and run your Gate checks there before rolling it anywhere else.
The package is archived and has no README. Every API name in this assessment is inferred from the description and release notes. Before writing production code, read src/ and the published config — budget half a day, not fifteen minutes. There will be no upstream fix for anything you find.
Pre-1.0 semantics are live. Patch releases may break behavior. Pin the exact version ("0.1.3"), not ^0.1. Add your own tests around the Gate checks you depend on, because upstream CI on the older majors is unverifiable from the outside.
Gate registration order matters. If tuples are loaded during provider boot but you resolve the authenticated user later, early checks will silently return false rather than error — the worst failure mode. Load tuples lazily, or inside Gate::before/Gate::after where the user is guaranteed resolved. Test with $this->actingAs($user) and an unauthenticated request; the latter should fail closed.
Relation name casing and typos fail silently. A tuple authorizing Owner won't satisfy can:view resolving through owner. There's no schema to catch this. Define relations as constants or config keys and reference those, never raw strings at the call site.
Hierarchy cycles hang or recurse. owner → editor → owner will either infinite-loop or blow the stack depending on implementation. If you hand-edit the hierarchy config, add a test that walks the whole graph, and keep the graph a DAG by review discipline.
Clear the config cache after editing tuples. If tuples live in a published config file, php artisan config:cache in deployment means edits made directly on the server do nothing until you re-cache. This bites during incident response, when someone "fixes" permissions by editing the file and nothing changes.
Middleware alias may need manual registration on older Laravel. Same reason as the service provider — the versions 0.1.3 specifically courts predate auto-discovery. If can: throws "middleware not found," check Kernel.php before assuming the package is broken.
Don't reach into internals. The temptation with an undocumented package is to call its tuple-store classes directly. Every internal call you make is a line you must rewrite when the package is replaced. Stay on Gate and middleware — those survive a backend swap.
Have an exit plan on day one. The extension point that matters is the Gate facade, which is Laravel's, not the package's. If all your checks go through Gate::allows() and can: middleware, migrating to native policies is mechanical: write policies that express the same rules, delete the provider. Keep that property true and the archived status is a manageable risk rather than a trap.
How can I help you explore Laravel packages today?