# Cloudflare R2 with Active Storage

Handling file uploads in Rails applications has never been easier.

With Active Storage, we can be up and running in a matter of minutes with local uploads and, with some extra effort, we can get cloud uploads rapidly.

In this article, we will learn **how to set Cloudflare R2 with Active Storage** to use it as our cloud provider and also use Cloudflare's CDN so we get fast 
## Why use R2 for object storage?
R2 is Cloudflare's object storage offering. It's relatively recent, as it was officially released in September 2022.

If you've added cloud storage to a Rails application, you've probably used something like AWS S3, which seems to be the most common choice for developers nowadays.

But, an alternative like R2 has the following advantages over S3:

- **No egress fees:** this is what you pay for retrieving the stored data.
- **S3 compatibility:** its API is fully compatible with S3, which means we can reuse our own or third-party integrations with the AWS service. Migrating out of S3 or any S3-compatible service like Digital Ocean Spaces is easier.
- **Globally distributed:** our stored objects can be accessed from the more than 125 edge locations that Cloudflare has. This means that we don't need to add a CDN on top of it, like we did in the [CDN with Active Storage](https://avohq.io/blog/cdn-for-active-storage-uploads) article. Note that the Cloudflare offers 6 bucket locations: Asia-Pacific (apac), Eastern Europe (eeur), Eastern North America (enam), Oceania (oc), Western Europe (weur) and Western North America (wnam), but the objects get distributed along their global network when they're accessed by users. 
- **Simplified pricing:** R2 offers a simplified pricing where we get 10 GB per month of free storage space plus 10 million “Class B” operations which read existing state and 1 million “Class A” operations which mutate state. After we pass those limits, each GB of storage costs $0.015, each million of reads costs $0.36 and each million of mutations costs $4.5.
- **Better developer experience:** working with the Cloudflare dashboard feels like a breeze compared to AWS. The interface is cleaner and more understandable, which results in a better experience overall.

{{info}}
The pricing differences between Cloudflare R2 and a competing service like S3 are probably not as noticeable if your application doesn't have many users or if file uploads are not a main part of it. Consider that when making the decision of whether to use this alternative or not.
{{/info}}
## Application setup
We will build a simple application where users can create posts and upload cover images, just for the sake of demonstrating that the integration works.

Let's start by creating the application:

```shell
rails new cloudflare_r2 --css=tailwind
```

Let's also install Active Storage so it copies the required migrations for blobs, attachments, and variants:

```shell
bin/rails active_storage:install
```

Next, let's create a simple `Post` model that with a `title`, a `body` and an attached `cover`:

```shell
bin/rails generate scaffold Post title body:text cover:attachment
```

Which will generate a scaffold with the associated cover:

```ruby
class Post < ApplicationRecord
  has_one_attached :cover
end
```

Finally, let's run the pending migrations

```shell
bin/rails db:migrate
```

After some work on the scaffold visuals using Tailwind and using our own Marksmith [markdown editor for Rails](https://github.com/avo-hq/marksmith) we get the following:

![Post form interface](https://i.postimg.cc/mrRQFN5B/post-creation-form-interface.png)
## R2 configuration
The first step of the process, given that you have a Cloudflare account, is to create a new bucket.

From the Cloudflare dashboard, we have to navigate to the **“R2 Object Storage”** link, which currently sits below the “Storage and databases” option on the sidebar.

After navigating to it, we should see a list of the buckets associated with our account. Logically, if we haven't created one, yet it should be empty.

![Cloudflare R2 object storage overview](https://i.postimg.cc/5txBTFY9/r2-object-storage-overview.webp)

Next, we click on the “Create bucket” button, and then we should see the following:

![Bucket creation flow in Cloudflare R2](https://i.postimg.cc/d1ywP6xG/r2-create-bucket-view.webp)

We just have to give our bucket a name, `avo-r2` in our case, and change the location and storage class if we need to. Otherwise, we can leave the default options.

After we click on the **“Create bucket”** button, we should get redirected back to the bucket overview, but this time we should see the recently created bucket on the list:

![R2 bucket overview after bucket creation](https://i.postimg.cc/9XPHdD5X/r2-bucket-overview-after-bucket-creation.webp)

The next step is to create an API token to use with Active Storage when uploading files. To create a token, we have to click on the **“{} API”** button, which will show a dropdown:

![R2 create an API token dropdown](https://i.postimg.cc/hvNm42Dg/r2-manage-api-tokens-dropdown.webp)

Then, we click on the **“Manage API tokens”** link, which should take us to the following screen:

![R2 API tokens list view](https://i.postimg.cc/nLVGPwMw/r2-api-tokens-view.webp)

Next, we click on the **“Create Account API Token”** button because we just need an API key associated with our account for now, and we should see:

![R2 Account API tokens view](https://i.postimg.cc/9XBTvB56/account-api-tokens-view.webp)

Here, we can configure the following:

- **Token name:** we can give the token a name if pertinent.
- **Permissions:** because we want to handle uploads from our Rails app, we have to give **“Admin Read & Write”** permissions to this token.
- **TTL:** set to “Forever” by default, but we can change it if we need the token to expire earlier.
- **Client IP Address filtering:** we can limit the IP address that can use the token. Unless you want to filter the access to the token, you can safely leave this empty.

Next, after confirming the setup values, we should see something like this:

![R2 API token values screen](https://i.postimg.cc/yYg1FVPL/api-token-values-screen.webp)

This provides us with access to four different values:

- **Cloudflare API token:** we can use this token to authenticate requests to the Cloudflare API.
- **Access Key ID and Secret Access Key:** these credentials are used to replace AWS credentials when using S3-compatible clients, like we will.
- **Jurisdiction-specific endpoint:** this is used when we have jurisdictional restrictions needed to meet data requirements.

Finally, we need to retrieve the ID associated with our Cloudflare account.

There are two ways to retrieve it: it's in the URL for most parts of the dashboard: `https://dash.cloudflare.com/#{ACCOUNT_ID}/r2/overview` for example.

Otherwise, you can navigate to your buckets settings and check the header of the **General** section, where you will be presented with something like this:

![Retrieving our account ID in Cloudflare](https://i.postimg.cc/76Z4wsgR/retrieving-account-id-from-cloudflare-r2.webp)

The `account_id` corresponds to the blurred section of this image.

Now that we have our credential's setup, let's configure Active Storage:

{{cta /}}
## Active Storage configuration
Now that we have the tokens, let's configure Active Storage so we can upload files to our R2 bucket.

Let's start by opening an editor to keep our tokens using Rails credentials:

```shell
# Vim
EDITOR=vim bin/rails credentials:edit --environment=development
# VS Code
EDITOR='code --wait' bin/rails credentials:edit --environment=development
# Cursor
EDITOR='cursor --wait' bin/rails credentials:edit --environment=development
```

In there, let's remove the placeholder values and replace them with something like this:

```yml
cloudflare:
  account_id: YOUR_ACCOUNT_ID
  r2:
    account_api_token: YOUR_ACCOUNT_LEVEL_TOKEN
    access_key_id: YOUR_ACCESS_KEY_ID
    secret_access_key: YOUR_SECRET_ACCESS_KEY
    endpoint: YOUR_JURISDICTION_SPECIFIC_ENDPOINT
    bucket: YOUR_BUCKETS_NAME
```

Now, we can safely access these values in the `storage.yml` file, which is committed to our repository by default:

```yml
# config/storage.yml
cloudflare:
  service: S3
  endpoint: https://<%= Rails.application.credentials.dig(:cloudflare, :account_id) %>.r2.cloudflarestorage.com
  access_key_id: <%= Rails.application.credentials.dig(:cloudflare, :r2, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:cloudflare, :r2, :secret_access_key) %>
  bucket: <%= Rails.application.credentials.dig(:cloudflare, :r2, :bucket) %>
  region: auto
```

The next step is to add the official `aws_sdk_s3` gem, which Active Storage uses to upload files to S3-compatible APIs. Make sure to check the latest version of the gem whenever you're implementing this feature:

```ruby
# Gemfile
gem "aws-sdk-s3", "~> 1.185", require: false
```

Make sure to have the `image_processing` gem if you will be using Active Storage's variants:

```ruby
gem "image_processing", "~> 1.2"
```

Then, we can tell Active Storage to use the `cloudflare` service we defined in `storage.yml`:

```ruby
# config/environments/development.rb
config.active_storage.service = :cloudflare
```

Now, after running `bundle install` and restarting our server, we should be set to go with file uploads.

However, if we try to upload a file with our current configuration, we will receive the following error: `Aws::S3::Errors::InvalidRequest` with the message `You can only specify one checksum at a time`.

![Checksum error with AWS SDK S3 integration](https://i.postimg.cc/pdmkXC5T/r2-cloudflare-invalid-request-error.webp)

The error originates from the `ActiveStorage::Blob.create_and_upload!` method, which sends two checksums, something that's allowed by AWS but rejected by R2 in this case.

This problem seems to happen on newer versions of the AWS SDK, so setting a previous version is an alternative, but the easiest fix is to modify the configuration in `storage.yml` by adding the following:

```yml
request_checksum_calculation: "when_required"
response_checksum_validation: "when_required"
```

Now, after restarting the server, we can upload a file to R2:

{{gumlet video_id="681a87a53284da48f9354908" width="1260" height="760" /}}

Checking our R2 dashboard, we can see that the object was successfully uploaded:

![Successful file upload in R2 dashboard](https://i.postimg.cc/XvZpsjv2/object-storage-upload-r2.webp)

Everything is working now, but with this configuration, we won't be able to handle direct uploads like we did in the [S3 uploads with Active Storage](https://avohq.io/blog/s3-uploads-with-active-storage) article. 

Let's implement the feature with R2 as well:

{{info}}
Have you ever wondered why some gems have the `require: false` argument in the Gemfile? As its name states, `require: false` prevents the gem from being loaded automatically when our Rails app starts up. This is done to avoid loading large gems until the point they're actually used. If we explore the `activestorage` code, the gem is explicitly required in the `ActiveStorage::Service:S3Service` class.
{{/info}}
## Direct uploads
Sometimes, especially with customer-facing apps, we want the users to upload files directly from their browser to improve the perceived experience and save server resources.

To achieve this, we need to request a signed URL from our server, which has access to the Cloudflare keys, with information about the file we want to upload.

Then, the server responds with a signed URL, which we use to make a request to Cloudflare from the browser.

If the upload is successful, the cloud server returns information about the uploaded file, which we use to persist to our database.

The flow works like this:

![Direct file upload to the cloud flow](https://avohq.io/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MzEwLCJwdXIiOiJibG9iX2lkIn19--dfc0f2efd633b0eb6708c9562c74739fd7b3bdf6/direct-uploads-to-s3-flow.png)

To enable direct uploads, let's configure CORS in the Cloudflare dashboard:
### Configuring CORS
CORS means **Cross-Origin Resource Sharing,** which is a way to allow a server to indicate the origins from which a browser should permit resource loading.

In other words, when our browser tries to upload a file to R2, the request is considered cross-origin, and it's not allowed.

To set CORS, we have to open our bucket's setting page and then navigate to the **CORS Policy section**, then click on the `+ Add` button:

![CORS Policy option in Cloudflare's dashboard](https://i.postimg.cc/c4VCVftQ/cloudflare-r2-cors-policy-dashboard.webp)

Then, we will be presented with a screen where we can edit our CORS policy, which by default is:

```javascript
[
  {
    "AllowedOrigins": [
      "http://localhost:3000"
    ],
    "AllowedMethods": [
      "GET"
    ]
  }
]
```

We only have `localhost:3000` GET requests, but those are not enough for direct uploads, so let's add the correct headers and methods:

```javascript
[
  {
    "AllowedOrigins": ["http://localhost:3000"],
    "AllowedMethods": ["GET", "POST", "PUT"],
    "AllowedHeaders": ["Content-Type", "Content-Md5", "Content-Disposition"],
    "ExposeHeaders": ["Origin", "Content-Type", "Content-Md5", "Content-Disposition"],
    "MaxAgeSeconds": 3600
  }
]
```

We're adding `POST` and `PUT` requests, setting the `AllowedHeaders` which are the headers our application can send and setting the `ExposeHeaders` which are the headers that are exposed in the response from R2 and `MaxAgeSeconds` which defines how long the results of the preflight request can be cached.

If you're using a custom development domain or deploying the app to production, don't forget to add your domains to the `AllowedOrigins` array and try to keep those contained to origins you'll actually use.

{{info}}
CORS preflight requests are `OPTIONS` requests that use specific headers like `Access-Control-Request-Method`, `Origin` and `Access-Control-Request-Headers` to see if the server allows the request conditions by checking if the response matches the allowed request methods and headers and then proceeds with the actual request which, in our case, would be the actual file upload to the presigned URL we got from Active Storage
{{/info}}
### Direct uploads with Active Storage
Now that we have our CORS policy correctly setup, we can easily add direct uploads.

The first step is to install the `@rails/activestorage` JavaScript dependency:

```shell
bin/importmap pin "@rails/activestorage" # If using Import maps
yarn add @rails/activestorage # If using JavaScript bundlers
```

Next, we have to import the ActiveStorage JS code and run `ActiveStorage.start()`, we can do that in `application.js` or in any other part of the app:

```javascript
import * as ActiveStorage from "@rails/activestorage";

ActiveStorage.start();
```

Then, we just have to pass `direct_upload: true` to our file field form helper:

```erb
<%= form.file_field :cover, direct_upload: true %>
```

Now, we get direct uploads after we submit the form, but we won't notice any difference, so let's add an event listener to get an `alert` when the direct upload finishes:

```javascript
window.addEventListener("direct-upload:end", (evt) => {
  alert("Direct upload started", evt)
})
```

{{gumlet video_id="681b98c23284da48f93ceca1" width="1260'" height="754" /}}
## TL;DR;
Cloudflare R2 is an S3-compatible cloud object storage service which provides some advantages regarding S3:

- No egress fees.
- Out of the box CDN integration.
- Simpler pricing
- A better experience with their dashboard.

Being compatible with S3, it means that we can use Active Storage just like we with S3 without making any further adjustments or writing any custom code.

The first step, provided we have a Cloudflare account, is to create the bucket we will use and gather the `account_id`, the bucket name, the `access_key_id` and `secret_access_key` and then safely store them with Rails credentials or environment variables if you prefer them.

Then, we add the following service to `storage.yml`:

```yml
cloudflare:
  service: S3
  endpoint: https://<%= Rails.application.credentials.dig(:cloudflare, :account_id) %>.r2.cloudflarestorage.com
  access_key_id: <%= Rails.application.credentials.dig(:cloudflare, :r2, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:cloudflare, :r2, :secret_access_key) %>
  bucket: <%= Rails.application.credentials.dig(:cloudflare, :r2, :bucket) %>
  region: auto
  request_checksum_calculation: "when_required"
  response_checksum_validation: "when_required"
```

And that's it, we can have simple file uploads to R2 without any extra effort.

But, if you want to allow direct uploads, we have to configure CORS:

```javascript
[
  {
    "AllowedOrigins": ["http://localhost:3000"],
    "AllowedMethods": ["GET", "POST", "PUT"],
    "AllowedHeaders": ["Content-Type", "Content-Md5", "Content-Disposition"],
    "ExposeHeaders": ["Origin", "Content-Type", "Content-Md5", "Content-Disposition"],
    "MaxAgeSeconds": 3600
  }
]
```

Then, we install the Active Storage JS code: `yarn add @rails/activestorage` or `bin/importmap pin "@rails/activestorage"`.

We require it and call the `start` method in `application.js` or elsewhere:

```javascript
import * as ActiveStorage from "@rails/activestorage";
ActiveStorage.start();
```

And finally, by passing `direct_upload: true` to the `form.file_field` helper we get direct uploads after users submit the form.

That's about it, now your application has direct uploads to Cloudflare R2 using Active Storage.

I hope you enjoyed the article and that it helped you build an awesome project with Rails. Have a good one!

