Flipper Feature Flags¶
Shipping a risky task? Wrap it in a Flipper flag and turn it on per user, per company, or for just your team — no redeploy required.
Two flavors matter for CMDx:
- Fail when off — callers get a failed
Result(good for APIs that should return an error). - Skip when off — callers get a skipped
Result(good for “nice to have” background work).
Setup — fail when the flag is off¶
Middleware calls task.fail! before work. Runtime turns that into a failed result with your message and code.
class CmdxFlipperMiddleware
def initialize(feature:, actor: :user)
@feature = feature
@actor = actor
end
def call(task)
actor = resolve_actor(task)
unless Flipper.enabled?(@feature, actor)
task.fail!("feature #{@feature} is disabled", code: :feature_disabled)
end
yield
end
private
def resolve_actor(task)
case @actor
when nil then nil
when Symbol then task.context[@actor]
when Proc then task.instance_exec(&@actor)
else @actor.respond_to?(:call) ? @actor.call(task) : @actor
end
end
end
Usage — fail when off¶
class RebuildSearchIndex < CMDx::Task
register :middleware, CmdxFlipperMiddleware.new(feature: :search_v2, actor: :company)
required :company
def work
SearchIndex.rebuild!(company)
end
end
Usage — skip when off¶
When “off” means “do nothing, but that's OK,” halt inside work with skip! so the result reports skipped?:
class RebuildSearchIndex < CMDx::Task
required :company
def work
skip!("search_v2 disabled for #{company.id}") unless Flipper.enabled?(:search_v2, company)
SearchIndex.rebuild!(company)
end
end
Notes¶
Skip from middleware too¶
Middleware can throw success!, skip!, fail!, or throw! — Runtime catches them via catch(Signal::TAG). Use task.skip!(...) before yield when you want a skipped result without entering work. Throw before yielding; signals thrown after the lifecycle finishes are ignored.