Soft delete
Every schema gets soft-delete by default. DELETEs don’t remove the
row — they set a deletedAt: Date tombstone. Every list / get /
relation query filters tombstones out. The deleted record can be
restored by clearing the tombstone via a dedicated route, MCP tool,
or GraphQL mutation.
Default on, opt out
Section titled “Default on, opt out”module.exports = { path: 'account', softDelete: false, // hard-delete on DELETE; no `deletedAt` field fields: [/* ... */],};Without the explicit false, soft-delete is on. The loader adds
the deletedAt: Date field, the tombstone-filtering predicate, and
the restore endpoints.
What gets generated
Section titled “What gets generated”| Surface | Endpoint |
|---|---|
| REST | DELETE /api/v1/<path>/:id — sets deletedAt. |
| REST | POST /api/v1/<path>/:id/restore — clears deletedAt. |
| GraphQL | <path>RemoveById — soft-deletes. |
| GraphQL | <path>Restore — clears the tombstone. |
| MCP | delete_<path> — soft-deletes. |
| MCP | restore_<path> — clears. |
| Typed client | api.<resource>.delete(id) and api.<resource>.restore(id). |
Tombstone filter
Section titled “Tombstone filter”Every framework-level query (REST list, GET by id, GraphQL find,
MCP list / get, relation traversal) injects deletedAt: null.
Mongo’s null predicate matches both null and missing fields, so
the same query is correct against schemas where softDelete: false
(no deletedAt field on documents) and schemas where it’s enabled.
__includeDeleted
Section titled “__includeDeleted”To see tombstoned rows on a list:
GET /api/v1/account?__includeDeleted=trueThe same flag on get_<path> and the GraphQL findMany resolver
opts those reads into tombstones. Defaults to false — so the
common case (GET /:resource) never returns deleted rows.
Relations never honour __includeDeleted. A parent’s tombstoned
children stay invisible, regardless of the parent request. This
prevents a soft-deleted record from leaking through a relation.
Restore
Section titled “Restore”POST /api/v1/account/abc/restoreAuthorization: Bearer <token>Returns the now-restored record. Restore is symmetric with delete —
same audit row (action: 'restore'), same webhook event
(account.restored), same ACL bypass via acl.delete (the role
that can soft-delete can restore).
Hard delete (softDelete: false)
Section titled “Hard delete (softDelete: false)”When opted out:
deletedAtfield is not added.- DELETE removes the row.
- The
restoreroute, mutation, and MCP tool are absent. __includeDeletedis a no-op.
Use this for resources where there’s no business reason to recover deleted state (e.g. session tokens, idempotency rows that have their own TTL).
Retention: auto-purge tombstones
Section titled “Retention: auto-purge tombstones”If you want soft-deleted rows to eventually go away — say, GDPR deletion windows — opt in per-schema:
module.exports = { path: 'contact', softDelete: { retentionDays: 30 }, fields: [/* ... */],};Or globally via env:
SOFT_DELETE_RETENTION_DAYS=30A periodic sweep hard-deletes any tombstoned row older than the configured retention; matching file blobs are removed too. See Backup & retention.
Cross-tenant delete bypass
Section titled “Cross-tenant delete bypass”Some operators legitimately need to soft-delete records they don’t
own (admin staff cleaning up). Opt in via acl.delete:
acl: { delete: ['admin'] },Without the slot, DELETE is owner-only. With it, the listed roles
bypass the userId filter on DELETE — same posture as acl.list.
See ACL.
See also
Section titled “See also”- Schema file shape — top-level syntax.
- Audit log —
deleteandrestoreactions. - Backup & retention —
softDelete: { retentionDays }.