WORSHIPONLINE / STORAGE MODERNIZATION

Moving media off Paperclip, without taking production offline.

A staged migration of a gigabyte-scale S3 library to ActiveStorage, designed to keep uploads and public media available throughout the transition.

Migration flow from Paperclip through dual-writing, backfill and staged model cutovers to ActiveStorage and CloudFront
ApproachIncremental cutover
Migration windowJune - July 2025
DeliveryAmazon S3 + CloudFront

Replace the storage layer, not the user experience.

Paperclip was deprecated and had become a compatibility obstacle to moving the application onto newer Rails versions. WorshipOnline also had a large existing media library and depended on established attachment behavior, variant names, and public asset URLs.

A single release that copied files, changed every model, and retired Paperclip at once would concentrate too much operational risk. We separated the work into reversible stages, kept both systems active during the transition, and removed Paperclip only after model cutovers.

One controlled step at a time.

Dual-write first. Copy existing objects. Move reads model by model. Retire the old dependency last.

  1. JUNE 24, 2025

    Write to both systems

    Kept Paperclip in place while creating ActiveStorage blobs and attachment records for new uploads. This established the new path without forcing an immediate read cutover.

    Dual-write adapter
  2. JUNE 26, 2025

    Backfill existing files

    Added a task that found Paperclip-backed records missing an ActiveStorage attachment, copied the original file through a tempfile, and created the new blob and attachment.

    Resumable backfill
  3. JUNE 30 - JULY 16, 2025

    Switch models incrementally

    Moved attachment ownership model by model, retaining named image styles, default images, and custom key behavior while each slice was validated.

    Per-model cutover
  4. JULY 17-18, 2025

    Remove Paperclip after the cutover

    Removed Paperclip usage and then dropped its attachment metadata columns, after the ActiveStorage path had replaced it across the application.

    Dependency + schema cleanup
  5. AUGUST 2025

    Continue with framework upgrades

    The Rails and Ruby upgrades followed the storage cutover. They were enabled by removing the deprecated attachment dependency, not performed as part of the file-copy task itself.

    Rails + Ruby upgrade

Keep URLs useful. Keep the cutover boring.

ActiveStorage was adapted to the application’s attachment contract and delivery path rather than forcing a user-facing URL redesign.

  1. 01

    Paperclip-style attachment API

    A temporary adapter let existing Paperclip model declarations continue to work while new ActiveStorage records were created after save. Later, models moved to native has_one_attached declarations with a small styles/default-URL concern.

  2. 02

    Named styles and variants

    ActiveStorage::Attached::One#url(style) and #variant(style) were overridden to map old named styles to ActiveStorage transformations and preserve fallback behavior for non-variant files.

  3. 03

    Custom object and variant keys

    New objects use a model/attachment/UUID/filename key shape. Variant keys include a transformation digest. UUID paths distinguish replacement uploads; the public URL does not rely on a timestamp parameter.

  4. 04

    Direct CloudFront URLs

    The attachment URL helper builds public URLs from CLOUD_FRONT_HOST and the escaped object key instead of sending S3 assets through Rails redirect routes. The filename stays in the path for legibility.

PUBLIC OBJECT URLhttps://cdn.worshiponline.com/model/attachment/uuid/filename.jpg

Readable filename and UUID-based object path. No timestamp query parameter.

AFTER THE CUTOVER

Audit the blobs, not just the attachment table.

A post-migration audit checked for blobs without attachments. The investigation found that asynchronous purge can leave a temporary orphan while a purge job is queued; remaining orphaned files were reviewed and removed to complete the cleanup.

2,488Store::Sample records rekeyed in about 10 minutes, according to the migration notes.

Paperclip out. A supported storage path in place.

ActiveStorage took over in stages, while public delivery stayed on the CloudFront path. Removing Paperclip cleared a dependency barrier; later Rails and Ruby upgrades could proceed on their own schedule.

Talk through a Rails migration Legacy Rails maintenance and modernization →
Back to TopBack to Top