skip to content

Why do Livewire update requests re-run some of the page route's middleware, and when must you call Livewire::addPersistentMiddleware()?

level: seniorimportance: should knowfreq 30%

answer

  1. one update endpoint, not the page route
  2. path and method memoized in the snapshot
  3. only listed middleware is re-applied
  4. custom middleware needs registering in boot()
  5. no middleware arguments in the list

basics

~20 s

Livewire updates hit one shared endpoint, so the page route's middleware would not run again. Livewire re-applies only middleware on its persistent list, such as auth and can; custom middleware needs Livewire::addPersistentMiddleware() in a service provider.

solid answer

~40 s

After the first render, every click posts to Livewire's update route, which carries only `web` and a header guard, not the page route's middleware. To keep route-level checks alive, Livewire records the page's path and method in the signed snapshot memo; on each update it rebuilds a fake request for that path, matches the route, gathers its middleware and re-runs only those on its **persistent list**: `Authenticate`, `Authorize` (`can:`), `SubstituteBindings`, `AuthenticateWithBasicAuth`, Sanctum's stateful middleware and a few others. So a user whose permission is revoked after the page loaded is stopped on the next click. Your own middleware, such as `EnsurePayrollAdmin`, is not re-run unless you call `Livewire::addPersistentMiddleware([EnsurePayrollAdmin::class])` in a service provider's `boot()`, passing class names without arguments.

go deeper

for a junior

Know that Livewire clicks go to one shared update endpoint, so page middleware does not automatically run on each click.

for a middle

Explain the memoized path and method, the fake request, and which built-in middleware Livewire re-applies by default.

for a senior

Spot custom middleware that silently stops guarding updates, register it in boot() without arguments, and still authorize per action.

for a principal

Decide which checks belong in persistent middleware, a custom update route, or per-action authorization, and document it so new pages inherit it.

## Why this problem exists A **Livewire page** is first served by a normal Laravel route, for example: ```php Route::livewire('/payroll/{run}', PayrollRun::class) ->middleware(['auth', 'can:view,run', EnsurePayrollAdmin::class]); ``` That request runs the full middleware stack. But every interaction afterwards (a click, a `wire:model.live` keystroke) is an AJAX POST to Livewire's own **update endpoint**. By default that route carries the `web` group and a header guard, `RequireLivewireHeaders`, nothing else. If Livewire did nothing, a user whose payroll access was revoked five minutes after loading the page could keep clicking Approve. ## How persistent middleware works Livewire calls the fix **persistent middleware**: 1. **On dehydrate**, Livewire adds the original request's `path` and `method` to the component's `memo`. The memo is part of the snapshot, so the checksum protects it from being rewritten. 2. **On each update**, after the snapshot is verified and only when the request really hit the Livewire update route, Livewire duplicates the current request, swaps in the memoized path and method, and matches it against the router. 3. It **gathers that route's middleware** and filters it down to the classes on its persistent list, comparing names with any `:arguments` stripped. 4. It runs the survivors through a pipeline. `can:view,run` survives as `Authorize` and keeps its arguments; `SubstituteBindings` re-binds `{run}`, and Livewire reuses that model for any matching model property instead of querying again. ## The default list | Middleware | Why it persists | |---|---| | `Illuminate\Auth\Middleware\Authenticate` (and `App\Http\Middleware\Authenticate`) | Logged-out users stop being able to act | | `Illuminate\Auth\Middleware\Authorize` | `can:` checks are re-evaluated | | `Illuminate\Routing\Middleware\SubstituteBindings` | Route models are bound again | | `Illuminate\Auth\Middleware\AuthenticateWithBasicAuth` | Basic-auth pages stay gated | | `Laravel\Sanctum\...\EnsureFrontendRequestsAreStateful` | SPA session auth keeps working | | `Laravel\Jetstream\...\AuthenticateSession`, `App\Http\Middleware\RedirectIfAuthenticated` | Legacy and app-level auth helpers | Anything else, including every middleware you wrote, is skipped. ## Registering your own ```php // app/Providers/AppServiceProvider.php public function boot(): void { Livewire::addPersistentMiddleware([ \App\Http\Middleware\EnsurePayrollAdmin::class, ]); } ``` Now, on any page whose route uses `EnsurePayrollAdmin`, every Livewire update re-runs it. Two limits: - **No arguments in the registration.** Pass the class, not `EnsureRole::class.':admin'`. The route's own arguments are still used when the middleware runs, because the comparison strips them only for matching. - **Only middleware the page route actually has** is re-run. Registering a class does not add it to routes that lack it. ## The other lever: a custom update route When you want some middleware on **every** Livewire update regardless of the page, register the update route yourself: ```php Livewire::setUpdateRoute(function ($handle, $path) { return Route::post($path, $handle)->middleware(SetPayrollLocale::class); }); ``` Livewire adds `web` and the header guard back if you leave them out, so CSRF protection cannot be dropped by accident. ## A worked scenario A payroll manager opens the approval page for run 14 at 09:00. At 09:05 an administrator removes the manager's access to that run. At 09:06 the manager clicks Approve: 1. The update request reaches Livewire's endpoint and the snapshot verifies. 2. Livewire reads `/payroll/14` and `GET` from the memo, rebuilds a request, and matches the route. 3. It keeps `Authenticate`, `Authorize` (`can:view,run`) and `SubstituteBindings`, and drops `EnsurePayrollAdmin` unless it was registered. 4. `can:view,run` now fails, so the update stops with a 403 before `approve()` runs. Had the revocation only been enforced by `EnsurePayrollAdmin`, and that class not registered, the click would have succeeded. Auditing a Livewire app therefore means comparing every page route's custom middleware with the persistent list. ## What it does not do - It does nothing in `Livewire::test()`: the test mounts the component on a temporary route with no middleware and sends its updates with middleware disabled, so test route middleware at the HTTP layer. - It is not a replacement for authorizing inside actions: middleware sees the page route, not which payslip an action targets. - Middleware that mutates the response of a page load (headers, view sharing) usually has no meaning on an update and should stay off the list.

  • Why can't Livewire::addPersistentMiddleware() take 'role:admin'?
    Livewire matches the page route's middleware against its list by class name with any `:arguments` stripped, so the list stores bare class names. Registering `EnsureRole::class.':admin'` would never match anything. Register `EnsureRole::class`; the route's own `role:admin` arguments are still passed when Livewire re-runs it.
  • A Livewire component test passes although the route's custom middleware would block the user. Why?
    `Livewire::test()` mounts the component on a temporary test route that carries none of your page's middleware, and its simulated update requests run with middleware disabled, so persistent middleware has nothing to re-apply. The test proves the component's own logic; the page route's middleware needs an HTTP-level test.

saying these in an interview costs you the question

  • Livewire update requests run the page route's full middleware stack.
  • Livewire updates run no route middleware, so auth ends at page load.
  • Every custom middleware is persisted automatically once it is on the route.
  • addPersistentMiddleware adds the middleware to routes that lack it.
  • Persistent middleware makes authorization inside actions unnecessary.