Open-source project
varvet/pundit avatar
varvet/pundit

Pundit: plain Ruby policy classes for Rails authorization

Minimal authorization through OO design and pure Ruby classes

8,523 stars640 forksRubyMIT

At a glance

What is it?
Pundit is a Ruby gem that turns authorization into ordinary policy objects instead of a DSL. It fits Rails apps that want the rules readable in Ruby, but the README leaves error handling and scope conventions for you to decide.
Who is it for?
Adopt Pundit if your Rails app already has a current_user method and you want authorization rules written as ordinary Ruby classes you can unit test without a request. Do not adopt it if you expect the gem to define your roles, your scopes or your error responses; the README documents the policy convention and the authorize call, not a role model.
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 33 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 September 29, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem Pundit solves: authorization as Ruby objects, not a DSL

Authorization logic in a Rails app tends to end up in three bad places: conditionals inside controllers, helper methods shared between views and controllers, and callbacks that run before an action with no obvious owner. Pundit's answer is to give each model a matching policy class and route every permission question through it. The README describes the project as "a set of helpers which guide you in leveraging regular Ruby classes and object oriented design patterns" to build an authorization system. The operative word is regular. There is no schema file, no ability definition block, no rule language to learn.

The audience is Rails developers who already accept that authorization is application logic and want it to live in app/policies, testable in isolation. Pundit's assumptions are narrow and stated plainly: the policy class shares a name with a model class plus the suffix Policy, the first constructor argument is a user (Pundit calls current_user on the controller), the second is the object being authorized, and the class implements query methods that usually map to controller action names. Anything satisfying those assumptions works. The README notes the second argument "does not need to be an ActiveRecord or even an ActiveModel object, it can be anything really."

How authorize infers the policy class and the query method

The mechanism is name-based inference, and it is worth understanding precisely because that is where surprises come from. When a controller calls authorize @post inside the update action, Pundit derives PostPolicy from the object's class, instantiates it with current_user and the record, then calls update? on that instance because the action is named update. The README spells out the equivalent: unless PostPolicy.new(current_user, @post).update? then raise Pundit::NotAuthorizedError. That exception is the failure path, and the README does not say what your application should render when it fires.

Two escape hatches exist when inference is wrong. You can pass the permission explicitly, as in authorize @post, :update? when the action name and the permission name diverge, and you can override the class with policy_class: PublicationPolicy when the object's class does not match the policy you want, for example a Post instance authorized against PublicationPolicy. There is also a class-level form: authorize Post with no instance, which the README illustrates with an admin_list action and a policy method named admin_list?, a name that does not correspond to a RESTful action at all.

The return value matters for chaining. authorize returns the instance passed to it, so @user = authorize User.find(params[:id]) assigns and checks in one line. In views, the policy helper gives you the same object: policy(@post).update? inside an ERB conditional. Headless policies extend the pattern to things that are not models. Passing the symbol :dashboard to authorize produces a DashboardPolicy whose second constructor argument is that symbol, which the README shows named _record in the initializer to signal it is unused. The policy still has to accept two arguments even though there is no record.

Installing Pundit and authorizing your first action

Pundit is distributed as a gem. The README's installation section gives a single command, and it warns that the GitHub README tracks the latest code rather than the version you have installed, pointing readers to the documentation for the latest released version instead. Start there.

bash
bundle add pundit

Next, include the authorization module in your application controller so that authorize, policy and the related helpers are available in every controller. Without this include, none of the controller-side methods exist.

ruby
class ApplicationController < ActionController::Base
  include Pundit::Authorization
end

The optional generator creates an application policy with defaults, and the README adds a step people skip: restart the Rails server afterwards so Rails picks up classes in the new app/policies/ directory.

bash
rails g pundit:install

With a base class in place, a policy is an ordinary class. This one permits an update when the user is an admin or the post is still unpublished, and it inherits the record reader from the generated ApplicationPolicy.

ruby
class PostPolicy < ApplicationPolicy
  def update?
    user.admin? or not record.published?
  end
end

In the controller, authorize sits between loading the record and mutating it. The README's update action loads the post, calls authorize, and only then attempts the update, redirecting on success and rendering the edit template on failure.

ruby
def update
  @post = Post.find(params[:id])
  authorize @post
  if @post.update(post_params)
    redirect_to @post
  else
    render :edit
  end
end

What you should see: no new tables, no configuration file, and no change to your routes. The only new artifacts are the policy classes and the include line.

Where Pundit stops: no roles, no rescue, no scope convention

The most common misreading of Pundit is expecting it to be an authorization framework in the sense of defining who can do what. It does not. The README's example policy calls user.admin?, a method that must already exist on your user model. Pundit never inspects a role column, never loads a permission table, and never tells you what an admin is. If your application has no notion of an admin, that example policy is not usable as written.

The second gap is error handling. Pundit::NotAuthorizedError is raised on a failed check, and the README shows the raise but not the rescue. In a Rails app that means an unhandled exception reaches the framework's default handling unless you add your own rescue_from. Whether that surfaces as a 403, a redirect to a login page, or a JSON error body is a decision the gem leaves entirely to you. That is a deliberate boundary, and it is also the thing most likely to be configured inconsistently across a large application.

The scopes section is the third place where the README sets up a pattern without finishing it. It opens by describing the familiar need: a view listing records a particular user may access. The excerpt available here stops mid-sentence, so the concrete scope API is not something this article can describe. Treat scope design as work you own rather than something the gem hands you.

There is also a naming tax. Policy classes are matched to models by convention, so a model renamed without its policy, or a policy placed outside the expected directory, fails at the point of the authorize call rather than at boot. Nothing validates the pairing ahead of time.

Pundit compared with CanCanCan's ability-file approach

The obvious alternative in the Rails ecosystem is CanCanCan, which centralizes rules in a single Ability class using a can/cannot DSL, typically with a line like can :update, Post, published: false. The difference in approach is structural, not cosmetic. CanCanCan keeps every rule in one file and derives both controller checks and record filtering from that single definition, which makes it easy to see the whole permission model at once and gives you scopes for free.

Pundit inverts that. Rules are distributed across one class per model, each policy a plain Ruby object with methods named after permissions. The README's framing is explicit about the trade: Pundit is "one tool that does one thing well," and the thing it does is route authorization questions to objects you write. The benefit is that a policy is testable without a request cycle and that complex conditions are just Ruby, including calls to other services. The cost is that there is no single place to read the full permission model, and no built-in filtering to match the checks.

If your rules are few and uniform, a centralized ability file is less ceremony. If your rules are per-model and branch on domain state, policy objects keep each rule next to the data it concerns. Neither is a default winner; the shape of your rules decides it.

Maintenance, upgrades and the MIT licence

Pundit is maintained by Varvet, the Swedish product studio that has kept the project going since Jonas Nicklas released it in 2012. The repository is not archived, and the last push to the main branch was on 2026-08-28, which is recent. The README states the project has passed 100 million downloads. Releases are not represented in the repository information available here, so this article cannot describe a version history or a deprecation policy.

The upgrade cost is low by design. Because a policy is a plain Ruby class with no base-class machinery beyond what you write yourself, a Pundit upgrade cannot invalidate your rules the way a DSL change would. The parts that can break are the integration points: the include of Pundit::Authorization, the inference rules for policy class and query method names, and the exception class. Those are small surfaces, and the README's warning that the GitHub README tracks unreleased code is the practical caution here. Pin the gem and read the documentation for the version you actually have rather than the README on main.

Pundit is released under the MIT licence, which is permissive and imposes no copyleft obligation on your application. This is a description of the licence identifier, not legal advice; if your organization has questions about distributing modified copies, ask counsel.

Editorial conclusion

Adopt Pundit if your Rails app already has a current_user method and you want authorization rules written as ordinary Ruby classes you can unit test without a request. Do not adopt it if you expect the gem to define your roles, your scopes or your error responses; the README documents the policy convention and the authorize call, not a role model. Before wiring it in, verify three things in your own codebase: that every controller action either calls authorize or explicitly skips it, that each policy class name matches its model plus Policy, and that Pundit::NotAuthorizedError is rescued somewhere with a response you have chosen.

Frequently asked questions

How do I install Pundit in a Rails app?

The README gives bundle add pundit as the install command, then include Pundit::Authorization in your application controller. An optional rails g pundit:install generator creates an application policy, and the server must be restarted afterwards so Rails picks up classes in app/policies/.

How does Pundit know which policy class and method to call?

It infers both from names. The object's class plus the Policy suffix gives the policy class, and the current controller action name gives the query method, so authorize @post inside update calls PostPolicy#update?. You can override either by passing a permission symbol or a policy_class: argument.

What happens when a Pundit authorization check fails?

Pundit raises Pundit::NotAuthorizedError. The README shows the raise but does not document how an application should rescue it, so the response your users see is something you configure yourself.

Does Pundit define roles like admin or editor?

No. The README's example policy calls user.admin?, which assumes that method already exists on your user model. Pundit routes authorization questions to your policy objects; it does not supply a role model.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. varvet/pundit on GitHub
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/varvet-pundit.svg)](https://hysenlabs.com/projects/varvet-pundit)