marcelweidum/filament-expiration-notice
Filament plugin that adds configurable expiration notices to your Laravel admin panels, alerting admins before records expire or after they lapse. Display expiry-aware banners and badges on resources and tables with minimal setup.
1. Install the package
composer require marcelweidum/filament-expiration-notice
2. Publish what you need
Most Filament plugins ship publishable config/views/translations. Publish the config first, then only publish views if you intend to restyle:
php artisan vendor:publish --tag="filament-expiration-notice-config"
# optional, only if you need to override markup:
php artisan vendor:publish --tag="filament-expiration-notice-views"
php artisan vendor:publish --tag="filament-expiration-notice-translations"
3. Register the plugin on your panel
This is a panel-level plugin, so it belongs in your PanelProvider, not in AppServiceProvider:
use MarcelWeidum\FilamentExpirationNotice\FilamentExpirationNoticePlugin;
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
// ...
->plugins([
FilamentExpirationNoticePlugin::make(),
]);
}
4. Where to look first
config/filament-expiration-notice.php (once published) — this is where the enable flag, title/body copy, button label, and redirect behaviour typically live.resources/views directory — the modal is a Blade view, so anything not exposed in config is overridable there.config/session.php — the modal only ever appears because SESSION_LIFETIME elapsed, so tune that first if the notice feels "too aggressive" in testing.First use case: you have a Filament admin where users leave a tab open over lunch, come back, click "Save", and get Livewire's raw grey "Page Expired" overlay (or a bare 419 page). Registering the plugin replaces that with a Filament-styled modal that tells the user what happened and gives them a single action button to get back to work.
Default registration (most apps stop here)
->plugins([
FilamentExpirationNoticePlugin::make(),
])
Use this for the whole panel and move on. It is a UX-only plugin; there is no data model, no migration, no API surface to design around.
Configure the copy centrally
Session-expiry copy is one of those strings that product/legal actually cares about (data loss warnings, "your unsaved changes are gone"). Keep it in config or translations rather than hardcoding, so a copy change is not a deploy:
// config/filament-expiration-notice.php (names vary by version — read the published file)
return [
'enabled' => true,
'title' => 'Your session has expired',
'body' => 'For security, we signed you out after a period of inactivity.',
'button' => 'Sign in again',
];
If your app is multilingual, prefer the translation route: override the package's lang file per locale in lang/vendor/filament-expiration-notice/{locale}/... and keep config to booleans/URLs.
Pair it with an intentional redirect
A reload of an expired page usually bounces the user to the login screen anyway — but only after a second failed request. Point the modal's action at the login route (or a ->url()) so the flow is one click, not two:
FilamentExpirationNoticePlugin::make()
->redirectTo(route('filament.admin.auth.login'))
The exact fluent method depends on the installed version; if it isn't there, the view override is the fallback (see Gotchas).
Per-panel behaviour
Because it's a plugin, panels are independent. A customer-facing panel might want a soft "You were away, please refresh" message with a reload button, while the internal admin panel wants "Session expired — sign in again" pointing at the logout route. Register it twice with different config, or use ->plugin() conditionally:
->plugins(array_filter([
$panel->getId() === 'admin' ? FilamentExpirationNoticePlugin::make() : null,
]))
Testing session expiry locally without waiting an hour
You cannot reasonably wait out SESSION_LIFETIME (default 120 minutes). Fast loop:
SESSION_LIFETIME=1 in .env (minutes) and php artisan config:clear.If you want to see it instantly, clear the session cookie from devtools before triggering an action — same code path, no waiting.
Verify you didn't break the normal 419 path
Keep a manual test that hits a route with an invalid CSRF token outside Livewire. The plugin only replaces the Livewire page-expired UX; your resources/views/errors/419.blade.php should still exist and look sane for full-page requests.
You probably already have Livewire's default modal — make sure you're replacing, not stacking. Livewire ships its own "Page Expired" dialog. If both render, users see two overlays or a half-styled one behind the other. After registering the plugin, confirm only one modal appears; if two do, check for a stale published Livewire asset or a custom JS listener on the page-expired browser event.
SESSION_LIFETIME is in minutes, session.expire_on_close is separate. A surprising number of "the plugin doesn't fire" reports are really "the user's browser is restoring the tab from bfcache". When a tab is restored, Livewire may not make a request at all until the user interacts — which is correct behaviour, but it means the modal appears on the next click, not on return. Set expectations with stakeholders accordingly.
Long-running workers and Octane. If you run Octane or a queue-heavy setup, remember session expiry is driven by the cookie + server-side store, not by PHP process lifetime. Nothing in this plugin changes that, but don't "fix" expiry issues by touching Octane config — check config/session.php and your cookie domain (a mismatch between SESSION_DOMAIN and the panel host makes every request look expired, and the modal will fire constantly).
Multiple tabs = logout storm. Logging in on tab B regenerates the session ID; tab A's next Livewire request fails. The modal will show on tab A and its button will take the user to login — where they're already logged in on tab B. Consider wording the button as "Reload" rather than "Sign in again" in apps where users commonly work in multiple tabs.
Don't rely on it for actual auth protection. This is presentation. Authorisation still lives in policies, auth middleware, and Filament::auth(). A pretty modal does not prevent a stale page from showing cached data — if you render anything sensitive, ensure it's re-fetched server-side on interaction (Filament resources do this by default; custom widgets that cache in Alpine state do not).
Cache the config after every change. php artisan config:cache is standard, but it's the #1 reason "I changed the title and nothing happened" in production. Same for translations if you run php artisan optimize.
Extension points, in order of preference:
resources/views after every minor bump.Log::info('session expired', ['user' => auth()->id()])) or fire analytics. Listen for Livewire's page-expired browser event in a small JS snippet in your panel's renderHook, rather than forking the package.Upgrade hygiene. At 95 stars and a small surface area, this is a "read the diff" dependency, not a "trust the semver" one. Pin it ("marcelweidum/filament-expiration-notice": "^1.0") and check the release notes against your Filament major version — Filament v3→v4 style bumps are where panel plugins break, typically on the plugin contract or the modal component namespace.
If the modal never appears at all, work down this list: is the plugin registered on this panel? is enabled true in the cached config? does your custom panel layout still include Filament's @livewireScripts/asset stack (removing it kills Livewire's error handling entirely)? and finally, is the failure actually a 419, or a 403/500 that just looks similar in the console?
How can I help you explore Laravel packages today?