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.
Architecture fit. The package is a Filament panel plugin, not a domain-layer library: it registers a single panel page that aggregates trashed records across a configured set of Eloquent models and exposes restore / force-delete actions from one screen. Architecturally it sits entirely in the presentation layer, which is the right place — it imposes no runtime footprint on the request path outside the panel, and it requires no changes to existing models beyond their already using SoftDeletes.
The design is registry-based (a config list of model classes) rather than trait-discovery. That is a defensible trade-off — predictable, explicit, no reflection magic — but it introduces a silent-gap failure mode: any soft-deletable model not added to the config is invisible in the bin, so the page's usefulness degrades quietly as the schema evolves. Assume a coverage test is needed regardless of what the package ships.
The genuine value proposition is the centralized cross-model view. Filament resources already offer a per-resource trashedFilter; what they don't offer is one place for a support analyst or data steward to answer "where did this record go, across the whole app?" That's an ops-workflow win, not a developer-ergonomics win — size the business case accordingly.
Integration feasibility. Low for the happy path, and gated hard on the unhappy ones.
composer require, register the plugin in the PanelProvider, publish the config, enumerate models, wire a page gate.SoftDeletes, adding deleted_at to large tables is a separate migration project (online DDL, index build cost), not a plugin install.Technical risk (ranked).
| # | Risk | Severity | Why |
|---|---|---|---|
| 1 | Multi-tenancy scoping leakage | Critical | If the aggregate query doesn't apply Filament::getTenant() and model global scopes, an admin in tenant A sees/deletes tenant B's records |
| 2 | Policy bypass on force delete | Critical | Destructive and irreversible; must delegate to per-model forceDelete/restore policies, not a single page gate |
| 3 | Maintainer / bus factor | High | Single author, pre-1.0, tracks a moving Filament major, no security-response process |
| 4 | Supply chain | High | Unaudited third-party code with hard-delete powers in an admin panel |
| 5 | Heterogeneous schema handling | Medium | Assumed title columns, cross-model search/sort over disjoint columns; may throw or render broken |
| 6 | Restore / force-delete integrity | Medium | Restore can violate a unique index; force delete can hit FK constraints or orphan polymorphic media |
| 7 | Query performance at scale | Medium | Cross-model listing needs deleted_at indexes and real pagination; no built-in retention means unbounded growth |
Key questions to answer before commitment.
UNION? What happens at 100k trashed rows?forceDeleting so existing observers handle file/relation cleanup?Recommendation: conditional spike, 1–2 dev-days. Do not take a production dependency until tenancy scoping and policy delegation are verified by source review and a test against our own models. Budget for a vendored fork from the outset.
Stack fit. Laravel 11/12 + Filament v5 panel + PHP 8.2+. It occupies the admin-panel layer only. No impact on domain code, jobs, or API surface, and no effect on any consumer that doesn't render the panel page — which makes it cheap to feature-flag and cheap to remove.
Migration path. Three distinct starting positions:
SoftDeletes in place: direct install. Publish config, list models, register the page in the panel's navigation group, restrict visibility behind a permission.trashedFilter plus a manual admin runbook.deleted_at + index migration per table, online DDL for large tables, then opt models in one at a time behind the registry.Compatibility. Verify the package's Composer constraints against our locked Filament/Livewire versions before anything else. Then check for interference with: Filament's built-in tenancy, any audit-logging package (force deletes must land in the audit trail, not bypass it), and other plugins that add panel pages or override table behavior. Theme/navigation collisions are cosmetic but will generate support tickets.
Sequencing.
SoftDeletes, its policy, and its tenancy posture. (Day 1)deleted_at index verification and a scheduled purge job before rollout completes.Maintenance. The dominant recurring cost is tracking Filament majors. Pin exact versions, watch releases, and budget ~0.5–1 day per Filament minor for regression verification if forked. Registry upkeep is the quiet tax: add a test asserting every model using SoftDeletes appears in the config, otherwise the bin silently loses coverage over time.
Support. No vendor, no community, no dependents to lean on. This requires named internal ownership: a code owner, a fork living in our VCS, and a short runbook. Expect two recurring ticket classes — "why can't I see the deleted record for X" (registry gap) and "restore failed" (unique-constraint collision).
Scaling. Trash grows monotonically unless something purges it. Required before production: a deleted_at index on each soft-deletable table, verified pagination behavior, and a scheduled retention job (e.g. force-delete older than 90 days) that writes to the audit log. The cross-model listing is the bottleneck; if p95 page load exceeds ~2s, split the page by model group or move bulk export async.
Failure modes.
forceDeleting observers run, plus a periodic cleanup job.Ramp-up. Developers: under an hour — it's a conventional Filament plugin with a familiar registration pattern. Support/ops users: ~30 minutes, but pair it with a written destructive-action policy (confirmation step, and ideally a second approver for force delete). SRE needs to know two things: where the retention job lives and where the audit trail is. Learning cost is negligible; the real cost is governance, and that cost is entirely on us, not the package.
How can I help you explore Laravel packages today?