# Setting up ActiveStorage with Cloudflare R2 or AWS S3 in Property Web Builder

> Learn to set up ActiveStorage with Cloudflare R2 or AWS S3 in Property Web Builder. Integrate cloud storage for scalable file uploads and CDN delivery with ease.

- Repository: [Ed Tee/property_web_builder](https://github.com/etewiah/property_web_builder)
- Tags: how-to-guide
- Published: 2026-03-01

---

**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`](https://github.com/etewiah/property_web_builder/blob/main/config/storage.yml), defining a `cloudflare_r2` service that references the custom R2 service class:

```yaml

# 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`](https://github.com/etewiah/property_web_builder/blob/main/lib/active_storage/service/r2_service.rb), the `R2Service` class inherits from `ActiveStorage::Service::S3Service`:

```ruby

# 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`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/config/initializers/active_storage_r2.rb) forces eager loading of the custom service before Rails parses [`storage.yml`](https://github.com/etewiah/property_web_builder/blob/main/storage.yml):

```ruby

# 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`](https://github.com/etewiah/property_web_builder/blob/main/config/storage.yml).

### SSL and CRL Workarounds

[`config/initializers/ssl_crl_fix.rb`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/config/storage.yml):

```yaml

# 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`](https://github.com/etewiah/property_web_builder/blob/main/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

```ruby

# app/models/property.rb

class Property < ApplicationRecord
  has_one_attached :photo
end

```

### Controller Attachment

```ruby

# app/controllers/properties_controller.rb

def update
  @property.photo.attach(params[:photo])
  render json: { url: url_for(@property.photo) }
end

```

### View Rendering

```erb
<%= 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.yml`](https://github.com/etewiah/property_web_builder/blob/main/config/storage.yml) using either the custom `R2` service for Cloudflare or the built-in `S3` service for AWS
- **Custom Service**: The `R2Service` class in [`lib/active_storage/service/r2_service.rb`](https://github.com/etewiah/property_web_builder/blob/main/lib/active_storage/service/r2_service.rb) enables CDN-friendly public URLs while maintaining S3 API compatibility
- **Registration**: [`config/initializers/active_storage_r2.rb`](https://github.com/etewiah/property_web_builder/blob/main/config/initializers/active_storage_r2.rb) ensures the custom service loads before configuration parsing
- **Compatibility**: [`ssl_crl_fix.rb`](https://github.com/etewiah/property_web_builder/blob/main/ssl_crl_fix.rb) resolves macOS Ruby 3.4+ SSL issues with S3-compatible endpoints
- **Flexibility**: Environment variables support both legacy `R2_*` and modern `CDN_*` naming via [`cdn_aliases.rb`](https://github.com/etewiah/property_web_builder/blob/main/cdn_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`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/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.