Use Pundit as a Rails Feature Flag System
In this tutorial, I’ll show you how to create a feature flag system in Rails using pundit and a features
column on the users
table.
Step 1: Initial Setup
This tutorial assumes you are using devise and have a User
model. However, you should still be able to follow along and implement this pattern even if that’s not the case.
-
Create a
Post
scaffold.rails g scaffold Post title:string user:references meta_description:text
2. Add a `features` column to the `users` table by running the following command.
```sh
rails g migration add_features_to_users features:jsonb
```
3. Set a default value on the `features` column.
```ruby
class AddFeaturesToUsers < ActiveRecord::Migration[6.1]
def change
add_column :users, :features, :jsonb, default: {}
end
end
```
> **What's Going On Here?**
>
> - We add a [JSONB Column](https://guides.rubyonrails.org/active_record_postgresql.html#json-and-jsonb) to our `users` table. This will allow us to store multiple features in one column, compared to making a column for each feature.
> - We add `default: {}` simply to add a formatted default value to this column.
4. Run the migrations.
```sh
rails db:migrate
```
5. Set features on `User` model.
```ruby
class User < ApplicationRecord
FEATURES = %i[enable_post_meta_description].freeze
store :features, accessors: User::FEATURES
end
```
> **What's Going On Here?**
>
> - We create a `FEATURES` constant that will store the names of our features as symbols by calling `%i` on the array. We call `.freeze` to ensure this constant cannot be updated anywhere else.
> - We use [ActiveRecord::Store](https://api.rubyonrails.org/classes/ActiveRecord/Store.html) to interface with the `features` column. This will allow us to call `@user.enable_post_meta_description` instead of `user.features.enable_post_meta_description`. By passing `User::FEATURES` into the `accessors` parameter we can continue to add new features in the `FEATURES` constant.
Setting a `features` column on the `users` table will allow us to enable/disable features on a per-user basis.
6. Enable the `enable_post_meta_description` for a user. That way you have something to test.
```ruby
User.last.update(enable_post_meta_description: true)
```
## Step 2: Install Pundit and Build a Policy
Next, we'll need to install and configure [pundit](https://github.com/varvet/pundit).
1. Install [pundit](https://github.com/varvet/pundit).
```sh
bundle add pundit
```
2. Generate the base pundit files.
```sh
rails g pundit:install
```
3. Include pundit in the `ApplicationController`
```ruby
class ApplicationController < ActionController::Base
include Pundit
end
```
## Step 3: Build a Feature Flag Policy
1. Generate a namespaced pundit policy.
```sh
rails g pundit:policy feature/enable_post_meta_description
```
2. Build the policy
```ruby
class Feature::EnablePostMetaDescriptionPolicy < ApplicationPolicy
def ceate?
user.present? && (user.enable_post_meta_description == true)
end
def permitted_attributes
if user.enable_post_meta_description == true
%i[title user_id meta_description]
else
%i[title user_id]
end
end
end
```
> **What's Going On Here?**
>
> - We generate a policy under the `feature` namespace. This is not required, but it helps keep things organized and will allow us to add new policies for new features later. We also name this policy to match the name of the feature in the `User` model.
> - We build a `ceate?` method that returns `true` or `false` based on whether or not that user has the `enable_post_meta_description` feature set to true. We could have called the method `index?`, `new?`, `update?`, `edit?` or `destroy?` but `create?` makes the most sense in this context. We're building a policy that enables a user to **create** a meta description on a post.
> - We used pundit's [permitted_attributes](https://github.com/varvet/pundit#strong-parameters) method to return an array of paramters to be used in the `PostsController`. This will allow us to conditionally permit the `meta_description` parameter.
## Step 4: Implement the Feature Flag
1. Update the `post_params` to hook into the `permitted_attributes` method.
```ruby
class PostsController < ApplicationController
before_action :authenticate_user!, except: %i[show index]
before_action :set_post, only: %i[show edit update destroy]
private
def post_params
params.require(:post).permit(
Feature::EnablePostMetaDescriptionPolicy.new(
current_user,
Post,
).permitted_attributes,
)
end
end
```
> **What's Going On Here?**
>
> - We instantiate a new instance of the `Feature::EnablePostMetaDescriptionPolicy` policy class and pass in the `current_user` and `Post` per pundit's API. Then we call `permitted_attributes` to load the correct parameters based on whether the user has access to the `meta_description`.
> - Note that we call `authenticate_user!` before all actions except `show` and `index` since the `Feature::EnablePostMetaDescriptionPolicy` relies on a user.
2. Conditionally show the `meta_description` in the post form partial.
```erb
# app/views/posts/_form.html.erb
<%= form_with(model: post) do |form| %>
<% if Feature::EnablePostMetaDescriptionPolicy.new(current_user, post).create? %>
<div class="field">
<%= form.label :meta_description %>
<%= form.text_area :meta_description %>
</div>
<% end %>
<% end %>
```
> **What's Going On Here?**
>
> - We wrap the `meta_description` field in a new instance of the `Feature::EnablePostMetaDescriptionPolicy` policy class. We call `create?` which returns `true` or `false` based on whether the user has access to the `meta_description`.