Skip to content

File Storage

Butterbase provides file storage with presigned URLs. Files are organized per-app and per-user. Your frontend uploads and downloads files directly — file data never flows through your backend code.

Storage is co-located with your app’s region, so uploads and downloads stay close to your users.

  1. Request an upload URL — Your app asks Butterbase for a presigned upload URL, providing the filename, content type, and size.
  2. Upload directly — Your frontend uses the presigned URL to upload the file directly to storage.
  3. Reference the file — Store the returned objectId in your database (e.g., as an image_url column).
  4. Download when needed — Request a presigned download URL using the object ID.
ValueWhat it isWhat to do with it
objectIdA stable UUID for this filePersist this in your database. Use it for downloads and deletes.
objectKeyThe path inside the bucketNot a URL. Metadata only; do not store for display.
uploadUrl / downloadUrlTemporary presigned HTTPS URLsUse only for immediate operations. They expire.
  • Saving objectKey as a URL — It’s a path, not a URL. The UI will show broken images.
  • Using objectKey as img src — Use objectId with the download endpoint to get a downloadUrl.
  • Storing only a presigned URL — Presigned URLs expire. Store objectId as the source of truth.

Step 1: Request the upload URL.

POST /storage/{app_id}/upload
Authorization: Bearer {token}
{
"filename": "profile.jpg",
"contentType": "image/jpeg",
"sizeBytes": 102400,
"public": false
}

Set public: true to mark the file as downloadable by any authenticated user. See Public files.

Response:

{
"uploadUrl": "https://storage.example.com/...",
"objectKey": "app_id/user_id/uuid_profile.jpg",
"objectId": "uuid",
"expiresIn": 300
}

Step 2: Upload the file using the presigned URL.

await fetch(uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': 'image/jpeg' },
body: fileBlob
});

Step 3: Save the objectId in your database.

GET /storage/{app_id}/download/{object_id}
Authorization: Bearer {token}

Response:

{
"downloadUrl": "https://storage.example.com/...",
"filename": "profile.jpg",
"expiresIn": 3600
}

After loading rows that reference stored files by objectId:

  1. For each file, call the download API or SDK getDownloadUrl(objectId).
  2. Use the returned downloadUrl as <img src> or download link.
  3. For lists, resolve download URLs in parallel (Promise.all) for speed.
GET /storage/{app_id}/objects
Authorization: Bearer {token}

Returns an array of objects with id, filename, content_type, size_bytes, and created_at.

LimitDefault
Max file size10 MB per file
Total storage1 GB per app
Allowed content typesAll types (configurable)
  • Service key: Full access to all files. Uploads have no user association.
  • End-users: Can only see and manage their own files. Uploads are automatically associated with the authenticated user.

By default every uploaded file is private — only its uploader (or callers with a service key) can mint a download URL. There are two ways to make files publicly downloadable.

Set public: true in the upload request to mark a single file as publicly downloadable:

{
"filename": "post-image.jpg",
"contentType": "image/jpeg",
"sizeBytes": 204800,
"public": true
}

Any authenticated end-user can then call GET /storage/{app_id}/download/{object_id} regardless of who uploaded it.

Flip the app-level switch to make all files in the app readable by any authenticated user:

PATCH /v1/{app_id}/config/storage
Authorization: Bearer {token}
{ "publicReadEnabled": true }

When enabled:

  • Any authenticated user can download any file in the app.
  • Uploads and deletes remain user-scoped — users can still only manage their own files.
  • The per-object public flag becomes redundant.

A download URL is issued if any of the following is true:

  1. The caller authenticated with a service key (bb_sk_...).
  2. The app has publicReadEnabled: true.
  3. The object has public: true.
  4. The authenticated user owns the object.
  • Upload URLs expire after 5 minutes
  • Download URLs expire after 1 hour
const { data } = await butterbase.storage.upload(file);
const { data: url } = await butterbase.storage.getDownloadUrl(objectId);
const { data: objects } = await butterbase.storage.list();
await butterbase.storage.delete(objectId);
// Mark a file as publicly downloadable
await butterbase.storage.upload(file, 'avatar.png', { public: true });