
# Why we never modify content — only create new versions

The rule is simple: when you update a Binding, we create a new Version. The previous Version stays exactly as it was, forever, until you delete the entire Binding.

This seems excessive. Most knowledge tools just update in place. Why carry all this history?

Because LLMs leave traces.

## The problem with mutable content

When Claude annotates a Binding — "this section is outdated, see the newer version" — that comment references a specific state of the document. If the document is updated in place, the comment still exists but the thing it was pointing at has changed. The comment might now be wrong, or misleading, or just confusing.

Same with Sets. A Set is a curated playlist of Versions. If you add "Version 2 of the Cloudflare architecture doc" to a Set because Version 2 has the specific information you want, you don't want that Set to silently change when Version 3 is created. Sets pin to specific Versions for exactly this reason.

Immutability makes everything predictable. A URL always points to the same content. A Set always contains what you put in it. A comment always references the document as it was when the comment was written.

## What actually happens when you "update"

```sql
-- 1. Read current state
SELECT current_version FROM bindings WHERE id = 'bnd_abc';
-- → 2

-- 2. Create new version row
INSERT INTO versions (id, binding_id, version, hash, content_key, ...)
VALUES ('ver_new', 'bnd_abc', 3, 'sha256:...', 'content/bnd_abc/ver_new.md', ...);

-- 3. Write content to R2
-- PUT content/bnd_abc/ver_new.md

-- 4. Bump the pointer
UPDATE bindings SET current_version = 3 WHERE id = 'bnd_abc';
```

Version 2 is still in D1. Its content is still in R2 at `content/bnd_abc/ver_old.md`. The Binding's `current_version` pointer moved forward, but nothing was deleted. `mcp_get_version({ bindingId, version: 2 })` returns exactly what it always returned.

## Title changes don't create versions

One thing that surprised people: changing the title doesn't create a new Version.

`Binding.name` is mutable — it's just a display label. `Binding.slug` is also separately mutable. Neither triggers version creation. Only content changes create new Versions.

This was a deliberate choice. If you typo the title on a new Binding and fix it immediately, you shouldn't end up with Version 1 (bad title) and Version 2 (fixed title). Versions track knowledge changes, not label changes.

In `mcp_update_binding`, this means:

```typescript
// No content change → no new Version
mcp_update_binding({ id, title: "Better title" })

// Content change → new Version created
mcp_update_binding({ id, content: "...", summary: "..." })
```

## Sets and the playlist model

`set_versions` has no foreign key constraint to the `versions` table. That's intentional:

```sql
CREATE TABLE set_versions (
  set_id TEXT NOT NULL REFERENCES sets(id) ON DELETE CASCADE,
  version_id TEXT NOT NULL,  -- no FK to versions
  position INTEGER NOT NULL,
  added_at INTEGER NOT NULL
);
```

When a Binding (and all its Versions) gets deleted, the Set entries pointing to those Versions stay in the database. The next time you fetch the Set, a LEFT JOIN detects which versions are missing and returns `{ versionId, status: "deleted" }` for those positions.

The Set keeps its shape. The gap is visible. This is the playlist model — a deleted song shows as unavailable, not silently removed from your playlist.

## The hash field

Each Version stores a SHA-256 hash of its content. Two purposes:

**Integrity**: you can verify the content hasn't been corrupted in R2.

**Deduplication**: if someone calls `mcp_update_binding` with the same content that's already in the current version, the hash matches and we can either skip creating a new Version or create one with a note. Prevents accidental version bloat from tooling that retries updates.

We also store `previous_hash` — the hash of the preceding Version. This creates a chain, not just a bag of versions. If you care about audit trails, the chain gives you that.