Guides
Storage
FileHutch owns the control plane. The bytes live in a bucket — ours or yours — and your application never learns which.
Two ways in#
Managed. Create a project and upload immediately. Cloudflare R2 underneath, with an allowance included in your plan.
Bring your own bucket. Connect one under Storage → Connect a bucket. Credentials are encrypted at rest and never returned after creation. FileHutch verifies by writing and deleting a probe object before it will use the bucket.
| Provider | What it needs |
|---|---|
| Cloudflare R2 | Account ID, keys |
| Amazon S3 | Region, keys |
| Backblaze B2 | Region, keys |
| Hetzner Object Storage | Region (fsn1, nbg1, hel1), keys |
| DigitalOcean Spaces | Region (nyc3, ams3, …), keys |
| Wasabi | Region, keys |
| Tigris | Keys |
| Google Cloud Storage | HMAC keys, from Settings → Interoperability |
| MinIO | Your server's URL, keys |
Every one of them stores files, signs URLs and moves between the others. Named transforms render only on R2 behind a Cloudflare zone; see Transforms.
CORS#
Browsers PUT directly to your bucket, so it has to allow your origin. The connection page prints the exact JSON for your bucket; it looks like this.
[{ "AllowedOrigins": ["https://app.example.com"],
"AllowedMethods": ["PUT", "GET", "HEAD"],
"AllowedHeaders": ["Content-Type", "Content-MD5", "x-amz-*"],
"ExposeHeaders": ["ETag"], "MaxAgeSeconds": 3600 }]If a browser upload fails with a network error, this is almost always why.
Importing files already in your bucket#
Connecting a bucket you already use does not copy anything, and neither does importing from it. Storage → Import existing files lists every object under a prefix and registers each one as a file where it is: same bucket, same key, a new file ID. Run it again to pick up objects added since; the ones already imported are left alone and keep their IDs.
When it finishes, download the key-to-ID map (CSV) and backfill your own column from it: every object key under the prefix, with the file ID that now stands for it.
POST /api/v1/storage_imports { "storage_connection": "conn_…", "prefix": "avatars/",
"visibility": "public", "policy": "avatars" }
GET /api/v1/storage_imports/imp_… # files_found, files_adopted, files_existing, files_failed
GET /api/v1/storage_imports/imp_…/files # { files: [{ key, file_id }], next_after }; pass after= for the next page
POST /api/v1/files/adopt { "storage_connection": "conn_…", "key": "avatars/42.png" }files/adopt does one object and answers 201 when it is new, 200 when it was
already a file, so a backfill that stops half-way can simply run again.
A policy given to an import sets the files' visibility and their named transforms. Its type and size limits guard new uploads and are not applied to what is already in the bucket. A file you deleted in FileHutch is not brought back by importing its key again.
Moving between buckets#
Storage → Move copies every ready file onto another connection, verifies each one, and repoints the record. File IDs do not change, so your application carries on without noticing. You can watch it fill in as it runs.
A move never deletes the source object. If something goes wrong, the originals are still there. Once you are satisfied, Storage offers to delete them — a separate, deliberate step that names how many objects it will remove and leaves your files exactly where they are now. Re-running a move picks up whatever is still elsewhere, so an interrupted move is safe to repeat.
POST /api/v1/storage_migrations { "target": "conn_…" }
GET /api/v1/storage_migrations # files_total, files_done, files_failedLeaving#
GET /api/v1/manifest lists every ready file with its object key, checksum and
metadata. With a bucket you own and that manifest, you have everything you need
without asking us for anything.