Every Publish in memo creates a new immutable row in memo.versions and a fresh <userId>/<projectId>/<versionId>/ prefix in R2. Versions are content-addressed by that ID and never overwritten. projects.current_version_id points at whichever version the editor preview and new-link defaults reflect; rolling forward or back just flips that pointer.
projects.current_version_id to the chosen version, bump updated_at, fire version.rollback webhook + audit row.links row stores its version_id at create time; that's the exact version the visitor sees. Rollback only affects the editor preview and what new links default to.This is the single most important property of memo's publishing model: shareable URLs are permanent against the version they were minted on. You can experiment freely in the editor, roll forward, roll back, roll forward again — every link out in the world stays bound to whatever you published when you sent it.
If you do want a link to start pointing at a different version, delete it and create a new one with the new version_id (UI + MCP both support this).
| Surface | Route | Purpose |
|---|---|---|
| List | /u/<username>/projects/<id>/versions | Every version, oldest-newest, with file count + total bytes + compile status. "current" badge on the live one. "Make current" button rolls back. |
| List API | GET /api/owner/projects/<id>/versions | JSON { versions: [...] } |
| Rollback API | POST /api/owner/projects/<id>/versions/rollback | Body { versionId } |
| MCP | list_versions, rollback_version, diff_versions, read_version_file | Same surface for Claude/scripts |
diff_versions (MCP tool) returns a four-way categorisation between any two versions of the same project:
{
"added": [{ "path": "hero.tsx", "sha256": "…", "size": 1280 }],
"removed": [{ "path": "old-cta.html", "sha256": "…", "size": 540 }],
"changed": [{ "path": "app.ts", "fromSha256": "…", "toSha256": "…", "fromSize": 820, "toSize": 911 }],
"unchanged": [{ "path": "favicon.svg", "sha256": "…" }]
}
Both UI and MCP clients can stream read_version_file for any pair of files to do a unified-diff render — memo deliberately doesn't ship its own diff algorithm so clients are free to use whatever they prefer (Monaco diff, diff package, claude-rendered prose, etc.).
Every rollback emits:
audit_logs row: action="version.rollback", target_id=<versionId>, meta={ projectId, from, to, versionNumber }.version.rollback webhook to every subscribed endpoint.The webhook payload looks like:
{
"event": "version.rollback",
"ts": "2026-...",
"data": {
"projectId": "uuid",
"versionId": "uuid",
"from": "uuid (or null)"
}
}
memo.files; rolling back surfaces it again.