Skip to main content
View as Markdown

Attachment Fields & Storage

An attachment field does not store a file. It stores a reference to one, and the bytes live in a bucket like any other upload. Understanding that indirection explains most of the behaviour on this page.

tables:
  - id: 1
    name: contracts
    fields:
      - id: 1
        name: document
        type: single-attachment
        bucket: documents
        allowedFileTypes: [application/pdf]
        maxFileSize: 10485760

Which Bucket

bucket names an entry in your buckets[] array. Omit it and the field writes to the implicit default bucket, whose visibility follows whether app.auth is configured — see Buckets Overview.

Two layers of limits now apply to the same upload, and both must pass:

Limit On the field On the bucket
Accepted MIME types allowedFileTypes allowedMimeTypes
Size ceiling maxFileSize maxFileSize, then STORAGE_MAX_FILE_SIZE

Declaring the tighter of the two on the field keeps the intent next to the column it constrains; the bucket limit remains the backstop for every field that points at it.

What the Column Holds

The file reaches storage through the ordinary upload endpoint, which returns a key of the form <uuid>-<filename>. That key — not the filename, not a URL — is what the column holds.

With storeMetadata: true the column instead holds an object carrying the key plus the metadata captured at upload (dimensions, duration). Either shape resolves to the same stored object; the difference is only how much you can render without a second request.

What the Record API Returns

Read a record and its attachment values come back decorated with a way to actually fetch the file. Which decoration you get depends on STORAGE_DEFAULT_ACCESS:

{
  "id": 1,
  "fields": {
    "document": {
      "key": "9f3c1e2a-7b44-4d10-9e21-8a6f0c1d2e3b-contract.pdf",
      "signedUrl": "https://app.example.com/api/buckets/default/signed?path=…&op=download&expires=…&token=…",
      "signedUrlExpiresAt": "2026-04-05T11:00:00Z"
    }
  }
}
Storage access mode Added to the value Lifetime
private (the default) signedUrl + signedUrlExpiresAt 1 hour
STORAGE_DEFAULT_ACCESS=public url — no token, no expiry Forever

The two are mutually exclusive by design: in public mode signedUrl is deliberately absent, so a client can rely on url never containing a token and on signedUrl always meaning "this expires".

Because the URL is minted per read, a record fetched an hour ago carries a link that has since died. Re-read the record rather than caching the URL — the key is the durable handle, the URL is not.

Images on an Attachment

An image attachment's signed URL takes transform parameters appended to it, so one stored original serves every size a page needs:

<signedUrl>&width=150&height=150&fit=cover

The token covers the path and the expiry, not the transform query, so appending parameters never invalidates it.

Deleting

Record deletion drives file deletion — see Lifecycle & Quotas for the full table. In short: a soft delete keeps the bytes, ?purge=true removes them, ?permanent=true does not, and replacing or clearing a single-attachment value deletes the file it displaced.

Last updated July 27, 2026

This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta — contributions and corrections are welcome.

Built with Sovrium