DDuvappsDocumentationStudio

Versioning and undo

Three different things get versioned here, and conflating them is the usual mistake.

The API

Versioned in the path: /v1. Every response names the contract version that answered in x-duvapps-api-version, and the OpenAPI document carries the same number. Additive changes ship without a new path. An operation that is being retired is marked deprecated in the reference and answers with a Deprecationheader (and a Link to what replaces it); once it has a date after which it may stop answering, a Sunset header says when. Operation names never change silently: they are also the names of the MCP tools and the CLI commands.

Your pages

A page's code is stored as immutable content. Publishing writes a new bundle and moves a pointer; nothing is ever overwritten. So rolling back is a pointer move rather than a rebuild, and two people using a page while it is being deployed each keep the version they loaded.

GET  /v1/apps/{appId}/pages/{pageId}/versions
POST /v1/apps/{appId}/pages/{pageId}/publish  { "version": 4 }

Publishing an earlier version is the rollback. To keep editing from an older version, read its code with GET …/code?version=4 and save it as a new version.

Your records

Every write, from any surface, appends to an op log: who, when, how it arrived, and what the row looked like before and after. Record history, per-field history, audit and undo all come from that one log rather than four separate mechanisms.

Undo

Every write is grouped into a changeset, written in the same transaction as the change itself, so either both land or neither does. The change history lists them, newest first, and says for each whether it can be undone (revertible). Record creates, edits and deletes can; comments, saved views, pages and changes to the app's tables and fields are not undone from here, and the list says where each is changed back instead. The undo is itself recorded, which is why undoing an undo is just undo again.

GET  /crm/v1/changesets                                 (in an app)
POST /crm/v1/changesets/{id}/revert
GET  /v1/apps/{appId}/changesets                     (in the Studio)
POST /v1/apps/{appId}/changesets/{id}/revert

Deleting a table or a field is not undone from the change history, but it is not lost: what it held goes to the app's trash for thirty days, and trash.restore puts it back with the same ids, its values, its links and the rules that named it. Both still ask for the thing's name, and both are refused with the list of rules, views, pages and fields that read what they remove until you send acknowledge=true.

GET  /v1/apps/{appId}/trash
POST /v1/apps/{appId}/trash/{id}/restore

Every row an undo can put back is put back in one transaction. A row deleted since is listed in skipped with the reason, rather than failing the rest.