Delete Entities
How entity deletion works in Arke -- soft deletion via tombstoning.
Soft Deletion
Arke doesn't permanently delete entities. Instead, entities are tombstoned -- a new version is created with a _tombstone property containing deletion metadata, while all other properties and relationships are cleared.
DELETE /entities/01KFNR849AZNBWE9DYJRZR7PSA
Authorization: Bearer <token>
Content-Type: application/json
{
"expect_tip": "bafyreif4...",
"reason": "Duplicate entry"
}Request Body
| Field | Type | Required | Description |
|---|---|---|---|
expect_tip | string | Yes | Current CID for optimistic concurrency (CAS) |
reason | string | No | Reason for deletion (max 500 chars) |
note | string | No | Version note for audit trail |
Response (200 OK)
{
"id": "01KFNR849AZNBWE9DYJRZR7PSA",
"cid": "bafyreig7...",
"deleted_at": "2025-01-15T10:30:00.000Z",
"ver": 3,
"prev_cid": "bafyreif4..."
}What Happens
- A new version is created with
properties: { _tombstone: { deleted_at, deleted_by, reason?, original_ver } } - All other properties are cleared
- All relationships are cleared
- The entity's type remains unchanged
- Previous versions remain accessible via the
prevlink chain - The entity is removed from search indexes
Tombstone Structure
The tombstone version contains only:
{
"properties": {
"_tombstone": {
"deleted_at": "2025-01-15T10:30:00.000Z",
"deleted_by": "01KUSER123...",
"reason": "Duplicate entry",
"original_ver": 2
}
},
"relationships": []
}Error Responses
| Status | Condition |
|---|---|
| 400 | Entity is already deleted |
| 404 | Entity not found |
| 409 | CAS conflict (tip changed since you fetched it) |
Permissions
Deleting an entity requires the entity:delete action, which is available to admin and owner roles in the entity's collection. For collections specifically, collection:delete permission is required.
Restoring Deleted Entities
Deleted entities can be restored via POST /entities/:id/restore. This walks the version chain to find the last non-deleted version and creates a new version with that content.
POST /entities/01KFNR849AZNBWE9DYJRZR7PSA/restore
Authorization: Bearer <token>
Content-Type: application/json
{
"expect_tip": "bafyreig7..."
}Request Body
| Field | Type | Required | Description |
|---|---|---|---|
expect_tip | string | Yes | Current CID of the tombstone version |
note | string | No | Version note for audit trail |
Response (200 OK)
{
"id": "01KFNR849AZNBWE9DYJRZR7PSA",
"cid": "bafyreih8...",
"type": "document",
"properties": { "label": "Chapter 1. Loomings", "text": "..." },
"relationships": [...],
"ver": 4,
"created_at": "2025-01-10T08:00:00.000Z",
"ts": 1736931000000,
"edited_by": { "user_id": "01KUSER123...", "method": "manual" },
"prev_cid": "bafyreig7...",
"restored_from_ver": 2
}Error Responses
| Status | Condition |
|---|---|
| 400 | Entity is not deleted, or no restorable version found |
| 404 | Entity not found |
| 409 | CAS conflict |
Permissions
Restoring an entity requires the entity:restore action.
Cascade Delete
For deleting an entity and all related entities, use DELETE /entities/:id/cascade. This performs a BFS traversal following specified relationship predicates, deleting all matching entities within a scoped collection.
DELETE /entities/01KFOLDER123.../cascade
Authorization: Bearer <token>
Content-Type: application/json
{
"expect_tip": "bafyreif4...",
"collection_id": "01KCOLLECTION...",
"cascade_predicates": ["contains", "has_*"],
"max_depth": 10,
"reason": "Cleanup old project"
}Request Body
| Field | Type | Required | Description |
|---|---|---|---|
expect_tip | string | Yes | Current CID of the root entity |
collection_id | string | Yes | Collection to scope the cascade (permission check) |
cascade_predicates | string[] | Yes | Predicate patterns to follow |
edited_by_filter | string | No | Only delete entities edited by this actor |
max_depth | number | No | Max traversal depth (default: 10, max: 20) |
reason | string | No | Reason for deletion |
note | string | No | Version note |
Predicate Patterns
"child"-- exact match only"has_*"-- matcheshas_document,has_image, etc."*_copy"-- matchesfile_copy,document_copy, etc."*"-- matches ALL predicates
Important: The collection predicate NEVER cascades, even with "*". This protects collection structure from accidental deletion.
Response (200 OK)
{
"root": {
"id": "01KFOLDER123...",
"cid": "bafyreig7...",
"deleted_at": "2025-01-15T10:30:00.000Z",
"ver": 5,
"prev_cid": "bafyreif4..."
},
"deleted": [
{ "id": "01KFILE456...", "cid": "bafyreih8...", "type": "file", "depth": 1 },
{ "id": "01KCHUNK789...", "cid": "bafyreii9...", "type": "chunk", "depth": 2 }
],
"skipped": [
{ "id": "01KOTHER...", "type": "file", "reason": "not_in_collection" }
],
"summary": {
"total_traversed": 15,
"total_deleted": 12,
"total_skipped": 2,
"max_depth_reached": 3
}
}Skip Reasons
| Reason | Description |
|---|---|
not_in_collection | Entity is outside the scoped collection |
already_deleted | Entity was already tombstoned |
edited_by_mismatch | Entity doesn't match edited_by_filter |
cas_conflict | CAS conflict after retry |
Permissions
Cascade delete requires entity:delete permission on the specified collection_id. Individual entity permissions are NOT checked during cascade -- the collection permission covers all entities within it.
Transparency
Tombstoned entities remain in the version chain. Anyone with access can see:
- That the entity was deleted
- When it was deleted
- Who deleted it
- The reason (if provided)
- All previous versions of the content
This follows Arke's principle: preserve, don't delete. Content is never silently erased.