Versioning & CAS
How content-addressed versioning and Compare-And-Swap work in Arke.
Version Chains
Every update creates a new immutable version. Versions are linked through prev CID references:
v3 (tip) → v2 → v1 → nullEach version is stored in KV by its content hash (CID). The tip pointer in a Durable Object always points to the latest version.
Compare-And-Swap (CAS)
CAS prevents lost updates. Every update request must include expect_tip -- the CID you believe is the current tip.
Success flow:
- Read entity:
{ cid: "abc", ver: 2, ... } - Update with
expect_tip: "abc"-- succeeds, creates version 3
Conflict flow:
- Read entity:
{ cid: "abc", ver: 2, ... } - Someone else updates: tip changes to
"def" - Your update with
expect_tip: "abc"-- fails with 409 - Re-read, reapply changes, retry with
expect_tip: "def"
IPLD-Style Links
Version links use the IPLD convention:
{
"prev": { "/": "bafyreig..." }
}This provides compatibility with content-addressed systems while storage is on Cloudflare infrastructure.
Traversing History
List Version Metadata
GET /versions/{id}Returns version metadata in reverse chronological order (newest first). Supports pagination.
Query parameters:
limit: Maximum versions to return (1-10, default 10)from: CID to start from (for pagination)
Response:
{
"entity_id": "01KDETYWYWM0MJVKM8DK3AEXPY",
"versions": [
{
"ver": 3,
"cid": "bafyrei...",
"ts": "2025-01-15T10:00:00Z",
"edited_by": {
"user_id": "01JUSER...",
"method": "manual"
},
"note": "Updated description",
"arweave_tx": "abc123...",
"arweave_url": "https://arweave.net/abc123..."
}
],
"has_more": true,
"next_cursor": "bafyrei..."
}The arweave_tx and arweave_url fields are included if the version has been attested to Arweave.
Get Full Manifest by CID
GET /versions/manifest/{cid}Retrieves the complete manifest for any historical version by its CID. Useful for viewing the full state at a specific point in time.
Response:
{
"cid": "bafyrei...",
"manifest": {
"schema": "arke/eidos@v1",
"id": "01KDETYWYWM0MJVKM8DK3AEXPY",
"type": "document",
"created_at": "2025-01-10T08:00:00Z",
"ver": 2,
"ts": "2025-01-12T14:30:00Z",
"prev": { "/": "bafyrei..." },
"properties": { "label": "My Document", "text": "..." },
"relationships": [],
"edited_by": { "user_id": "01JUSER...", "method": "manual" }
}
}Comparing Versions (Diff)
GET /entities/{id}/diffComputes the difference between two versions of an entity.
Query parameters:
from: CID of the "from" version (defaults to prev of "to" version)to: CID of the "to" version (defaults to current tip)format: Output format -semantic(default) orpatch(RFC 6902)
Modes:
- No params: Compare current tip with its previous version
toonly: Compare that version with its prevfromonly: Compare from that version to current tip- Both: Compare any two versions
Semantic format response:
{
"entity_id": "01KDETYWYWM0MJVKM8DK3AEXPY",
"from": { "cid": "bafyrei...", "ver": 2, "ts": "..." },
"to": { "cid": "bafyrei...", "ver": 3, "ts": "..." },
"format": "semantic",
"changes": {
"properties": {
"added": { "new_field": "value" },
"removed": { "old_field": "was this" },
"changed": { "label": { "from": "Old", "to": "New" } }
},
"relationships": {
"added": [{ "predicate": "contains", "peer": "01KFILE..." }],
"removed": [],
"changed": []
}
}
}Patch format response (RFC 6902):
{
"entity_id": "01KDETYWYWM0MJVKM8DK3AEXPY",
"from": { "cid": "bafyrei...", "ver": 2, "ts": "..." },
"to": { "cid": "bafyrei...", "ver": 3, "ts": "..." },
"format": "patch",
"patch": [
{ "op": "replace", "path": "/properties/label", "value": "New" },
{ "op": "add", "path": "/properties/new_field", "value": "value" }
]
}For version 1 entities (no previous version), from is null and all content appears as added.
Soft Delete and Restore
Entities are soft-deleted by creating a tombstone version. The original data is preserved in the version chain.
Delete
DELETE /entities/{id}Request body:
{
"expect_tip": "bafyrei...",
"reason": "Optional deletion reason"
}Response:
{
"id": "01KDETYWYWM0MJVKM8DK3AEXPY",
"cid": "bafyrei...",
"deleted_at": "2025-01-15T12:00:00Z",
"ver": 4,
"prev_cid": "bafyrei..."
}Restore
POST /entities/{id}/restoreRestores a deleted entity by finding the last non-deleted version in the chain and creating a new version with that data.
Request body:
{
"expect_tip": "bafyrei..."
}Response:
{
"id": "01KDETYWYWM0MJVKM8DK3AEXPY",
"cid": "bafyrei...",
"type": "document",
"properties": { "label": "My Document", "..." },
"relationships": [],
"ver": 5,
"created_at": "2025-01-10T08:00:00Z",
"ts": "...",
"edited_by": { "user_id": "01JUSER...", "method": "manual" },
"prev_cid": "bafyrei...",
"restored_from_ver": 3
}The restored_from_ver field indicates which version the entity was restored from.
Lightweight Tip Lookup
GET /entities/{id}/tipReturns only the current CID without fetching the full manifest. Useful for CAS operations where you only need the tip for expect_tip.
Response:
{
"id": "01KDETYWYWM0MJVKM8DK3AEXPY",
"cid": "bafyrei..."
}This endpoint has no authentication requirement and performs a single Durable Object lookup.