Storage

Files for your app's users, in storages you create, governed by the same policies and organizations as your data.

Your app can store files: avatars, invoice PDFs, CSV exports, anything your users upload or your handlers produce. Files live in storages you create in the console, inside folders, and every read and write is authorized against your app's policies, exactly as a row is.

There are three ways in, all onto the same rules:

DoorWho uses it
The REST API, /{appUid}/storage/...Your app's own users, from your frontend or backend. See Uploading and downloading.
sdk.storageYour handlers.
Agent toolsYour agents, derived from the same permissions as the REST API.

The console's Storage section is the fourth: you, the app's owner, browsing and managing every file.

Storages

A storage is a named container for files, such as Avatars, Invoice scans or Exports. Create as many as you like: storages themselves are free, and what counts against your plan is the total bytes stored.

To create one, open the app's Storage section and choose New storage:

SettingWhat it does
NameShown in the console and returned by the API. Unique within the app.
AccessPrivate (the default): links to a file are signed and expire. Public: links are permanent and anyone who has one can open the file.
Max file sizeRejects any single file larger than this. It caps each upload, not the storage's total. Leave it empty to use your plan's limit.
Allowed file typesComma-separated MIME types, with wildcards: image/*, application/pdf. Leave it empty to allow every type.

Each storage has a uid, which every API call that works inside it takes as storageUid. Find it by listing the storages over the API, or with sdk.storage.listStorages() in a handler.

Separate storages are how you give different files different rules. Avatars belong in a public storage limited to image/*; signed contracts belong in a private one limited to application/pdf. Permissions apply to all of your storages alike (see below), so use them to separate what is visible, not who may use it.

Public or private

Access is a property of the storage, so every file inside it follows it. There is no per-file setting: you can tell what a storage exposes at a glance.

PrivatePublic
A link to a fileSigned, and expires (15 minutes unless you ask for longer)Permanent
Can a browser or CDN cache itNoYes
Who can open itWhoever holds the link, until it expiresAnyone who has the link, without signing in
Use it forDocuments, user uploads, anything governedAvatars, logos, product images

Signing is right for a document and wrong for an avatar: a signed URL changes on every render, so nothing can cache it, and a 15-minute link in an email is dead before it is opened. That is what public storages are for. Don't put anything user-specific or confidential in one.

Switching a public storage back to private stops its permanent links from working at once. There is nothing to revoke and no cache to wait out.

Folders and files

Inside a storage, files sit in a tree of folders, each named by its uid and its path (/2026/q3). The storage's root is the folder with no uid.

  • Names are a single path segment: no / or \, not . or .., at most 255 characters.
  • Two files with the same name in one folder are kept apart automatically: a second report.pdf is stored as report (1).pdf. Two folders with the same name are refused with 409.
  • Every file has an owner: the user it belongs to, returned as owner_uid. A file a user uploads is theirs by default, which is what makes "users may only open their own files" a policy rather than code (below).
  • A file can carry metadata: a JSON object of your own, such as { "invoice_uid": "0199b8e4-…", "source": "email" }, returned with the file.

Deleting a file is soft by default: it disappears from every listing but its bytes are kept. A hard delete removes the bytes too, and cannot be undone.

Who can do what

Storage permissions are three switches in a policy, under People using the app:

SwitchGrantsPolicy keys
Read filesList storages and folders, read a file's details, get a link, downloadstorage:read_list, storage:list, storage:read
Upload filesUpload, create folders, rename, move, change metadata or ownerstorage:upload, storage:presign, storage:confirm, storage:create_folder, storage:update
Delete filesDelete a filestorage:delete

Reading and uploading are separate on purpose: you can let users fetch files without letting them add any. An agent whose role holds these keys gets the same abilities as tools, with no tool to write.

Limiting users to their own files. In the policy's advanced view, add the isOwner condition to the actions that name one file: storage:read, storage:update and storage:delete. A user can then open, change and delete only files whose owner_uid is theirs. Don't add it to the listing actions (storage:read_list, storage:list): a listing names no single file, so isOwner would refuse it outright.

Organizations

Your app's users each act in one of your app's organizations, and storage follows the same line as your tables. Your storages are shared containers: every organization can use them, and each sees only its own folders and files inside. Another organization's file is the same 404 as one that does not exist.

Your own folders and files, the ones you add in the console, are private to you until you share them with some or all organizations. See Organizations and sharing.

Limits

  • Total bytes. Everything stored counts against your workspace's storage, shared across your apps. An upload that would go over it is refused before any bytes are written, with 402 and SUBSCRIPTION_LIMIT_REACHED.
  • Per file. The largest file your plan allows, tightened by the app's own setting and by the storage's Max file size. A file over the limit is 413 with FILE_TOO_LARGE.
  • File types. A type outside the storage's allowed list is 415 with MIME_NOT_ALLOWED.

From handlers

sdk.storage in a handler works on the same storages, folders and files, under the same rules:

export default async function attachScan(data = {}, ctx) {
  const { sdk } = ctx;

  const file = await sdk.storage.upload(data.storageUid, {
    name: `${data.invoiceNumber}.pdf`,
    contentType: "application/pdf",
    body: data.pdfBase64,
    encoding: "base64",
    path: "/2026/q3", // created if missing
    metadata: { invoice_uid: data.invoiceUid },
  });

  const { url, kind, expiresAt } = await sdk.storage.url(file.uid);
  return { fileUid: file.uid, url, kind, expiresAt };
}

Reading files (list, get, url, download, search) needs the handler's sdk:storage_read capability, which is granted by default. Anything that writes or spends storage (upload, uploadFromUrl, createFolder, move, rename, update, delete) needs sdk:storage_write, which is not. See Capabilities and the Handler SDK reference for every method.

Next

On this page