Make Admin Deletion Recoverable Without Rewriting History

Deleting a row from an admin screen can mean several different things. It might remove clutter from a list, stop someone signing in, or erase data permanently. Those operations shouldn’t accidentally share the same implementation.
In the record-management work for AdzByte App, I added recoverable deletion for supported business records while retaining their original rows. The design keeps visibility state separate from the record’s business status. That choice matters for orders, payments, support conversations, and the history connected to them.
Keep the original row and mark its visibility separately
The implementation records deletion state in a separate table, identified by record type and record ID. A marker holds the deletion time, actor, and version. A separate event records each actual delete or restore transition.
This lets an order retain its financial status and a support case retain its description and revision. The delete action changes how supported screens find the record; it doesn’t invent a new payment result or rewrite an earlier support message.
The first regression test compares the record’s attributes before deletion, after deletion, and after restoration. They must match. It also checks that repeating the same deletion doesn’t create another lifecycle event. That’s a stronger statement than simply asserting the row still exists.
This isn’t a drop-in use of Eloquent’s global soft-deletion scope. Presentation queries explicitly use the visibility service. That keeps financial processing able to work with retained records, but it also means every relevant read path needs deliberate coverage.
Define which parents can hide a child
A record’s own marker isn’t the whole visibility rule. A support message belongs to a case, and a case can belong to a project. If the project is hidden, showing its linked case as an ordinary active record would make the screen inconsistent.
A closed registry lists the supported models and their parent relationships. The visibility query walks those relationships to decide whether a record is active. The registry also lists fields that the management screen may display, keeping credentials and queue internals outside that generic view.
The distinction between direct deletion and inherited hiding is important during recovery. A child hidden only because its parent was deleted can become visible when the parent returns. A child that was separately deleted should stay deleted.
The tests exercise that exact sequence: delete a milestone on its own, delete the owning project, then restore the project. The case becomes visible again while the independently deleted milestone stays hidden. Recovery respects both decisions.
Make restoration explain the dependency
Trying to restore a child while its parent remains deleted produces a validation message. The management detail shows that the owning record is responsible and links to the parent that needs attention. The user doesn’t have to guess why Restore appears ineffective.
That behavior is useful technically too. Automatically restoring the whole ancestry would turn a small action into a broader state change. Rejecting the action with a precise dependency keeps its effect reviewable.
For a different data model, I’d start by drawing the relationships that affect visibility. Don’t assume every foreign key should participate. A historical actor reference, for example, may need to remain readable even when that person’s account is no longer active.
The registry is an explicit application choice. Adding another model or relationship should include examples of how deletion and restoration behave from both ends.
Bind confirmation to the record and its version
The Filament action includes the selected record’s type, ID, and visibility version in the confirmation data. Before applying a change, it checks that the confirmation still refers to the same target. The service also checks the expected version when making a new transition.
One test changes the submitted record ID after opening confirmation. Neither record should be deleted. Another tries a stale version after a delete-and-restore sequence and expects rejection.
Those tests cover different mistakes. A changed target is about what the person confirmed. A stale version is about whether the record’s state changed while that confirmation was open. A confirmation modal alone doesn’t answer either question.
The service reloads and locks relevant rows inside the operation. The concurrency tests then run separate processes against shared test data, including repeated deletion of the same target. The assertions check the surviving markers and events rather than just whether each process returned.
Keep retained identities from signing in
A deleted user can’t keep using the portal merely because their row remains available for recovery. The change adds access checks for existing sessions and bearer-token requests, alongside restrictions on later edits. Retained storage and active identity are different decisions.
Keep financial processing separate
Orders and payments have a different concern. Their underlying records stay available for the processing paths that need them. The confirmation explicitly explains that deleting the visible record doesn’t cancel, refund, or stop provider processing.
History models reject ordinary model updates and deletes through their event hooks. That’s useful protection in the application’s normal code path, but it isn’t a claim that a privileged database operator cannot change history. Keep the word immutable tied to the mechanism actually in use.
Test recovery as a sequence
The useful test isn’t just delete, then restore. Include parent deletion, an independently deleted child, a stale confirmation, a repeated action, and an existing signed-in identity. Compare the original business attributes as well as the active list.
The later qualification record describes passing source tests and targeted browser recovery checks for this work. It also says permanent purge wasn’t implemented. This feature retains information for recovery; it isn’t anonymization or erasure.
That leaves the implementation with a clear job: change supported visibility safely, preserve the original business facts, and give restoration enough context to reverse the intended action. Keeping those responsibilities explicit makes deletion easier to reason about when records are connected.
Written by
Adrian Saycon
A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.



