# RailsAdmin vs Trestle: Automatic Admin Panels vs Explicit Control

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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/1f8c96a5-daee-48d8-0bd9-6cd091f72f00/md2x =3024x1532)

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:**

```ruby
# Gemfile
gem 'rails_admin', '~> 3.0'
```

```ruby
# config/routes.rb
Rails.application.routes.draw do
  mount RailsAdmin::Engine => '/admin', as: 'rails_admin'
end
```

```ruby
# 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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/98fdceb0-ddb5-4b1d-ec65-b661b3a38900/lg2x =2440x1700)

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:**

```ruby
# Gemfile
gem 'trestle'
gem 'trestle-auth'
```

```ruby
# 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:

```ruby
[label 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:

```command
rails generate trestle:install
```

```command
rails generate trestle:auth:install
```

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

```ruby
[label 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:

```ruby
[label 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:

```ruby
[label 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:

```ruby
[label 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:

```ruby
[label 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:

```ruby
[label 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:

```ruby
[label 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:

```ruby
[label 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:

```ruby
[label 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:

```ruby
[label 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:

```ruby
[label 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:

```ruby
[label 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:

```ruby
[label 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:

```ruby
[label 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
```

```ruby
[label 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:

```ruby
[label 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:

```ruby
[label 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:

```ruby
[label 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.
