
# Soft Delete & Restore

Deleting a record is **non-destructive by default**: the row is marked deleted (`deletedAt`/`deletedBy` stamped) and disappears from normal queries, but remains recoverable. This protects against accidental deletion, supports a trash/undo workflow, and keeps an audit trail. Permanent (hard) delete is a separate, permission-gated operation for cases that demand irreversible erasure.

| Method & Path                                                  | Description                   |
| -------------------------------------------------------------- | ----------------------------- |
| `DELETE /api/tables/:tableId/records/:recordId`                | Soft delete a record          |
| `DELETE /api/tables/:tableId/records/:recordId?permanent=true` | Permanently delete a record   |
| `GET /api/tables/:tableId/trash`                               | Browse soft-deleted records   |
| `POST /api/tables/:tableId/records/:recordId/restore`          | Restore a soft-deleted record |
| `POST /api/tables/:tableId/records/batch/restore`              | Restore many records          |

## Soft delete

```
DELETE /api/tables/orders/records/123
```

This sets `deletedAt` to the current time and `deletedBy` to the acting user, then excludes the row from default list and read responses. The operation is logged to the record's [change history](record-history).

## Permanent (hard) delete

```
DELETE /api/tables/orders/records/123?permanent=true
```

Hard delete removes the row irreversibly and requires the `permanentDelete` permission. When force-delete is not allowed for a table, the endpoint returns `404` (anti-enumeration) rather than `403`.

:::callout
**Hard-delete is opt-in and admin-gated.** Reserve `?permanent=true` for genuine erasure needs — e.g. GDPR right-to-erasure on personal data. Financial ledgers and audit-adjacent tables should rely on soft delete only. Permanent delete is an irreversible operation: there is no restore.
:::

## Cascade behavior on related records

When a record is deleted, related records are handled per the relationship field's `onDelete` policy. Configure it on the [relational field](relational-fields).

```yaml
tables:
  - id: 1
    name: orders
    permissions:
      delete: ['admin', 'member']
      permanentDelete: ['admin'] # Permanent delete requires admin role
    fields:
      - id: 1
        name: customer_id
        type: relationship
        relatedTable: customers
        onDelete: cascade # cascade | set-null | restrict
```

| `onDelete` | Effect when the parent is deleted             |
| ---------- | --------------------------------------------- |
| `cascade`  | Delete the dependent (child) records too      |
| `set-null` | Clear the foreign-key reference on dependents |
| `restrict` | Block the delete while dependents exist       |

## Trash view

`GET /api/tables/:tableId/trash` lists soft-deleted records so a UI can present a recoverable bin. The same result is available on the list endpoint via `includeDeleted=only`.

## Restore

```
POST /api/tables/orders/records/123/restore
```

Restore clears `deletedAt` (returning the row to normal queries), stamps a `restored_at`, captures the restoring user, and logs the operation to history.

```json
{
  "id": 123,
  "deleted_at": null,
  "restored_at": "2025-01-15T11:00:00Z"
}
```

| Status             | Meaning                         |
| ------------------ | ------------------------------- |
| `200 OK`           | Record restored                 |
| `400 Bad Request`  | Record is not currently deleted |
| `401 Unauthorized` | No active session               |
| `404 Not Found`    | Record absent or not visible    |

### Batch restore

Recover many records in one transaction. Records that are not currently deleted are skipped; a missing `id` rolls the whole batch back (`404`). See [Batch Operations](records-batch).

```json
POST /api/tables/orders/records/batch/restore
{
  "ids": ["123", "124", "125"]
}
```

## Querying deleted records

By default, soft-deleted rows are excluded from list and read responses. Override with the `includeDeleted` query parameter:

| Value       | Behavior                                                 |
| ----------- | -------------------------------------------------------- |
| _(omitted)_ | Active records only (default)                            |
| `true`      | Active **and** deleted records                           |
| `only`      | Soft-deleted records only (equivalent to the trash view) |

```
GET /api/tables/orders/records?includeDeleted=true
GET /api/tables/orders/records?includeDeleted=only
```

## Permissions summary

| Operation        | Permission                                                                      |
| ---------------- | ------------------------------------------------------------------------------- |
| Soft delete      | `delete` (per [table permissions](table-permissions) + [RBAC](auth-roles-rbac)) |
| Permanent delete | `permanentDelete` (typically admin only)                                        |
| Restore          | `delete` / restore grant for the role                                           |

## Related pages

- [Records Overview](records-overview) — `deletedBy`/`deletedAt` authorship
- [CRUD & Upsert](records-crud) — the single-record delete endpoint
- [Batch Operations](records-batch) — batch delete and restore
- [Relational Fields](relational-fields) — `onDelete` cascade configuration
- [Record History & Comments](record-history) — deletes and restores are logged
