Skip to content

Engine loads ActiveRecord::Base at require time, breaking host apps' initializer-configured Active Record encryption #38

Description

@phanilkumar

Summary

lib/solid_objects/engine.rb loads SolidObjects::Record (a subclass of ActiveRecord::Base) at require time:

# lib/solid_objects/engine.rb
require_relative "../../app/models/solid_objects/record"

Because lib/solid_objects.rb is required during Bundler.require, this forces ActiveRecord::Base to load before the host Rails app boots. That changes the timing of every ActiveSupport.on_load(:active_record) / on_load(:active_record_encryption) hook in the host app from "first real AR use, after initializers" to "immediately, during railtie setup" — with at least one concrete breakage:

Observed breakage: Active Record encryption silently loses its keys

Rails applies encryption config via (activerecord/lib/active_record/railtie.rb):

initializer "active_record_encryption.configuration" do |app|
  ActiveSupport.on_load(:active_record_encryption) do
    ActiveRecord::Encryption.configure(
      primary_key: app.credentials.dig(:active_record_encryption, :primary_key),
      ...
      **app.config.active_record.encryption
    )
  end
end

Normally that hook fires on first AR use, long after config/initializers/* have populated config.active_record.encryption. With solid_objects in the bundle, AR is already loaded when the railtie registers the hook, so it fires right there — before config/initializers run. Any app that assigns its encryption keys in an initializer (a common pattern, e.g. to support env-var key rotation with a credentials fallback) gets Encryption.configure called with nils, the initializer's later assignment is silently discarded, and every encrypted attribute raises at first use:

ActiveRecord::Encryption::Errors::Configuration:
  Missing Active Record encryption credential: active_record_encryption.deterministic_key

The failure is far from the cause (model specs unrelated to solid_objects), which made it expensive to trace. Verified on solid_objects 0.13.0, Rails 8.1.3.1, Ruby 4.0.6: adding the gem to the Gemfile breaks these specs with no other change; removing it (or moving the app's key assignment into the application class body) fixes them.

Encryption is just the visible symptom — pre-loading AR at require time shifts the semantics of every deferred on_load(:active_record) hook in the host app and other gems.

Suggested fix

Defer the model load until Active Record actually loads, the way engines conventionally do:

# lib/solid_objects/engine.rb
module SolidObjects
  class Engine < ::Rails::Engine
    ...
    initializer "solid_objects.record" do
      ActiveSupport.on_load(:active_record) do
        require_relative "../../app/models/solid_objects/record"
      end
    end
  end
end

(and similarly audit lib/solid_objects.rb's top-level require "active_record" — requiring the framework is fine, but nothing should touch ActiveRecord::Base until on_load fires). The other models under app/models/solid_objects/ appear to load lazily via the engine's autoload paths already; Record is the one eager exception.

Host-app workaround

Move encryption key assignment out of config/initializers into the Application class body in config/application.rb, which executes before railtie initializers regardless of AR load timing.


Found while evaluating solid_objects for a production Rails 8.1 e-commerce app (the payment-link recovery actor worked beautifully otherwise — thanks for the library).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions