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.
Resources
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
Postscaffold.
rails g scaffold Post title:string user:references meta_description:text
- Add a
featurescolumn to theuserstable by running the following command.
rails g migration add_features_to_users features:jsonb
- Set a default value on the
featurescolumn.
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 to our
userstable. 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.
- Run the migrations.
rails db:migrate
- Set features on
Usermodel.
class User < ApplicationRecord
...
FEATURES = %i[enable_post_meta_description].freeze
store :features, accessors: User::FEATURES
end
What's Going On Here?
- We create a
FEATURESconstant that will store the names of our features as symbols by calling%ion the array. We call.freezeto ensure this constant cannot be updated anywhere else.- We use ActiveRecord::Store to interface with the
featurescolumn. This will allow us to call@user.enable_post_meta_descriptioninstead ofuser.features.enable_post_meta_description. By passingUser::FEATURESinto theaccessorsparameter we can continue to add new features in theFEATURESconstant.
Setting a features column on the users table will allow us to enable/disable features on a per-user basis.
- Enable the
enable_post_meta_descriptionfor a user. That way you have something to test.
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.
- Install pundit.
bundle add pundit
- Generate the base pundit files.
rails g pundit:install
- Include pundit in the
ApplicationController
class ApplicationController < ActionController::Base
include Pundit
end
Step 3: Build a Feature Flag Policy
- Generate a namespaced pundit policy.
rails g pundit:policy feature/enable_post_meta_description
- Build the policy
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
[:title, :user_id, :meta_description]
else
[:title, :user_id]
end
end
...
end
What's Going On Here?
- We generate a policy under the
featurenamespace. 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 theUsermodel.- We build a
ceate?method that returnstrueorfalsebased on whether or not that user has theenable_post_meta_descriptionfeature set to true. We could have called the methodindex?,new?,update?,edit?ordestroy?butcreate?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 method to return an array of paramters to be used in the
PostsController. This will allow us to conditionally permit themeta_descriptionparameter.
Step 4: Implement the Feature Flag
- Update the
post_paramsto hook into thepermitted_attributesmethod.
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::EnablePostMetaDescriptionPolicypolicy class and pass in thecurrent_userandPostper pundit's API. Then we callpermitted_attributesto load the correct parameters based on whether the user has access to themeta_description.- Note that we call
authenticate_user!before all actions exceptshowandindexsince theFeature::EnablePostMetaDescriptionPolicyrelies on a user.
- Conditionally show the
meta_descriptionin the post form partial.
# 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_descriptionfield in a new instance of theFeature::EnablePostMetaDescriptionPolicypolicy class. We callcreate?which returnstrueorfalsebased on whether the user has access to themeta_description.
Did you like this post? Follow me on Twitter to get even more tips.
Top comments (0)