Skip to content

Storage & Files

The client.storage module provides methods for file management — upload, download, listing, and deletion. It works with both local disk and S3-compatible storage backends, depending on your server configuration.

All storage methods use the shared transport, so authentication tokens are injected automatically.

Use putObject() to upload a file. It accepts a File or Blob object along with an optional storage key and metadata:

const result = await client.storage.putObject({
file: fileObject, // File or Blob
key: "products/images/camera.jpg", // Storage path (optional)
bucket: "uploads", // Bucket name (optional)
public: false, // Store public (permanent token-less URL) — optional, default false
metadata: { // Custom metadata (optional)
description: "Product photo",
uploadedBy: "user-123"
}
});
// result: { key: string; bucket: string; storageUrl: string }

A file over 8 MiB goes up through the server’s resumable (TUS) endpoint, 8 MiB per request, instead of as one multipart request the server parses whole — so a large upload costs the server one chunk of memory, not three to five times the file. A chunk that fails on the way is sent again from where the server got to. The result, the stored object and the server’s checks are the same either way. When the resumable endpoint cannot take the file (an older server, or several instances that do not share uploads), it goes as one POST /api/storage/upload, and so does every later file from that client. See Resumable uploads (TUS).

const input = document.querySelector<HTMLInputElement>("#file-input");
const file = input?.files?.[0];
if (file) {
const result = await client.storage.putObject({
file,
key: `avatars/${userId}/${file.name}`
});
console.log("Uploaded to:", result.key);
}

Retrieve a download URL and metadata for a stored file:

const { url, metadata, fileNotFound } = await client.storage.getSignedUrl(
"products/images/camera.jpg"
);
if (url) {
console.log("Download URL:", url);
console.log("Content type:", metadata?.contentType);
} else {
console.log("File not found");
}

With a specific bucket:

const { url } = await client.storage.getSignedUrl(
"camera.jpg",
"product-images" // bucket
);

The SDK caches signed URLs to avoid redundant server calls, until each one’s download token expires. Lookups made together — a page of previews calling getSignedUrl() in the same tick — go to the server as one POST /api/storage/metadata per 100 files, which the rate limiter counts as a tenth of the requests it replaces; a file that one caller may not read fails that call alone. The cache belongs to the credential that asked: when the access token changes — a sign-out, another user signing in, a refresh — it starts over, so one user is never handed a URL minted for another.

  • Private files get a URL with a short-lived, path-scoped download token (?token=…, 5 minutes unless the server sets STORAGE_DOWNLOAD_TOKEN_TTL, which 0.23 does not read) — never your access token. Because it expires, don’t persist a private URL; store the file path and call getSignedUrl() again when you render it.
  • Public files (stored under the public/ prefix — set storage: { public: true } on the property, or pass public: true to putObject) get a stable, token-less, permanent, CDN-cacheable URL with no server round-trip. These are safe to store in a database and hotlink.

Text that embeds a file — the images in a markdown field — cannot hold a private URL either, so it holds a storage reference instead: rebase-storage:posts/cover.png, with ?storageId=media when the file is in a named source. That is what the admin panel’s markdown editor writes for an uploaded image. Exchange the references for URLs when you render the text:

import { resolveStorageReferences } from "@rebasepro/client";
const post = await client.data.collection("posts").findById("post-1");
const body = await resolveStorageReferences(String(post?.body ?? ""), client);
// `body` is the same markdown, each reference replaced by a fresh URL.

Each object is signed once per call, from its own source, so a private image keeps working however long ago it was uploaded. A reference that names an object that no longer exists is left as it is. storageReference(key, storageId?) builds one, for text you write yourself.

Text saved before references existed holds a private file’s download URL itself, …/storage/file/posts/cover.png?token=…, whose token expired minutes after it was written. resolveStorageReferences renews those too: the token still names the object, so the URL is replaced by a fresh one. To store references instead, rewrite the text once with storageReferencesForDownloadUrls(text), which leaves every other URL as it is.

Retrieve a file as a File object:

const file = await client.storage.getObject("products/images/camera.jpg");
if (file) {
console.log("File name:", file.name);
console.log("File type:", file.type);
console.log("File size:", file.size);
// Create a download link
const url = URL.createObjectURL(file);
window.open(url);
} else {
console.log("File not found");
}

With a specific bucket:

const file = await client.storage.getObject("camera.jpg", "product-images");
await client.storage.deleteObject("products/images/camera.jpg");
// With bucket
await client.storage.deleteObject("camera.jpg", "product-images");

Deleting a non-existent file does not throw an error.

List files by prefix, with optional pagination:

const result = await client.storage.listObjects("products/images/", {
bucket: "uploads",
maxResults: 50,
pageToken: undefined // for pagination
});
for (const item of result.items) {
console.log(item.fullPath, item.name, item.size, item.updated, item.contentType);
}
// Paginate
if (result.nextPageToken) {
const nextPage = await client.storage.listObjects("products/images/", {
pageToken: result.nextPageToken
});
}

Each item carries its size in bytes and updated (an ISO 8601 timestamp) on local storage, S3 and GCS, and its contentType on local storage and GCS — enough to show a folder without a getSignedUrl() per file. An S3 listing has no content type; folders (result.prefixes) carry none of the three.

The SDK transparently handles storage key prefixes. You can pass keys with or without the protocol prefix:

// All equivalent — the SDK strips the prefix internally
await client.storage.getSignedUrl("local://products/image.jpg");
await client.storage.getSignedUrl("s3://products/image.jpg");
await client.storage.getSignedUrl("products/image.jpg");
Method Description Returns
putObject({ file, key?, bucket?, metadata? }) Upload a file UploadFileResult
getSignedUrl(key, bucket?) Get download URL + metadata DownloadConfig
getObject(key, bucket?) Download as File object File | null
deleteObject(key, bucket?) Delete a file void
listObjects(prefix, options?) List files by prefix StorageListResult