Skip to main content

Soft Deletes and Restore

Use this page when records should be recoverable instead of permanently removed.

When delete() is soft

delete() behaves as a soft delete when the model has a deleted_at soft-delete field in its schema. Valid schema patterns:
deleted_at: column("softDeletes")
For PostgreSQL-focused apps, prefer the explicit timezone-aware form:
deleted_at: column("softDeletes", undefined, { useTz: true })
or:
softDeletes: mixin("SoftDeletes")
Do not define both at once.

What soft delete does

Soft delete updates deleted_at instead of permanently removing the record. Typical service call:
const user = await User.find(1);
if (user) {
  await user.delete();
}

How restore() works

restore() clears deleted_at and makes the record visible again to normal reads.
const user = await User.find(1);
if (user) {
  await user.restore();
}

Hard delete after soft delete

Use forceDelete() only when the record should be permanently removed:
const model = new User() as User & { forceDelete?: (id: number | string) => Promise<void> };
await model.forceDelete?.(1);
After forceDelete(), do not assume restore() can recover the record.

Querying deleted records

const model = new User() as User & {
  withTrashed?: () => Promise<unknown[]>;
  onlyTrashed?: () => Promise<unknown[]>;
};

const allRows = await model.withTrashed?.();
const deletedRows = await model.onlyTrashed?.();
  • controllers expose restore routes when recovery is part of the API
  • services own restore, onlyTrashed, withTrashed, and forceDelete
  • models define the schema requirement

Safe usage notes

  • use soft delete for recoverable business entities
  • use forceDelete only in admin or cleanup paths
  • do not document restore as guaranteed after force delete
  • keep restore paths covered in tests