Pundit: the naming convention is the API, and the README tracks main
Minimal authorization through OO design and pure Ruby classes
At a glance
- What is it?
- Pundit is a Ruby authorization library that adds no policy engine of its own: it infers a policy class name from your model, infers a query method from your controller action, and raises when the answer is no. That simplicity is its appeal and its trap, since every convention you break has to be compensated for at the call site, and the README on GitHub describes unreleased code.
- Who is it for?
- Pundit suits a Rails application that wants authorization logic in ordinary Ruby classes it can read in a debugger, and whose team is willing to keep policy classes, their nested scopes and their views in step. It does not suit a codebase looking for a configurable rules engine, since every rule you add is code you write and name yourself.
- 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 29 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 November 14, 2026, and from our analysis. They are not legal advice.
Star history
- varvet/pundit8,519 ★ · -3
| Repository | 2026-09-22 | 2026-11-14 |
|---|---|---|
| varvet/pundit | 8.5k | 8.5k |
-3 Stars in the last 53 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
authorize infers the class, the method, and the error
A single controller line carries three inferences. Given `authorize @post` inside an update action, Pundit derives the class name `PostPolicy` from the record, calls the query method `update?`, and hands the policy the current user and the record. The generated behaviour, spelled out in the docs, is equivalent to this.
unless PostPolicy.new(current_user, @post).update?
raise Pundit::NotAuthorizedError, "not allowed to PostPolicy#update? this Post"
endThe convention has three assumptions behind it: the policy class carries the model name plus the suffix Policy, the first constructor argument is a user which the controller supplies from `current_user`, and the second is whatever record you are authorizing, which does not need to be ActiveRecord or even ActiveModel. The query method usually maps to the controller action name.
Consequences follow from every inference being a naming convention. When the action and the permission differ, as with a publish action that checks `update?`, you pass the name yourself with `authorize @post, :update?`. When the model is not what the policy is about, you pass `policy_class:`. Every deviation from the convention becomes an extra argument you must remember at each call site.
The README on GitHub tracks code you may not be running
The installation section opens with a warning that is easy to skim past. The README on GitHub is accurate with the latest code on GitHub, while most people run a released version of Pundit, so the project points you at the API documentation for the latest released version at rubydoc.info/gems/pundit. The repository itself has no GitHub releases, so there is no release list to check against.
What this means in practice is that the tutorial you read may describe a version you do not have. The generator is the most visible example, since `rails g pundit:install` creates an application policy with defaults that the project is free to change, and your existing policies inherit whatever that base class looked like on the day you ran it. A reader comparing the docs on GitHub with the gem in their lockfile can end up chasing a difference that is a version gap rather than a bug.
Two habits remove the problem. Pin the gem version in your Gemfile.lock and read the rubydoc page that matches it, not the README on the branch. And when a behaviour differs from what the branch README shows, check CHANGELOG.md before assuming your setup is broken: with no GitHub releases to compare, the changelog and the locked version are the only record of what actually changed between the two.
The generator writes app/policies and then wants a restart
Installation is three small steps. Add the gem, include the module in your base controller.
bundle add punditclass ApplicationController < ActionController::Base
include Pundit::Authorization
endThen, optionally, run the generator to get an application policy with some useful defaults.
rails g pundit:installTwo details in that last step cause real friction. First, you have to restart the Rails server afterwards, so that Rails can pick up any classes in the new `app/policies/` directory. Skipping the restart is the classic beginner failure here: policies you just wrote are not found until the process reloads.
Second, in the generated `ApplicationPolicy` the model object is called `record`, while the standalone example in the documentation calls its reader `post`. The pragmatic move is to read the generated file before writing the first policy, because that base class is the contract every policy you add will inherit.
Headless policies still take two arguments
Not everything worth authorizing is a database row. When there is a policy with no corresponding model or Ruby class, you retrieve it by passing a symbol, and the policy class still accepts two arguments.
# app/policies/dashboard_policy.rb
class DashboardPolicy
attr_reader :user
# `_record` in this example will be :dashboard
def initialize(user, _record)
@user = user
end
def show?
user.admin?
end
endThe controller side names the permission explicitly, because there is no action to infer it from.
# In controllers
def show
authorize :dashboard, :show?
...
endAnd the view uses the same symbol with the policy method, which is what lets you hide the link.
<% if policy(:dashboard).show? %>
<%= link_to 'Dashboard', dashboard_path %>
<% end %>The consequences of this shape are worth being explicit about. Headless is not a lighter signature: your class takes a user and an unused record, conventionally named with a leading underscore to mark it ignored, and the value that arrives is the symbol `:dashboard` rather than an object. Anything the policy needs beyond the user has to come from somewhere else, since there is no record to read it off. And because the record is a symbol, a headless policy is one of the few places where passing the wrong symbol produces a policy that still answers, just for the wrong thing.
authorize hands the record back, which is why chaining works
`authorize` returns the instance passed to it, and that return value is the basis for the compact form you see in the docs, where a controller fetches and authorizes in one line.
def show
@user = authorize User.find(params[:id])
endThe other call form is the one that catches people out. If you have no particular instance to authorize, you pass the class itself.
def admin_list
authorize Post # we don't have a particular post to authorize
# Rest of controller action
endThat form is for actions about the collection rather than a member, and it is paired with a policy whose query method does not touch the record at all, such as one whose body is only `user.admin?`. The consequence for your own policies is that they have to be written for both call styles or split into two classes. A policy that reads the record, for example one that checks ownership through the record, cannot answer the class-level call at all, because there is no record to check, so an index action and a show action end up needing different methods or different policy classes.
The view has its own accessor rather than reusing the controller's result. Calling `policy(@post).update?` in the view means a policy method can run twice for a single request, once in the controller and once while the view builds its conditional links.
Scopes nest inside the policy class and answer a different question
Policy classes answer questions about one record. The listing question, which records may this user see, is answered by a policy scope, defined as a class nested inside the policy. The scope is initialized with a user and a relation, and its `resolve` method returns the relation narrowed for that user; the example in the documentation returns the whole scope for an admin and otherwise filters the relation on the published flag, so the rule about who sees what lives in exactly one place per policy class.
What follows from the nesting is that a listing guard is a separate object from the per-record guard. The documentation presents scopes as a class you are expected to define. That makes the policy class inventory worth reviewing: for every policy you write, ask separately whether a member-level query method and a collection-level Scope are both covered, since an update action guarded by `update?` leaves the index action completely independent of it.
The same nesting also means scopes are per policy, so a listing that crosses models, such as a dashboard joining posts and comments, does not have an obvious home.
One maintainer, no GitHub releases, and a deliberately small surface
Pundit was first released by Jonas Nicklas in 2012 and is built and maintained by Varvet, a digital product studio in Sweden, which describes the goal as one tool that does one thing well. The README claims over 100 million downloads and use by more than 38,000 repositories on GitHub. It started as a pattern written up in a blog post on simple authorization in Ruby on Rails apps, and the license is MIT.
That combination explains the shape of the library. The core surface is small: query methods on a policy class, a nested Scope, headless policies keyed by symbol, the authorize and policy methods, and the install generator. Everything else is a Ruby class you write. If your application needs role hierarchies, a rules engine, or authorization fields derived from several sources, none of that arrives here; you build it in your own classes and Pundit just calls them, which is a feature when you want readable code and a cost when you wanted configuration.
The repository layout shows the same restraint, with `lib/`, `spec/`, `bin/`, `config/` and a gemspec, alongside SECURITY.md, CONTRIBUTING.md, CODE_OF_CONDUCT.md and CHANGELOG.md. Style is handled by .standard.yml with a .rubocop_ignore_git.yml, and .yardopts drives generated documentation. The last push to the repository is dated 2026-10-16, and with no releases published on GitHub, the changelog is the place to look when a behaviour changes between gem versions.
Editorial conclusion
Pundit suits a Rails application that wants authorization logic in ordinary Ruby classes it can read in a debugger, and whose team is willing to keep policy classes, their nested scopes and their views in step. It does not suit a codebase looking for a configurable rules engine, since every rule you add is code you write and name yourself. Before building on it, read the documentation for the gem version you actually have rather than the README on GitHub, run `rails g pundit:install` so your policies inherit a base class, remember to restart the server after the generator writes app/policies, and grep your own controllers for `authorize` calls to confirm each one names a permission that exists.
Frequently asked questions
What does the Pundit gem add to a Rails application?
It adds a set of helpers for building authorization out of ordinary Ruby classes and object oriented design, centred on policy classes kept in app/policies. Include Pundit::Authorization in your application controller and the authorize call infers the policy class and the query method from the record and the action name.
How do I install Pundit in a Rails app?
Run `bundle add pundit`, include Pundit::Authorization in your ApplicationController, then optionally run `rails g pundit:install` to create an application policy with useful defaults. You have to restart the Rails server afterwards so it picks up the classes in the new app/policies directory.
How does Pundit decide which policy class to use?
It derives the class name from the record, appending the Policy suffix, so Post is checked against PostPolicy, and it derives the query method from the controller action, so update calls update?. When the names do not line up you pass the permission yourself, as in `authorize @post, :update?`, or override the class with policy_class:.
Can Pundit authorize something that is not a database model?
Yes. The record argument does not need to be an ActiveRecord or even an ActiveModel object, and a policy with no matching class at all can be fetched by symbol, as in `authorize :dashboard, :show?`. The policy still has to accept two arguments, with the symbol arriving as the second one.
Who maintains Pundit and how active is it?
Pundit is built and maintained by Varvet, a digital product studio in Sweden, and Jonas Nicklas first released it in 2012. The project states over 100 million downloads and use in more than 38,000 GitHub repositories, with the last push to the repository dated 2026-10-16.
Does Pundit publish releases on GitHub?
The repository has no GitHub releases. The README warns that the copy on GitHub is accurate with the latest code on GitHub while most people run a released version, so it points you to the API documentation for the latest released version of Pundit at rubydoc.info.
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/varvet-pundit)Star history chart for your README
[](https://hysenlabs.com/projects/varvet-pundit)Was this useful?