Storage
Global file storage for the workspace. Upload, organize, and share files across apps and users. PostgreSQL-backed (BYTEA), organized in buckets, and protected by the same RBAC system as everything else.
How It Works
Files are stored in buckets. Every workspace starts with a default bucket. You can create additional buckets to organize files by purpose (invoices, contracts, assets) and assign granular permissions per bucket.
Any authenticated app, agent, or user can read and write to storage if they have the appropriate permission. Files persist independently of apps. Uninstalling an app does not remove files it uploaded.
Buckets
A bucket is a logical container with optional constraints.
| Property | Description |
|---|---|
name |
Unique identifier. Alphanumeric, dashes, underscores. 1-63 chars. |
public |
If true, objects in this bucket can be downloaded without authentication. |
max_file_size |
Maximum upload size in bytes. null for no limit (up to 64 MB global max). |
allowed_types |
Array of allowed MIME types. Supports wildcards like image/*. null allows any type. |
Permissions
Storage uses the platform RBAC system. Four permission keys are registered automatically:
| Permission | Grants |
|---|---|
storage:read |
List buckets, list objects, download files. |
storage:write |
Upload files. |
storage:delete |
Delete files. |
storage:admin |
Create and delete buckets. |
Per-bucket permissions are supported via the standard wildcard convention:
storage:invoices:readgrants read access only to theinvoicesbucket.storage:*grants full access to all buckets.*(global admin) grants everything.
Assign these to roles like any other permission key. See RBAC for details.
Using the Console
Manage storage from the Storage panel in the sidebar. The Files tab lets you browse objects, upload files, and download or delete them. The Buckets tab lets you create and remove buckets.
Using Code
Create a Bucket
POST /api/v1/storage/buckets
{
"name": "contracts",
"public": false,
"max_file_size": 10485760,
"allowed_types": ["application/pdf", "image/*"]
}
List Buckets
GET /api/v1/storage/buckets
Delete a Bucket
DELETE /api/v1/storage/buckets/{name}
Fails with 409 Conflict if the bucket contains objects. Delete all objects first.
Upload a File
curl -X POST https://<your-ref>.rootcx.com/api/v1/storage/objects/contracts \
-H "Authorization: Bearer $TOKEN" \
-F "file=@contract-2026.pdf"
{
"id": "a1b2c3d4-...",
"bucket": "contracts",
"path": "contract-2026.pdf",
"name": "contract-2026.pdf",
"content_type": "application/pdf",
"size": 84521
}
The file path defaults to the filename. Uploading to the same path in the same bucket returns an error. Delete the existing object first to replace it.
List Objects
GET /api/v1/storage/objects/{bucket}?prefix=invoices/&limit=50&offset=0
{
"data": [{ "id": "...", "bucket": "contracts", "path": "...", "name": "...", "size": 84521, "content_type": "application/pdf", "created_at": "..." }],
"total": 12
}
Use prefix to filter by path prefix (virtual folder navigation).
Download a File
GET /api/v1/storage/objects/{bucket}/{id}
Returns the raw file bytes with Content-Type, Content-Disposition, and Content-Length headers.
Public buckets allow unauthenticated downloads.
Delete a File
DELETE /api/v1/storage/objects/{bucket}/{id}
From the SDK
const client = useRuntimeClient();
// Upload via fetch (multipart)
const form = new FormData();
form.append("file", file);
await fetch(`${client.getBaseUrl()}/api/v1/storage/objects/contracts`, {
method: "POST",
headers: { Authorization: `Bearer ${client.getAccessToken()}` },
body: form,
});
// List
const { data, total } = await client.fetchJson(
`${client.getBaseUrl()}/api/v1/storage/objects/contracts?limit=50`
);
From a Backend Worker
Use caller.authToken for authenticated requests to storage, just like any other Core API call:
async function dispatch(method: string, params: any, caller: Caller) {
if (method === "save-report") {
const form = new FormData();
form.append("file", new Blob([params.csv], { type: "text/csv" }), "report.csv");
await fetch(`${runtimeUrl}/api/v1/storage/objects/reports`, {
method: "POST",
headers: { Authorization: `Bearer ${caller.authToken}` },
body: form,
});
return { saved: true };
}
}
Storage vs App Files
RootCX has two file storage mechanisms:
| Platform Storage | App Files | |
|---|---|---|
| Scope | Global workspace | Per-app |
| Lifecycle | Survives app uninstall | Deleted with app |
| Access | Any app/user via RBAC | Only the owning app |
| Use case | Shared documents, cross-app assets, org files | Attachments on records, agent working files |
| API | /api/v1/storage/objects/{bucket} |
/api/v1/apps/{appId}/storage/upload |
Use Platform Storage when multiple apps need to access the same files, or when files represent organizational assets that outlive any single app.
Use App Files for attachments tied to a specific record or agent session.