Security & access control
Who can reach the dashboard — and who can reach the buttons that delete things.
Access to the Laravel Skyline dashboard is controlled by exactly what controls access to Horizon's: the
viewHorizon gate defined in your HorizonServiceProvider, and the middleware listed under
horizon.middleware. Skyline changes neither. What does change is what a person who passes that
gate is able to do — so it is worth re-reading a gate that was written when the dashboard could only look.
Skyline's destructive actions — deleting a job, emptying a queue, pausing a queue, running a delayed job
immediately — sit behind the same viewHorizon gate as read-only access. There is no
separate permission. Anyone who could previously watch your queues can now also act on them.
The viewHorizon gate#
Horizon's installer publishes a HorizonServiceProvider with a gate that, by default, allows nobody in
production:
protected function gate(): void
{
Gate::define('viewHorizon', function ($user) {
return in_array($user->email, [
'taylor@laravel.com',
]);
});
}
If that list is still the stub, or if the gate was widened to "any authenticated user" back when the dashboard was
read-only, now is the moment to narrow it. A reasonable gate for Skyline is the set of people you would trust to run
redis-cli DEL against a production queue — because that is materially what Empty queue does.
Gate::define('viewHorizon', fn ($user) => $user->isAdmin() && $user->hasTeam('platform'));
The local environment#
Horizon's Horizon::auth() callback allows access in the local environment without
consulting the gate at all, which is what lets you open /horizon on your laptop without logging in.
Skyline inherits that behaviour unchanged. It means the gate is the only thing standing between a request
and a queue-emptying button everywhere else — so a misconfigured APP_ENV on a deployed box is worth
treating as a security issue, not a cosmetic one.
Middleware#
Everything the dashboard serves, including every API endpoint Skyline adds, runs through the middleware stack in
config/horizon.php:
'middleware' => ['web'],
Adding your own middleware here — an IP allow-list, an SSO guard, a second factor — protects the Skyline endpoints exactly as it protects Horizon's, because they are the same stack. There is no separate route group to remember.
CSRF, and calling the API from a script#
Because the endpoints sit inside the web group, they are session-authenticated and CSRF-protected. This
is the part that surprises people writing a runbook: a bare curl at the pause endpoint does not pause a
queue, it gets a redirect to your login page or a 419.
A script that genuinely needs to drive the API has to carry a session cookie and a CSRF token, like a browser would. If what you actually want is a deploy-time or incident-time hook, prefer the Artisan commands — they need no HTTP session and are the intended surface for automation:
php artisan horizon:pause
php artisan horizon:pause-supervisor supervisor-1
php artisan horizon:continue
See the HTTP API reference for the endpoints themselves.
What the destructive actions can and cannot do#
The guards are in the backend, not only in the UI, so they hold even for a request that skips the confirmation modal:
- Only pending and delayed jobs can be deleted. A job a worker has already reserved is rejected with a
422. - Completed, silenced and failed jobs cannot be deleted at all — they are historical records, not queue entries.
- Per-job deletion requires a Redis connection; other drivers are rejected with a
422. - A job picked up by a worker between the click and the delete landing fails with a
409rather than silently doing nothing.
None of that is an access control. It bounds the blast radius of a mistake; it does not bound the blast radius of the wrong person having the gate. Set the gate.
The license key is a credential#
Finally, and separately from the dashboard: your Skyline license key authenticates Composer against a private
registry, so it is a secret in the ordinary sense. Keep it in a secret manager or in CI secrets, supply it through
COMPOSER_AUTH rather than a committed auth.json, and never put it in a public repository.
See Installation.