Soft Deletion
Soft-deleted rows stay in the database with deleted_at set but are hidden from normal queries. This preserves history and foreign-key integrity while supporting restore and admin visibility.
The behavior lives on Base (see Database Models), so every model inherits it: deleted_at, soft_delete(), and restore().
How filtering works
A before_compile event listener (app/models/soft_delete_events.py) adds deleted_at IS NULL to every query automatically, so callers never append the predicate manually.
The listener applies the filter before LIMIT/OFFSET, which matters for pagination: when a query already has a limit applied, adding .filter() raises InvalidRequestError, so the listener falls back to modifying the query’s _where_criteria directly. Both count queries and paginated result sets therefore exclude deleted rows and report accurate totals.
Bypassing the filter
Three escape hatches disable filtering when you need deleted rows:
CRUD operations
crud_utils wraps deletion and restore. delete_item soft-deletes by default (and cascades to configured children — see Cascade Operations).
Recycle Bin API (superuser only)
app/routers/recycle.py exposes REST endpoints for managing deleted records:
Key files
| File | Purpose |
|---|---|
app/models/base.py | deleted_at column, soft_delete() / restore() |
app/models/soft_delete_events.py | before_compile auto-filter listener |
app/database.py | without_soft_delete_filter() context manager |
app/utils/crud_utils.py | soft/hard delete and restore helpers |
app/utils/model_utils.py | QueryBuilder with with_deleted() / only_deleted() |
app/routers/recycle.py | recycle-bin REST API |