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:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
path | string | File path within the library (e.g., index.html, css/styles.css) |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
download | boolean | false | Force 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 library404 Not Found- App doesn't exist, user lacks access, or file not found403 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:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
path | string | File path within the library |
Payload:
{
"content": "<!DOCTYPE html>\n<html>...",
"contentType": "text/html",
"encoding": "utf8"
}
Payload Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
content | string | Yes | - | File content (can be empty string) |
contentType | string | No | Auto-detect | MIME type |
encoding | string | No | utf8 | utf8 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 derivedcontentModifiedAt - Creates parent directories as needed
- Overwrites existing file if present
Error Responses:
400 Bad Request- App has no library, or invalid payload404 Not Found- App doesn't exist or user lacks access403 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:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
path | string | File path within the library |
Payload:
The payload structure depends on the operation:
Replace Operation
{
"operation": "replace",
"search": "oldText",
"replacement": "newText",
"replaceAll": false
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
operation | string | Yes | - | Must be "replace" |
search | string | Yes | - | Text to search for |
replacement | string | Yes | - | Replacement text (can be empty) |
replaceAll | boolean | No | false | Replace 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
}
| Field | Type | Required | Description |
|---|---|---|---|
operation | string | Yes | Must be "insert" |
text | string | Yes | Text to insert |
insertAt | integer | Yes | Character 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 derivedcontentModifiedAt - File must exist (PATCH doesn't create new files)
Error Responses:
400 Bad Request- Invalid operation or missing required fields404 Not Found- App or file doesn't exist403 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:
| Parameter | Type | Description |
|---|---|---|
id | string | App 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:
| Field | Description |
|---|---|
directory | true if this is a directory, false for files |
parentId | UUID of parent directory (null for root level) |
contentType | MIME type (only present for files) |
size | File size in bytes (only present for files) |
Error Responses:
404 Not Found- App doesn't exist or user lacks access403 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:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
fileId | string | File 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 derivedcontentModifiedAt
Error Responses:
404 Not Found- App or file doesn't exist403 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:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
Response:
Returns 204 No Content on success.
Behavior:
- Files and directories are removed in a single SQL
DELETEstatement scoped to the app's library; the per-rowfile.parentId → file.idcascade clears the whole tree without ordering concerns. - Large objects (LOBs) for files with stored content are unlinked via the
per-row
delete_file_lobtrigger 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
contentModifiedAtbecomesnull. - Idempotent: calling on an already-empty library returns
204with no state change.
Error Responses:
400 Bad Request- The app has no associated library403 Forbidden- User lacks write permission404 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:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
Request Body:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
filename | string | Yes | - | Single-segment basename (no slashes). Max 255 chars. |
parentId | string | null | No | null | Containing directory id; null means library root |
directory | boolean | No | false | true 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 /contentsis the upsert endpoint) - Files get an empty content blob; reads of new files succeed and return zero bytes
parentIdmust reference an existing directory in the same library, or benull- Does NOT modify the app row; bumps the affected file's
modifiedAt, surfaced via the app's derivedcontentModifiedAt
Error Responses:
404 Not Found- App doesn't exist403 Forbidden- User lacks write permission409 Conflict- A file or directory with the samefilenamealready exists underparentId400 Bad Request- Missing/invalidfilename, unknownparentId, orparentIdreferences 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:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
fileId | string | File UUID (from the files list) |
Request Body:
| Field | Type | Description |
|---|---|---|
filename | string | New basename (single segment, no slashes, max 255 chars) |
parentId | string | null | New 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 (
filenameonly), pure move (parentIdonly), 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 derivedcontentModifiedAt
Error Responses:
404 Not Found- App or file doesn't exist403 Forbidden- User lacks write permission409 Conflict- A sibling with the newfilenamealready exists underparentId400 Bad Request- Invalid filename, unknownparentId,parentIdreferences 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:
| Parameter | Type | Description |
|---|---|---|
id | string | App 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 access403 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}`);
}