FileHutch
Documentation

Guides

Browser uploads

Bytes go from the browser straight to storage. Two small JSON calls touch your app, which is the only thing holding the API key.

browser → your server (holds the key) → FileHutch    two control-plane calls
browser ────────────────────────────→ storage        the bytes

Two routes on your server#

In Rails, the gem's engine is those two routes, and file_hutch:install mounts it. In Laravel, the package registers them at /file_hutch/uploads, closed until DirectUploads::authorize says who may upload (see Laravel). Anywhere else, wire them to createUpload and completeUpload.

HTTP

POST <endpoint>              { policy, filename, content_type, byte_size } → { upload, file }
POST <endpoint>/:id/complete                                              → { file }

Say who may upload#

Closed until you open it. The authorizer runs on both calls; on the second there is no policy in the request, so it is looked up from the file being finalised rather than trusted from the client.

config/initializers/file_hutch.rb

FileHutch.config.authorize_direct_upload = ->(controller, policy) do
  controller.current_user.present? && policy == "avatars"
end

Drop in the element#

A custom element, so the same tag works in React, Vue, Svelte, Hotwire or a plain HTML file. It builds its controls in the light DOM, so your own CSS styles them like any other input.

TypeScript

import { defineUploadElement } from "@filehutch/sdk/browser"
defineUploadElement()

html

<form action="/users/1" method="post">
  <filehutch-upload policy="avatars" name="user[avatar_file_id]" accept="image/*"></filehutch-upload>
  <button>Save</button>
</form>

On success the hidden field holds the file id, which is all your form submits. Submit buttons are disabled while bytes are in flight, so a half-uploaded form cannot be posted, and a failed upload clears the field rather than leaving a stale id behind.

Events bubble: filehutch:start, filehutch:progress (detail.percent), filehutch:complete, filehutch:error.

Or just the function#

TypeScript

const file = await directUpload(input.files[0], {
  policy: "avatars",
  onProgress: (percent) => (bar.value = percent),
  signal: controller.signal,
})

Progress comes from XMLHttpRequest, because fetch still cannot report upload progress. A network failure names CORS, since a misconfigured bucket is nearly always the cause.