# Security & access control

> Who can reach the dashboard — and who can reach the buttons that delete things.

Source: https://boring-observability.dev/skyline/docs/security-and-access
Section: Reference — Laravel Skyline documentation
Updated: 2026-07-13

---

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.

> **Read this before deploying Skyline**
>
> 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:

```php
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.

```php
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`:

```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:

```bash
php artisan horizon:pause
php artisan horizon:pause-supervisor supervisor-1
php artisan horizon:continue
```

See the [HTTP API reference](https://boring-observability.dev/skyline/docs/api) 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 `409` rather 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](https://boring-observability.dev/skyline/docs/installation#ci-and-production).


## Common questions

### Who can access the Laravel Skyline dashboard?

Exactly who could access Horizon's. Access is controlled by the viewHorizon gate defined in your HorizonServiceProvider and by the middleware listed under horizon.middleware, and Skyline changes neither. In the local environment Horizon's auth callback allows access by default; everywhere else the gate decides.

### Does Skyline need a separate permission for its destructive actions?

No — and that is the thing to know before deploying it. Deleting a job, emptying a queue and pausing a queue all sit behind the same viewHorizon gate as read-only access. Anyone who could previously watch your queues can now also act on them, so revisit a gate that was written when the dashboard could only look.
