Skip to content

File upload fields

A file field is an ordinary string property with a storage block. The column holds text — the object’s key in the bucket, or its URL if you ask for that — and the block says where the bytes go and what the form will accept.

That split matters more than it looks. Most of storage is read by the admin panel’s uploader on its way to the API, so a write that does not come from a form does not go through it — those options carry the label below. The two that decide what a file may be, maxSize and acceptedFiles, are the exception: the server checks them on every upload into the property’s storagePath.

avatar: {
type: "string",
name: "Main image",
storage: {
storagePath: "avatars",
acceptedFiles: ["image/*"],
maxSize: 2 * 1024 * 1024
}
}

An array of the same thing. The storage block moves to of, because it describes each element rather than the list:

images: {
type: "array",
name: "Images",
of: {
type: "string",
storage: { storagePath: "images", acceptedFiles: ["image/*"] }
}
}

Everything inside storage.

Property Type Description
storagePath string | function Required. Where in the bucket the file goes. A string with placeholders — {file}, {file.name}, {file.ext}, {rand}, {entityId}, {propertyKey}, {path} — or a function of the upload’s context. Admin form only — not enforced by the API or the database: it is the panel’s uploader that resolves it, and client.storage.upload() names its own key. A string path is also where the server holds every upload to this property’s maxSize and acceptedFiles
fileName string | function The uploaded object’s file name, with the same placeholders. Admin form only — not enforced by the API or the database.
storageSource string Which registered bucket, when a project has more than one. The bucket("media") handle works here too
public boolean Store under the public prefix and serve through a stable, token-less, cacheable URL. false by default, which means private objects and short-lived signed URLs
acceptedFiles FileType[] MIME types this property takes. The asterisk form works — image/*. Enforced by the server for every upload into the property’s storagePath — see What the server checks
maxSize number Largest accepted file, in bytes. Enforced by the server, like acceptedFiles, on the plain and the resumable upload paths alike
metadata Record<string, unknown> Object metadata to store alongside the file
includeBucketUrl boolean Write s3://my-bucket/path/to/file.png into the column instead of path/to/file.png. false by default
storeUrl boolean Write the download URL into the column instead of the key. false by default, and worth leaving that way: a URL that carries a token stops working when the token does, and the original reference is then gone
imageResize ImageResize Resize and crop before uploading, for image/jpeg, image/png and image/webp. Admin form only — not enforced by the API or the database.
processFile (file: File) => Promise<File> Transform the file before it is uploaded. Runs in the browser. Admin form only — not enforced by the API or the database.
postProcess (pathOrUrl: string) => Promise<string> Transform the resolved path or URL before it is saved. Runs in the browser. Admin form only — not enforced by the API or the database.
previewUrl (fileName: string) => string Build the preview URL, for when the file’s real address is not the field’s value. Admin form only — not enforced by the API or the database.

imageResize takes maxWidth and maxHeight in pixels, a mode of "contain" (scale down to fit, the default) or "cover" (fill the bounds and crop), a format of "original", "jpeg", "png" or "webp", and a quality between 0 and 100 for the lossy formats — 80 by default. It happens in the browser, before the bytes are sent, so an upload that did not come from the panel is stored at its original size.

maxSize and acceptedFiles used to be enforced by exactly one thing: the browser. The upload route knew a single global size cap and nothing about the property a file was destined for, so curl -F file=@payload.exe past the picker put a 40 MB executable in the avatar bucket and the config that said otherwise was never consulted.

Both are now checked by the server against where the file lands. On 0.23 they are checked only against the property an upload names, and an upload that names none meets the source’s own cap alone. A property’s storagePath is where its files go, so it is also where its limits hold: every upload whose key is a file directly under that path — from the panel, client.storage.putObject, a resumable upload, Studio’s storage browser or curl — meets them, whether or not it names the property, on the property’s own storage source. Placeholders match whatever the panel would fill in: {entityId}, {rand}, {file} and the like stand for one path segment, {path} for any number. A public: true property’s path is under public/.

  • Several properties sharing one path: a file one of them accepts is accepted.
  • An upload that also names its property (collection + property, as putObject({ context }) sends them) is held to that property too. Naming a looser property does not unlock a stricter one’s path. The panel’s uploads (file fields, table cells and markdown images) always name it.
  • A storagePath written as a function cannot be read without running it, so it has no server-side rule: those limits hold only for uploads that name the property, which the panel’s do.
  • A path no property claims is held to the source’s own limit (STORAGE_MAX_FILE_SIZE) alone.

A markdown property (admin.markdown: true) with a storage block takes image uploads in its editor. The text stores each image as a storage reference — ![](rebase-storage:posts/abc_cover.png) — not as a URL: a private file’s URL carries a download token that expires in minutes, and the text is saved. The admin panel’s editor and previews exchange each reference for a fresh URL when they show it; a site that renders the field does the same with resolveStorageReferences(text, client) — see Files inside text.

Text saved before references holds the download URLs themselves, with expired tokens. The editor, the previews and resolveStorageReferences renew them when they show the text, because a token still names its object; re-inserting the image or rewriting the text with storageReferencesForDownloadUrls(text) stores a reference.

storagePath is a default, not a boundary: nothing on the server confines an upload to that prefix. Deciding who may write where is what a storage authorization rule is for — see Storage.

Clearing a file field, replacing its file, or deleting the row it belongs to leaves the stored object where it is. Every upload gets a key of its own, so a replacement writes a new object rather than overwriting the old one, and nothing removes the previous one: two rows can hold the same key, so the panel cannot know a file is unused. Objects no row points at stay in the bucket, and are billed, until you delete them — from Studio’s storage browser, with client.storage.deleteObject(key), or with a lifecycle rule on the bucket. Since 0.25 rebase storage orphans lists the files no row names, and deletes them with --delete — see the CLI.

Moving a property to another storage source

Section titled “Moving a property to another storage source”

A file field stores the object’s key, not its source. Changing a property’s storageSource points every existing value at the new source, where those keys do not exist, so previews of files uploaded before the change answer “not found”. Copy the objects to the new source under the same keys before changing the property — rebase storage move --from <old> --to <new> --prefix <storagePath>, or the provider’s own tooling, such as aws s3 sync or gcloud storage cp — or keep the old source for the existing files.

  • Properties — every property type and its options
  • Storage — buckets, signed URLs, and what the server does with an upload
  • Security Rules (RLS) — who may read the rows these files hang off