muazzambuilds/filament-recycle-bin
Add a recycle bin to your Filament resources. Deleted records move to a dedicated bin where you can browse, restore, or permanently delete them, with bulk actions and a familiar, native Filament interface for safer data management.
What it is: a single Filament v5 panel page that aggregates every soft-deletable model you register, so admins restore or hard-delete trashed rows from one place instead of hunting through each Resource's TrashedFilter.
Prerequisites: Filament v5 (this is not a v3/v4 package), Laravel with SoftDeletes on the models you want to expose, and a working panel provider.
Minimal setup:
composer require muazzambuilds/filament-recycle-bin
php artisan vendor:publish --tag="filament-recycle-bin-config" # verify tag in README
Register the plugin on the panel you want the bin to live in:
// app/Providers/Filament/AdminPanelProvider.php
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(\MuazzamBuilds\FilamentRecycleBin\RecycleBinPlugin::make());
}
Then declare which models go in the bin — typically in config/recycle-bin.php or via a fluent plugin method:
->plugin(
RecycleBinPlugin::make()
->models([
\App\Models\Invoice::class,
\App\Models\Customer::class,
])
)
Where to look first, in order:
config/recycle-bin.php — the model registry, navigation label/group, and any per-model column or label overrides live here.RecycleBinPlugin) — confirms whether options are config-driven or fluent.First use case: register one model, delete a record from its Resource, open the Recycle Bin page in the panel, hit Restore. That single loop validates the whole install; if the row doesn't appear, the problem is almost always a missing SoftDeletes trait or a model not in the registry.
Start narrow, then widen. Register one or two high-value models (invoices, orders, CMS pages) rather than every soft-deletable table on day one. A central bin with fifteen model types is a UI problem before it's a technical one — add grouping/labels per model as you go.
Use it as the recovery surface, keep Resource-level filters for context. Filament's own TrashedFilter is still the right tool when an editor is inside a Resource and wants to see deleted siblings. The recycle bin is the cross-model, admin-level surface. Don't try to remove one in favour of the other.
Authorization is the whole game here. Restore is usually safe; forceDelete is not. Gate them separately — a restore ability for support staff, forceDelete for a small admin role — via policies on each model or a single gate checked in the page. If the package doesn't ship granular permissions, wrap the page's mount/actions yourself.
Multi-tenant panels. The single most important integration check: confirm the bin's query respects your tenancy scope. If you use Filament's tenancy or a global scope on tenant_id, verify a tenant admin cannot see another tenant's trashed rows. If the package queries withTrashed() without the scope, override the page's table query:
protected function getTableQuery(): Builder
{
return parent::getTableQuery()
->where('tenant_id', filament()->getTenant()?->id);
}
Bulk operations. Restore/force-delete in bulk is where this page earns its keep during incident cleanup. Pair bulk force-delete with a confirmation modal and, ideally, an audit log entry (model observer or a ForceDeleting listener) since the operation leaves no trace.
Audit trail pattern. Hook model events rather than the UI — a deleted/restored/forceDeleted observer on registered models gives you a durable record regardless of whether the action came from the bin, a Resource, or a tinker session.
Programmatic reuse. The restore/delete logic is useful outside the UI (support scripts, queued cleanup of old trashed records). Check whether the package exposes a service or action class; if not, Model::onlyTrashed()->find($id)->restore() is the fallback and is fine.
Scheduled purge. Combine the bin with a retention policy: a nightly command that force-deletes trashed records older than N days per model. The bin then becomes the "last N days" safety net rather than an ever-growing table.
Testing. Feature-test the page with livewire(RecycleBinPage::class)->call('restore', $record->id) style assertions, and separately test that a non-privileged user cannot reach the page or trigger force-delete. Cover the tenancy case explicitly if you're multi-tenant.
Missing SoftDeletes = silent no-show. The bin only sees models using the trait. If a model "isn't in the bin," check the trait, then the registry, then whether the config is cached (php artisan config:clear).
It's Filament v5 only. Composer will happily leave you on v4 with an incompatible constraint if you're mid-upgrade. Check composer why-not if the page renders blank or the plugin class is missing.
Force delete bypasses your normal safety rails. No deleting observer will save you the way it might on a soft delete — the row is gone. Restrict the ability, add a typed confirmation, and consider a "copy row to archive table" listener before forceDelete for anything regulated.
Early-stage package — pin and read the source. At ~11 stars and zero dependents, this is not battle-tested. Pin an exact version in composer.json, read the page class before relying on tenancy or authorization behaviour, and expect breaking changes between minor releases. There's also no third-party security review; treat it as you would your own code.
Timezone drift on deleted_at. The "deleted 2 hours ago" column and any retention purge both hinge on deleted_at. If your app timezone differs from the DB's, off-by-hours sorting looks like a bug but isn't.
Large trashed tables. withTrashed() across many models with no index on deleted_at gets slow fast. Ensure deleted_at is indexed on every registered table, and watch for N+1 on relation-heavy tables in the bin view — the cross-model table is exactly where eager loading gets forgotten.
Relation cascades. Restoring a parent does not restore children you soft-deleted alongside it, and force-deleting a parent may cascade at the DB level. Decide explicitly per model and document it, or you'll get half-restored object graphs.
Navigation clutter. The page registers into your panel navigation by default. Set the group, sort, and label in config, or hide it and link from a dashboard widget if the panel is already dense.
Extension points worth knowing: the page class (table columns, query scoping), the plugin's fluent config, and per-model labels/columns. If the package doesn't expose what you need, subclassing the page is the cleanest escape hatch — much better than forking.
How can I help you explore Laravel packages today?