When building Ruby on Rails applications, managing access control can quickly become complex. Without a proper authorization layer, your codebase can end up with scattered conditionals and hard-to-maintain logic. This is where Pundit comes into play—a lightweight, flexible, and developer-friendly authorization library for Rails.

Pundit uses plain Ruby objects and a policy-based structure, making it easy to maintain, test, and understand. In this article, we’ll walk through how to integrate and use Pundit effectively in a Rails app to simplify access control.

What Is Pundit?

Pundit is a Ruby gem that provides a clean way to handle authorization. It follows a minimalist approach, where every model gets a corresponding policy class. Each policy encapsulates the logic for whether a user can perform a certain action on a resource.

Key features:

  • Plain Ruby objects—no DSLs

  • Scopes for handling record filtering

  • Easy to test

  • Integration-friendly with Rails controllers and views

Installing Pundit

To get started, add Pundit to your Gemfile:

ruby
gem 'pundit'

Then run:

bash
bundle install

After installing, include the Pundit module in your application controller:

ruby
class ApplicationController < ActionController::Base
include Pundit
end

You’re now ready to use Pundit in your Rails app.

Creating a Sample Rails App

Let’s walk through an example where we have a blog application with User and Post models. We’ll set up access control such that:

  • Admins can do everything.

  • Regular users can only manage their own posts.

  • Visitors can only read public posts.

First, generate the models:

bash
rails generate model User email:string role:string
rails generate model Post title:string content:text user:references public:boolean
rails db:migrate

Seed some data:

ruby
# db/seeds.rb
admin = User.create(email: "admin@example.com", role: "admin")
user = User.create(email: "user@example.com", role: "user")
Post.create(title: “Admin Post”, content: “Secret stuff”, user: admin, public: false)
Post.create(title: “User Post”, content: “User’s content”, user: user, public: true)

Generating a Pundit Policy

Run the following command to create a policy:

bash
rails generate pundit:policy post

This creates app/policies/post_policy.rb. Let’s edit it:

ruby
class PostPolicy
attr_reader :user, :post
def initialize(user, post)
@user = user
@post = post
enddef index?
true
enddef show?
post.public? || user_is_owner_or_admin?
enddef create?
user.present?
enddef update?
user_is_owner_or_admin?
enddef destroy?
user_is_owner_or_admin?
endprivatedef user_is_owner_or_admin?
user.present? && (post.user_id == user.id || user.role == ‘admin’)
end
end

Here, you can clearly see who can do what with each post.

Using Policies in Controllers

In your PostsController, authorize user actions like this:

ruby
class PostsController < ApplicationController
before_action :set_post, only: %i[show edit update destroy]
def index
@posts = policy_scope(Post)
enddef show
authorize @post
enddef new
@post = Post.new
authorize @post
enddef create
@post = Post.new(post_params)
@post.user = current_user
authorize @post
if @post.save
redirect_to @post
else
render :new
end
enddef edit
authorize @post
enddef update
authorize @post
if @post.update(post_params)
redirect_to @post
else
render :edit
end
enddef destroy
authorize @post
@post.destroy
redirect_to posts_path
endprivate

def set_post
@post = Post.find(params[:id])
end

def post_params
params.require(:post).permit(:title, :content, :public)
end
end

Each time authorize @post is called, Pundit uses the PostPolicy to verify if the current user can perform that action.

Using Scopes in Pundit

To handle which posts a user can see in the index, we use policy scopes. Update the PostPolicy with a Scope class:

ruby
class PostPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.role == 'admin'
scope.all
else
scope.where(public: true).or(scope.where(user_id: user.id))
end
end
end
end

This ensures users only see public posts or their own. Admins see everything.

In the controller:

ruby
def index
@posts = policy_scope(Post)
end

Handling Unauthorized Access

If a user tries to access a resource they shouldn’t, Pundit raises a Pundit::NotAuthorizedError. You can handle this globally:

ruby
class ApplicationController < ActionController::Base
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorizedprivatedef user_not_authorized(exception)
flash[:alert] = “Access denied: #{exception.policy.class}##{exception.query}
redirect_to(request.referrer || root_path)
end
end

This gives a user-friendly message when access is denied.

Authorizing in Views

You can also use policies in your views to conditionally show or hide elements:

erb
<% if policy(@post).edit? %>
<%= link_to 'Edit', edit_post_path(@post) %>
<% end %>

Or:

erb
<% if policy(Post).create? %>
<%= link_to 'New Post', new_post_path %>
<% end %>

This keeps your view logic consistent with controller logic.

Testing Policies

Pundit policies are plain Ruby classes, which makes them very testable. Here’s an RSpec example:

ruby

require 'rails_helper'

RSpec.describe PostPolicy do
let(:user) { User.create(email: “user@example.com”, role: “user”) }
let(:admin) { User.create(email: “admin@example.com”, role: “admin”) }
let(:post) { Post.create(title: “Test”, content: “Body”, user: user, public: false) }

subject { described_class }

permissions :show? do
it “grants access to owner” do
expect(subject).to permit(user, post)
end

it “grants access to admin” do
expect(subject).to permit(admin, post)
end

it “denies access to other users” do
another_user = User.create(email: “other@example.com”, role: “user”)
expect(subject).not_to permit(another_user, post)
end
end
end

Best Practices for Using Pundit

  1. Always use authorize in controllers to avoid bypassing policies.

  2. Use policy_scope for index actions to control visibility.

  3. Define fine-grained policies rather than bundling logic in controllers.

  4. Test your policies as they are critical to application security.

  5. Avoid role-checks in controllers; keep them within policies.

Conclusion

Pundit is a powerful tool that simplifies access control in Ruby on Rails applications by promoting clear, maintainable policy objects. It eliminates the need for role-based conditionals scattered throughout controllers and views, making your codebase easier to understand and extend.

By leveraging Pundit’s policy classes, scopes, and view helpers, developers can cleanly separate authorization logic from business logic. It integrates naturally with Rails conventions and doesn’t add heavy dependencies or complexity.

Whether you’re building a small startup app or a large enterprise system, incorporating Pundit early can save you from tangled authorization nightmares later. With robust testing and consistent usage, Pundit offers a solid foundation for securing your Rails application in a clean and scalable way.