Use cases
Four file jobs, and the policy that does each
Every one of these is the same three steps: name a policy, quote it on upload, store the ID. What changes is the policy.
User-generated images
Avatars, logos and cover images
Public, small, and resized to a handful of sizes you will change your mind about twice. The crop belongs in a transform, not in your templates.
- Types and size are capped by the policy, so the browser cannot send a 40 MB TIFF
- Uploads go straight to the bucket — your server issues the policy and never sees the bytes
- Rename or resize the transform later and every app picks it up without a deploy
class User < ApplicationRecord
has_hutch :avatar, policy: "avatars"
end
# users.avatar_file_id => "file_3p7wn4c2"
image_tag user.avatar_transform_url("avatar")// One string column: users.avatar_file_id
class User extends Model
{
use HasHutch;
protected function hutches(): array
{
return ['avatar' => 'avatars'];
}
}
// $user->avatar_file_id => "file_3p7wn4c2"
$user->avatarTransformUrl('avatar');const file = await hutch.upload(bytes, { policy: "avatars", filename: "me.png" })
await db.users.update(user.id, { avatarFileId: file.id }) // "file_3p7wn4c2"
// Public images carry their transform URLs on the file
file.transforms.avatar# The stable URL: always the avatar crop, wherever the bytes live
curl -L https://filehutch.com/f/file_3p7wn4c2/avatar -o avatar.pngPrivate documents
Invoices, contracts and exports
The files where getting access control wrong is the whole problem. Private by policy, delivered through a link that expires, and every download recorded.
- Links are issued per request and expire on the policy's TTL, not on a global setting
- Content-Disposition is set on the signed URL, so a PDF downloads instead of opening
- The audit log answers who downloaded the contract, and when, without a database query
# Private stays private: this URL is dead in an hour.
def download
authorize! :read, @invoice
redirect_to @invoice.file.signed_url(
expires_in: 1.hour,
disposition: "attachment"
), allow_other_host: true
end// Private stays private: this URL is dead in an hour.
public function download(Invoice $invoice)
{
$this->authorize('view', $invoice);
return redirect()->away(
$invoice->fileSignedUrl(expiresIn: 3600, disposition: 'attachment')
);
}// Private stays private: this URL is dead in an hour.
app.get("/invoices/:id/download", async (req, res) => {
const invoice = await authorize(req.user, "read", req.params.id)
const { url } = await hutch.signedUrl(invoice.fileId, {
expiresIn: 3600,
disposition: "attachment"
})
res.redirect(url)
})# Private stays private: this URL is dead in an hour.
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" }'And the rest
Same three steps, different policy
- Client projects, one per customer
- An agency runs a project per client, each pointed at that client's own bucket. When the engagement ends you hand over the bucket and delete the project — the files were never in your account.
- CSV imports and generated exports
- Upload a spreadsheet, process it, write the result back as a private file with a short TTL. The export link in the email expires before the email does.
- Agent-built applications
- An MCP server and scoped agent keys mean a coding agent can create policies and transforms itself. Every action it takes is attributed to its key in the audit log, never to you.
- Marketplace and seller uploads
- Sellers upload product images against a public policy with a hard size cap; verification documents go to a private one. Two policies, one bucket, no ambiguity about which is which.
Where it is a bad fit
Said here rather than discovered in month three.
- A video product
- Files are stored and delivered, never transcoded. If you need adaptive bitrate or a player, use Mux or Cloudflare Stream.
- A marketing asset library
- No collections, approvals or brand portals. That is a DAM, and there are good ones.
- Petabyte archives
- Plans top out at 750 GB. Past that you want a storage contract, not a control plane.
- Heavy on-the-fly image manipulation
- Named transforms cover resize, crop and format. Overlays, face detection and generative fill are Cloudinary's job.
Whichever shape yours is, it starts with one policy.
Create a project, name a policy, store the ID. The free plan is enough to find out.