Skip to main content

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

  1. Create Draft: POST /apps/{id}/_edit - Creates a personal draft copy
  2. Edit Draft: Modify the draft's files via /apps/{draftId}/contents/...
  3. Commit: POST /apps/{id}/draft/_commit - Publish changes to original
  4. 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:

FieldDescription
editingIdID of the original app this draft is editing
editingEmbedded original app object

Behavior:

  • Returns all apps where ownerId matches the current user and editingId is not null
  • Includes the original app via the editing association
  • Ordered by updatedAt descending (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:

ParameterTypeDescription
idstringApp UUID or natural ID

Response:

Returns the draft app with status 200 OK. The draft has:

  • A new UUID (different from the original)
  • editingId set to the original app's ID
  • ownerId set 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 beforeCreateDraft hook can modify the draft data
  • Library Copy: The draft shares the same libraryId as 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 access
  • 403 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:

ParameterTypeDescription
idstringOriginal 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:

ParameterTypeDescription
idstringOriginal 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 access
  • 403 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:

ParameterTypeDescription
idstringOriginal 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 beforeDiscardDraft hook for cleanup
  • Original app remains unchanged

Error Responses:

  • 404 Not Found - App or draft doesn't exist, or user lacks read access
  • 403 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:

ParameterTypeDescription
idstringOriginal 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 committed
  • settings - Always committed
  • description - Always committed
  • defnUpdatedAt - Set to current timestamp
  • name - Only if the draft's name differs from the original

Dataset Reassignment:

All AppDataset references are moved from the draft to the original:

  1. Existing AppDatasets on the original are deleted
  2. Draft's AppDatasets are reassigned to the original app's ID
  3. Timestamps are updated

Driver Hook:

The driver's afterCommitDraft hook can modify the changes object before it's applied.

Cleanup:

After successful commit:

  1. Changes are applied to the original app
  2. AppDatasets are reassigned
  3. Draft app is deleted

Error Responses:

  • 404 Not Found - App or draft doesn't exist, or user lacks read access
  • 403 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);
}
}