Setting up ActiveStorage with Cloudflare R2 or AWS S3 in Property Web Builder
ActiveStorage in Property Web Builder delegates file uploads to either Cloudflare R2 or AWS S3 through a pluggable service architecture that uses a custom R2Service subclass for CDN-compatible URL generation while maintaining standard S3 API compatibility.
The Property Web Builder repository implements a production-ready ActiveStorage configuration that supports both Cloudflare R2 (an S3-compatible object storage with built-in CDN) and traditional AWS S3 backends. The implementation resides in the etewiah/property_web_builder Rails application and requires no changes to model or controller code when switching between storage providers.
Configuring Cloudflare R2 Storage
Property Web Builder uses a custom service definition to connect ActiveStorage to Cloudflare R2's S3-compatible API while exposing clean CDN URLs for public assets.
Service Definition in storage.yml
The primary configuration lives in config/storage.yml, defining a cloudflare_r2 service that references the custom R2 service class:
# config/storage.yml
cloudflare_r2:
service: R2
access_key_id: <%= Rails.application.credentials.dig(:r2, :access_key_id) || ENV['R2_ACCESS_KEY_ID'] %>
secret_access_key: <%= Rails.application.credentials.dig(:r2, :secret_access_key) || ENV['R2_SECRET_ACCESS_KEY'] %>
region: auto
bucket: <%= ENV['CDN_IMAGES_BUCKET'] || Rails.application.credentials.dig(:r2, :bucket) || ENV['R2_BUCKET'] %>
endpoint: <%= "https://#{ENV['R2_ACCOUNT_ID']}.r2.cloudflarestorage.com" %>
force_path_style: true
public: true
public_url: <%= ENV['CDN_IMAGES_URL'] || Rails.application.credentials.dig(:r2, :public_url) %>
This configuration uses ERB evaluation to support both Rails encrypted credentials and environment variables, with the endpoint dynamically constructed from your R2_ACCOUNT_ID.
Environment Variables vs Credentials
You can supply R2 credentials through two mechanisms:
- Rails Encrypted Credentials: Store values under the
r2:key inconfig/credentials.yml.encfor production security - Environment Variables: Export
R2_ACCESS_KEY_ID,R2_SECRET_ACCESS_KEY,R2_ACCOUNT_ID, andCDN_IMAGES_BUCKETfor containerized deployments
The public_url parameter (mapped to CDN_IMAGES_URL) enables direct CDN delivery instead of signed S3 URLs when combined with the R2_USE_CDN environment variable.
Custom R2 Service Implementation
Unlike standard S3 storage, Cloudflare R2 requires a custom service class to handle CDN URL generation while maintaining S3 API compatibility for uploads and deletes.
The R2Service Class
Located at lib/active_storage/service/r2_service.rb, the R2Service class inherits from ActiveStorage::Service::S3Service:
# lib/active_storage/service/r2_service.rb
class ActiveStorage::Service::R2Service < ActiveStorage::Service::S3Service
def url(key, **options)
# Returns CDN URL if public_url configured and R2_USE_CDN != "false"
# Otherwise falls back to standard signed S3 URL
end
end
This subclass overrides the url method to intercept URL generation. When public_url is configured in storage.yml and the R2_USE_CDN environment variable is not set to "false", the method returns #{public_url}/#{key}—a clean, cache-friendly URL that bypasses S3 signature generation.
URL Generation Logic
The service implements conditional URL behavior:
- CDN Mode: Returns
https://cdn.example.com/blob-key.jpg(fast, cacheable, no expiration) - S3 Mode: Returns presigned S3 URLs with expiration (secure, temporary access)
Toggle between modes by setting CDN_IMAGES_URL and ensuring R2_USE_CDN remains unset or set to "true".
Initializers and Service Registration
Three initializers in config/initializers/ handle service registration, SSL compatibility, and legacy environment variable support.
Loading the Custom Service
config/initializers/active_storage_r2.rb forces eager loading of the custom service before Rails parses storage.yml:
# config/initializers/active_storage_r2.rb
require "active_storage/service/r2_service"
This explicit require prevents autoloading race conditions during boot when ActiveStorage attempts to instantiate the R2 service referenced in config/storage.yml.
SSL and CRL Workarounds
config/initializers/ssl_crl_fix.rb addresses macOS Ruby 3.4+ compatibility issues with S3-compatible services:
- Disables CRL (Certificate Revocation List) verification that breaks connections to R2 endpoints
- Disables AWS SDK checksum validation (R2 does not support AWS's multiple checksum algorithms)
This ensures stable connections on development machines running recent Ruby versions.
Legacy Environment Variable Support
config/initializers/cdn_aliases.rb maintains backward compatibility by mapping legacy R2_* variable names to the newer CDN_* naming convention:
- Maps
R2_BUCKETtoCDN_IMAGES_BUCKET - Provides deprecation warnings while keeping existing deployment scripts functional
This allows gradual migration to the clearer CDN_IMAGES_* variable names without breaking existing production environments.
AWS S3 Alternative Configuration
To use standard AWS S3 instead of Cloudflare R2, uncomment the amazon block in config/storage.yml:
# config/storage.yml
# amazon:
# service: S3
# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
# region: us-east-1
# bucket: your_aws_bucket
The built-in S3 service requires no custom initializers or service classes. URLs will be signed by default using AWS's native presigned URL generation, and the SSL workarounds in ssl_crl_fix.rb are unnecessary for standard AWS endpoints.
Application Usage Examples
Regardless of the backend (R2 or S3), application code remains identical thanks to ActiveStorage's abstraction layer.
Model Attachment
# app/models/property.rb
class Property < ApplicationRecord
has_one_attached :photo
end
Controller Attachment
# app/controllers/properties_controller.rb
def update
@property.photo.attach(params[:photo])
render json: { url: url_for(@property.photo) }
end
View Rendering
<%= image_tag url_for(@property.photo), alt: @property.title %>
When configured for R2 with CDN enabled, url_for returns https://cdn.example.com/active_storage_key.jpg. For S3 or R2 without CDN, it returns a signed URL like https://bucket.s3.region.amazonaws.com/key?X-Amz-Algorithm=....
Summary
- Configuration: Define services in
config/storage.ymlusing either the customR2service for Cloudflare or the built-inS3service for AWS - Custom Service: The
R2Serviceclass inlib/active_storage/service/r2_service.rbenables CDN-friendly public URLs while maintaining S3 API compatibility - Registration:
config/initializers/active_storage_r2.rbensures the custom service loads before configuration parsing - Compatibility:
ssl_crl_fix.rbresolves macOS Ruby 3.4+ SSL issues with S3-compatible endpoints - Flexibility: Environment variables support both legacy
R2_*and modernCDN_*naming viacdn_aliases.rb
Frequently Asked Questions
How do I switch between Cloudflare R2 and AWS S3 in Production?
Set config.active_storage.service = :cloudflare_r2 or config.active_storage.service = :amazon in your environment configuration files. No model or controller changes are required—the has_one_attached and attach APIs remain identical across both backends.
Why does Property Web Builder use a custom R2Service class instead of the standard S3 service?
Standard ActiveStorage::Service::S3Service generates presigned URLs for all requests. The custom R2Service subclass in lib/active_storage/service/r2_service.rb overrides the url method to return clean CDN URLs (e.g., https://cdn.example.com/file.jpg) when public_url is configured, enabling browser caching and reducing request latency compared to signed S3 URLs.
How do I troubleshoot SSL certificate errors when connecting to R2 on macOS?
If you encounter CRL verification failures on Ruby 3.4+, the initializer at config/initializers/ssl_crl_fix.rb automatically disables CRL checks and AWS SDK checksum validation. Ensure this file loads before ActiveStorage initializes, or manually set OpenSSL::X509::Store.new flags to disable CRL verification in your environment.
Can I use both encrypted credentials and environment variables simultaneously?
Yes. The ERB templates in config/storage.yml use the || operator to check Rails credentials first, then fall back to environment variables. For example: <%= Rails.application.credentials.dig(:r2, :access_key_id) || ENV['R2_ACCESS_KEY_ID'] %>. This supports both credential-based deployments and containerized environments that inject secrets via environment variables.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →