Draft Management
Apps use a draft-based editing workflow: changes happen on a separate draft copy and only reach the published version on an explicit commit.
The model
- One draft per app. A draft is a second App row whose
editingIdpoints at the original. Opening an editor when a draft already exists returns that draft (with its lock state), regardless of who created it; there is never a second draft. - A draft is a full working copy. Creating one copies the library (every file), copies all entity bindings, clones the workspace database schema, and deploys the draft's server routes, so the builder preview runs against the draft end to end.
- Drafts carry an edit lock.
editingLockedBynames the active editor;editingLockedis computed from the last heartbeat (60-second window). Another editor can take the lock with_takeover. - Managed apps cannot be drafted. Apps with origin
deployedormarketplacerefuse_editoutright (403); their changes would be overwritten by the next CLI deploy or marketplace sync. This is an entity-level invariant that even superusers cannot bypass.
Workflow Overview
- Open:
POST /apps/{id}/_editcreates (or returns) the draft - Edit: modify the draft's files via
/apps/{draftId}/contents/... - Review:
GET /apps/{id}/draft/_diffsummarizes binding changes - Commit:
POST /apps/{id}/draft/_commitpublishes to the original - Discard:
DELETE /apps/{id}/draftabandons the draft
GET /api/apps/drafts
List all drafts belonging to the current user across all apps, most
recently modified first, each embedding the original app under editing.
Authentication: Required
[
{
"naturalId": "admin:order-desk-1",
"isManaged": false,
"permissions": {
"assignTags": false,
"bind": false,
"changeOwner": false,
"copy": false,
"delete": false,
"edit": false,
"manage": false,
"purge": false,
"restore": false,
"run": false,
"share": false,
"switchRoles": false,
"write": false
},
"id": "00000000-0000-4000-8000-000000000001",
"type": "report",
"name": "Order Desk",
"tenant": "acme",
"ownerId": "admin",
"slug": "order-desk-1",
"description": "Track and fulfill open orders",
"defn": {
"entryPoint": "index.html"
},
"settings": {
"sandbox": true,
"passDatasets": true
},
"origin": "ui",
"source": null,
"sourceId": null,
"shared": false,
"defnUpdatedAt": null,
"libraryId": "00000000-0000-4000-8000-000000000002",
"webhookSecret": null,
"dbRolePassword": null,
"deletedAt": null,
"editingId": "00000000-0000-4000-8000-000000000003",
"deployedAt": "2026-01-15T16:20:00.000Z",
"editingSession": "00000000-0000-4000-8000-000000000004",
"editingHeartbeatAt": null,
"editingLockedBy": "admin",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"folderId": null,
"editing": {
"naturalId": "admin:order-desk",
"isManaged": false,
"permissions": {
"assignTags": false,
"bind": false,
"changeOwner": false,
"copy": false,
"delete": false,
"edit": false,
"manage": false,
"purge": false,
"restore": false,
"run": false,
"share": false,
"switchRoles": false,
"write": false
},
"id": "00000000-0000-4000-8000-000000000003",
"type": "report",
"name": "Order Desk",
"tenant": "acme",
"ownerId": "admin",
"slug": "order-desk",
"description": "Track and fulfill open orders",
"defn": {
"entryPoint": "index.html"
},
"settings": {
"sandbox": true,
"passDatasets": true
},
"origin": "ui",
"source": null,
"sourceId": null,
"shared": false,
"defnUpdatedAt": null,
"libraryId": "00000000-0000-4000-8000-000000000005",
"webhookSecret": "encrypted:iv:0000000000000000",
"dbRolePassword": null,
"deletedAt": null,
"editingId": null,
"deployedAt": null,
"editingSession": null,
"editingHeartbeatAt": null,
"editingLockedBy": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"folderId": null
}
}
]
POST /api/apps/{id}/_edit
Open a draft for editing. If a draft already exists for this app (created by anyone), it is returned with its current lock state instead.
Authentication: Required
Permissions Required: app:edit (app:write and the app is not
managed). The managed-app refusal is unconditional and happens before the
permission check.
Payload (optional):
| Field | Type | Description |
|---|---|---|
chatId | uuid | An existing builder chat to link to the draft |
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit"
},
"inf:owner": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/owner"
},
"inf:draft": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft"
},
"inf:app-shares": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/shares"
},
"inf:app-tags": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/tags"
},
"inf:favorite": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/favorite"
},
"inf:app-tag": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/tags/{tagId}",
"templated": true
},
"inf:export-bundle": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/_export"
},
"inf:cloud-publish": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/_cloud-publish"
},
"inf:copy": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/_copy"
},
"inf:print": [
{
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/_print"
},
{
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_print",
"title": "Print to PDF"
}
],
"inf:migrate": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/_migrate"
},
"inf:deploy": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/_deploy"
},
"inf:app-tokens": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/tokens"
},
"inf:app-roles": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/roles"
},
"inf:app-dependencies": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/dependencies"
},
"inf:app-dependency": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/dependencies/{name}",
"templated": true
},
"inf:app-environment": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/environment"
},
"inf:app-environment-var": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/environment/{key}",
"templated": true
},
"inf:app-suggest-icon": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_suggest-icon"
},
"inf:app-modernize-manifest": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_modernize-manifest"
},
"inf:app-draft-diff": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/draft/_diff"
},
"inf:commit-draft": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/draft/_commit"
},
"inf:app-takeover": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover"
},
"inf:app-share": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/shares/{principalId}",
"templated": true
},
"inf:entrypoint": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/view/",
"title": "index.html"
},
"inf:library": {
"href": "https://informer.example.com/api/libraries/00000000-0000-4000-8000-000000000003",
"title": "App Files"
},
"inf:library-contents": {
"href": "https://informer.example.com/api/libraries/00000000-0000-4000-8000-000000000003/contents/index.html",
"title": "Raw Contents"
},
"share": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/view/?token=%7Btoken%7D",
"title": "Share Page"
}
},
"naturalId": "admin:order-desk-1",
"isManaged": false,
"permissions": {
"assignTags": true,
"bind": true,
"changeOwner": true,
"copy": true,
"delete": true,
"edit": true,
"manage": true,
"purge": true,
"restore": true,
"run": true,
"share": true,
"switchRoles": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000001",
"type": "report",
"name": "Order Desk",
"tenant": "acme",
"ownerId": "admin",
"slug": "order-desk-1",
"description": "Track and fulfill open orders",
"defn": {
"entryPoint": "index.html"
},
"settings": {
"sandbox": true,
"passDatasets": true
},
"origin": "ui",
"source": null,
"sourceId": null,
"shared": false,
"defnUpdatedAt": null,
"libraryId": "00000000-0000-4000-8000-000000000003",
"webhookSecret": null,
"dbRolePassword": null,
"deletedAt": null,
"editingId": "00000000-0000-4000-8000-000000000002",
"deployedAt": "2026-01-15T16:20:00.000Z",
"editingSession": "00000000-0000-4000-8000-000000000004",
"editingHeartbeatAt": null,
"editingLockedBy": "admin",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"folderId": null,
"contentModifiedAt": "2026-01-15T16:20:00.000Z"
}
Key Behaviors:
- The draft has its own UUID, its own auto-suffixed slug (
order-desk-1),editingIdpointing at the original, andownerIdset to the caller - The library is copied (the draft's
libraryIddiffers from the original's), entity bindings are copied, the workspace schema is cloned, and the draft is deployed before the route returns editingSessionandeditingLockedByare initialized to the caller- If the workspace clone or deploy fails, the error is surfaced (no silently broken draft)
Error Responses:
404 Not Found- App does not exist or user lacks read access403 Forbidden- App is managed, or user lacks edit permission
GET /api/apps/{id}/draft
Retrieve the app's draft (addressed by the original app's id).
Authentication: Required
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002"
},
"inf:owner": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/owner"
},
"inf:draft": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/draft"
},
"inf:app-shares": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/shares"
},
"inf:app-tags": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/tags"
},
"inf:favorite": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/favorite"
},
"inf:app-tag": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/tags/{tagId}",
"templated": true
},
"inf:export-bundle": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_export"
},
"inf:cloud-publish": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_cloud-publish"
},
"inf:copy": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_copy"
},
"inf:print": [
{
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_print"
},
{
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_print",
"title": "Print to PDF"
}
],
"inf:migrate": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_migrate"
},
"inf:deploy": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_deploy"
},
"inf:app-tokens": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/tokens"
},
"inf:app-roles": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/roles"
},
"inf:app-dependencies": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/dependencies"
},
"inf:app-dependency": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/dependencies/{name}",
"templated": true
},
"inf:app-environment": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/environment"
},
"inf:app-environment-var": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/environment/{key}",
"templated": true
},
"inf:app-suggest-icon": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_suggest-icon"
},
"inf:app-modernize-manifest": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_modernize-manifest"
},
"inf:app-draft-diff": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_diff"
},
"inf:commit-draft": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit"
},
"inf:app-takeover": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_takeover"
},
"inf:app-share": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/shares/{principalId}",
"templated": true
},
"inf:entrypoint": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/view/",
"title": "index.html"
},
"inf:library": {
"href": "https://informer.example.com/api/libraries/admin%3Aorder-desk-files-draft",
"title": "App Files"
},
"inf:library-contents": {
"href": "https://informer.example.com/api/libraries/admin%3Aorder-desk-files-draft/contents/index.html",
"title": "Raw Contents"
},
"share": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/view/?token=%7Btoken%7D",
"title": "Share Page"
},
"inf:original": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001"
}
},
"naturalId": "admin:order-desk-1",
"isManaged": false,
"permissions": {
"assignTags": true,
"bind": true,
"changeOwner": true,
"copy": true,
"delete": true,
"edit": true,
"manage": true,
"purge": true,
"restore": true,
"run": true,
"share": true,
"switchRoles": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000002",
"type": "report",
"name": "Order Desk",
"tenant": "acme",
"ownerId": "admin",
"slug": "order-desk-1",
"description": "Track and fulfill open orders",
"defn": {
"entryPoint": "index.html"
},
"settings": {
"sandbox": true,
"passDatasets": true
},
"origin": "ui",
"source": null,
"sourceId": null,
"shared": false,
"defnUpdatedAt": null,
"libraryId": "00000000-0000-4000-8000-000000000003",
"webhookSecret": null,
"dbRolePassword": null,
"deletedAt": null,
"editingId": "00000000-0000-4000-8000-000000000001",
"deployedAt": "2026-01-15T16:20:00.000Z",
"editingSession": "00000000-0000-4000-8000-000000000004",
"editingHeartbeatAt": null,
"editingLockedBy": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"folderId": null,
"datasets": {},
"library": {
"naturalId": "admin:order-desk-files-draft",
"connectionOwner": "admin",
"isLocal": true,
"permissions": {
"assignTags": true,
"changeOwner": true,
"configure": true,
"connect": true,
"edit": true,
"delete": true,
"write": true,
"sync": true,
"rename": true,
"share": true,
"editConnection": true
},
"id": "00000000-0000-4000-8000-000000000003",
"type": "local",
"name": "Order Desk Files (Draft)",
"slug": "order-desk-files-draft",
"description": null,
"ownerId": "admin",
"data": null,
"folderId": null,
"shared": false,
"assistantAccess": false,
"embedded": true,
"syncedAt": null,
"syncInterval": null,
"snapshotOfId": null,
"chatId": null,
"source": null,
"sourceId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"integration": null
},
"editingLocked": false,
"contentModifiedAt": "2026-01-15T16:20:00.000Z"
}
editingLocked is true while an editor heartbeat was seen within the
last 60 seconds; editingLockedBy is only reported while the lock is
live. The response carries a Location header with the draft's own URL.
Error Responses:
404 Not Found- Original app doesn't exist, user lacks read access, or no draft exists
PUT /api/apps/{id}/draft
Create or return the draft, like POST /_edit with PUT semantics (no
chatId payload). Same permission gate and same create-side effects.
GET /api/apps/{id}/draft/_diff
Structured diff of the app's bindings between original and draft, for the commit confirmation UI.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_diff"
}
},
"dependencies": [],
"whitelist": {
"added": [],
"removed": []
}
}
Response Shape:
| Field | Description |
|---|---|
dependencies | One entry per dependency slot: { name, status, was, now, changes } where status is added, removed, changed, or preserved, and changes lists which of target/targetId/runAs/options moved |
whitelist | { added, removed } arrays of raw API-whitelist rows |
Error Responses:
404 Not Found- No draft exists for this app
POST /api/apps/{draftId}/_takeover
Take over the edit lock on a draft. Addressed at the draft's id (a
request against a non-draft app is 400).
Authentication: Required
Permissions Required: app:write on the draft
Payload (optional): { clientId: <uuid> } identifying the new editor
session.
{}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover"
},
"inf:owner": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/owner"
},
"inf:draft": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft"
},
"inf:app-shares": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/shares"
},
"inf:app-tags": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/tags"
},
"inf:favorite": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/favorite"
},
"inf:app-tag": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/tags/{tagId}",
"templated": true
},
"inf:export-bundle": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/_export"
},
"inf:cloud-publish": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/_cloud-publish"
},
"inf:copy": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/_copy"
},
"inf:print": [
{
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/_print"
},
{
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_print",
"title": "Print to PDF"
}
],
"inf:migrate": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/_migrate"
},
"inf:deploy": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/_deploy"
},
"inf:app-tokens": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/tokens"
},
"inf:app-roles": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/roles"
},
"inf:app-dependencies": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/dependencies"
},
"inf:app-dependency": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/dependencies/{name}",
"templated": true
},
"inf:app-environment": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/environment"
},
"inf:app-environment-var": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/environment/{key}",
"templated": true
},
"inf:app-suggest-icon": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_suggest-icon"
},
"inf:app-modernize-manifest": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_modernize-manifest"
},
"inf:app-draft-diff": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/draft/_diff"
},
"inf:commit-draft": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/draft/_commit"
},
"inf:app-takeover": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover"
},
"inf:app-share": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/shares/{principalId}",
"templated": true
},
"inf:entrypoint": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/view/",
"title": "index.html"
},
"inf:library": {
"href": "https://informer.example.com/api/libraries/admin%3Aorder-desk-files-draft",
"title": "App Files"
},
"inf:library-contents": {
"href": "https://informer.example.com/api/libraries/admin%3Aorder-desk-files-draft/contents/index.html",
"title": "Raw Contents"
},
"share": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/view/?token=%7Btoken%7D",
"title": "Share Page"
}
},
"naturalId": "admin:order-desk-1",
"isManaged": false,
"permissions": {
"assignTags": true,
"bind": true,
"changeOwner": true,
"copy": true,
"delete": true,
"edit": true,
"manage": true,
"purge": true,
"restore": true,
"run": true,
"share": true,
"switchRoles": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000001",
"type": "report",
"name": "Order Desk",
"tenant": "acme",
"ownerId": "admin",
"slug": "order-desk-1",
"description": "Track and fulfill open orders",
"defn": {
"entryPoint": "index.html"
},
"settings": {
"sandbox": true,
"passDatasets": true
},
"origin": "ui",
"source": null,
"sourceId": null,
"shared": false,
"defnUpdatedAt": null,
"libraryId": "00000000-0000-4000-8000-000000000003",
"webhookSecret": null,
"dbRolePassword": null,
"deletedAt": null,
"editingId": "00000000-0000-4000-8000-000000000002",
"deployedAt": "2026-01-15T16:20:00.000Z",
"editingSession": "00000000-0000-4000-8000-000000000004",
"editingHeartbeatAt": "2026-01-15T16:20:00.000Z",
"editingLockedBy": "admin",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"folderId": null,
"datasets": [],
"library": {
"naturalId": "admin:order-desk-files-draft",
"connectionOwner": "admin",
"isLocal": true,
"permissions": {
"assignTags": true,
"changeOwner": true,
"configure": true,
"connect": true,
"edit": true,
"delete": true,
"write": true,
"sync": true,
"rename": true,
"share": true,
"editConnection": true
},
"id": "00000000-0000-4000-8000-000000000003",
"type": "local",
"name": "Order Desk Files (Draft)",
"slug": "order-desk-files-draft",
"description": null,
"ownerId": "admin",
"data": null,
"folderId": null,
"shared": false,
"assistantAccess": false,
"embedded": true,
"syncedAt": null,
"syncInterval": null,
"snapshotOfId": null,
"chatId": null,
"source": null,
"sourceId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"integration": null
},
"editing": {
"naturalId": "admin:order-desk",
"isManaged": false,
"permissions": {
"assignTags": true,
"bind": true,
"changeOwner": true,
"copy": true,
"delete": true,
"edit": true,
"manage": true,
"purge": true,
"restore": true,
"run": true,
"share": true,
"switchRoles": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000002",
"type": "report",
"name": "Order Desk",
"tenant": "acme",
"ownerId": "admin",
"slug": "order-desk",
"description": "Track and fulfill open orders",
"defn": {
"entryPoint": "index.html"
},
"settings": {
"sandbox": true,
"passDatasets": true
},
"origin": "ui",
"source": null,
"sourceId": null,
"shared": false,
"defnUpdatedAt": null,
"libraryId": "00000000-0000-4000-8000-000000000005",
"webhookSecret": "encrypted:iv:0000000000000000",
"dbRolePassword": null,
"deletedAt": null,
"editingId": null,
"deployedAt": null,
"editingSession": null,
"editingHeartbeatAt": null,
"editingLockedBy": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"folderId": null
},
"contentModifiedAt": "2026-01-15T16:20:00.000Z"
}
The previous editor is notified over the app-edit-activity channel and
their builder surfaces the takeover dialog.
POST /api/apps/{id}/draft/_commit
Commit the draft, publishing its state onto the original app. This is the only save path: there is no "save but keep editing" variant; committing always ends the editing session.
Authentication: Required
Permissions Required: app:edit (managed apps cannot reach commit)
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit"
},
"inf:owner": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/owner"
},
"inf:draft": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/draft"
},
"inf:app-shares": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/shares"
},
"inf:app-tags": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/tags"
},
"inf:favorite": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/favorite"
},
"inf:app-tag": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/tags/{tagId}",
"templated": true
},
"inf:export-bundle": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/_export"
},
"inf:cloud-publish": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/_cloud-publish"
},
"inf:copy": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/_copy"
},
"inf:print": [
{
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/_print"
},
{
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_print",
"title": "Print to PDF"
}
],
"inf:migrate": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/_migrate"
},
"inf:deploy": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/_deploy"
},
"inf:app-tokens": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/tokens"
},
"inf:app-roles": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/roles"
},
"inf:app-dependencies": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/dependencies"
},
"inf:app-dependency": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/dependencies/{name}",
"templated": true
},
"inf:app-environment": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/environment"
},
"inf:app-environment-var": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/environment/{key}",
"templated": true
},
"inf:app-suggest-icon": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_suggest-icon"
},
"inf:app-modernize-manifest": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_modernize-manifest"
},
"inf:app-draft-diff": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/draft/_diff"
},
"edit": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit"
},
"inf:app-share": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/shares/{principalId}",
"templated": true
},
"inf:entrypoint": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/view/",
"title": "index.html"
},
"inf:library": {
"href": "https://informer.example.com/api/libraries/admin%3Aorder-desk-files",
"title": "App Files"
},
"inf:library-contents": {
"href": "https://informer.example.com/api/libraries/admin%3Aorder-desk-files/contents/index.html",
"title": "Raw Contents"
},
"share": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/view/?token=%7Btoken%7D",
"title": "Share Page"
}
},
"naturalId": "admin:order-desk",
"isManaged": false,
"permissions": {
"assignTags": true,
"bind": true,
"changeOwner": true,
"copy": true,
"delete": true,
"edit": true,
"manage": true,
"purge": true,
"restore": true,
"run": true,
"share": true,
"switchRoles": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000001",
"type": "report",
"name": "Order Desk",
"tenant": "acme",
"ownerId": "admin",
"slug": "order-desk",
"description": "Track, fulfill, and audit open orders",
"defn": {
"entryPoint": "index.html"
},
"settings": {
"sandbox": true,
"passDatasets": true
},
"origin": "ui",
"source": null,
"sourceId": null,
"shared": false,
"defnUpdatedAt": "2026-01-15T16:20:00.000Z",
"libraryId": "00000000-0000-4000-8000-000000000002",
"webhookSecret": "encrypted:iv:0000000000000000",
"dbRolePassword": null,
"deletedAt": null,
"editingId": null,
"deployedAt": "2026-01-15T16:20:00.000Z",
"editingSession": null,
"editingHeartbeatAt": null,
"editingLockedBy": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"folderId": null,
"datasets": [],
"library": {
"naturalId": "admin:order-desk-files",
"connectionOwner": "admin",
"isLocal": true,
"permissions": {
"assignTags": true,
"changeOwner": true,
"configure": true,
"connect": true,
"edit": true,
"delete": true,
"write": true,
"sync": true,
"rename": true,
"share": true,
"editConnection": true
},
"id": "00000000-0000-4000-8000-000000000003",
"type": "local",
"name": "Order Desk Files",
"slug": "order-desk-files",
"description": null,
"ownerId": "admin",
"data": null,
"folderId": null,
"shared": false,
"assistantAccess": false,
"embedded": true,
"syncedAt": null,
"syncInterval": null,
"snapshotOfId": null,
"chatId": null,
"source": null,
"sourceId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"integration": null
},
"editing": null,
"contentModifiedAt": "2026-01-15T16:20:00.000Z"
}
What commits:
defn,settings, anddescriptionare copied from the draft;nameonly when the draft renamed the app;defnUpdatedAtis stamped- The draft's library replaces the original's (the old library is destroyed; snapshot libraries are detached first so snapshots survive)
- Entity bindings on the original are replaced with the draft's
- The app's
originbecomesui(a UI-committed app is user-owned) - The draft row, its workspace schema, and its routes are destroyed
- The original app is redeployed with its new content. If that deploy
fails the commit itself has already landed; re-trigger with
POST /apps/{id}/_deploy
Error Responses:
404 Not Found- App or draft doesn't exist403 Forbidden- App is managed or user lacks edit permission
DELETE /api/apps/{id}/draft
Discard the draft without committing.
Authentication: Required
Permissions Required: app:write on the original app
Behavior:
- Destroys the draft row, its copied library, its workspace schema, and its deployed routes; re-parents any snapshots taken while editing onto the original app
- The original app is unchanged
POST /api/apps/{id}/draft/_heartbeat
Deprecated no-op. Older clients posted this every 30 seconds to hold
the edit lock; the route now always answers 204 No Content without
writing anything (the row-update storm it caused under load was removed).
It still 404s when the draft is gone and 403s without write access, so
older clients degrade correctly.
Common Patterns
Edit → Modify → Commit
// 1. Start editing
const draft = await POST('/api/apps/analytics:sales-dashboard/_edit');
// 2. Make changes to draft files
await PUT(`/api/apps/${draft.id}/contents/index.html`, {
content: updatedHTML
});
// 3. Update draft metadata
await PUT(`/api/apps/${draft.id}`, {
description: 'Updated description'
});
// 4. Review and commit
const diff = await GET('/api/apps/analytics:sales-dashboard/draft/_diff');
await POST('/api/apps/analytics:sales-dashboard/draft/_commit');
Edit → Cancel
const draft = await POST('/api/apps/analytics:sales-dashboard/_edit');
await PUT(`/api/apps/${draft.id}/contents/index.html`, { ... });
await DELETE('/api/apps/analytics:sales-dashboard/draft');
Respect the lock
const draft = await POST('/api/apps/analytics:sales-dashboard/_edit');
if (draft.editingLocked && draft.editingLockedBy !== me) {
// someone else is editing; offer a takeover
await POST(`/api/apps/${draft.id}/_takeover`, { clientId });
}