Skip to main content

File Management

Apps store their files (HTML, CSS, JavaScript, images, etc.) in an associated Library. The file management endpoints provide CRUD operations for app content.

Important Concepts

Library Association: Each app has a libraryId field that references its file storage. Apps without a library cannot use file management endpoints.

Path Structure: Files are organized hierarchically using forward-slash paths (e.g., index.html, css/styles.css, images/logo.png).

Content Types: Files have MIME types that determine how they're served. The system auto-detects types from file extensions.

Content Recency: File operations do NOT modify the app row. The app's content-modified time is exposed as the derived contentModifiedAt field — the latest modifiedAt across the app's library files (null when the library has no files). The app's own updatedAt/defnUpdatedAt reflect only changes to the app row itself (e.g. draft commits), not file edits.


GET /api/apps/{id}/contents/{path*}

Read a file from the app's library.

Authentication: Required (session or token)

Permissions Required: Member role (run permission)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID
pathstringFile path within the library (e.g., index.html, css/styles.css)

Query Parameters:

ParameterTypeDefaultDescription
downloadbooleanfalseForce download instead of inline display

Response:

Returns the file content as a binary stream with appropriate Content-Type header.

Example Request:

GET /api/apps/analytics:sales-dashboard/contents/index.html

Example Response:

HTTP/1.1 200 OK
Content-Type: text/html

<!DOCTYPE html>
<html>
<head><title>Sales Dashboard</title></head>
<body>...</body>
</html>

Download Example:

GET /api/apps/analytics:sales-dashboard/contents/logo.png?download=true
HTTP/1.1 200 OK
Content-Type: image/png
Content-Disposition: attachment; filename="logo.png"

[binary data]

Error Responses:

  • 400 Bad Request - App has no associated library
  • 404 Not Found - App doesn't exist, user lacks access, or file not found
  • 403 Forbidden - User lacks run permission

PUT /api/apps/{id}/contents/{path*}

Write or create a file in the app's library.

Authentication: Required

Permissions Required: Member+ role (write permission)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID
pathstringFile path within the library

Payload:

{
"content": "<!DOCTYPE html>\n<html>...",
"contentType": "text/html",
"encoding": "utf8"
}

Payload Fields:

FieldTypeRequiredDefaultDescription
contentstringYes-File content (can be empty string)
contentTypestringNoAuto-detectMIME type
encodingstringNoutf8utf8 or base64

Response:

Returns the created/updated file metadata with status 201 Created (new file) or 200 OK (updated file).

{
"id": "file-uuid-1234",
"libraryId": "lib-uuid-5678",
"filename": "index.html",
"directory": false,
"parentId": null,
"contentType": "text/html",
"size": 1234,
"encoding": "utf8",
"createdAt": "2024-02-13T10:00:00.000Z",
"updatedAt": "2024-02-13T10:15:00.000Z",
"modifiedAt": "2024-02-13T10:15:00.000Z"
}

The modifiedAt field is the per-file content timestamp that backs the app's derived contentModifiedAt (MAX(file.modifiedAt) across the library — see Content Recency above).

Example - Create HTML File:

PUT /api/apps/analytics:sales-dashboard/contents/index.html
Content-Type: application/json

{
"content": "<!DOCTYPE html>\n<html>\n <head><title>Dashboard</title></head>\n <body><h1>Sales Dashboard</h1></body>\n</html>",
"contentType": "text/html"
}

Example - Upload Binary File (Base64):

PUT /api/apps/analytics:sales-dashboard/contents/logo.png
Content-Type: application/json

{
"content": "iVBORw0KGgoAAAANSUhEUgAAAAUA...",
"contentType": "image/png",
"encoding": "base64"
}

Side Effects:

  • Does NOT modify the app row; bumps the affected file's modifiedAt, surfaced via the app's derived contentModifiedAt
  • Creates parent directories as needed
  • Overwrites existing file if present

Error Responses:

  • 400 Bad Request - App has no library, or invalid payload
  • 404 Not Found - App doesn't exist or user lacks access
  • 403 Forbidden - User lacks write permission

PATCH /api/apps/{id}/contents/{path*}

Perform a patch operation on an existing file (replace, append, prepend, or insert text).

Authentication: Required

Permissions Required: Member+ role (write permission)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID
pathstringFile path within the library

Payload:

The payload structure depends on the operation:

Replace Operation

{
"operation": "replace",
"search": "oldText",
"replacement": "newText",
"replaceAll": false
}
FieldTypeRequiredDefaultDescription
operationstringYes-Must be "replace"
searchstringYes-Text to search for
replacementstringYes-Replacement text (can be empty)
replaceAllbooleanNofalseReplace all occurrences (default: first only)

Append Operation

{
"operation": "append",
"text": "Text to add at end"
}

Prepend Operation

{
"operation": "prepend",
"text": "Text to add at beginning"
}

Insert Operation

{
"operation": "insert",
"text": "Text to insert",
"insertAt": 100
}
FieldTypeRequiredDescription
operationstringYesMust be "insert"
textstringYesText to insert
insertAtintegerYesCharacter position (0-based)

Response:

Returns the updated file metadata.

{
"id": "file-uuid-1234",
"libraryId": "lib-uuid-5678",
"filename": "index.html",
"size": 1456,
"updatedAt": "2024-02-13T10:20:00.000Z",
"modifiedAt": "2024-02-13T10:20:00.000Z"
}

Example - Replace Text:

PATCH /api/apps/analytics:sales-dashboard/contents/index.html
Content-Type: application/json

{
"operation": "replace",
"search": "<title>Old Title</title>",
"replacement": "<title>New Title</title>"
}

Example - Append Script Tag:

PATCH /api/apps/analytics:sales-dashboard/contents/index.html
Content-Type: application/json

{
"operation": "append",
"text": "\n<script src=\"analytics.js\"></script>"
}

Side Effects:

  • Does NOT modify the app row; bumps the affected file's modifiedAt, surfaced via the app's derived contentModifiedAt
  • File must exist (PATCH doesn't create new files)

Error Responses:

  • 400 Bad Request - Invalid operation or missing required fields
  • 404 Not Found - App or file doesn't exist
  • 403 Forbidden - User lacks write permission

GET /api/apps/{id}/files

List all files in the app's library.

Authentication: Required

Permissions Required: Member+ role (write permission)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID

Response:

Returns an array of file metadata objects.

[
{
"id": "file-uuid-1",
"libraryId": "lib-uuid-5678",
"filename": "index.html",
"directory": false,
"parentId": null,
"contentType": "text/html",
"size": 1234,
"createdAt": "2024-02-13T10:00:00.000Z",
"updatedAt": "2024-02-13T10:15:00.000Z"
},
{
"id": "file-uuid-2",
"libraryId": "lib-uuid-5678",
"filename": "styles.css",
"directory": false,
"parentId": "dir-uuid-1",
"contentType": "text/css",
"size": 567,
"createdAt": "2024-02-13T10:00:00.000Z",
"updatedAt": "2024-02-13T10:00:00.000Z"
},
{
"id": "dir-uuid-1",
"libraryId": "lib-uuid-5678",
"filename": "css",
"directory": true,
"parentId": null,
"createdAt": "2024-02-13T10:00:00.000Z",
"updatedAt": "2024-02-13T10:00:00.000Z"
}
]

Key Fields:

FieldDescription
directorytrue if this is a directory, false for files
parentIdUUID of parent directory (null for root level)
contentTypeMIME type (only present for files)
sizeFile size in bytes (only present for files)

Error Responses:

  • 404 Not Found - App doesn't exist or user lacks access
  • 403 Forbidden - User lacks write permission

DELETE /api/apps/{id}/files/{fileId}

Delete a file or directory from the app's library.

Authentication: Required

Permissions Required: Member+ role (write permission)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID
fileIdstringFile UUID (from the files list)

Response:

Returns 204 No Content on success.

Behavior:

  • Deleting a directory also deletes all contained files and subdirectories
  • Does NOT modify the app row; bumps the affected file's modifiedAt, surfaced via the app's derived contentModifiedAt

Error Responses:

  • 404 Not Found - App or file doesn't exist
  • 403 Forbidden - User lacks write permission

POST /api/apps/{id}/files/_clear

Bulk-delete every file in the app's library in a single transaction. Intended for callers that need to wipe the library before re-uploading a fresh tree (e.g. CLI deploy workflows). One round-trip replaces N parallel per-file DELETEs; like the per-file path, the bulk-clear leaves the app row untouched (see Content Recency above).

Authentication: Required

Permissions Required: Member+ role (write permission). Same level as the per-file DELETE — bulk-clear is semantically a multi-row variant of it, not an app-lifecycle action.

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID

Response:

Returns 204 No Content on success.

Behavior:

  • Files and directories are removed in a single SQL DELETE statement scoped to the app's library; the per-row file.parentId → file.id cascade clears the whole tree without ordering concerns.
  • Large objects (LOBs) for files with stored content are unlinked via the per-row delete_file_lob trigger as part of the cascade.
  • The bulk delete does NOT touch the app row. After it completes the library is empty, so the app's derived contentModifiedAt becomes null.
  • Idempotent: calling on an already-empty library returns 204 with no state change.

Error Responses:

  • 400 Bad Request - The app has no associated library
  • 403 Forbidden - User lacks write permission
  • 404 Not Found - App doesn't exist

Examples:

curl -X POST "${SERVER_URL}/api/apps/myapp/files/_clear" \
-H "Authorization: Basic ${AUTH}"

POST /api/apps/{id}/files

Create a file or directory under a given parentId. Polymorphic — passing directory: true makes a directory; otherwise an empty file is created. Both share the same endpoint because directories are just File rows with directory: true.

Authentication: Required

Permissions Required: Member+ role (write permission)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID

Request Body:

FieldTypeRequiredDefaultDescription
filenamestringYes-Single-segment basename (no slashes). Max 255 chars.
parentIdstring | nullNonullContaining directory id; null means library root
directorybooleanNofalsetrue to create a directory; false for a file

Response:

Returns the created File row on 201 Created.

Behavior:

  • Rejects sibling-name collisions with 409 Conflict (no silent overwrite — PUT /contents is the upsert endpoint)
  • Files get an empty content blob; reads of new files succeed and return zero bytes
  • parentId must reference an existing directory in the same library, or be null
  • Does NOT modify the app row; bumps the affected file's modifiedAt, surfaced via the app's derived contentModifiedAt

Error Responses:

  • 404 Not Found - App doesn't exist
  • 403 Forbidden - User lacks write permission
  • 409 Conflict - A file or directory with the same filename already exists under parentId
  • 400 Bad Request - Missing/invalid filename, unknown parentId, or parentId references a non-directory

Example - Create a file at the root:

curl -X POST "${SERVER_URL}/api/apps/myapp/files" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"filename": "index.html"}'

Example - Create a directory under another directory:

curl -X POST "${SERVER_URL}/api/apps/myapp/files" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"filename": "components", "parentId": "src-dir-uuid", "directory": true}'

PUT /api/apps/{id}/files/{fileId}

Rename and/or move a file or directory. Both filename and parentId are independently optional — pass either, both, or hit the no-op path by passing the current values.

Authentication: Required

Permissions Required: Member+ role (write permission)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID
fileIdstringFile UUID (from the files list)

Request Body:

FieldTypeDescription
filenamestringNew basename (single segment, no slashes, max 255 chars)
parentIdstring | nullNew parent directory id; null means library root

At least one of filename or parentId must be present.

Response:

Returns the updated file resource on 200 OK.

Behavior:

  • Pure rename (filename only), pure move (parentId only), or both in one call
  • For directories, children's paths follow automatically (paths derive from the parentId chain)
  • Cycle prevention: cannot move a directory into itself or any of its descendants
  • Does NOT modify the app row; bumps the affected file's modifiedAt, surfaced via the app's derived contentModifiedAt

Error Responses:

  • 404 Not Found - App or file doesn't exist
  • 403 Forbidden - User lacks write permission
  • 409 Conflict - A sibling with the new filename already exists under parentId
  • 400 Bad Request - Invalid filename, unknown parentId, parentId references a non-directory, or attempted cycle

Example - Rename in place:

curl -X PUT "${SERVER_URL}/api/apps/myapp/files/${fileId}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"filename": "Header.tsx"}'

Example - Move to a different directory:

curl -X PUT "${SERVER_URL}/api/apps/myapp/files/${fileId}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"parentId": "components-dir-uuid"}'

POST /api/apps/{id}/_upload

Upload a file using chunked multipart upload. This endpoint is designed for large file uploads from web browsers.

Authentication: Required

Permissions Required: Member+ role (write permission)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID

Payload:

Multipart form data with file chunks. Refer to the chunked upload implementation for specific field requirements.

Response:

Returns upload status and file metadata.

Use Case:

This endpoint is used by the file upload UI for:

  • Large binary files
  • Progress tracking
  • Resume capability

Error Responses:

  • 404 Not Found - App doesn't exist or user lacks access
  • 403 Forbidden - User lacks write permission

Common File Operations

Create a Simple App

// 1. Create the app
const app = await POST('/api/apps', {
type: 'report',
name: 'My Dashboard'
});

// 2. Add HTML file
await PUT(`/api/apps/${app.id}/contents/index.html`, {
content: '<!DOCTYPE html><html>...',
contentType: 'text/html'
});

// 3. Add CSS file
await PUT(`/api/apps/${app.id}/contents/styles.css`, {
content: 'body { margin: 0; }',
contentType: 'text/css'
});

// 4. Add JavaScript file
await PUT(`/api/apps/${app.id}/contents/app.js`, {
content: 'console.log("Ready");',
contentType: 'application/javascript'
});

Update a File

// Read current content
const response = await GET('/api/apps/analytics:sales-dashboard/contents/index.html');
const currentHTML = await response.text();

// Modify it
const updatedHTML = currentHTML.replace('Old Title', 'New Title');

// Write back
await PUT('/api/apps/analytics:sales-dashboard/contents/index.html', {
content: updatedHTML,
contentType: 'text/html'
});

Bulk Replace Across File

// Replace all occurrences of a string
await PATCH('/api/apps/analytics:sales-dashboard/contents/index.html', {
operation: 'replace',
search: 'oldClassName',
replacement: 'newClassName',
replaceAll: true
});

List and Delete Old Files

// Get all files
const files = await GET('/api/apps/analytics:sales-dashboard/files');

// Find old temp files
const tempFiles = files.filter(f => f.filename.startsWith('temp-'));

// Delete them
for (const file of tempFiles) {
await DELETE(`/api/apps/analytics:sales-dashboard/files/${file.id}`);
}