A soft delete marks a row as deleted while retaining it for recovery, audit, or a retention period. In a multi-tenant system, every delete, restore, lookup, unique constraint, and purge must remain scoped to the authenticated tenant.
Last updated: September 26, 2026.
UPDATE projects
SET deleted_at = CURRENT_TIMESTAMP,
deleted_by_user_id = :user_id
WHERE tenant_id = :tenant_id
AND project_id = :project_id
AND deleted_at IS NULL;The tenant ID comes from trusted authentication context. Checking deleted_at IS NULL makes repeated delete requests harmless and lets the application confirm whether an active row changed.
Hide deleted rows by default
Repositories and query builders should add tenant_id = ? AND deleted_at IS NULL centrally. Administrative screens can opt into deleted records explicitly. Avoid a global filter that background exports or security checks can accidentally disable without tenant scope.
Relationships need a policy too. Decide whether deleting a parent hides children, blocks the action, or starts an asynchronous cascade. Do not leave active child rows pointing at an invisible parent without defining application behavior.
Plan restore and uniqueness
A restored record can conflict with a newer active row that reused the same name or external key. Define whether active-only uniqueness is supported by the database, whether deleted values are renamed, or whether reuse is forbidden until purge. Restore through the same authorization and tenant-isolation checks as normal updates.
Separate retention from recovery
- Record the delete actor, time, reason, and request ID in the audit log.
- Keep recoverable data for a documented period.
- Run a tenant-aware purge job in bounded batches.
- Delete dependent files, search documents, and cache entries.
- Record purge completion without retaining prohibited personal data.
Soft delete is not legal erasure by itself. The purge workflow belongs in tenant-aware background processing, and sensitive administrative actions belong in SaaS audit logs.