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
Post
scaffold.
rails g scaffold Post title:string user:references meta_description:text
- Add a
features
column to theusers
table by running the following command.
rails g migration add_features_to_users features:jsonb
- Set a default value on the
features
column.
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
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.
- Run the migrations.
rails db:migrate
- Set features on
User
model.
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 to interface with the
features
column. This will allow us to call@user.enable_post_meta_description
instead ofuser.features.enable_post_meta_description
. By passingUser::FEATURES
into theaccessors
parameter we can continue to add new features in theFEATURES
constant.
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_description
for 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
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 theUser
model.- We build a
ceate?
method that returnstrue
orfalse
based on whether or not that user has theenable_post_meta_description
feature 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_description
parameter.
Step 4: Implement the Feature Flag
- Update the
post_params
to hook into thepermitted_attributes
method.
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 thecurrent_user
andPost
per pundit's API. Then we callpermitted_attributes
to load the correct parameters based on whether the user has access to themeta_description
.- Note that we call
authenticate_user!
before all actions exceptshow
andindex
since theFeature::EnablePostMetaDescriptionPolicy
relies on a user.
- Conditionally show the
meta_description
in 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_description
field in a new instance of theFeature::EnablePostMetaDescriptionPolicy
policy class. We callcreate?
which returnstrue
orfalse
based 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)