ActiveStorage::Crucible
An Active Storage transformer that sends image and video variant processing to the Crucible web service. Built on top of active_storage-async_variants.
The Problem
Processing image variants and video previews on your Rails server ties up workers and requires installing tools like vips and ffmpeg in production. Crucible is an external service that handles these transformations -- but you need a bridge between Active Storage's variant system and Crucible's HTTP API.
This gem provides that bridge. It implements the async_variants external transformer interface, delegating all image/video processing to Crucible via presigned S3 URLs.
Installation
gem "active_storage-crucible"
Requires an S3-compatible storage service (the gem generates presigned URLs for Crucible to read source files and write results).
Configuration
# config/initializers/crucible.rb
ActiveStorage::Crucible.endpoint = "https://crucible.example.com"
Or with a block:
ActiveStorage::Crucible.configure do |config|
config.endpoint = ENV["CRUCIBLE_ENDPOINT"]
end
Usage
Image and Video Variants
Use ActiveStorage::Crucible::Transformer as the transformer for any variant:
class User < ApplicationRecord
has_one_attached :avatar do |attachable|
attachable.variant :thumb,
resize_to_limit: [100, 100],
format: :webp,
transformer: ActiveStorage::Crucible::Transformer,
fallback: :original
end
has_one_attached :video do |attachable|
attachable.variant :web,
resize_to_limit: [1280, 720],
format: :webp,
transformer: ActiveStorage::Crucible::Transformer,
fallback: :original
end
end
The transformer auto-detects image vs. video based on the blob's content type and calls the appropriate Crucible endpoint (/image/variant or /video/variant).
In views, use standard Active Storage helpers:
<%= image_tag user.avatar.variant(:thumb).url %>
While the variant is processing, this serves the original file. Once Crucible finishes and calls back, it serves the processed variant.
Video Previews
The gem also extends ActiveStorage::Preview to process video previews through Crucible. This happens automatically for video blobs on S3-compatible services -- no extra configuration needed.
<%= image_tag user.video.preview(resize_to_limit: [640, 480], format: :webp).url %>
While the preview is processing, the original video URL is served as a fallback.
How It Works
Variant flow
- A file is attached to a model with a Crucible-backed variant defined
async_variantsenqueues a background job for the variant- The job calls
Crucible::Transformer#initiate, which:- Creates a placeholder output blob in the database
- Attaches it to the variant record
- Generates presigned GET/PUT URLs for the source and output blobs
- POSTs to Crucible with the URLs, dimensions, format, and a signed callback URL
- Crucible processes the image/video, uploads the result to the presigned PUT URL
- Crucible POSTs to the callback URL with
{"status": "success"} - The
async_variantscallback controller marks the variant record as processed
Preview flow
- A video preview is requested in a view
PreviewExtension#processcreates placeholder blobs for the preview image and its variant- POSTs to Crucible's
/video/previewendpoint with presigned URLs and a callback URL - Crucible extracts a frame, resizes it, uploads both the preview image and variant
- Crucible POSTs to the callback URL to mark the variant as processed
What gets sent to Crucible
Variant requests (POST /image/variant or /video/variant):
{
"blob_url": "https://s3.example.com/source?presigned...",
"variant_url": "https://s3.example.com/output?presigned...",
"dimensions": "100x100",
"rotation": 0,
"format": "webp",
"callback_url": "https://app.example.com/active_storage/async_variants/callbacks/signed-token"
}
Preview requests (POST /video/preview):
{
"blob_url": "https://s3.example.com/source?presigned...",
"preview_image_url": "https://s3.example.com/preview?presigned...",
"preview_image_variant_url": "https://s3.example.com/variant?presigned...",
"dimensions": "640x480",
"rotation": 0,
"callback_url": "https://app.example.com/active_storage/async_variants/callbacks/signed-token"
}
Callbacks
Callbacks are handled by active_storage-async_variants, not this gem. The callback endpoint is auto-mounted at:
POST /active_storage/async_variants/callbacks/:token
Crucible must POST {"status": "success"} or {"status": "failed", "error": "..."} to this URL after processing. The token is signed -- no authentication headers are needed. The endpoint must be publicly reachable by the Crucible service.
License
MIT