Featury provides two primary ways to interact with features: direct method calls and the .with() method. Both approaches support the same actions and produce the same results.
Call actions directly on the feature class:
class User::OnboardingFeature < ApplicationFeature
prefix :user_onboarding
resource :user, type: User, option: true
feature :passage
end
user = User.find(1)
User::OnboardingFeature.enabled?(user: user) # => true
User::OnboardingFeature.enable(user: user) # => true
User::OnboardingFeature.disable(user: user) # => trueThis approach is concise and works well for one-off checks or actions.
The .with() method creates a feature instance with resources bound:
user = User.find(1)
feature = User::OnboardingFeature.with(user: user)
feature.enabled? # => true
feature.enable # => true
feature.disable # => trueMultiple Operations on the Same Resources:
feature = User::OnboardingFeature.with(user: user)
if feature.disabled?
feature.enable
NotificationService.notify(user, "Onboarding enabled!")
endPassing Feature Instances:
def enable_feature_for_user(feature)
return if feature.enabled?
feature.enable
log_feature_change(feature)
end
enable_feature_for_user(User::OnboardingFeature.with(user: user))Cleaner Syntax:
# Without .with()
if User::OnboardingFeature.enabled?(user: user)
User::OnboardingFeature.disable(user: user)
end
# With .with()
feature = User::OnboardingFeature.with(user: user)
feature.disable if feature.enabled?Resources are passed as keyword arguments:
class User::ProfileFeature < ApplicationFeature
resource :user, type: User, option: true
feature :editing
end
User::ProfileFeature.enabled?(user: user)
# Or
User::ProfileFeature.with(user: user).enabled?class OrganizationFeature < ApplicationFeature
resource :organization, type: Organization, option: true
resource :user, type: User, option: true
feature :admin_panel
end
OrganizationFeature.enabled?(organization: org, user: user)
# Or
OrganizationFeature.with(organization: org, user: user).enabled?class ExperimentalFeature < ApplicationFeature
resource :user, type: User, option: true, required: false
feature :beta_ui
end
# Without user (global check)
ExperimentalFeature.enabled?
# With user (user-specific check)
ExperimentalFeature.enabled?(user: user)
# Using .with()
ExperimentalFeature.with(user: user).enabled?
ExperimentalFeature.with.enabled? # No resourcesActions operate on all features in a class (including nested groups) and aggregate results based on your action definition.
class ApplicationFeature < Featury::Base
action :enabled?, web: :enabled? do |features:, **options|
features.all? { |feature| Flipper.enabled?(feature, *options.values) }
end
action :disabled?, web: :regular do |features:, **options|
features.any? { |feature| !Flipper.enabled?(feature, *options.values) }
end
action :enable, web: :enable do |features:, **options|
features.all? { |feature| Flipper.enable(feature, *options.values) }
end
action :disable, web: :disable do |features:, **options|
features.all? { |feature| Flipper.disable(feature, *options.values) }
end
action :add, web: :regular do |features:, **options|
features.all? { |feature| Flipper.add(feature, *options.values) }
end
end
class User::OnboardingFeature < ApplicationFeature
feature :passage
feature :completion
group BillingFeature # Contains :billing_api
end
# Returns true only if ALL three features are enabled:
# - :user_onboarding_passage
# - :user_onboarding_completion
# - :billing_api
User::OnboardingFeature.enabled?(user: user)class ApplicationFeature < Featury::Base
action :enabled?, web: :enabled? do |features:, **options|
features.all? { |feature| Flipper.enabled?(feature, *options.values) }
end
action :disabled?, web: :regular do |features:, **options|
features.any? { |feature| !Flipper.enabled?(feature, *options.values) }
end
action :any_enabled?, web: :enabled? do |features:, **options|
features.any? { |feature| Flipper.enabled?(feature, *options.values) }
end
action :enable, web: :enable do |features:, **options|
features.all? { |feature| Flipper.enable(feature, *options.values) }
end
action :disable, web: :disable do |features:, **options|
features.all? { |feature| Flipper.disable(feature, *options.values) }
end
action :add, web: :regular do |features:, **options|
features.all? { |feature| Flipper.add(feature, *options.values) }
end
end
# Returns true if ANY feature is enabled
User::OnboardingFeature.any_enabled?(user: user)class ApplicationFeature < Featury::Base
action :enabled?, web: :enabled? do |features:, **options|
features.all? { |feature| Flipper.enabled?(feature, *options.values) }
end
action :disabled?, web: :regular do |features:, **options|
features.any? { |feature| !Flipper.enabled?(feature, *options.values) }
end
action :enable, web: :enable do |features:, **options|
features.all? { |feature| Flipper.enable(feature, *options.values) }
end
action :disable, web: :disable do |features:, **options|
features.all? { |feature| Flipper.disable(feature, *options.values) }
end
action :add, web: :regular do |features:, **options|
features.all? { |feature| Flipper.add(feature, *options.values) }
end
end
# Returns true if ANY feature is disabled
User::OnboardingFeature.disabled?(user: user)class ApplicationFeature < Featury::Base
action :status, web: :regular do |features:, **options|
features.map do |feature|
{ feature: feature, enabled: Flipper.enabled?(feature, *options.values) }
end
end
end
User::OnboardingFeature.status(user: user)
# => [
# { feature: :user_onboarding_passage, enabled: true },
# { feature: :user_onboarding_completion, enabled: false },
# { feature: :billing_api, enabled: true }
# ]When calling actions on a parent feature, they cascade through all nested groups:
class User::OnboardingFeature < ApplicationFeature
resource :user, type: User, option: true, nested: true
feature :passage
group BillingFeature
end
class BillingFeature < ApplicationFeature
resource :user, type: User, option: true
feature :api
feature :webhooks
end
# This checks THREE features:
# 1. :user_onboarding_passage
# 2. :billing_api
# 3. :billing_webhooks
User::OnboardingFeature.enabled?(user: user)The nested: true option ensures resources are passed to BillingFeature. See Resources for details.
To work with individual features, create dedicated classes:
class BillingAPIFeature < ApplicationFeature
prefix :billing
resource :user, type: User, option: true
feature :api
end
class BillingWebhooksFeature < ApplicationFeature
prefix :billing
resource :user, type: User, option: true
feature :webhooks
end
# Control each feature independently
BillingAPIFeature.enable(user: user)
BillingWebhooksFeature.disable(user: user)Or interact with your feature flag system directly:
Flipper.enable(:billing_api, user)
Flipper.disable(:billing_webhooks, user)Features can have conditions that guard action execution:
class User::OnboardingFeature < ApplicationFeature
resource :user, type: User, option: true
condition ->(resources:) { resources.user.onboarding_awaiting? }
feature :passage
end
# If user.onboarding_awaiting? is false:
User::OnboardingFeature.enabled?(user: user) # => false (condition failed)
# If user.onboarding_awaiting? is true:
User::OnboardingFeature.enabled?(user: user) # => (checks Flipper)See Conditions for complete details.
feature = User::OnboardingFeature.with(user: user)
if feature.enabled?
feature.disable
else
feature.enable
endfeature = User::OnboardingFeature.with(user: user)
feature.enable if user.premium?feature = BillingFeature.with(user: user)
if feature.disabled?
feature.enable
NotificationService.notify(user, "Billing features enabled")
endusers = User.where(premium: true)
users.each do |user|
PremiumFeature.with(user: user).enable
endclass FeatureManager
def enable_and_log(feature)
return if feature.enabled?
feature.enable
Rails.logger.info("Enabled feature: #{feature.class.name}")
end
end
manager = FeatureManager.new
manager.enable_and_log(User::OnboardingFeature.with(user: user))
manager.enable_and_log(BillingFeature.with(user: user))class User::OnboardingFeature < ApplicationFeature
resource :user, type: User, option: true
end
# Raises Servactory validation error
User::OnboardingFeature.enabled?
# => Error: :user is required
# Correct usage
User::OnboardingFeature.enabled?(user: user)# Raises Servactory validation error
User::OnboardingFeature.enabled?(user: "not a user")
# => Error: :user must be of type User
# Correct usage
User::OnboardingFeature.enabled?(user: User.new)begin
feature = User::OnboardingFeature.with(user: user)
feature.enable
rescue Servactory::Errors::InputError => e
Rails.logger.error("Invalid feature parameters: #{e.message}")
rescue StandardError => e
Rails.logger.error("Feature error: #{e.message}")
endRSpec.describe User::OnboardingFeature do
let(:user) { create(:user) }
describe ".enabled?" do
it "returns true when all features are enabled" do
allow(Flipper).to receive(:enabled?).and_return(true)
expect(User::OnboardingFeature.enabled?(user: user)).to be(true)
end
end
describe ".with" do
it "creates a feature instance" do
feature = User::OnboardingFeature.with(user: user)
expect(feature).to respond_to(:enabled?)
end
end
end- Learn about Actions and aggregation logic
- Review Resources for parameter handling
- Explore Conditions for conditional execution
- See Examples for real-world usage patterns