---
title: "Trash — Soft Delete"
aliases: [soft-delete, trash, deletedAt, restore-documents]
tags: [payloadcms, collections, admin-panel, access-control, api]
sources: [raw/trash__overview.md]
created: 2026-05-15
updated: 2026-05-15
---

## Overview

Trash (soft delete) marks documents as deleted without permanently removing them. Enable it per collection with `trash: true`. Payload injects a `deletedAt` timestamp field automatically.

```ts
export const Posts: CollectionConfig = {
  slug: 'posts',
  trash: true,
  fields: [...],
}
```

## Admin Panel

- New route added: `/collections/:collectionSlug/trash`
- Trash view shows all documents with a `deletedAt` timestamp
- Bulk actions: **Restore**, **Delete** (permanent), **Empty Trash**
- Edit view of trashed document: all fields **read-only**; only Restore and Permanently Delete actions available
- API / Versions / Preview tabs remain accessible from trashed doc edit view
- Main list view delete → soft-deletes by default; checkbox in modal allows skipping trash

## API — `trash` Parameter

Applies to: `find`, `findByID`, `update`, `updateByID`, `delete`, `deleteByID`, `findVersions`, `findVersionByID`

| Goal | Query |
|------|-------|
| All documents (normal + trashed) | `trash: true` |
| Only trashed | `trash: true` + `where: { deletedAt: { exists: true } }` |
| Only non-trashed (explicit) | `trash: false` |

### Local API

```ts
// Only trashed
const result = await payload.find({
  collection: 'posts',
  trash: true,
  where: { deletedAt: { exists: true } },
})
```

### REST

```http
GET /api/posts?trash=true&where[deletedAt][exists]=true
```

### GraphQL

```graphql
query {
  Posts(trash: true, where: { deletedAt: { exists: true } }) {
    docs { id deletedAt }
  }
}
```

## Access Control

All trash actions (soft-delete, restore, permanent delete) respect the collection's `delete` access control.

The `delete` function receives `data`:
- **Trashing** → `data.deletedAt` is set to a timestamp
- **Permanent delete** → `data` is `undefined`

### Pattern: editors can trash, only admins can permanently delete

```ts
access: {
  delete: ({ req: { user }, data }) => {
    if (!user) return false
    if (user.roles?.includes('admin')) return true
    // Allow trash (deletedAt being set), deny permanent delete
    if (data?.deletedAt) return true
    return false
  },
},
```

In the Admin Panel, when a user can trash but not permanently delete:
- Delete button is visible
- "Delete permanently" checkbox is hidden

## Versions + Trash Interaction

- A trashed document **cannot** have a version restored until the document itself is restored from trash
- Attempting to restore a version of a trashed document throws an error
- Version history is still **visible** from the trashed document's edit view

## Key Takeaways

- Enable with `trash: true` on the [[wiki/payloadcms/collection-config|Collection Config]]
- `deletedAt` field is auto-injected; set on soft-delete, cleared on restore
- Default delete from list view = soft delete; checkbox overrides to permanent
- `trash: true` query param *includes* trashed docs — combine with `where: { deletedAt: { exists: true } }` to get *only* trashed
- [[wiki/payloadcms/access-control|Access Control]] `delete` function can distinguish trash vs permanent via `data.deletedAt`
- Must restore document from trash before restoring any of its versions
- Similar access control pattern to publish (`data._status`) — use `data` arg to differentiate operations

## Sources

- `raw/trash__overview.md` (https://payloadcms.com/docs/trash/overview)
