Home Blog Rails Active Record Encryption: Setup, Queries, and Safe Migrations
Ruby on Rails
July 24, 2026

Rails Active Record Encryption: Setup, Queries, and Safe Migrations

Rails Active Record Encryption: Setup, Queries, and Safe Migrations

Active Record Encryption is one of those Rails features that feels intimidating until you wire it up once. After that, it is mostly a matter of choosing the right columns, picking which attributes need deterministic queries, and migrating old data carefully. In this post, we will go from zero setup to a version you can actually ship.

Active Record Encryption was introduced in Rails 7, and it gives you application-level encryption for sensitive model attributes like email addresses, phone numbers, notes, tax identifiers, and other PII. Rails encrypts before writing to the database and decrypts when loading the record, so your app code still works with plain values.

What problem does it solve?

Database encryption at rest is good, but it is not the full story.

If someone gets access to:

  • a database dump
  • a snapshot
  • raw SQL logs
  • a backup copied somewhere it should not be

they can still read the data if it is stored as plain text.

Active Record Encryption adds another layer. Your Rails app can still work with the real values, but the database stores ciphertext instead.

That said, it is worth being clear about what this is not:

  • It is not a replacement for good access controls.
  • It is not a replacement for encrypted disks or managed database encryption.
  • It does not mean every encrypted field should become queryable.

The sweet spot is sensitive attributes that your app needs to read, but that should not be exposed in raw database storage.

Step 1: Generate the encryption keys

Rails ships with a task for this:

bin/rails db:encryption:init

That command prints the values you need to add to credentials:

# config/credentials.yml.enc
active_record_encryption:
  primary_key: your_primary_key_here
  deterministic_key: your_deterministic_key_here
  key_derivation_salt: your_key_derivation_salt_here

If you prefer environment variables, you can also configure them explicitly:

# config/application.rb
config.active_record.encryption.primary_key = ENV["ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY"]
config.active_record.encryption.deterministic_key = ENV["ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY"]
config.active_record.encryption.key_derivation_salt = ENV["ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT"]

For most Rails apps, credentials are the simplest option.

Step 2: Start with a real schema

Let’s say we are building a Customer model and we want to protect:

  • email_address
  • phone_number
  • ssn_last4
  • support_notes

One important detail: encrypted data needs more storage than plain text.

As a practical rule:

  • Use text columns by default for encrypted fields.
  • If you need indexing on a short value like email, size the column generously.

Here is a solid starting migration:

class CreateCustomers < ActiveRecord::Migration[8.0]
  def change
    create_table :customers do |t|
      t.string :email_address, limit: 510, null: false
      t.text :phone_number
      t.text :ssn_last4
      t.text :support_notes
      t.timestamps
    end

    add_index :customers, :email_address, unique: true
  end
end

Why is email_address a bigger string column?

Because short encrypted values grow. Rails stores encrypted payload plus metadata, and the result is Base64-safe text. If you want to keep a unique index on a short field, you need to plan for that extra size.

Step 3: Declare the encrypted attributes

Now we can encrypt at the model level:

class Customer < ApplicationRecord
  encrypts :email_address, deterministic: true, downcase: true
  encrypts :phone_number
  encrypts :ssn_last4
  encrypts :support_notes

  validates :email_address, presence: true, uniqueness: true
end

This is where the first important design choice appears.

Non-deterministic encryption

This is the default:

encrypts :support_notes

If you encrypt the same value twice, Rails will produce different ciphertext each time. That is better from a security perspective, but it means exact-match database queries do not work reliably.

That makes non-deterministic encryption a good choice for:

  • free-form notes
  • comments
  • internal descriptions
  • anything you only read by loading the record

Deterministic encryption

This is what we used for email_address:

encrypts :email_address, deterministic: true, downcase: true

Deterministic encryption makes the same input produce the same ciphertext, which allows:

  • find_by(email_address: ...)
  • uniqueness validations
  • unique indexes

That makes it a good fit for lookup fields like email addresses.

The downcase: true part is also important. Email lookups are usually case-insensitive, and downcase: true normalizes the value before encrypting it.

Step 4: Use it like a normal model

Once the model is declared, working with it feels very normal:

customer = Customer.create!(
  email_address: "[email protected]",
  phone_number: "+1 555 123 4567",
  ssn_last4: "4242",
  support_notes: "Prefers contact by email in the morning."
)

customer.email_address
# => "[email protected]"

Customer.find_by(email_address: "[email protected]")
# => same customer

Behind the scenes, Rails stores encrypted values in the database, but your app code keeps dealing with usable plain values.

You can also inspect the ciphertext for debugging:

customer.ciphertext_for(:email_address)

That can be helpful when you want to confirm a field is really encrypted and not silently staying in plain text.

Step 5: Know what you can and cannot query

This is the part that usually trips people up.

This works:

Customer.find_by(email_address: "[email protected]")

This is a bad idea if phone_number is non-deterministic:

Customer.find_by(phone_number: "+1 555 123 4567")

So the rule is simple:

  • If you need exact-match queries, use deterministic encryption.
  • If you do not need queries, keep the default non-deterministic encryption.

Do not make everything deterministic just because it feels more convenient. Deterministic fields leak more pattern information than non-deterministic ones, so use them only where the product actually needs lookup behavior.

Step 6: Migrating an existing model safely

If you already have live plain-text data, do not flip encrypts on and hope for the best. Migrate in stages.

Temporary migration settings

During the transition, enable support for mixed encrypted and unencrypted data:

# config/application.rb
config.active_record.encryption.support_unencrypted_data = true
config.active_record.encryption.extend_queries = true

This lets Rails:

  • read old unencrypted values without crashing
  • query deterministic attributes while both encrypted and plain-text rows coexist

These settings are for migration periods only. They should not be your final state.

Backfill the data

Once the model has encrypts declarations, you can re-save existing records in batches:

class BackfillCustomerEncryption
  def self.run
    Customer.in_batches(of: 500) do |relation|
      relation.each do |customer|
        customer.update!(
          email_address: customer.email_address,
          phone_number: customer.phone_number,
          ssn_last4: customer.ssn_last4,
          support_notes: customer.support_notes
        )
      end
    end
  end
end

That looks repetitive, but it is very explicit: read the current value, assign it again, and let Rails persist the encrypted version.

You could wrap this in a rake task:

# lib/tasks/encryption.rake
namespace :data do
  desc "Encrypt existing customer data"
  task encrypt_customers: :environment do
    BackfillCustomerEncryption.run
  end
end

Then run:

bin/rails data:encrypt_customers

Finish the migration

After the backfill is complete and you have confirmed the data is encrypted:

  1. Remove support_unencrypted_data.
  2. Remove extend_queries if you only enabled it for the migration.
  3. Deploy again with the stricter configuration.

That is the point where your app stops tolerating old clear-text rows.

A small but production-friendly example

Here is a compact setup I would feel good about shipping as a first version:

class Customer < ApplicationRecord
  encrypts :email_address, deterministic: true, downcase: true
  encrypts :phone_number
  encrypts :ssn_last4
  encrypts :support_notes

  validates :email_address, presence: true, uniqueness: true
end
class CreateCustomers < ActiveRecord::Migration[8.0]
  def change
    create_table :customers do |t|
      t.string :email_address, limit: 510, null: false
      t.text :phone_number
      t.text :ssn_last4
      t.text :support_notes
      t.timestamps
    end

    add_index :customers, :email_address, unique: true
  end
end

That gives you:

  • encrypted storage for sensitive fields
  • exact-match lookups by email
  • case-insensitive email uniqueness
  • a safe default for notes and other non-queryable sensitive values

Gotchas worth knowing early

1. Do not commit encryption keys

This sounds obvious, but it is the whole game. Store them in Rails credentials or environment variables, never in source control.

2. Use deterministic encryption only when you need it

If you are not querying a field, do not force it into deterministic mode.

3. Be careful with column sizes

Encrypted values are larger than the original input. text is the easiest safe default. Short indexed columns need extra planning.

4. downcase: true changes the stored value

If preserving original case matters for display, look into ignore_case: true instead. That requires an additional original_<column_name> column.

5. Migration flags should be temporary

support_unencrypted_data is great for transitions and not great as a forever setting.

Testing it

A small model spec goes a long way:

RSpec.describe Customer, type: :model do
  it "encrypts the email address" do
    customer = Customer.create!(email_address: "[email protected]")

    expect(customer.ciphertext_for(:email_address)).to be_present
    expect(customer.email_address).to eq("[email protected]")
  end

  it "finds deterministically encrypted email addresses" do
    customer = Customer.create!(email_address: "[email protected]")

    expect(Customer.find_by(email_address: "[email protected]")).to eq(customer)
  end
end

Even two or three tests like this will protect you from configuration regressions later.

Final thoughts

Active Record Encryption is one of the nicest examples of the Rails philosophy: powerful, declarative, and surprisingly approachable once you know the few important tradeoffs.

If you are starting fresh, the most practical approach is:

  1. Put keys in credentials.
  2. Encrypt only the fields that are actually sensitive.
  3. Use deterministic encryption only for values you must query exactly.
  4. Migrate old data in batches.
  5. Turn the migration compatibility flags back off when you are done.

That is more than enough to go from “we should probably protect this data” to something real and usable in production.

Ruby on Rails Back to Blog