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.
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.
# 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"// One string column: users.avatar_file_id
class User extends Model
{
use HasHutch;
protected function hutches(): array
{
return ['avatar' => 'avatars'];
}
}
$user->avatar = $request->file('avatar');
$user->save();
$user->avatar_file_id; // "file_3p7wn4c2"const file = await hutch.upload(bytes, {
policy: "avatars",
filename: "me.png"
})
await db.users.update(user.id, { avatarFileId: file.id })
// file.id === "file_3p7wn4c2"# 1. Ask for an upload against the policy
curl -X POST https://filehutch.com/api/v1/uploads \
-H "Authorization: Bearer $FILE_HUTCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "policy": "avatars", "filename": "me.png",
"content_type": "image/png", "byte_size": 48213 }'
# => { "upload": { "url": "…", "headers": { … } },
# "file": { "id": "file_3p7wn4c2", … } }
# 2. Send the bytes straight to storage
curl -X PUT "$UPLOAD_URL" -H "Content-Type: image/png" --data-binary @me.png
# 3. Say it is done; FileHutch checks the size
curl -X POST https://filehutch.com/api/v1/uploads/file_3p7wn4c2/complete \
-H "Authorization: Bearer $FILE_HUTCH_API_KEY"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.
# 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)// Public: a stable FileHutch URL
$user->avatar_url;
// The named crop
$user->avatarTransformUrl('avatar');
// Private: gone in ten minutes
$user->avatarSignedUrl(expiresIn: 600);// Public: a stable FileHutch URL
file.url
// The named crop
file.transforms.avatar
// Private: gone in ten minutes
const { url } = await hutch.signedUrl(file.id, { expiresIn: 600 })# Public, and the named crop: stable URLs that redirect to storage
curl -L https://filehutch.com/f/file_3p7wn4c2
curl -L https://filehutch.com/f/file_3p7wn4c2/avatar
# Private: a link that expires
curl -X POST https://filehutch.com/api/v1/files/file_3p7wn4c2/signed_url \
-H "Authorization: Bearer $FILE_HUTCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "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.