shuvroroy/filament-spatie-laravel-backup
A Filament page for creating and managing application backups using spatie/laravel-backup. Register the plugin in your panel provider, publish assets, and get a ready-made Backups page with customizable icon and heading.
The package is a thin Filament page on top of spatie/laravel-backup, so the day-one job is really "get Spatie working, then bolt on the UI".
1. Install and publish assets
composer require shuvroroy/filament-spatie-laravel-backup
php artisan filament:assets
The filament:assets step is easy to forget and is the usual cause of a page that renders but looks broken after an upgrade.
2. Make sure spatie/laravel-backup is actually configured
The plugin respects your config/backup.php, so publish and review it before touching the panel:
php artisan vendor:publish --provider="Spatie\Backup\BackupServiceProvider"
At minimum set backup.destination.disks (e.g. ['backups'] pointing at S3 or a local disk outside the webroot), and sanity-check backup.source.files.include/exclude and backup.source.databases. Run one from the CLI first to prove credentials and paths:
php artisan backup:run
php artisan backup:list
If backup:list is empty, the UI will be empty too — debug there, not in Filament.
3. Register the plugin in the panel
use ShuvroRoy\FilamentSpatieLaravelBackup\FilamentSpatieLaravelBackupPlugin;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(
FilamentSpatieLaravelBackupPlugin::make()
->navigationGroup('Settings')
->navigationSort(99)
->authorize(fn (): bool => auth()->user()->hasRole('admin'))
);
}
First real use case: give operators a "take a backup before I deploy / run this migration" button instead of a terminal. That is the moment this package pays for itself — the page is a controlled, permissioned trigger for backup:run plus a list of what already exists on disk.
Where to look first when something is off: config/backup.php (Spatie), then the panel provider (plugin options), then ShuvroRoy\FilamentSpatieLaravelBackup\Pages\Backups (the page class you extend for anything UI-level).
Lock the page down before you ship it. The authorize() callback on the plugin is the coarse gate for who can even see the page:
FilamentSpatieLaravelBackupPlugin::make()
->authorize(fn (): bool => auth()->user()->can('view_backups'))
Then handle the fine-grained actions. The page checks three ability names — create-backup, download-backup, delete-backup — so with spatie/laravel-permission or Filament Shield you must create them yourself; they are not auto-discovered by Shield's policy generation:
foreach (['create-backup', 'download-backup', 'delete-backup'] as $name) {
Permission::firstOrCreate(['name' => $name, 'guard_name' => 'web']);
}
This split is genuinely useful: an on-call role gets create-backup + download-backup but not delete-backup, so nobody can wipe the retention window from the UI.
Make backups asynchronous in production. Synchronous backups in a web request are how you get 504s and half-written zips:
FilamentSpatieLaravelBackupPlugin::make()
->usingQueue('backups')
->usingPolingInterval('10s')
->timeout(600)
Run a dedicated worker (php artisan queue:work --queue=backups --timeout=900). The trade-off is UX: the modal closes immediately and the archive appears in the list a minute later, so the polling interval is doing real work here — don't leave it at 4s against an S3 disk.
Extend the page class when you need more than navigation. Plugin setters cover icon/label/group/sort, but for anything structural (extra table columns, a custom header action, your own empty-state copy) subclass it and register via usingPage():
class Backups extends BaseBackups
{
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-circle-stack';
public function getHeading(): string | Htmlable
{
return 'Application Backups';
}
public static function getNavigationGroup(): ?string
{
return 'Core';
}
}
// AdminPanelProvider
FilamentSpatieLaravelBackupPlugin::make()->usingPage(Backups::class);
Keep the override in App\Filament\Pages so Composer updates never fight you.
Pair scheduled backups with the UI. Let Spatie's scheduler own the nightly cadence (backup:run then backup:clean then backup:monitor), and treat the Filament page as the on-demand + inspection surface. That keeps the backup list meaningful (retention stays predictable) while still giving humans a button when they need one.
Environment-scoped registration. In local you generally don't want the page at all:
if (! app()->isLocal()) {
$panel->plugin(FilamentSpatieLaravelBackupPlugin::make());
}
Or keep it and gate with authorize(fn () => auth()->user()->isAdmin()).
Localisation. Publish translations and override the keys per panel language:
php artisan vendor:publish --tag="filament-spatie-backup-translations"
Multi-panel / multi-tenant note. Each panel gets its own plugin instance, so an ops panel and a customer panel can use different pages, groups and authorization — but they all read and write the same disks defined in config/backup.php. If you need per-tenant backup isolation, that's a Spatie config concern (per-tenant disks / backup.backup.name), not something the plugin does for you.
usingPolingInterval() is spelled with one L. That's the actual method name in the package; typing usingPollingInterval gives an undefined-method error. Rely on IDE completion rather than memory.spatie/laravel-permission installed, can('create-backup') returns false for a permission that doesn't exist, so the buttons just vanish with no error. Seed the three permissions before users complain.usingQueue(), the job sits in the jobs table forever and the page shows nothing new. Failed jobs also die quietly — watch failed_jobs and Horizon/logs, not the Filament page.timeout() is not the queue worker's timeout. It maps to set_time_limit, which is a no-op under CLI and often disabled on shared hosting. Set --timeout on the worker and retry_after in config/queue.php higher than your timeout(), otherwise the job is retried mid-run and you get duplicate archives. noTimeout() is the honest option for large databases.backups table. An empty or stale list almost always means the disk in config/backup.php doesn't match what the UI is configured against, or credentials lack ListBucket. There is no error surface for this.wire:poll on a page that lists an S3 prefix generates a LIST request every 4 seconds per open tab. Bump the interval to 10s/30s for remote disks; it also reduces latency complaints.memory_limit/max_execution_time or proxy body-size limits. For big restores, prefer grabbing the signed URL / copying from the bucket directly, and keep the download button for small DB-only archives.gunzip → mysql < db-dump.sql → sync storage) and rehearse it, because a backup you've never restored is a hypothesis.backup.source.files.include/exclude including node_modules, vendor, logs or storage/app caches. Fixing that in Spatie's config is far cheaper than raising disk quotas.backup:monitor to the schedule (daily), with a notification channel configured. The UI tells you what exists; backup:monitor tells you when nothing new has appeared, which is the failure mode that actually hurts.backup.backup.name must be unique per app if disks are shared. Two apps writing to the same S3 prefix will show each other's archives in the same page.UPGRADE.md, and re-run php artisan filament:assets afterwards.Pages\Backups for UI → config/backup.php for what actually gets backed up. Reaching for a full custom page is almost never necessary.How can I help you explore Laravel packages today?