Authorization
Truss is designed to be safe to install in production and gated there. Access is guarded by two independent layers, and both must pass.
The two layers
truss.enabledis the deploy switch. When it is off, the routes respond 404, as if they do not exist. It defaults to thelocalenvironment only, so a production deploy stays dark until you setTRUSS_ENABLED=true.- The fixed
viewTrussgate is the access control. It is consulted only in non-local environments (inlocalit is open). A denial returns 404, not 403, so the dashboard never confirms it exists to someone who may not view it.
enabled alone does not grant access, and the gate alone does not make the routes exist. To expose Truss in a non-local environment you must do both: enable it, and authorize the viewers.
Authorize by email (zero code)
For the common “let these admins in” case, the shipped viewTruss gate admits the emails you list. Set them via TRUSS_ALLOWED_EMAILS as a comma-separated list:
TRUSS_ENABLED=trueTRUSS_ALLOWED_EMAILS="ada@example.com,grace@example.com"The list is ignored in local, and it fails closed: an empty list means no one may view in a non-local environment until you add emails or override the gate.
Authorize by role (override the gate)
For anything beyond an email list, define your own viewTruss gate in one of your service providers’ boot() methods (for example AppServiceProvider). Your definition fully replaces the shipped default, and it works with any authorization system, so this is where roles and permissions go:
use Illuminate\Support\Facades\Gate;
Gate::define('viewTruss', fn ($user) => $user->isAdmin());The ability name is fixed (viewTruss); only the callback varies, and that lives in your app. Truss registers its default gate only if you have not defined one, and a definition in your app wins regardless of load order.
Once you define your own gate, TRUSS_ALLOWED_EMAILS is unused and can be dropped from your environment. Keep TRUSS_ENABLED.
With Spatie roles or permissions
If you use spatie/laravel-permission, gate by role, or preferably by a dedicated permission:
// By roleGate::define('viewTruss', fn ($user) => $user->hasRole('admin'));
// By permission (survives role renames; grant it to whichever roles should see Truss)Gate::define('viewTruss', fn ($user) => $user->can('view-truss'));Truss stays framework-agnostic on purpose: it does not read roles or permissions itself (no assumption that your users even have them), so a role check lives in your gate, not in Truss’s config.
Gate::before still applies
Any Gate::before callback in your app runs ahead of viewTruss as well, so an app-wide bypass covers it too. A common pattern is a super-admin bypass:
Gate::before(fn ($user) => $user->hasRole('super-admin') ? true : null);With this, super-admins pass viewTruss automatically, whatever its callback.
Guests
An unauthenticated request resolves to a null user, so a non-nullable check such as fn ($user) => $user->hasRole('admin') safely denies guests (they get 404). If you would rather send guests to the login page than return 404, add auth to truss.middleware.
The auth context (middleware)
The gate can only identify the viewer if the request has already passed through session and auth middleware. The truss.middleware config key provides that stack, and it defaults to ['web']:
'middleware' => ['web'],Without it, $request->user() would be null in production and the gate would deny everyone, including the admins who should have access. If your app authenticates differently (a custom guard, or Sanctum for an SPA), swap the stack here. The viewTruss check is always appended after this stack and cannot be configured away.
Testing your gate
Two things catch people out when writing a test for a custom gate:
- The test environment is non-local, so the gate is consulted (good), but
truss.enableddefaults to off there. Enable it in the test, or even an authorized user gets 404 from the deploy switch before the gate runs. - A denied user gets 404, not 403.
it('admits an admin and hides Truss from other roles', function () { config()->set('truss.enabled', true); // test env is non-local, so enable it
$this->actingAs(tap(User::factory()->create())->assignRole('admin')) ->get('/truss')->assertOk();
$this->actingAs(tap(User::factory()->create())->assignRole('operatore')) ->get('/truss')->assertNotFound(); // 404, not 403});Summary
| Environment | enabled off | enabled on |
|---|---|---|
| local | 404 | open (the gate is not consulted) |
| non-local | 404 | the viewTruss gate decides; a denial is 404 |
The production recipe: set TRUSS_ENABLED=true, list admin emails in TRUSS_ALLOWED_EMAILS (or override the gate), and be logged in as an allowed user.