FileHutch

How it works

Your app stores an ID. FileHutch does the rest.

Four steps from a file in a browser to a URL on a page, the same in every stack. Pick yours once below and every example follows.

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 adapter

One line nothing crosses

Your code talks to FileHutch
Policies, uploads, signed URLs and transforms, by name, through the gem, the SDK or the REST API.
FileHutch talks to storage
Every provider sits behind one Storage::Adapter contract: create an upload, finalize it, sign a URL, delete. R2, S3 and B2 all speak it.
Only the ID comes back
Your database holds file_3p7wn4c2. Never a bucket, a region, an object key or a provider's URL.

Step 1

Say what a file is allowed to be

A policy names the types, the size cap, the visibility and how long a private link lasts. Every upload quotes one, so the browser cannot send more than you allowed.

filehutch.yml

policies:
  avatars:
    content_types: [image/png, image/jpeg, image/webp]
    max_size: 5MB
    visibility: public
    transforms: [avatar]

Step 2

Upload, and keep the ID

The bytes go straight to storage; FileHutch checks the policy, verifies the size and hands back an ID. That string is the only thing you store.

app/models/user.rb

# One string column: users.avatar_file_id
class User < ApplicationRecord
  has_hutch :avatar, policy: "avatars"
end

user.avatar = params[:avatar]
user.save!
user.avatar_file_id # => "file_3p7wn4c2"

Step 3

Ask for a URL when you need one

Public files get a stable FileHutch address. Private ones get a link that expires. Transforms are asked for by name, never by width or by a provider's syntax.

app/views/users/show.html.erb

# Public: a stable FileHutch URL
user.avatar_url

# The named crop
user.avatar_transform_url("avatar")

# Private: gone in ten minutes
user.avatar_signed_url(expires_in: 600)

Step 4

Move the bytes, not your code

Point the project at another bucket, from managed R2 to your own S3 or the other way, and FileHutch copies the files across. Every ID and every URL you have already stored keeps working.

What changes
A storage connection in the dashboard, and where the bytes live.
What does not
The file IDs in your database, the URLs on your pages, and every line of code above.
Why it can
Nothing above the adapter ever learned which provider it was talking to, so there is nothing to rewrite.

Try it on one attachment.

The free plan is a real project: managed storage or your own bucket, and every step above.