ReboFilesDocs
Start3
What is ReboFiles
Getting started
Demos to run
Put it in your app6
React component
Headless: your own UI
Without a framework
Styling and themes
Keyboard and accessibility
Languages
Files9
Uploads
Previews and the viewer
Search
Live changes and large folders
Comments
Starred and fields
Versions, trash and space
Download as .zip
Word documents
The server14
The route
Frameworks and sign-in
Who may do what
Storage: S3, GCS, Azure and disk
Checking uploads
Metadata: SQLite and Postgres
Database schema
Sharing and links
Activity and notifications
Hooks, logs and limits
Retention, holds and export
Desktop and other origins
Sync a folder
Going to production
Together1
AI assistants (MCP)
Reference5
Packages
Components and hooks
Client API
HTTP routes
Status and roadmap
↑↓ to move↵ to openEsc to close38 pages

The server Storage: S3, GCS, Azure and disk

Storage: S3, GCS, Azure and disk

The bytes go from the browser straight to the bucket and back, at addresses the server signs for a few minutes. That asks two things of the bucket: a key to sign with, and a rule that lets your pages in.
S3

The key

On Cloudflare R2: R2 → Create bucket, then Manage API tokens → Create API token with “Object Read & Write” for that bucket only.

.env
# The S3 API of the account, not the public address of the bucket (r2.dev)
S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
S3_BUCKET=acme-files
S3_REGION=auto
S3_ACCESS_KEY_ID=…
S3_SECRET_ACCESS_KEY=…
# Optional: before every key, to share the bucket with something else
S3_PREFIX=files/
# What tokens are signed with: long, random, the same in every process
REBOFILES_SECRET=…

s3Storage(s3OptionsFrom(process.env)) reads them. On AWS, leave S3_ENDPOINT out and give the region; MinIO and Backblaze take the same variables.

CORS

The rule of origins

In Settings → CORS policy, with the addresses your application is served from. ETag has to be exposed.

CORS policy
[
  {
    "AllowedOrigins": ["https://your-app.com", "http://localhost:3000"],
    "AllowedMethods": ["GET", "PUT"],
    "AllowedHeaders": ["content-type"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3600
  }
]

A lifecycle rule that aborts multipart uploads left unfinished after a day keeps the parts of an upload never finished from staying in the bucket.

GCS

Google Cloud Storage

Through its XML API, which speaks S3: the same addresses, signed with an HMAC key of the bucket.

import { gcsOptionsFrom, gcsStorage } from '@rebofiles/server';

// An HMAC key of the bucket: Cloud Storage → Settings → Interoperability.
// GCS_BUCKET, GCS_ACCESS_KEY_ID (GOOG1…), GCS_SECRET_ACCESS_KEY, GCS_PREFIX
createDriveHandler({ ...options, storage: gcsStorage(gcsOptionsFrom(process.env)) });

What that API does not do is done otherwise: each key deleted by itself, and every part of a new version sent, as it copies none. The bucket’s CORS, set with gcloud storage buckets update --cors-file, has to expose ETag.

Azure

Azure Blob Storage

A container of a storage account: addresses signed with the account’s key (a SAS of each blob, for minutes), each part a block, the file made of its list of blocks.

import { azureOptionsFrom, azureStorage } from '@rebofiles/server';

// AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_KEY, AZURE_STORAGE_CONTAINER, AZURE_STORAGE_PREFIX
createDriveHandler({ ...options, storage: azureStorage(azureOptionsFrom(process.env)) });
  • Uploads resumed from the blocks Azure lists, and a new version sent with only its parts that changed (Put Block From URL).
  • Blocks of an upload never finished go by themselves after a week: there is nothing to sweep.
  • The account’s CORS (Resource sharing) lets in the origins of your pages, with PUT, GET and HEAD; no header has to be exposed, as the blocks are put together by their numbers.
  • Tried against Azurite, Microsoft’s emulator, in the tests of the server.
Disk

On the disk

diskStorage({ folder, secret }) keeps the bytes in a folder, and the server carries them itself, at addresses of its own: for one machine, a demo, or a test.

Errors

When something fails

S3_ENDPOINT is the public address of the bucket (r2.dev)

The S3 API of R2 is at https://<account-id>.r2.cloudflarestorage.com, on the bucket’s settings page.

The part could not be sent. If the bytes go to a bucket, check that it lets this origin in.

The rule of origins: the address of the page is missing from AllowedOrigins, or content-type from AllowedHeaders.

The bucket did not let the page read the ETag of a part.

Small files go up and large ones do not: ExposeHeaders needs ETag.

The bucket answered 403 to …

The key: it may not write in that bucket, or the endpoint, region or bucket name are not the ones it was made for.

There is no room for it: … are used, and it is ….

The person’s quota (413 over-quota): more room in the settings of the drive, or less in their files and their trash.

In the repository, npm run storage:check goes the whole way a file goes against the bucket of apps/storage/.env and says what the bucket answered at the step that failed.