Skip to main content

Soft delete

Soft delete marks rows as deleted by setting a column, instead of removing them from the table. Reads hide soft-deleted rows by default; pass ?withDeleted=true to include them.

Configuration

useResource(postsTable, {
id: postsTable.id,
db,
softDelete: {
field: postsTable.deletedAt, // column used as the deletion marker
// deletedValue: () => Date.now(), // optional; override the auto-detected marker
},
});

A row is considered deleted when the field column is non-null. Pass the Drizzle column (like id); a string column name also works but is deprecated.

Behavior

  • DELETE /:id sets the marker column instead of removing the row, and still returns 204.
  • Subscribers receive a removed event, because the row leaves the (not-deleted) read scope.
  • GET /, GET /:id, GET /count, GET /aggregate, and subscription snapshots exclude soft-deleted rows.
  • Pass ?withDeleted=true to include soft-deleted rows in any of those reads.
  • Re-deleting an already soft-deleted row returns 404 — it is invisible to the delete scope.
DELETE /api/posts/p1 # sets deletedAt, returns 204
GET /api/posts # p1 is hidden
GET /api/posts?withDeleted=true # p1 is included

Marker values

The default marker matches the column's storage type: a Date for date/timestamp columns (e.g. integer(..., { mode: "timestamp" }) or a Postgres timestamp) and an ISO 8601 string for text columns. This avoids the value.getTime is not a function driver error you'd get from handing a timestamp column a string.

Override with deletedValue for any other representation. The column counts as "deleted" whenever it is non-null.

softDelete: { field: postsTable.isDeleted, deletedValue: () => 1 } // integer flag
softDelete: { field: postsTable.deletedAt, deletedValue: () => Date.now() } // numeric epoch

Restoring

There is no dedicated restore endpoint — clear the marker with an update (the row must be reachable via ?withDeleted=true and your update scope):

PATCH /api/posts/p1
{ "deletedAt": null }