Features
The control plane above commodity storage
FileHutch is not another storage provider. It owns file IDs, policies, delivery, transforms and audit; the provider owns the bytes. That split is the whole product.
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
Why it is built this way
A file ID outlives the bucket it was in
Application code that names a provider is application code you will rewrite. So nothing above the storage adapter is allowed to: not the models, not the controllers, not the serializers, and above all not the database.
The cost of that rule is that FileHutch cannot expose a provider's cleverest features. The benefit is that moving between R2, S3 and B2 is a settings change and existing files keep working.
Upload policies
The rules live on the policy, not the call site
A policy names the types, the size cap, the visibility and the TTL. Every upload quotes a policy, so the browser cannot ask for more than you allowed and two call sites cannot disagree.
Policies are also YAML you can diff and apply in CI, so an environment's rules are reviewable rather than clicked into a dashboard and forgotten.
policies:
invoices:
content_types: [application/pdf]
max_size: 25MB
visibility: private
signed_url_ttl: 1h
avatars:
content_types: [image/png, image/jpeg, image/webp]
max_size: 5MB
visibility: public
transforms: [avatar]Private files
Signed URLs with a real expiry
Private stays private. Links are issued per request rather than baked into a page, expire on the TTL the policy sets, and carry the download headers you asked for.
The alternative most apps ship is a public URL with a long random component. That is not access control, it is a bet that the link never gets forwarded.
def download
redirect_to invoice.file.signed_url(
expires_in: 1.hour,
disposition: "attachment"
), allow_other_host: true
endpublic function download(Invoice $invoice)
{
return redirect()->away(
$invoice->fileSignedUrl(expiresIn: 3600, disposition: 'attachment')
);
}app.get("/invoices/:id/download", async (req, res) => {
const { url } = await hutch.signedUrl(invoice.fileId, {
expiresIn: 3600,
disposition: "attachment"
})
res.redirect(url)
})curl -X POST https://filehutch.com/api/v1/files/file_8f2kq1x9/signed_url \
-H "Authorization: Bearer $FILE_HUTCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "expires_in": 3600, "disposition": "attachment" }'Named transforms
Your code says avatar, not 256x256
Define the size once. Every app that asks for that name gets the new crop, with no deploy, no cache-busting query string and no provider syntax in your templates.
Transforms render on managed storage, or on your own R2 behind a Cloudflare zone. An S3 or B2 bucket stores and serves your files but tells you plainly that it cannot transform them, rather than handing back a URL that turns out to be broken in production.
transforms:
avatar: { width: 256, height: 256, fit: cover }
thumb: { width: 480, height: 270, fit: cover }
hero: { width: 1600, height: 600, fit: cover }image_tag user.avatar_transform_url("avatar")<img src="{{ $user->avatarTransformUrl('avatar') }}" alt=""><img src={user.avatar.transforms.avatar} alt="" />curl -L https://filehutch.com/f/file_3p7wn4c2/avatar -o avatar.webpEverything else
The parts you only notice when they are missing
- Audit history
- Every upload, deletion and signed URL, with the user or API key behind it. Agent actions are attributed to the agent key, never to you.
- Direct browser uploads
- Bytes go from the browser to the bucket. Your server issues a policy and never handles the file.
- An MCP server
- The same API keys and the same services, exposed to coding agents. Setting up a project is a sentence rather than a tutorial.
- Webhooks
- Upload finished, file deleted, storage health changed. Signed, retried, and inspectable in the dashboard.
- Environments
- Staging and production are separate projects sharing one bill, each with their own keys and buckets.
- Scoped API keys
- files:read, policies:write, and an expiry. A key that leaks is a key you can reason about.
What it deliberately does not do
A short list of things FileHutch will not grow into. If you need one of them, you need a different product, and it is cheaper to find that out here.
Digital asset management
No collections, tagging, approval workflows or brand portals.
Video
Stored and delivered, never transcoded. No adaptive bitrate, no player.
A file picker for end users
There is an upload control, not a Dropbox-style browser.
Per-seat pricing
The team is the billing unit. Adding people never changes the price.
On-premise
Managed or your bucket. Not your servers.
AI tagging or generative editing
Not now, and not on the roadmap.
Do you really need Cloudinary for avatars and PDFs?
FileHutch does application files for teams of one to twenty, and stops there on purpose.