madmin: a Rails admin built on resource classes and Hotwire
A robust Admin Interface for Ruby on Rails apps
At a glance
- What is it?
- madmin is an MIT licensed Ruby on Rails engine that generates an admin interface from Resource classes, with filters, scopes, search and customizable views. It runs on Turbo and Stimulus with import maps and no Node build step, and it inherits whatever your models already do.
- Who is it for?
- Adopt madmin when your team wants a Rails admin that reads like ordinary application code and inherits Active Storage, Action Text and Action Mailbox behaviour instead of reimplementing it. Skip it when you need a bespoke interface with its own design system, since every visual change means copying views into your app.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 6 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Resources are Ruby classes, not a configuration file
Madmin models each admin screen as a class that inherits from `Madmin::Resource`. That choice is the reason the library advertises familiarity with Rails scaffolds and less DSL. Instead of describing a screen in YAML, you write a Ruby class where ordinary Ruby methods apply.
The simplest resource declares which model it administers.
class BookResource < Madmin::Resource
attribute :id, form: false
attribute :title
attribute :subtitle, index: false
attribute :author
attribute :genre
attribute :pages, show: false
endEach `attribute` call controls where a column appears. The `form: false` option keeps the id out of forms, `index: false` hides a column from the list, and `show: false` hides it from the detail page. Field types can also be set as a second argument, so a select with an explicit collection is one call.
class BookResource < Madmin::Resource
attribute :genre, :select, collection: ["Fiction", "Mystery", "Thriller"]
endSince this is plain Ruby, conditional logic is available where you need it, and the documentation shows overriding `scope_label` when a label depends on more than a humanized name.
Two commands and a generator sweep
Installation is a gem and a generator.
bundle add madminThe gem is then installed by running the madmin generator, which installs Madmin and generates resources for each of the models it finds.
rails g madmin:installThat last part is the point worth understanding. Rather than asking you to declare every model by hand, the install generator walks your models and writes a resource class for each one, giving you a starting point to edit. The repository layout matches a normal engine: an `app/` directory for views, `lib/` for the engine code, `config/` for routes and generators, `madmin.gemspec` for packaging, and an `Appraisals` file with a `gemfiles/` directory for testing against dependency sets.
To add one model later, the resource generator takes the model class as an argument.
rails g madmin:resource ActionText::RichTextWorking with the Active Storage models is therefore not a special case. The generator takes any model class name, including namespaced ones.
Filters, scopes and search all end up in the URL
Index pages carry a Filters button that narrows records by column, with conditions such as admin being true or created at being after a given date. Filters combine with search, with scopes and with sorting, and the documentation notes they are kept in the URL so a filtered list can be bookmarked and shared with someone else.
Which columns can be filtered is decided by type. String, text, number, date, datetime and boolean columns qualify. Encrypted columns and attributes that are not database columns are left out, which is a sensible default for anything sensitive or virtual. You can turn filtering off for a specific attribute with `filter: false`, as in `attribute :api_token, filter: false`.
Custom fields can opt in by returning a type from `self.filter_type`, one of `:string`, `:number`, `:date`, `:datetime` or `:boolean`.
Scopes are the second narrowing mechanism, declared on the resource and rendered as filter buttons.
class PostResource < Madmin::Resource
scope :published
scope :draft
endEach scope must correspond to a scope or class method on the model. Button labels default to the humanized scope name, and translations use an I18n key of `madmin.scopes.<param_key>.<scope>` for one resource, or `madmin.scopes.<scope>` shared across resources.
Read-only resources are enforced on write, not just hidden
Some models belong in an admin for viewing and nothing else. Madmin handles that with a class method override.
class AuditLogResource < Madmin::Resource
def self.readonly?
true
end
endThe documentation is specific about the behaviour: the new, edit and delete links are hidden for read-only resources, and any write action redirects back to the index. Hiding the links alone would be a UI suggestion, so the redirect is the part that matters. A crafted request that reaches a create or update action does not write.
That makes audit logs and generated records safe to expose to a wider group of staff without worrying that a stray form post changes history. The alternative approach of leaving write actions enabled and relying on permissions elsewhere in the stack is what this replaces.
One performance note belongs with the controller side. The documentation describes preloading an association to avoid N+1 queries by overriding a method in the controller, and the example overrides `scoped_resources` with `super.includes(:user)`. Note that the prose names it `scoped_resource` while the sample code uses `scoped_resources`, so check which one the current version expects before you rely on it.
Views are copied out of the gem when you need to change them
The gem ships its own views, which the documentation calls a good starting point. When they are not enough, a generator copies them into your application, where they become ordinary Rails views you can edit.
rails generate madmin:viewsThat copies the standard action views (index, new, edit, show and the form partial) along with three extras: the layout file `application.html.erb`, the default JavaScript setup in `_javascript.html.erb`, and `_navigation.html.erb`, which renders the navigation and sidebar menu.
You can narrow the scope of the copy. There are generators for the navigation only and for the layout, where the layout generator also writes the JavaScript and navigation files.
rails g madmin:views:navigation
# -> app/views/madmin/_navigation.html.erb
rails g madmin:views:layout
# -> app/views/madmin/application.html.erb
# -> app/views/madmin/_javascript.html.erb
# -> app/views/madmin/_navigation.html.erbA single view can also be copied for one resource only, with `rails generate madmin:views:index Book` writing to `app/views/madmin/books/index.html.erb`. The trade-off is explicit in the design. Out of the box you get a consistent admin for free; once you copy views, you own them, including keeping them working across upgrades.
Nested forms need model support before they work
Editing associated records inside the parent's form is supported through two field types, `:nested_has_many` and `:nested_has_one`. The model has to opt in with `accepts_nested_attributes_for`, `allow_destroy: true` is needed for the Remove link, and the associated model needs its own resource.
class User < ApplicationRecord
has_many :posts
has_one :profile
accepts_nested_attributes_for :posts, :profile, allow_destroy: true
end
class UserResource < Madmin::Resource
attribute :posts, :nested_has_many, skip: %I[user]
attribute :profile, :nested_has_one, skip: %I[user]
endThe `skip` option leaves out attributes of the associated resource, which is how you avoid a nested form that offers to edit the back reference and create a cycle.
There is one behaviour difference between the two types that the documentation spells out. A nested has one shows its fields when the record exists, or an Add link when it does not, so saving the parent does not create an empty record. A nested has many behaves differently in the sense that it always renders its collection.
Direct uploads are also part of the form story: file fields upload through the form by default, and `direct_upload: true` makes the browser upload straight to storage.
Turbo, Stimulus and import maps, with Propshaft or Sprockets
Madmin is built with Hotwire, specifically Turbo, Stimulus and import maps, and the documentation is explicit that there is no Node or build step. That removes an entire class of setup work for a Rails team: no package.json, no bundler configuration, no asset compilation for the admin.
The asset pipeline is not locked to one choice, since the project works with both Propshaft and Sprockets. Rails versions are also not pinned to one generation, because the admin is meant to work with the framework features an application already has. Action Text, Active Storage, Action Mailbox and `has_secure_password` are all listed as supported out of the box, which is the practical argument for using an engine over building your own screens.
Search, filters and scopes on index pages and dark mode are part of the default interface. Resource names in the menu, headings and buttons come from the model translation when one exists, following the Rails `activerecord.models` keys, so `one` and `other` forms set the label. Models without a translation keep their default names, such as `Posts` and `Active Storage / Blobs`.
This is a Rails-only tool. Nothing in the documentation suggests a way to reuse it outside a Rails application, and the resource model, the generators and the view paths are all tied to that framework.
Three releases in October and an MIT licence
The project is not archived and the last push was on 2026-10-17. Recent releases came in quick succession: v3.3.0 on 2026-10-05, v3.2.0 on 2026-10-02 and v3.1.2 on 2026-10-01. That is a fast release cadence for an engine that ships generated views and generators, and it means upgrade notes in the `CHANGELOG.md` at the repository root are worth reading before bumping.
The licence is MIT, stated in the `madmin.gemspec` and covered by the `MIT-LICENSE` file at the repository root. That permits commercial use and modification, including embedding the engine in a proprietary application, provided the copyright notice and licence text are retained. That is a reading of the licence terms and not legal advice, and your own counsel should confirm it fits how you distribute the application.
The upgrade cost for a team is not really the gem bump. It is the copied views. Once `rails generate madmin:views` has run, your copies no longer receive upstream fixes, and a release that changes a shipped template will not reach them until you diff and merge by hand. The `attribute` method in `model_resource.rb` is what the documentation points to when you need an attribute visible in some views and hidden in others, which keeps more of that variation in resource classes rather than in templates.
Editorial conclusion
Adopt madmin when your team wants a Rails admin that reads like ordinary application code and inherits Active Storage, Action Text and Action Mailbox behaviour instead of reimplementing it. Skip it when you need a bespoke interface with its own design system, since every visual change means copying views into your app. Verify first that your models expose the attributes you want to filter on as real database columns, because encrypted and non column attributes are excluded from filtering by design.
Frequently asked questions
How do I install madmin in a Rails application?
Add the gem with `bundle add madmin`, then run `rails g madmin:install`. The generator installs Madmin and creates a resource for each model it finds in your application.
How do I add a model that was not covered by the install generator?
Run the resource generator with the model class name, for example `rails g madmin:resource ActionText::RichText`. The generated class inherits from `Madmin::Resource` and you edit it from there.
Can I hide a model from edits but still show it in the admin?
Yes. Define `self.readonly?` returning true on the resource. The new, edit and delete links are hidden and any write action redirects back to the index.
Which columns can I filter on in madmin?
String, text, number, date, datetime and boolean columns can be filtered. Encrypted columns and attributes that are not database columns are excluded, and you can disable filtering for a specific attribute with `filter: false`.
Do I need Node or a build step to use madmin?
No. Madmin is built with Hotwire using Turbo, Stimulus and import maps, and the documentation states there is no Node or build step. It works with both Propshaft and Sprockets for asset handling.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/excid3-madmin)Was this useful?