DocsPlatformStorage

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:read grants read access only to the invoices bucket.
  • 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.