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 in config/credentials.yml.enc for production security
  • Environment Variables: Export R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ACCOUNT_ID, and CDN_IMAGES_BUCKET for 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_BUCKET to CDN_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

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →