opensource rails ruby

Building Modular Rails Applications: A Deep Dive into Rails Engines Through Active Storage Dashboard

I’ve been building Rails applications for the last 10 years on a daily base and almost all of them use active storage now. Users are uploading files and then the questions start rolling in from the team and they are always the same:

“How much storage are we actually using?” “Can we see which files aren’t attached to anything anymore?” “What types of files are users uploading the most?” “Is there a way to browse through all our stored files?”

I usually open the Rails console, write a few queries, and get the answers for the team or for the stakeholders. But you know this isn’t sustainable. What I need is a proper dashboard, something visual, something that non-technical team members can use, something that doesn’t require SSH access to production servers.

This is exactly the problem I faced, and it led me down a fascinating journey into the world of Rails engines, ultimately resulting in the creation of Active Storage Dashboard, a mountable Rails engine that provides a modern interface for monitoring and managing Active Storage data. Always the same screen for every app, over and over. Easy to mount and easy to use.

Introducing Active Storage Dashboard

Active Storage Dashboard is more than just a simple admin interface. It’s a fully-featured Rails engine that seamlessly integrates into any Rails application, providing immediate visibility into your file storage ecosystem. With zero external dependencies (pure vanilla JavaScript and CSS), it offers a beautiful, animated interface that feels natural in modern web applications.

The gem provides comprehensive insights including:

  • Real-time storage statistics and usage metrics
  • Browsable interfaces for blobs, attachments, and variant records
  • Advanced filtering capabilities for finding specific files
  • Direct download functionality from the dashboard
  • Maintenance tasks for cleaning up orphaned files
  • Support for both table and card view layouts
  • Beautiful visualizations of content type distributions

Active Storage Dashboard is a Rails engine, and understanding Rails engines opens up a whole new world of possibilities for Ruby developers so I used this experience to share my journey and to invite you to do the same and try to use engines more often.

Rails Engines: The Heroes of Modular Design

Rails engines are perhaps one of the most powerful yet underutilized features of the Ruby on Rails framework. At their core, engines are miniature Rails applications that can be mounted inside a host Rails application. They can come complete with their own models, views, controllers, routes, and assets—essentially everything a Rails app has, but designed to be pluggable and reusable.

Think of Rails engines as the Ruby equivalent of microservices, but without the operational complexity. They allow you to build self-contained features that can be shared across multiple applications or extracted from existing monoliths when certain functionalities become too complex or need to be reused.

The Anatomy of a Rails Engine

Looking at the Active Storage Dashboard’s directory structure reveals the beautiful symmetry between engines and regular Rails applications:

app/
├── controllers/
├── helpers/
└── views/
config/
├── routes.rb
lib/
├── active_storage_dashboard.rb
├── active_storage_dashboard/
│   ├── engine.rb
│   └── version.rb

This structure should feel immediately familiar to any Rails developer. The app directory contains the MVC components, config holds the routes, and lib contains the engine’s core logic. The key difference is the presence of the engine.rb file, which is where the magic happens.

The Engine Class: Where Configuration Meets Convention

Let’s examine the heart of any Rails engine: the engine class itself.

module ActiveStorageDashboard
  class Engine < ::Rails::Engine
    isolate_namespace ActiveStorageDashboard

    initializer "active_storage_dashboard.url_options" do |app|
      ActiveStorageDashboard::Engine.routes.default_url_options = 
        app.routes.default_url_options
    end

    config.active_storage_dashboard = ActiveSupport::OrderedOptions.new

    config.before_initialize do
      config.active_storage_dashboard.each do |key, value|
        ActiveStorageDashboard.public_send("#{key}=", value)
      end
    end
  end
end

The isolate_namespace directive is crucial to ensures that all of the engine’s components (models, controllers, helpers) are namespaced, preventing naming conflicts with the host application. This isolation is what allows engines to be truly modular and reusable.

Building a Production-Ready Engine: Lessons learned from Active Storage Dashboard

Creating a Rails engine that’s both powerful and easy to use requires careful attention to several key areas. Let me walk you through the design decisions and implementation details that make Active Storage Dashboard a joy to integrate and use (or at least I hope so).

1. Routing and URL Generation

One of the trickiest aspects of building engines is handling routing correctly. Active Storage Dashboard uses a sophisticated approach to ensure URLs are generated properly regardless of where the engine is mounted:

# In routes.rb
ActiveStorageDashboard::Engine.routes.draw do
  root to: 'dashboard#index'
  
  resources :blobs, only: [:index, :show] do
    member do
      get 'download(/:disposition)', action: :download, as: :download
    end
  end
  
  resources :attachments, only: [:index, :show] do
    member do
      get 'download(/:disposition)', action: :download, as: :download
    end
  end
  
  resources :variant_records, only: [:index, :show]
end

Notice the optional disposition parameter in the download routes. This allows for flexible file handling—files can be downloaded as attachments or displayed inline in the browser.

2. Configuration Flexibility and Authentication Strategies

A well-designed engine should be configurable without being complicated, and security is often the first concern when integrating admin tools. Active Storage Dashboard provides multiple authentication strategies, allowing developers to choose the approach that best fits their application’s existing security infrastructure.

Controller-Based Authentication

The engine allows host applications to customize the base controller class, enabling seamless integration with existing authentication systems:

module ActiveStorageDashboard
  mattr_accessor :base_controller_class, default: "ActionController::Base"
end

This approach means that if your application already has an AdminController with authentication filters, you can simply configure the engine to inherit from it:

# In config/application.rb or config/environments/production.rb
config.active_storage_dashboard.base_controller_class = "AdminController"

This pattern is particularly powerful because it automatically inherits all the authentication logic, authorization rules, and even layout configurations from your existing admin infrastructure. Your AdminController might look something like:

class AdminController < ApplicationController
  before_action :authenticate_admin!
  before_action :set_admin_timezone
  layout 'admin'
  
  private
  
  def authenticate_admin!
    redirect_to login_path unless current_user&.admin?
  end
end

By setting this as the base controller, every Active Storage Dashboard controller automatically requires admin authentication without any additional configuration.

Route-Level Authentication Constraints

For applications that prefer to handle authentication at the routing layer, Active Storage Dashboard works seamlessly with Rails route constraints. This approach is often cleaner and more explicit:

# config/routes.rb
authenticate :user, -> (user) { user.admin? } do
  mount ActiveStorageDashboard::Engine, at: "/active-storage-dashboard"
end

For Devise users, the integration is even more sophisticated. You can use complex constraints that check both session and warden authentication:

# config/routes.rb
constraints lambda { |req| 
  req.session[:user_id].present? || 
  (req.env['warden'] && req.env['warden'].user(:user)) 
} do
  mount ActiveStorageDashboard::Engine, at: "/active-storage-dashboard"
end

3. Cross-Rails Version Compatibility

One of the biggest challenges in building Rails engines is maintaining compatibility across different Rails versions. Active Storage Dashboard handles this gracefully, detecting features available in different Rails versions and adapting accordingly:

if defined?(ActiveStorage::VariantRecord)
  @variant_records = paginate(ActiveStorage::VariantRecord.order(id: :desc))
else
  @variant_records = []
end

This approach ensures the engine works seamlessly from Rails 5.2 all the way to Rails 8.x, degrading gracefully when newer features aren’t available.

4. Zero External Dependencies Philosophy

In an ecosystem often plagued by dependency hell, Active Storage Dashboard takes a radical approach: zero external dependencies. The entire UI is built with vanilla JavaScript and CSS, featuring smooth animations, responsive layouts, and modern design patterns—all without requiring jQuery, Bootstrap, or any other framework.

This decision pays dividends in several ways:

  • Faster load times
  • No version conflicts with host applications
  • Easier maintenance and debugging
  • Better long-term stability

5. Database-Agnostic Implementation

The engine’s statistics and querying features are carefully designed to work across different database adapters. Here’s how the dashboard handles timeline data generation across SQLite, MySQL, and PostgreSQL:

def adapter_specific_timeline_data
  adapter = ActiveRecord::Base.connection.adapter_name.downcase
  
  if adapter.include?('sqlite')
    result = ActiveRecord::Base.connection.execute(
      "SELECT strftime('%Y-%m', created_at) as month, COUNT(*) as count " +
      "FROM #{table_name} GROUP BY strftime('%Y-%m', created_at)"
    )
  elsif adapter.include?('mysql')
    result = ActiveRecord::Base.connection.execute(
      "SELECT DATE_FORMAT(created_at, '%Y-%m') as month, COUNT(*) as count " +
      "FROM #{table_name} GROUP BY DATE_FORMAT(created_at, '%Y-%m')"
    )
  else # PostgreSQL
    result = ActiveRecord::Base.connection.execute(
      "SELECT TO_CHAR(created_at, 'YYYY-MM') as month, COUNT(*) as count " +
      "FROM #{table_name} GROUP BY TO_CHAR(created_at, 'YYYY-MM')"
    )
  end
end

Efficient Pagination Without Gems

Rather than depending on gems like Kaminari or WillPaginate, the engine implements its own lightweight pagination:

def paginate(scope, per_page = 20)
  @page = [params[:page].to_i, 1].max
  scope.limit(per_page).offset((@page - 1) * per_page)
end

This approach keeps the engine lightweight while providing all the pagination features users need.

Smart File Preview Handling

The engine intelligently handles file previews based on content type and Rails version capabilities:

def previewable_blob?(blob)
  return false unless blob.present?
  
  content_type = blob.content_type
  image_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
  return true if image_types.include?(content_type)
  
  if defined?(ActiveStorage::Blob.new.preview) && 
     blob.respond_to?(:previewable?) && 
     blob.previewable?
    return true
  end
  
  false
end

Rake Tasks for Maintenance

One of the most requested feature, that wansn’t present in the first versions was the ability to purge orphan files. I found a rake task to be the most efficient way to tackle this problem since every other solution would have required to use a queue system to schedule a massive deletion. I provided powerful maintenance tasks that can be run via command line:

namespace :active_storage do
  namespace :dashboard do
    desc "Purge blobs that have no attachments"
    task purge_orphans: :environment do
      ActiveStorageDashboard::OrphanPurger.call
    end

    desc "Re-analyze blobs that are not yet analyzed"
    task reanalyze: :environment do
      ActiveStorageDashboard::Analyzer.call
    end

    desc "Recreate missing or outdated variants"
    task regenerate_variants: :environment do
      ActiveStorageDashboard::VariantRegenerator.call
    end
  end
end

These tasks showcase how engines can provide not just UI features but also operational tools for maintaining application health.

The Broader Impact: Why Rails Engines Matter

The success of gems like Active Storage Dashboard points to a larger trend in the Rails ecosystem: the need for modular, reusable components that can be easily integrated into existing applications. Rails engines fulfill this need perfectly, offering several advantages:

1. Rapid Feature Development

Engines allow teams to develop features in isolation, with their own test suites and dependencies. This parallel development model can significantly speed up delivery times for complex features. We are using this pattern to build a bunch of specific-features that are shared in all our rails applications. This is basically allowing us to replace expensive microservices with engines.

2. Code Reusability Across Projects

Once built, an engine can be used across multiple applications. Imagine building an authentication engine, a payment processing engine, or an admin dashboard engine once and reusing it across all your projects.

3. Gradual Monolith Decomposition

For teams looking to break apart monolithic applications, engines provide a stepping stone. Features can be extracted into engines while still running in the same process, allowing for gradual decomposition without the operational overhead of microservices.

4. Open Source Contribution Opportunities

Engines make it easier to extract and share functionality with the community. A feature that starts as an internal tool can be polished and released as an open-source engine, benefiting the entire Rails ecosystem. This is the case of active_storage_dashboard

Best Practices for Engine Development I Learned

Based on my experience building Active Storage Dashboard and studying other successful engines, here are key best practices for engine development:

1. Namespace Everything

Always use isolate_namespace and ensure all your classes, modules, and database tables are properly namespaced. This prevents conflicts and makes your engine truly reusable.

module ActiveStorageDashboard
  class Engine < ::Rails::Engine
    isolate_namespace ActiveStorageDashboard
  end
end

This isolation extends beyond just Ruby classes. Consider these namespacing strategies:

  • Database tables: Prefix all tables with your engine name (e.g., active_storage_dashboard_settings)
  • CSS classes: Use a unique prefix for all styles (e.g., .asd-dashboard, .asd-card)
  • JavaScript functions: Wrap all JS in a namespaced object
  • Helper methods: Ensure helpers don’t conflict with common names
  • Route helpers: The engine’s routes are automatically namespaced

Remember that even seemingly unique names can conflict. A method like format_bytes might exist in the host application, so I suggesto you to namespace it within its own helper module.

2. Provide Configuration Options

Make your engine configurable but provide sensible defaults. Users should be able to get started quickly while having the flexibility to customize as needed.

Beyond basic configuration, consider these patterns:

module ActiveStorageDashboard
  # Class-level configuration
  mattr_accessor :base_controller_class, default: "ActionController::Base"
  mattr_accessor :per_page, default: 20
  mattr_accessor :enable_delete, default: false
  
  # Configuration block pattern
  def self.configure
    yield self
  end
  
  # Configuration validation
  def self.validate_configuration!
    unless base_controller_class.constantize < ActionController::Base
      raise ConfigurationError, "base_controller_class must inherit from ActionController::Base"
    end
  end
end

This allows for elegant configuration:

ActiveStorageDashboard.configure do |config|
  config.base_controller_class = "AdminController"
  config.per_page = 50
  config.enable_delete = Rails.env.development?
end

3. Document Thoroughly

Include comprehensive README documentation with clear installation instructions, configuration options, and usage examples. Consider including screenshots for UI-heavy engines.

Great documentation should include:

  • Quick Start Guide: Get users running in under 5 minutes
  • Detailed Installation: Cover edge cases and troubleshooting
  • Configuration Reference: Document every option with examples
  • Screenshots/GIFs: Visual elements dramatically improve comprehension
  • Upgrade Guides: Help users migrate between versions
  • API Documentation: If your engine provides programmatic interfaces
  • Contributing Guidelines: Encourage community involvement

4. Keep Dependencies Minimal

Every dependency you add is a potential source of conflicts for users. When possible, implement functionality yourself rather than pulling in additional gems.

Active Storage Dashboard demonstrates this principle by implementing:

  • Custom pagination instead of Kaminari/WillPaginate
  • Vanilla JS instead of jQuery or Stimulus
  • Pure CSS instead of Bootstrap or Tailwind
  • Native Rails helpers instead of additional view libraries

When you must add dependencies:

  • Pin to specific versions cautiously
  • Document why each dependency is necessary
  • Consider making optional features that require additional gems
  • Regularly audit and update dependencies

5. Design for Extension

Provide hooks and extension points where users might want to customize behavior. Use Rails’ built-in patterns like concerns and callbacks to make extension natural.

module ActiveStorageDashboard
  module Controllers
    module Blobs
      extend ActiveSupport::Concern
      
      included do
        before_action :set_blob, only: [:show, :download]
        after_action :track_download, only: [:download]
      end
      
      # Users can override these methods in their own controllers
      def blob_scope
        ActiveStorage::Blob.all
      end
      
      private
      
      def track_download
        # Override this method to add download tracking
      end
    end
  end
end

6. Handle Errors Gracefully

Engines should never crash the host application. Implement robust error handling:

def load_timeline_data
  begin
    # Complex database query
  rescue ActiveRecord::StatementInvalid => e
    Rails.logger.error "Timeline data error: #{e.message}"
    @timeline_data = []
  rescue => e
    Rails.logger.error "Unexpected error: #{e.message}"
    generate_fallback_data
  end
end

7. Security First

Never trust user input and always consider security implications:

  • Sanitize all user inputs
  • Use strong parameters
  • Implement CSRF protection
  • Validate file types and sizes
  • Never expose internal IDs in URLs when possible
  • Rate limit expensive operations

8. Semantic Versioning

Follow semantic versioning religiously:

  • MAJOR: Breaking changes
  • MINOR: New features (backward compatible)
  • PATCH: Bug fixes

Document breaking changes clearly in your CHANGELOG.

Engines as a Path to Better Software Architecture

Active Storage Dashboard demonstrates that Rails engines aren’t just a nice-to-have feature. They’re a powerful tool for building maintainable and reusable solutions to common problems. By embracing engines, we can build better software architectures that are both monolithic in deployment and modular in design.

The journey of building this engine taught me that the Rails ecosystem still has much to offer in terms of architectural patterns and best practices. Engines provide a middle ground between monolithic applications and microservices, offering the benefits of modularity without the operational complexity.

Go open source if you can

For developers looking to level up their Rails skills, I encourage you to explore engines. Start by extracting a feature from an existing application into an engine. Build tools that solve problems you face repeatedly. Share your solutions with the community. The Rails ecosystem thrives when developers create and share modular, reusable components.

The next time you find yourself building a feature that could benefit other applications, whether it’s an admin dashboard, an authentication system, or a domain-specific tool, consider building it as an engine. You might just create the next essential tool that thousands of developers will thank you for.

Active Storage Dashboard is available on GitHub at https://github.com/giovapanasiti/active_storage_dashboard, and I welcome contributions, feedback, and stories about how you’re using it in your applications. Together, we can continue to push the boundaries of what’s possible with Rails engines and create a more modular, maintainable future for Rails applications.