skip to content

After installing Laravel Pulse, why does the /pulse dashboard return 403 in production, and how do you grant access to it?

level: juniorimportance: must knowfreq 30%

answer

  1. composer require laravel/pulse, then migrate
  2. default viewPulse: local environment only
  3. Authorize middleware checks the gate
  4. Gate::define in AppServiceProvider::boot
  5. PULSE_PATH and PULSE_DOMAIN

basics

~20 s

Pulse registers a default viewPulse gate that only passes in the local environment, and its Authorize middleware checks that gate on /pulse, so production answers 403. Define viewPulse yourself in AppServiceProvider::boot, for example allowing only admins.

solid answer

~40 s

Installing Pulse is `composer require laravel/pulse`, publishing its config and migrations with `vendor:publish` for `Laravel\Pulse\PulseServiceProvider`, and running `php artisan migrate`; the dashboard then lives at `/pulse` (changeable with `PULSE_PATH`, optionally on its own `PULSE_DOMAIN`). Its routes run through the middleware in `pulse.middleware`: the `web` group plus `Laravel\Pulse\Http\Middleware\Authorize`, which calls `Gate::authorize('viewPulse')`. Pulse's own default for that gate is `fn ($user = null) => $app->environment('local')`, so anywhere but local the check fails with a 403. You grant access by defining `viewPulse` in `AppServiceProvider::boot()`, for example `fn (User $user) => $user->isAdmin()`, which replaces the default; because the closure requires a `User`, guests are refused. Pulse's storage is documented for MySQL, MariaDB or PostgreSQL, so an app on the SQLite default points `PULSE_DB_CONNECTION` at one of those.

code

bash · 3 lines
bash
composer require laravel/pulse
php artisan vendor:publish --provider="Laravel\Pulse\PulseServiceProvider"
php artisan migrate

go deeper

for a junior

Recall the install steps, that the dashboard is at /pulse, and that production needs a viewPulse gate defined in AppServiceProvider.

for a middle

Explain the chain: Pulse routes run web plus Authorize, Authorize calls the viewPulse gate, and the package default only passes in the local environment.

for a senior

Treat the dashboard as sensitive: narrow the gate, consider a separate domain, and check the documented database requirement before enabling Pulse in production.

for a principal

Decide who should see production telemetry and how access is reviewed, since Pulse exposes SQL, routes, exceptions and user identities.

## What Pulse is **Laravel Pulse** (`laravel/pulse`, 1.8 at the time of writing) is a first-party package that records aggregate production signals (slow requests, slow queries, slow jobs, exceptions, queue throughput, cache hits, the most active users and server load) and shows them on a Livewire-powered dashboard. It is built to stay on in production, unlike per-request debugging tools. ## Installing it 1. `composer require laravel/pulse`. 2. Publish its configuration and migrations with `php artisan vendor:publish` for the `Laravel\Pulse\PulseServiceProvider` provider (or the `pulse-config` tag for just `config/pulse.php`). 3. Run `php artisan migrate` to create the `pulse_entries`, `pulse_aggregates` and `pulse_values` tables. 4. Visit `/pulse`. The Pulse docs state that its first-party storage requires **MySQL, MariaDB or PostgreSQL**. A Laravel 13 app still on the skeleton's SQLite default should give Pulse a dedicated connection with `PULSE_DB_CONNECTION`. ## How the dashboard is protected The dashboard route is registered under the path in `pulse.path` (`PULSE_PATH`, default `pulse`) and the optional `pulse.domain` (`PULSE_DOMAIN`). Every Pulse route runs through the middleware listed in `pulse.middleware`, which by default is: - `web`: sessions, cookies and CSRF protection, so the logged-in user is known; - `Laravel\Pulse\Http\Middleware\Authorize`: its `handle()` calls `$gate->authorize('viewPulse')`, which throws an authorization exception, rendered as **403**, when the gate denies. ## The default gate and why production gets 403 The package's service provider defines a fallback gate as soon as the gate service is resolved: - `viewPulse` returns `$app->environment('local')`. - The callback's `$user` parameter is nullable, so locally even a guest is let in. - In `production`, `staging` or any other environment it returns false for everyone, including your admins. That is deliberate: the dashboard exposes SQL text, route names, exception classes and user identities, so it must be closed by default. ## Granting access The documented fix is to define `viewPulse` in the `boot()` method of `App\Providers\AppServiceProvider`. Pulse registers its default as soon as the gate service is resolved, so by the time your `Gate::define()` call runs the default already exists and your definition replaces it. A closure whose first parameter is a non-nullable `User` is never called for guests; the gate simply denies them. Keep the rule narrow (an admin flag, a role check or an allow-list of emails) because anyone who passes sees production internals. ## Settings that shape the dashboard | Setting | Env variable | Default | Effect | |---|---|---|---| | `pulse.path` | `PULSE_PATH` | `pulse` | URI of the dashboard | | `pulse.domain` | `PULSE_DOMAIN` | none | serve it on a subdomain | | `pulse.middleware` | none | `web`, `Authorize` | middleware on Pulse routes | | `pulse.enabled` | `PULSE_ENABLED` | `true` | master switch for all recorders | Turning `PULSE_ENABLED` off stops recording; it does not remove the dashboard route, which stays behind the same gate. The layout itself can be customised by publishing `resources/views/vendor/pulse/dashboard.blade.php` with the `pulse-dashboard` tag. ## Common mistakes - Deploying without defining `viewPulse` and assuming a login is enough; the default gate ignores who is logged in. - Removing `Authorize` from `pulse.middleware` to make the 403 go away, which exposes the dashboard to the internet. - Defining the gate with a nullable `$user = null` and returning true, which lets guests in. - Running Pulse's migrations against a SQLite default without checking the documented database requirement. ## How users appear on the dashboard Cards such as Application Usage store only user IDs. When the dashboard renders, Pulse resolves each ID through your default `Authenticatable` model and shows its `name` and `email` with a Gravatar avatar. Calling `Pulse::user(fn ($user) => [...])` in `AppServiceProvider::boot()` changes the `name`, `extra` and `avatar` it displays, and binding your own `Laravel\Pulse\Contracts\ResolvesUsers` implementation replaces the lookup entirely. This matters for access as well: whoever passes `viewPulse` sees those names and emails, which is one more reason to keep the gate narrow.

  • Why can a guest open /pulse on a developer's machine but not in production?
    Pulse's default `viewPulse` callback is `fn ($user = null) => $app->environment('local')`. The nullable `$user` means the gate is evaluated for guests too, and it only looks at the environment, so locally everyone passes and elsewhere everyone fails until you define your own gate.
  • How would you serve the Pulse dashboard on its own subdomain at a different path?
    Set `PULSE_DOMAIN` (for example an internal ops subdomain) and `PULSE_PATH` in the environment; Pulse registers its route group with that domain and prefix. Make sure DNS points the subdomain at the app, and keep the `viewPulse` gate in place, because a separate hostname is not access control.

saying these in an interview costs you the question

  • Logging in as any user is enough to see /pulse in production.
  • The fix for the 403 is removing the Authorize middleware from pulse.middleware.
  • Pulse's default gate lets admins in automatically if a User model has is_admin.
  • Setting PULSE_ENABLED=false removes the /pulse route.
  • Pulse works on any database, so the skeleton's SQLite default needs no thought.