[data-reveal]{opacity:1!important;transform:none!important}
File storage
Components

File storage

Configure native file and image fields, understand page/component authorization, and handle staged uploads and save retries.

The file and image DataTypes (Data types) give forms their native upload fields. Uploads start on a temporary staged key and are promoted during an authorized save. The DMS records each upload's tenant and originating page/component; knowing a resource key alone does not authorize a read.

The storage backend

The DMS talks to storage through the AntelopeJS interface-file-storage contract: load any module that implements it in your antelope.config.ts, and the file routes pick it up — nothing to wire on the DMS side. Each backend carries its own config; when it defines several named storages, a FileType's storage option selects one, and omitting it uses the default.

The browser uploads and downloads through the storage's signed URLs. The dashboard's frontend server proxies those of @antelopejs/file-storage-local, /file-storage/upload/** and /file-storage/files/**, without the member's session: the token in the URL authenticates them. A backend reachable only through the frontend then still works with the storage's baseUrl set to the dashboard's public URL. Uploads through the frontend stop at 500 MiB, with a 413, whatever uploads.maxSize says. On AntelopeJS Cloud the limit is lower: Cloudflare, in front of the dashboard, refuses a request body over 100 MB with its own 413. See Routing and the backend proxy.

In production, avoid a local-disk backend: the files live on one machine's filesystem, so they don't survive an instance being replaced and can't be shared between instances. Use a remote storage service.

Declare a file field

A file field is a form field whose type is a FileType:

{
  id: "avatar",
  label: "Avatar",
  type: new DefaultDataTypes.FileType({
    multiple: false,
    constraints: { maxSize: 2 * 1024 * 1024, allowedMimetypes: ["image/*"] },
  }),
}

FileType options:

OptionTypePurpose
multiplebooleanAccept several files; the value becomes an array of keys.
constraintsUploadConstraintsField-level maxSize (bytes) and allowedMimetypes (wildcards like image/* supported), checked against stored metadata before persistence. Global upload limits still apply when omitted.
pathstringStorage path the uploaded keys are filed under.
storagestringNamed storage to upload to, when the backend defines several. Omit for the default.
visibility"private" | "public"Object visibility, default "private". Explicitly public objects are readable through their storage URL even before a record saves.
attachmentFieldstringStable field identity for a custom save integration. It must match the server-declared AttachmentField.id; page registration derives an identity when omitted.
fallbackstringText rendered in place of an empty value (table cells, read views).

The upload lifecycle

From the user picking a file to the record holding its final key:

  1. Upload to staging. The form sends filename, size, mimetype, and its signed uploadToken to /api/files/presign. The server takes the destination and visibility from the registered field, checks live tenant membership, page/component access, and the upload's write permission, and rejects with 400 a declared size or mimetype that breaks the field's constraints (on top of the global uploads limits). It then issues a server-generated staged key. The browser uploads the bytes to the returned storage URL. The upload token itself grants no reads.
  2. Promote on save. After the route's write guards pass, the native save handler validates file provenance, promotes staged keys, checks the canonical object's metadata against the field constraints (the presign check trusts the client's declared size and type, so this one is authoritative), and passes the promoted values to persistence. A reference whose bytes never reached storage — the upload failed or never ran — is refused with 400; a provenance or access failure stays 403. A retry using the original staged reference requires the storage provider's trusted promotion replay; an unrelated object at the final key is not a valid substitute.
  3. Cleanup after persistence. Editing or deleting a row removes classified native files no longer present in the successfully saved document. Cleanup failures are logged. This is not reference-counted storage: applications that share a key across records must account for that sharing before deleting it.
If persistence fails after promotion, the promoted objects are retained, not moved back to staging or deleted: another save may already reference them. They retain their declared visibility, including public visibility. A failed save is not a guarantee that uploaded bytes disappear.

An abandoned upload leaves its staged object for the storage backend's garbage collection. That cleanup is separate from files already promoted during a failed save.

Custom save routes

TableView routes integrate native promotion and cleanup automatically. For a standalone Form or custom write route, call SaveComponentFiles from @antelopejs/interface-dms/attachments after authenticating and authorizing the write.

Supply the trusted request context, component IDs, server-declared fields, submitted values, and the previously stored document. Each field declares its id, document key, kind, and applicable storage, visibility, and constraints. The save callback receives the promoted document and returns { document, result }: document must describe what was actually persisted, because cleanup compares it with the previous values. Never accept the component identity, field declarations, or previous document from the client.

Upload tokens and custom components

Form and TableView use StampUploadFieldTokens from @antelopejs/interface-dms/uploads during serialization, including embedded forms such as a relation field's inline add form. A custom component that serializes native fields can attach the same helper through its builder's transformOptions.

Serialization alone does not authorize an upload. Page registration binds the field to its exact page/component mount and signs the authoritative declaration. An unbound token, a disabled upload field, or a field whose mount has been disposed cannot authorize a native upload.

Read authorization and visibility

/api/files/metadata resolves the saved upload provenance and checks the current tenant, page access, and component permissions before returning metadata and a storage read URL. It does not delegate to the document's GET route: an authorized component reader can read an uploaded file before the document saves, even if that reader cannot read the eventual row.

Private read URLs expire within 60 seconds; the provider must return a finite expiry within that bound. Explicitly public objects have public storage URLs, including while staged. The metadata endpoint still checks DMS access before returning either kind of URL, but that check cannot make an already public storage URL private.

Unclassified legacy keys do not gain native read access. A custom save can retain an existing legacy value, but cannot introduce an unknown key as a new native attachment. The dms-media module's asset fields use their own media-library access rules and are not native file/image registrations.

Storage paths are a layout choice, not a security boundary. Native file isolation depends on recorded upload provenance and live tenant/page/component authorization, not on unguessable keys or a tenant-looking path.