Pundit is a Ruby gem for authorization in Rails: it answers "is this user allowed to do this to this record?" with plain Ruby classes called policies. Each model gets a policy, such as PostPolicy, with one predicate method per action (show?, update?, destroy?). Your controllers call authorize @post, and Pundit finds the policy, runs the matching method, and raises an error if it returns false. A nested Scope class does the same job for lists, so each user only loads the records they may see.
Pundit handles authorization, not authentication. It assumes something else (Devise, the Rails 8 authentication generator, your own sessions) has already worked out who current_user is. The latest release is Pundit 2.5.2, published in September 2025 (checked in November 2026). Varvet maintains it, and Jonas Nicklas first released it in 2012.
Installing it
bundle add pundit
bin/rails g pundit:install
Then include the module in your base controller:
class ApplicationController < ActionController::Base
include Pundit::Authorization
end
The generator writes app/policies/application_policy.rb. Every query method in it returns false, so a policy that forgets to define a method denies by default. new? delegates to create? and edit? delegates to update?, which keeps the form and the save behind the same rule. Restart the server after generating it so Rails picks up the new app/policies directory.
Policies
A policy is a class named after the model plus Policy. It receives the user and the record, and each method returns true or false.
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def show?
record.published? || record.author == user
end
def update?
user.admin? || record.author == user
end
def destroy?
user.admin?
end
end
The record does not have to be an Active Record model. Pundit only needs an object whose class name points at a policy, and for things with no model (a dashboard, a reports page) you can pass a symbol: authorize :dashboard, :show? looks for DashboardPolicy.
Because policies are plain classes, the usual Ruby tools apply. Share rules through inheritance or a module, and test them like any other object. Pundit ships RSpec helpers in pundit/rspec:
require "pundit/rspec"
RSpec.describe PostPolicy do
subject { described_class }
permissions :update?, :edit? do
it "lets the author edit their own post" do
author = User.new
expect(subject).to permit(author, Post.new(author: author))
end
it "stops other users" do
expect(subject).not_to permit(User.new, Post.new(author: User.new))
end
end
end
Checking a single record with authorize
class PostsController < ApplicationController
def update
@post = Post.find(params[:id])
authorize @post
if @post.update(post_params)
redirect_to @post
else
render :edit, status: :unprocessable_entity
end
end
def publish
@post = authorize Post.find(params[:id]), :update?
@post.publish!
redirect_to @post
end
end
authorize infers PostPolicy from the record and update? from the action name. When the action and the rule differ, as in publish, pass the query as the second argument. It returns the record, so you can assign and authorize in one line. Pass a class (authorize Post) when there is no instance yet, for example on new.
A denied check raises Pundit::NotAuthorizedError. Rescue it once in ApplicationController, or map it to a 403 with config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden.
class ApplicationController < ActionController::Base
include Pundit::Authorization
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:alert] = "You are not allowed to do that."
redirect_back_or_to(root_path)
end
end
In views, policy(@post) gives you the policy instance, so a button and the action behind it read the same rule:
<% if policy(@post).update? %>
<%= link_to "Edit", edit_post_path(@post) %>
<% end %>
Hiding the button is a courtesy. The authorize call in the controller is what actually protects the record.
Limiting lists with policy_scope
authorize checks one record. An index page needs the opposite question: which records may this user load at all? That is the job of the policy's Scope class and its resolve method.
class PostPolicy < ApplicationPolicy
class Scope < ApplicationPolicy::Scope
def resolve
if user.admin?
scope.all
else
scope.where(published: true).or(scope.where(author: user))
end
end
end
end
def index
@posts = policy_scope(Post).order(created_at: :desc)
end
def show
@post = authorize policy_scope(Post).find(params[:id])
end
resolve returns a relation, so the filter runs in SQL and you can keep chaining order, includes or pagination after it. Starting show from the scope means a record outside it returns a 404 instead of revealing that it exists. Rules that depend on who owns or belongs to a row are record-level permissions, and the Scope class is where Pundit puts them.
Why you get AuthorizationNotPerformedError
Pundit only checks what you ask it to check. Forget authorize in one action and that action is open. To catch this, Pundit gives controllers verify_authorized, which you run after every action:
class ApplicationController < ActionController::Base
include Pundit::Authorization
after_action :verify_pundit_authorization
private
def verify_pundit_authorization
if action_name == "index"
verify_policy_scoped
else
verify_authorized
end
end
end
If an action finishes without calling authorize, verify_authorized raises Pundit::AuthorizationNotPerformedError. verify_policy_scoped does the same for policy_scope and raises Pundit::PolicyScopingNotPerformedError, a subclass of the first. When you see either error, the fix is one of three things:
- Add the missing
authorizeorpolicy_scopecall. - Call
skip_authorizationorskip_policy_scopeinside the action when skipping is deliberate. - Exclude public actions from the hook, for example with
skip_after_action :verify_pundit_authorization, only: :showin that controller.
Because the check runs in after_action, the action has already run when it raises. The README is explicit that this is a development aid that reminds you to authorize, not a security mechanism. Treat it as a check that fails loudly in development and in your test suite, so a missing authorize never reaches a pull request unnoticed.
Pundit, CanCanCan or Action Policy
All three are maintained and work with current Rails. They differ in where the rules live.
- Pundit puts rules in one policy class per model and keeps the API to a handful of helpers. It is the easiest to read in a code review and has no DSL to learn. You write more of the plumbing yourself.
-
CanCanCan puts every rule in a single
Abilityclass (can :read, Post, user: user).load_and_authorize_resourceloads and checks records for a whole RESTful controller, andPost.accessible_by(current_ability)turns hash conditions into a query. It is compact while the rules are simple, and the one file grows long as they get more conditional. -
Action Policy, from Evil Martians, uses per-model policies like Pundit and calls itself "Pundit, re-visited" in its own docs. It adds what larger apps tend to bolt on:
authorize!andallowed_to?,authorized_scopefor lists, pre-checks shared across rules, built-in caching, and failure reasons that say why a check failed.
Pick Pundit when you want small, explicit policies you can read top to bottom. Pick CanCanCan when your rules are mostly role tables and you like them in one place. Pick Action Policy when you already know you need caching, reasons or pre-checks. Whichever you choose, the roles behind the rules usually come from role-based access control, and a policy method like user.admin? is where that model meets the code.
Pundit in an Avo app
Back-office screens usually need the same rules as the rest of the app, and duplicating them in an admin is where they drift. Teams on Avo run their Avo app through the Pundit policies they already have with the Authorization add-on: index?, show?, update? and the rest decide what each user sees in the sidebar, the buttons and the pages, and the policy's Scope filters the records on your resource Index view. Pundit is the default client, with adapters for Action Policy and CanCanCan. Avo adds policy methods for associations and file fields; the post on advanced authorization rules introduced the association ones.
Frequently asked questions
What is Pundit in Rails?
Pundit is a Ruby gem that handles authorization in Rails apps. You write one plain Ruby policy class per model, with a method for each action, and call authorize in your controllers to enforce it. A nested Scope class in the same policy limits which records a user can list.
Why does Pundit raise AuthorizationNotPerformedError?
The controller runs verify_authorized after the action, and the action finished without calling authorize. Add the authorize call, call skip_authorization if skipping is deliberate, or skip the hook for that public action. Its sibling, PolicyScopingNotPerformedError, means an action checked by verify_policy_scoped never called policy_scope.
How do Pundit policy scopes limit which records a user sees?
Each policy can define a Scope class with a resolve method that returns a filtered relation, such as all posts for admins and only published or owned posts for everyone else. Calling policy_scope(Post) in the controller runs that method, so the filter happens in the database query. Records outside the scope never load.
Should I use Pundit, CanCanCan or Action Policy?
Use Pundit for small, explicit policy classes with almost no DSL. Use CanCanCan if you prefer every rule in one Ability class and want controller loading done for you. Use Action Policy if you want Pundit-style policies plus caching, pre-checks and failure reasons built in.
Does Pundit handle authentication?
No. Pundit only decides what a known user may do. Sign-in comes from Devise, the Rails authentication generator or your own code, and Pundit reads the result through current_user (or a pundit_user method you define).