FileHutch

Migrate from raw S3

Keep the bucket. Stop rewriting the same 400 lines

Presigned POST, a CORS rule, a uuid column, a cleanup job for the uploads that never completed, and no idea who uploaded what. It works. You have also written it three times.

Before and after

The same attachment, both ways

Raw S3

post = S3_BUCKET.presigned_post(
  key: "uploads/ebad6b9e-61b8-4c0e-95f2-6bca021abe14/${filename}",
  content_length_range: 0..25.megabytes,
  acl: "private", success_action_status: "201"
)
# Plus: a CORS rule, a completion callback, a reaper for the
# uploads that never finished, and a column holding the key.
user.update!(avatar_key: post.fields["key"], avatar_bucket: BUCKET)

FileHutch

# The policy already knows the types, the cap and the visibility.
upload = FileHutch.upload_policy("avatars").create_upload(filename: name)

user.update!(avatar_file_id: upload.file_id)

Why people move

What changes, and why it matters

The bucket name is in your database
Every row that stores a key or a URL is a row that has to change when the bucket does. FileHutch stores the key internally and hands your app an ID.
Nothing enforces what may be uploaded
A presigned POST can carry conditions, and they are easy to get subtly wrong. An upload policy is a named object with types, a size cap and a visibility, and it is the same in every app.
Orphans accumulate
Uploads that started and never finished sit in the bucket forever unless you wrote the reaper. FileHutch finalises or expires every upload it issued.
No transforms, no audit, no dashboard
You either build them or do without. Most teams do without, then need them during an incident.

Side by side

Where each one is stronger

Aspect Raw S3 FileHutch
Bucket Yours Still yours
Egress cost S3 charges it Same bucket, same bill — or move to R2, which does not
Upload rules Presigned conditions per call site A named policy
Incomplete uploads Your reaper Finalised or expired
Transforms Build it Named, at the edge
Audit CloudTrail, if enabled In the dashboard, per file
What your app stores Bucket, key, region file_8f2kq1x9

The move, in order

Every step is reversible until the last one.

  1. 1

    Connect the bucket you already have

    No copying. FileHutch reads and writes the same bucket — this is the one migration with no backfill.

  2. 2

    Write the policies you have been enforcing by hand

    Take the conditions out of your presigned POST calls and name them.

  3. 3

    Import the keys you already store

    Storage → Import existing files adopts a whole prefix and gives you a key-to-ID map; POST /api/v1/files/adopt does one key. Either way the object stays where it is.

  4. 4

    Replace presigning with the SDK

    One call per upload. The CORS rule is printed in the dashboard, so it is a copy and paste.

  5. 5

    Drop the key and bucket columns

    This is the moment the schema stops knowing about a storage provider.

What does not change

  • The bucket, the region and the objects already in it
  • Your existing object keys — import does not move them
  • Direct browser uploads, still straight to S3

When to stay where you are

If you have one upload path, one bucket and no need for transforms or audit, presigned POST is about forty lines and costs nothing. Adding a service to replace forty lines is a bad trade.

Try the move on one attachment first.

The free plan is enough to migrate a single model and see whether the rest is worth it.