After installing Laravel Pulse, why does the /pulse dashboard return 403 in production, and how do you grant access to it?
answer
- composer require laravel/pulse, then migrate
- default viewPulse: local environment only
- Authorize middleware checks the gate
- Gate::define in AppServiceProvider::boot
- PULSE_PATH and PULSE_DOMAIN
basics
~20 sPulse 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 sInstalling 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 linescomposer require laravel/pulse
php artisan vendor:publish --provider="Laravel\Pulse\PulseServiceProvider"
php artisan migratego deeper
Recall the install steps, that the dashboard is at /pulse, and that production needs a viewPulse gate defined in AppServiceProvider.
Explain the chain: Pulse routes run web plus Authorize, Authorize calls the viewPulse gate, and the package default only passes in the local environment.
Treat the dashboard as sensitive: narrow the gate, consider a separate domain, and check the documented database requirement before enabling Pulse in production.
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.