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.
THE CONSTRAINT
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.
THE ROLLOUT
One controlled step at a time.
Dual-write first. Copy existing objects. Move reads model by model. Retire the old dependency last.
- 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 - 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 - 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 - 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 - 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
COMPATIBILITY LAYER
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.
- 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.
- 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.
- 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.
- 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.
https://cdn.worshiponline.com/model/attachment/uuid/filename.jpgReadable filename and UUID-based object path. No timestamp query parameter.
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.
THE RESULT
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 →