Back to Scaling Ruby Applications guides

RailsAdmin vs Trestle: Automatic Admin Panels vs Explicit Control

Stanley Ulili
Updated on November 24, 2025

Admin interfaces shape how you work with your app’s data and how much effort setup takes. Automatic generation gives you a working panel right away, while explicit configuration lets you control every detail. The choice depends on whether you want to start quickly or fine-tune everything yourself.

RailsAdmin was released in 2010 as a Rails version of Django Admin. It reads your models and builds forms, tables, filters, and actions automatically. Trestle, launched in 2017 by Sam Pohlenz, offers a newer design that focuses on clear structure, modern components, and full customization through code.

When choosing between them, think about how much setup you want to do. RailsAdmin gives you everything working from the start, great for simple data management or internal tools. Trestle takes more setup but gives you complete control over layout, behavior, and features. Your choice affects how quickly you can launch and how much freedom you have to customize later.

What is RailsAdmin?

Screenshot of the RailsAdmin page

RailsAdmin creates an admin interface automatically by reading your ActiveRecord models. You mount it in your routes and get a complete panel without writing any admin code. It detects associations, validations, and data types to generate suitable forms and tables for each model.

You can customize it with a simple configuration file. Most settings go in one initializer, where you adjust field types, labels, and access rules or connect authentication and authorization.

The interface includes sortable tables, search, filters, file uploads, and bulk actions. It integrates easily with Devise, CanCanCan, and similar gems.

Basic setup:

 
# Gemfile
gem 'rails_admin', '~> 3.0'
 
# config/routes.rb
Rails.application.routes.draw do
  mount RailsAdmin::Engine => '/admin', as: 'rails_admin'
end
 
# config/initializers/rails_admin.rb
RailsAdmin.config do |config|
  config.authenticate_with { warden.authenticate! scope: :user }
  config.authorize_with :cancancan
end

Mounting the engine makes the admin panel available at /admin. You can log in, view data, and start managing records immediately.

What is Trestle?

Screenshot of Trestle dashboard

Trestle builds admin interfaces through explicit resource definitions. You choose which models appear, what data they show, and how each section behaves. Each admin resource has its own file in app/admin/, where you define tables, forms, and menus. Nothing is created automatically.

The framework uses a clean Ruby DSL that gives you fine-grained control. You decide which columns appear in tables, which fields appear in forms, and which actions are available. Search, sorting, and custom actions are supported but must be configured by you.

Trestle uses Bootstrap and Stimulus for a modern interface. Authentication works through trestle-auth or adapters for other systems.

Basic setup:

 
# Gemfile
gem 'trestle'
gem 'trestle-auth'
 
# app/admin/products_admin.rb
Trestle.resource(:products) do
  menu do
    item :products, icon: "fa fa-shopping-bag"
  end

  table do
    column :name
    column :price, align: :center
    column :category
    actions
  end

  form do |product|
    text_field :name
    number_field :price
    select :category_id, Category.all
  end
end

This file defines how products appear in your admin panel: a menu item, a table with specific columns, and a form with chosen fields. Only what you define is displayed, giving you full control over structure and layout.

RailsAdmin vs Trestle: quick comparison

Aspect RailsAdmin Trestle
Generation approach Automatic from models Explicit resource definitions
Initial setup time Minutes, zero config Moderate, requires resource files
Default interface All models exposed Only defined resources exposed
Customization method Override defaults via config DSL Define behavior explicitly
UI framework Bootstrap 3 Bootstrap 4/5
JavaScript approach jQuery-based Stimulus-based
Field type inference Automatic from schema Manual declaration required
Association handling Automatic dropdowns/selects Manual configuration
Search functionality Automatic across text fields Explicit search definition
File uploads Automatic with CarrierWave/ActiveStorage Manual configuration
Bulk actions Built-in Custom implementation
Learning curve Gentle, works immediately Steeper, requires understanding DSL
Configuration location Single initializer file Individual admin files per resource
Maintenance philosophy Convention over configuration Explicit is better than implicit

Setup and initial experience

I needed to add admin interfaces to an e-commerce application quickly, so I tried RailsAdmin first. After adding the gem and creating a single initializer file, I had a complete admin panel:

config/initializers/rails_admin.rb
RailsAdmin.config do |config|
  config.authenticate_with do
    warden.authenticate! scope: :user
  end

  config.authorize_with :cancancan
end

Restarting the server revealed something remarkable—every model in the application (Products, Orders, Users, Categories) appeared with functional CRUD screens. Forms included appropriate inputs based on column types. Associations rendered as select boxes. File upload fields appeared for ActiveStorage attachments. I got a production-ready admin panel in under five minutes.

But this automatic generation came with a catch I discovered immediately. Every single model appeared in the navigation, including internal models like ActiveStorage::Blob and ActiveStorage::Attachment. The interface exposed everything—even sensitive user data and system tables I never wanted admins to touch. RailsAdmin's philosophy of "everything by default, hide what you don't want" meant I now had to go back and explicitly exclude models.

Trestle took the opposite approach. After running the installer, I had an empty admin panel:

 
rails generate trestle:install
 
rails generate trestle:auth:install

Nothing appeared until I explicitly created admin resources. For the Products model alone, I wrote:

app/admin/products_admin.rb
Trestle.resource(:products) do
  menu do
    item :products, icon: "fa fa-cube"
  end

  table do
    column :name, link: true
    column :sku
    column :price do |product|
      number_to_currency(product.price)
    end
    column :category
    actions
  end

  form do |product|
    text_field :name
    text_field :sku
    number_field :price, step: 0.01
    select :category_id, Category.pluck(:name, :id)
    text_area :description, rows: 6
  end
end

This took thirty minutes for one model. But here's what struck me—I knew exactly what appeared in the interface. The navigation showed only Products because that's all I'd defined. No surprise models, no accidental data exposure. Trestle's "explicit only" philosophy meant more typing upfront but zero surprises.

Customizing list views

That philosophical difference became critical when I started customizing the product listing. With RailsAdmin showing everything by default, I needed to hide columns and reformat prices:

config/initializers/rails_admin.rb
RailsAdmin.config do |config|
  config.model 'Product' do
    list do
      field :name
      field :sku
      field :price do
        formatted_value do
          number_to_currency(value)
        end
      end
      field :category

      exclude_fields :description, :created_at
    end
  end
end

This worked, but I found myself constantly fighting RailsAdmin's defaults. It showed created_at and updated_at on every model. It displayed descriptions in the list view, making rows huge. I spent more time excluding fields than I saved from automatic generation. The exclude_fields list grew longer as I discovered more unwanted columns.

Trestle forced me to declare every column explicitly, which initially felt tedious:

app/admin/products_admin.rb
Trestle.resource(:products) do
  table do
    column :name, link: true, sort: { default: true }
    column :sku
    column :price do |product|
      number_to_currency(product.price)
    end
    column :stock_quantity, header: "Stock"

    actions do |toolbar, instance|
      toolbar.link "Restock", restock_path(instance),
                   method: :post, style: :success
    end
  end
end

But this explicitness meant I never saw surprise columns. When I added a wholesale_price column to the database for internal tracking, RailsAdmin automatically exposed it to all admins. I only discovered this when a support team member asked why they saw wholesale prices. Trestle wouldn't show the new column unless I explicitly added it to the table definition.

Form customization

The listing customization issues got worse when I tackled form editing. RailsAdmin generated forms automatically, which initially seemed great:

config/initializers/rails_admin.rb
RailsAdmin.config do |config|
  config.model 'Product' do
    edit do
      field :name
      field :description, :text
      field :price
      field :stock_quantity do
        label "Current Stock"
        help "Leave blank to keep current value"
      end
      field :category
      field :images, :multiple_file_upload
    end
  end
end

The :text type converted the description to a textarea. The :multiple_file_upload handled images. But look closely at this configuration—I'm still fighting defaults. RailsAdmin showed every field, and I needed to selectively configure the ones that needed special treatment. The form included timestamps and internal fields until I excluded them.

With Trestle, I built the exact form I wanted from scratch:

app/admin/products_admin.rb
Trestle.resource(:products) do
  form do |product|
    text_field :name
    text_area :description, rows: 6

    row do
      col(sm: 6) { number_field :price, prepend: "$" }
      col(sm: 6) { number_field :stock_quantity }
    end

    select :category_id, Category.pluck(:name, :id),
           include_blank: "Select category"

    check_box :published
  end
end

This took longer to write but gave me precise control. The row and col helpers created a two-column layout—something RailsAdmin couldn't do without overriding views. The prepend: "$" option added a dollar sign prefix. When my product manager requested the price and stock fields side-by-side, I changed two lines. With RailsAdmin, I'd need to override the entire form view template.

Handling associations

The form layout control mattered even more when handling nested data. My Order model had line items that needed inline editing:

config/initializers/rails_admin.rb
RailsAdmin.config do |config|
  config.model 'Order' do
    edit do
      field :user
      field :line_items do
        nested_form true
      end
      field :status
    end
  end
end

With nested_form true, RailsAdmin generated a working interface for adding, editing, and removing line items inline. This worked beautifully—I could manage an entire order without leaving the page. The gem handled all the JavaScript complexity of dynamically adding fields.

But when my team needed custom behavior—like auto-calculating the order total when line items changed—I hit a wall. RailsAdmin's nested form was a black box. The JavaScript lived inside the gem. Customizing it meant either monkey-patching the gem or abandoning nested forms entirely.

Trestle made me build the nested interface manually:

app/admin/orders_admin.rb
Trestle.resource(:orders) do
  form do |order|
    select :user_id, User.order(:email).pluck(:email, :id),
           label: "Customer"

    select :status, Order.statuses.keys.map { |k| [k.titleize, k] }

    sidebar do
      if order.persisted?
        render "admin/orders/line_items", order: order
      end
    end
  end
end

I had to create a custom partial for line items and write controller code to handle updates. This was significantly more work—probably three hours versus RailsAdmin's five minutes. But when the requirement for auto-calculating totals came, I added a Stimulus controller to my partial in fifteen minutes. With RailsAdmin, I'd still be reading gem source code trying to figure out how to hook into the nested form.

Search and filtering

After building forms for five models, I noticed users needed search functionality. RailsAdmin provided it automatically—no configuration:

config/initializers/rails_admin.rb
RailsAdmin.config do |config|
  config.model 'Product' do
    list do
      field :name
      field :sku
      field :price
      field :category
    end
  end
end

A search box appeared at the top. Users typed queries and RailsAdmin searched across all text fields—name, sku, description. This worked immediately but gave me no control over search behavior. When my team wanted to search by category name (a joined table), I couldn't configure it. RailsAdmin only searched columns on the Product table itself.

Trestle required implementing search explicitly:

app/admin/products_admin.rb
Trestle.resource(:products) do
  search do |query|
    if query.present?
      Product.joins(:category)
             .where("products.name ILIKE :q OR
                     products.sku ILIKE :q OR
                     categories.name ILIKE :q",
                    q: "%#{query}%")
    else
      Product.all
    end
  end

  scope :all, default: true
  scope :published
  scope :out_of_stock, -> { where("stock_quantity <= 0") }
end

Writing the search query took ten minutes, but I controlled exactly which fields to search and could join related tables. The scope declarations added filter buttons—something RailsAdmin required a custom configuration to achieve. When my team wanted fuzzy search using pg_search, I updated my search block. With RailsAdmin, I'd need to override the entire search action.

Bulk actions

That search implementation immediately led to requests for bulk actions. Users wanted to publish multiple products at once. RailsAdmin included bulk delete and export by default, and I added a custom bulk action:

config/initializers/rails_admin.rb
RailsAdmin.config do |config|
  config.model 'Product' do
    custom_action :publish_all do
      http_methods :post
      controller do
        @products = Product.where(id: params[:bulk_ids])
        @products.update_all(published: true)
        flash[:success] = "#{@products.count} products published"
        redirect_to back_or_index
      end
    end
  end
end

This worked, but RailsAdmin's bulk action API felt awkward. The custom_action block didn't follow Rails conventions. When I needed to add validation—preventing bulk publish of products missing required fields—I struggled to integrate Rails' standard validation patterns.

Trestle's bulk actions felt like normal Rails controllers:

app/admin/products_admin.rb
Trestle.resource(:products) do
  table do
    selectable
    actions
  end

  collection_action :bulk_publish, method: :post do
    Product.where(id: params[:ids]).update_all(published: true)
    flash[:success] = "Products published"
    redirect_to admin.path
  end

  toolbar do
    button "Publish Selected", bulk_publish_path,
           method: :post, data: { toggle: "bulk-action" }
  end
end

The collection_action was just a controller method. Adding validation meant using standard Rails patterns. The trade-off was clear—I had to wire up more pieces (the selectable checkbox, the toolbar button, the action itself) but each piece worked like normal Rails code.

File upload handling

Bulk actions worked fine until I tackled file uploads, which revealed how brittle RailsAdmin's automatic generation could be. With ActiveStorage attached to my Product model, RailsAdmin detected it:

config/initializers/rails_admin.rb
RailsAdmin.config do |config|
  config.model 'Product' do
    edit do
      field :images, :multiple_active_storage do
        delete_method :purge
      end
    end
  end
end

This generated a working upload interface with thumbnails and delete buttons. Perfect—except it wasn't. When users uploaded images, they got no progress indication. Large uploads appeared to hang. The interface didn't show image dimensions or file sizes. I needed to add image validation (maximum size, required aspect ratio) but RailsAdmin provided no hooks for that.

I could override the field's view partial, but that meant maintaining custom views that might break when upgrading RailsAdmin. The automatic generation got me 80% of the way there, then became an obstacle.

Trestle made me build file uploads from scratch:

app/admin/products_admin.rb
Trestle.resource(:products) do
  form do |product|
    if product.images.attached?
      render "admin/products/images", product: product
    end

    file_field :images, multiple: true, direct_upload: true
  end

  controller do
    def remove_image
      image = instance.images.find(params[:image_id])
      image.purge
      redirect_back fallback_location: admin.path(instance)
    end
  end
end

My custom partial showed image previews with dimensions and file sizes. Adding validation meant standard Rails before_save callbacks. Progress indicators came from ActiveStorage's direct upload JavaScript. Every piece was standard Rails—no special admin framework knowledge required.

Authorization and permissions

After building file uploads for three models, my security team requested granular permissions. Different admin users needed different capabilities. RailsAdmin integrated with CanCanCan seamlessly:

app/models/ability.rb
class Ability
  include CanCan::Ability

  def initialize(user)
    if user.admin?
      can :access, :rails_admin
      can :manage, :all
    elsif user.editor?
      can :access, :rails_admin
      can :read, :all
      can [:create, :update], [Product, Category]
    end
  end
end
config/initializers/rails_admin.rb
RailsAdmin.config do |config|
  config.authorize_with :cancancan
end

Two lines in the initializer and permissions worked throughout the admin. Edit buttons disappeared for resources users couldn't modify. Forms became read-only based on abilities. This integration was RailsAdmin's strongest feature—complete authorization without touching individual admin screens.

Trestle's authorization required manual implementation in each resource:

app/admin/products_admin.rb
Trestle.resource(:products) do
  remove_action :destroy unless current_user.admin?

  controller do
    before_action :require_admin, only: [:destroy]

    private

    def require_admin
      unless current_user&.admin?
        flash[:error] = "Not authorized"
        redirect_to admin.path
      end
    end
  end
end

I had to add authorization checks to every admin resource individually. This was tedious but gave me granular control. When my team needed custom authorization logic—like "editors can only edit products they created"—I implemented it as standard Rails controller filters. With RailsAdmin, I'd need to extend CanCanCan's abilities in ways that didn't always map to the admin's actions.

Custom dashboards

With authorization working, my team wanted a custom dashboard showing key metrics. RailsAdmin provided a default dashboard, but customizing it meant overriding views:

config/initializers/rails_admin.rb
RailsAdmin.config do |config|
  config.main_app_name = ['My Store', 'Admin']

  config.actions do
    dashboard do
      statistics false
    end
  end
end

The default dashboard showed model counts and recent activity. Disabling statistics gave me a blank slate, but adding custom widgets required creating view overrides in my application that mirrored RailsAdmin's view structure. The mountable engine architecture made customization awkward—I was fighting the framework rather than extending it.

Trestle treated the dashboard like any other admin resource:

app/admin/dashboard_admin.rb
Trestle.admin(:dashboard, path: "/") do
  controller do
    def index
      @total_products = Product.count
      @total_orders = Order.count
      @revenue_today = Order.where(created_at: Date.today.all_day).sum(:total)
    end
  end
end

The controller was standard Rails. The view was standard ERB with access to Bootstrap components. Adding charts meant dropping in a JavaScript library and writing normal frontend code. This was just Rails—no special admin framework concepts to learn.

Final thoughts

After using both tools, the difference is clear. RailsAdmin is quick to set up and gives you a working panel right away, but it becomes harder to adjust as your needs grow. What starts as a shortcut can turn into extra work later.

Trestle takes more effort at first, but it’s easier to change and extend because everything follows normal Rails patterns. Once it’s set up, adding new features or custom workflows feels natural.

If you need something fast and simple, RailsAdmin is enough. If you expect your admin to grow or need more control, Trestle is the better long-term choice.

Got an article suggestion? Let us know
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.