File infrastructure for apps that aren't Netflix.
Uploads, private files, signed URLs and image transforms. Your application stores one opaque ID and forgets file plumbing exists.
2 GB free. Managed storage, or connect your own R2, S3 or B2 bucket.
users
- avatar_file_id
- "file_8f2kq1x9"
- What that column holds without a control plane
-
- https://bucket.r2.cloudflarestorage.com/projects/a91/f22/avatar.png
- s3://acme-uploads-prod/users/8812/avatar.png
- https://res.cloudinary.com/acme/image/upload/w_256,h_256,c_fill/v1/avatar
- /rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6...
Every line below the first is a provider, a bucket, a region or a transform baked into your database. Change any of them and you are writing a migration.
Your application, or your coding agent
file_8f2kq1x9
File IDs. Nothing else crosses.
FileHutch control plane
Rails · Postgres
-
Upload policies
types · size · visibility
-
Signed delivery
TTL · disposition
-
Named transforms
avatar · thumb
-
Audit history
who · when · what
Storage::Adapter
Above this line nothing knows a provider exists. Below it, one contract: create upload, finalize, signed URL, delete.
Bucket, region, object key. Only here.
The bytes
Managed R2, or your own R2 · S3 · B2
The rule everything serves
Attach once. Move storage whenever you want.
- Nothing above the adapter knows a provider exists
- No bucket, no region, no object key reaches your models or your database.
- So switching storage is a settings change
- Point a project at a different bucket and existing files keep working. Your code never finds out.
- And an unsupported provider is an adapter
- Not a rewrite. R2, S3 and B2 already speak the same contract.
The dashboard
A console, not a media library
Files, policies, keys and an audit trail. No asset approvals, no collections, no video pipeline — the things a small team actually opens this to do.
Files
Upload a file| File | ID | Size | Visibility | Uploaded |
|---|---|---|---|---|
|
invoice-2026-09.pdf
|
file_8f2kq1x9 | 241 KB |
private
|
2 minutes ago |
|
avatar.png
|
file_3p7wn4c2 | 88 KB |
public
|
18 minutes ago |
|
payouts-q3.csv
|
file_x21bd9k4 | 1.2 MB |
private
|
1 hour ago |
|
site-backup.zip
|
file_r04ms8t6 | 46 MB |
private
|
yesterday |
Agent-native
Give this to your coding agent
Every policy, transform and key has a deterministic API behind it, and an MCP server in front of it. Paste this into Claude Code, Cursor or Codex and let it wire the whole thing up.
Agent keys are scoped and separate from yours, and every action they take is attributed to the key in the audit log — never to you.
Add FileHutch to this app.
Private PDF uploads, 25 MB max.
Public avatar images with a 256x256 crop.
Private links expire after an hour.What you get
The boring parts, done properly
- Private files, properly
- Expiring signed URLs with real TTLs and download headers. Not a public link you hope nobody guesses.
- Named transforms
- Your code says avatar. Resize every avatar in the app with one dashboard edit and no deploy.
- Audit history
- Every upload and deletion recorded with the user or API key behind it.
- Direct browser uploads
- Bytes go straight to the bucket. Your server issues a policy and never touches the file.
- Declarative config
- Policies and transforms as YAML you can diff, review and apply in CI.
- Webhooks
- Upload finished, file deleted, storage health changed. Signed, retried, and visible in the dashboard.
Your bucket, or ours to start with
Bring your own bucket on every plan, including Free. Same API, same file IDs, same dashboard either way - and moving between them keeps every ID, so it is not a choice you have to get right now.
- We run the bucket
- Create a project and upload immediately. Cloudflare R2 underneath, with a storage allowance included in your plan.
- You keep the bytes
- Connect R2, S3 or B2. Credentials are scoped and encrypted at rest. If we disappear, your files are still yours.
Start with the free plan and a real project.
2 GB, one project, bring your own bucket if you want to. No card.