Draft Management
Apps use a draft-based editing workflow that allows users to make changes without affecting the published version until they explicitly commit.
Workflow Overview
- Create Draft:
POST /apps/{id}/_edit- Creates a personal draft copy - Edit Draft: Modify the draft's files via
/apps/{draftId}/contents/... - Commit:
POST /apps/{id}/draft/_commit- Publish changes to original - Discard:
DELETE /apps/{id}/draft- Abandon changes
GET /api/apps/drafts
List all drafts belonging to the current user across all apps.
Authentication: Required
Response:
[
{
"id": "draft-uuid-5678",
"type": "report",
"name": "Sales Dashboard",
"ownerId": "john.doe",
"editingId": "d4f8a2b1-1234-5678-90ab-cdef12345678",
"description": "Interactive sales analytics",
"createdAt": "2024-01-15T10:00:00.000Z",
"updatedAt": "2024-02-08T14:30:00.000Z",
"editing": {
"id": "d4f8a2b1-1234-5678-90ab-cdef12345678",
"type": "report",
"name": "Sales Dashboard",
"ownerId": "analytics",
"slug": "sales-dashboard"
}
}
]
Key Fields:
| Field | Description |
|---|---|
editingId | ID of the original app this draft is editing |
editing | Embedded original app object |
Behavior:
- Returns all apps where
ownerIdmatches the current user andeditingIdis not null - Includes the original app via the
editingassociation - Ordered by
updatedAtdescending (most recently modified first)
POST /api/apps/{id}/_edit
Create a draft for editing an app. If a draft already exists for this user and app, returns the existing draft.
Authentication: Required
Permissions Required: Member+ role (write permission on the app)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
Response:
Returns the draft app with status 200 OK. The draft has:
- A new UUID (different from the original)
editingIdset to the original app's IDownerIdset to the current user's username- No slug (drafts are only addressable by UUID)
{
"_links": {
"self": { "href": "/api/apps/draft-uuid-5678" },
"inf:original": { "href": "/api/apps/analytics:sales-dashboard" }
},
"id": "draft-uuid-5678",
"type": "report",
"name": "Sales Dashboard",
"ownerId": "john.doe",
"editingId": "d4f8a2b1-1234-5678-90ab-cdef12345678",
"description": "Interactive sales analytics",
"defn": {
"icon": "sparkles",
"entryPoint": "index.html"
},
"settings": {},
"libraryId": "lib-uuid-1234",
"createdAt": "2024-02-13T10:00:00.000Z",
"updatedAt": "2024-02-13T10:00:00.000Z",
"datasets": {
"sales": {
"id": "rd-uuid-2",
"appId": "draft-uuid-5678",
"datasetId": "dataset-uuid-1",
"alias": "sales",
"label": "Sales Data"
}
}
}
Key Behaviors:
- Idempotent: Calling this multiple times returns the same draft
- Driver Hooks: The app driver's
beforeCreateDrafthook can modify the draft data - Library Copy: The draft shares the same
libraryIdas the original (edits are isolated by app context) - Dataset Copy: All AppDataset references are duplicated
Error Responses:
404 Not Found- App does not exist or user lacks read access403 Forbidden- User lacks write permission
GET /api/apps/{id}/draft
Retrieve the current user's draft of an app.
Authentication: Required
Permissions Required: None (user must be the draft owner)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Original app UUID or natural ID |
Response:
Returns the draft app with a Location header pointing to the draft's self URL.
{
"_links": {
"self": { "href": "/api/apps/draft-uuid-5678" },
"inf:original": { "href": "/api/apps/analytics:sales-dashboard" },
"inf:commit-draft": { "href": "/api/apps/analytics:sales-dashboard/draft/_commit" }
},
"id": "draft-uuid-5678",
"type": "report",
"name": "Sales Dashboard",
"ownerId": "john.doe",
"editingId": "d4f8a2b1-1234-5678-90ab-cdef12345678",
"description": "Interactive sales analytics",
"defn": {
"icon": "sparkles"
},
"createdAt": "2024-02-13T10:00:00.000Z",
"updatedAt": "2024-02-13T10:05:00.000Z"
}
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 retrieve a draft for the current user. This endpoint is similar to POST /apps/{id}/_edit but uses PUT semantics.
Authentication: Required
Permissions Required: Member+ role (write permission on the app)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Original app UUID or natural ID |
Response:
Returns the draft app (existing or newly created) with status 200 OK and a Location header.
Behavior:
- If a draft already exists for this user, returns it
- If no draft exists, creates one (same as
POST /_edit)
Error Responses:
404 Not Found- App does not exist or user lacks read access403 Forbidden- User lacks write permission
DELETE /api/apps/{id}/draft
Discard the current user's draft without committing changes.
Authentication: Required
Permissions Required: Member+ role (write permission on the original app)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Original app UUID or natural ID |
Response:
Returns 204 No Content on success.
Behavior:
- Deletes the draft app and all associated resources
- Calls the driver's
beforeDiscardDrafthook for cleanup - Original app remains unchanged
Error Responses:
404 Not Found- App or draft doesn't exist, or user lacks read access403 Forbidden- User lacks write permission
POST /api/apps/{id}/draft/_commit
Commit the current user's draft, publishing changes to the original app.
Authentication: Required
Permissions Required: Member+ role (write permission on the original app)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Original app UUID or natural ID |
Response:
Returns the updated original app.
{
"_links": {
"self": { "href": "/api/apps/analytics:sales-dashboard" },
"inf:owner": { "href": "/api/apps/analytics:sales-dashboard/owner" }
},
"id": "d4f8a2b1-1234-5678-90ab-cdef12345678",
"type": "report",
"name": "Sales Dashboard",
"slug": "sales-dashboard",
"ownerId": "analytics",
"description": "Interactive sales analytics (updated)",
"defn": {
"icon": "chart-bar"
},
"settings": {
"theme": "dark"
},
"defnUpdatedAt": "2024-02-13T10:15:00.000Z",
"updatedAt": "2024-02-13T10:15:00.000Z",
"createdAt": "2024-01-15T10:00:00.000Z"
}
Commit Behavior:
The following fields are committed from the draft to the original:
defn- Always committedsettings- Always committeddescription- Always committeddefnUpdatedAt- Set to current timestampname- Only if the draft's name differs from the original
Dataset Reassignment:
All AppDataset references are moved from the draft to the original:
- Existing AppDatasets on the original are deleted
- Draft's AppDatasets are reassigned to the original app's ID
- Timestamps are updated
Driver Hook:
The driver's afterCommitDraft hook can modify the changes object before it's applied.
Cleanup:
After successful commit:
- Changes are applied to the original app
- AppDatasets are reassigned
- Draft app is deleted
Error Responses:
404 Not Found- App or draft doesn't exist, or user lacks read access403 Forbidden- User lacks write permission
Transaction Safety:
All commit operations run in a database transaction. If any step fails, the entire operation is rolled back.
Draft Limitations
- One Draft Per User: Each user can have only one draft per app
- Personal Drafts: Drafts are owned by the user who created them (not the team)
- No Nested Drafts: You cannot create a draft of a draft
- Library Sharing: Drafts share the library with the original, but file changes are isolated by app context
- No Slug: Drafts don't have a slug and must be referenced by UUID
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. Commit changes
await POST('/api/apps/analytics:sales-dashboard/draft/_commit');
Edit → Cancel
// 1. Start editing
const draft = await POST('/api/apps/analytics:sales-dashboard/_edit');
// 2. Make some changes...
await PUT(`/api/apps/${draft.id}/contents/index.html`, { ... });
// 3. Decide to discard
await DELETE('/api/apps/analytics:sales-dashboard/draft');
Check for Existing Draft
// Try to get existing draft
try {
const draft = await GET('/api/apps/analytics:sales-dashboard/draft');
console.log('Continuing existing draft:', draft.id);
} catch (e) {
if (e.statusCode === 404) {
// No draft exists, create one
const draft = await POST('/api/apps/analytics:sales-dashboard/_edit');
console.log('Created new draft:', draft.id);
}
}