Delete & Restore

The project has three ways to remove a user: soft delete, anonymize, and hard delete. Each serves a different purpose, and they're all already wired up. This page covers how they work together and how to filter and restore trashed records.

Three delete strategies

Soft delete marks the row with deleted_at and revokes tokens. The data stays intact and the account can be restored. This is what DELETE /account/profile and DELETE /admin/users/{id} both do.

Anonymize strips PII (name, email, password, avatar) but keeps the row. Foreign keys to other tables stay valid. This is the default prune strategy for accounts past the grace period.

Hard delete (purge) removes the avatar from S3, revokes tokens, and force-deletes the row. Use this when you genuinely want the data gone and don't have foreign key dependencies that would break.

Both anonymize() and purge() live on the User model. The scheduled users:prune-deleted command picks whichever strategy auth.delete.prune_strategy is set to. See Account Self Delete for the full prune and grace period setup.

Filtering trashed records

Admin listing endpoints need to show active users, trashed users, or both. The trashed query parameter controls this with two values:

?trashed=only shows only soft-deleted records. ?trashed=with shows everything, including trashed. Omitting the parameter shows only active records, which is Laravel's default behavior.

These values are backed by the TrashedFilter enum (app/Enums/TrashedFilter.php). Validation rejects anything that isn't only or with, so there's no ambiguity about what the client is asking for.

HasTrashedScope trait

The filtering logic lives in HasTrashedScope (app/Models/Concerns/HasTrashedScope.php), a reusable trait for any model that uses SoftDeletes. It adds a single scope:

scopeForTrashed(?TrashedFilter $trashed) maps the enum to onlyTrashed(), withTrashed(), or no-ops on null. Controllers call it unconditionally without checking whether the parameter was provided.

This follows the same nullable no-op pattern as other filter scopes in the project (forSearch, forRole). The controller reads like a flat list of filters with no conditional branching.

Restore

Restoration happens implicitly through login. When a soft-deleted user logs in during the grace period, the account is restored automatically. There's no dedicated restore endpoint. See Login and Token Lifecycle for how withTrashed() is used in the login flow.

For admin-initiated restores, the endpoint doesn't exist yet. When it does, it would follow the same pattern: find the trashed user, check business rules, call restore().

When to use what

A user deleting their own account gets soft delete with a grace period. They can undo it by logging in. After the grace period, the prune command applies whichever strategy you configured.

An admin deleting a user is also a soft delete. The difference is the admin could eventually have a restore endpoint to undo it explicitly, rather than relying on the user to log in.

Hard delete and anonymize are backend operations, not API actions. They run through the prune command or could be triggered by a future admin action, but they're not exposed as user-facing endpoints.

© Websanova 2026 About Privacy