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?
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?
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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
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:
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:
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:
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.