Magick
A performant and memory-efficient feature toggle gem for Ruby and Rails applications.
Features
- Multiple Feature Types: Boolean, string, and number feature flags
- Flexible Targeting: Enable features for specific users, groups, roles, or percentages
- Dual Backend: Memory adapter (fast) with Redis fallback (persistent)
- Rails Integration: Seamless integration with Rails, including request store caching
- DSL Support: Define features in a Ruby DSL file (
config/features.rb) - Thread-Safe: All operations are thread-safe for concurrent access
- Performance: Lightning-fast feature checks with async metrics recording and memory-first caching strategy
- Advanced Features: Circuit breaker, audit logging, performance metrics, versioning, and more
Installation
Add this line to your application's Gemfile:
gem 'magick'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install magick
Setup
After adding the gem to your Gemfile and running bundle install, generate the configuration file:
rails generate magick:install
This will create config/initializers/magick.rb with a basic configuration.
ActiveRecord Adapter (Optional)
If you want to use ActiveRecord as a persistent storage backend, you must generate and run the migration:
rails generate magick:active_record
rails db:migrate
This will create a migration file that creates the magick_features table. The adapter will not auto-create the table - you must run migrations.
Note: The ActiveRecord adapter is optional and only needed if you want database-backed feature flags. The gem works perfectly fine with just the memory adapter or Redis adapter.
Configuration
Basic Configuration
The generator creates config/initializers/magick.rb with sensible defaults. You can also create it manually:
Magick.configure do
# Configure Redis (optional)
# Use database 1 by default to avoid conflicts with Rails cache (which uses DB 0)
redis url: ENV['REDIS_URL'], db: 1
# Enable features
performance_metrics enabled: true
audit_log enabled: true
versioning enabled: true
warn_on_deprecated enabled: true
end
Advanced Configuration
Magick.configure do
# Environment
environment Rails.env
# Memory TTL
memory_ttl 7200 # 2 hours
# Redis configuration
# Use separate database (DB 1) to avoid conflicts with Rails cache (DB 0)
# This ensures feature toggles persist even when Rails cache is cleared
redis url: ENV['REDIS_URL'], namespace: 'magick:features', db: 1
# Or include database in URL: redis url: 'redis://localhost:6379/1'
# Circuit breaker settings
circuit_breaker threshold: 5, timeout: 60
# Async updates
async_updates enabled: true
# Enable services
performance_metrics(
enabled: true,
redis_tracking: true, # Auto-enabled if Redis is configured
batch_size: 100, # Flush after 100 updates
flush_interval: 60 # Or flush every 60 seconds
)
audit_log enabled: true
versioning enabled: true
warn_on_deprecated enabled: true
end
Usage
Basic Usage
# Check if a feature is enabled
if Magick.enabled?(:new_dashboard)
# Show new dashboard
end
# With context (user, role, etc.)
if Magick.enabled?(:premium_features, user_id: current_user.id, role: current_user.role)
# Show premium features
end
Registering Features
# Register a boolean feature
Magick.register_feature(:new_dashboard,
type: :boolean,
default_value: false,
description: "New dashboard UI",
group: "UI" # Optional: group features for organization
)
# Register a string feature
Magick.register_feature(:api_version,
type: :string,
default_value: "v1",
description: "API version to use"
)
# Register a number feature
Magick.register_feature(:max_results,
type: :number,
default_value: 10,
description: "Maximum number of results"
)
Feature Targeting
feature = Magick[:new_dashboard]
# Enable globally (for everyone, no targeting)
feature.enable
# Disable globally (for everyone, no targeting)
feature.disable
# Enable for specific user
feature.enable_for_user(123)
# Enable for specific group
feature.enable_for_group("beta_testers")
# Enable for specific role
feature.enable_for_role("admin")
# Enable for percentage of users (consistent)
feature.enable_percentage_of_users(25) # 25% of users
# Enable for percentage of requests (random)
feature.enable_percentage_of_requests(50) # 50% of requests
# Enable for date range
feature.enable_for_date_range('2024-01-01', '2024-12-31')
# Enable for IP addresses
feature.enable_for_ip_addresses('192.168.1.0/24', '10.0.0.1')
# Enable for custom attributes
feature.enable_for_custom_attribute(:subscription_tier, ['premium', 'enterprise'])
Checking Feature Enablement with Objects
You can check if a feature is enabled for an object (like a User model) and its fields:
# Using enabled_for? with an object
user = User.find(123)
if Magick.enabled_for?(:premium_features, user)
# Feature is enabled for this user
end
# Or using the feature directly
feature = Magick[:premium_features]
if feature.enabled_for?(user)
# Feature is enabled for this user
end
# With additional context
if Magick.enabled_for?(:premium_features, user, ip_address: request.remote_ip)
# Feature is enabled for this user and IP
end
# Works with ActiveRecord objects, hashes, or simple IDs
Magick.enabled_for?(:feature, user) # ActiveRecord object
Magick.enabled_for?(:feature, { id: 123, role: 'admin' }) # Hash
Magick.enabled_for?(:feature, 123) # Simple ID
The enabled_for? method automatically extracts:
user_idfromidoruser_idattributegroupfromgroupattributerolefromroleattributeip_addressfromip_addressattribute- All other attributes for custom attribute matching
Return Values
All enable/disable methods now return true to indicate success:
# All these methods return true on success
result = feature.enable # => true
result = feature.disable # => true
result = feature.enable_for_user(123) # => true
result = feature.enable_for_group('beta') # => true
result = feature.enable_percentage_of_users(25) # => true
result = feature.set_value(true) # => true
DSL Configuration
Create config/features.rb:
# Boolean features
boolean_feature :new_dashboard,
default: false,
name: "New Dashboard",
description: "New dashboard UI"
boolean_feature :dark_mode,
default: false,
name: "Dark Mode",
description: "Dark mode theme"
# String features
string_feature :api_version, default: "v1", description: "API version"
# Number features
number_feature :max_results, default: 10, description: "Maximum results per page"
# With status
feature :experimental_feature,
type: :boolean,
default_value: false,
status: :deprecated,
description: "Experimental feature (deprecated)"
# With dependencies (feature will only be enabled if dependencies are enabled)
boolean_feature :advanced_feature,
default: false,
description: "Advanced feature requiring base_feature",
dependencies: [:base_feature]
# Multiple dependencies
boolean_feature :premium_feature,
default: false,
description: "Premium feature requiring multiple features",
dependencies: [:base_feature, :auth_feature]
# Add dependencies after feature definition
add_dependency(:another_feature, :required_feature)
In Controllers
class DashboardController < ApplicationController
def show
if Magick.enabled?(:new_dashboard, user_id: current_user.id, role: current_user.role)
render :new_dashboard
else
render :old_dashboard
end
end
end
Advanced Features
Feature Variants (A/B Testing)
feature = Magick[:button_color]
feature.set_variants([
{ name: 'blue', value: '#0066cc', weight: 50 },
{ name: 'green', value: '#00cc66', weight: 30 },
{ name: 'red', value: '#cc0000', weight: 20 }
])
variant = feature.get_variant
# Returns 'blue', 'green', or 'red' based on weights
Feature Dependencies
feature = Magick[:advanced_feature]
feature.add_dependency(:base_feature)
# advanced_feature can be enabled independently
# However, base_feature (dependency) cannot be enabled if advanced_feature (main feature) is disabled
# This ensures dependencies are only enabled when their parent features are enabled
# Example:
Magick[:advanced_feature].disable # => true
Magick[:base_feature].enable # => false (cannot enable dependency when main feature is disabled)
Magick[:advanced_feature].enable # => true
Magick[:base_feature].enable # => true (now can enable dependency)
Export/Import
# Export features
json_data = Magick.export(format: :json)
File.write('features.json', json_data)
# Import features
Magick.import(File.read('features.json'))
Versioning and Rollback
# Save current state as version
Magick.versioning.save_version(:my_feature, created_by: current_user.id)
# Rollback to previous version
Magick.versioning.rollback(:my_feature, version: 2)
Performance Metrics
# Get comprehensive stats for a feature
Magick.feature_stats(:my_feature)
# => {
# usage_count: 1250,
# average_duration: 0.032,
# average_duration_by_operation: {
# enabled: 0.032,
# value: 0.0,
# get_value: 0.0
# }
# }
# Get just the usage count
Magick.feature_usage_count(:my_feature)
# => 1250
# Get average duration (optionally filtered by operation)
Magick.feature_average_duration(:my_feature)
Magick.feature_average_duration(:my_feature, operation: 'enabled?')
# Get most used features
Magick.most_used_features(limit: 10)
# => {
# "my_feature" => 1250,
# "another_feature" => 890,
# ...
# }
# Direct access to performance metrics (for advanced usage)
Magick.performance_metrics.average_duration(feature_name: :my_feature)
Magick.performance_metrics.usage_count(:my_feature)
Magick.performance_metrics.most_used_features(limit: 10)
Configuration:
Magick.configure do
performance_metrics(
enabled: true,
redis_tracking: true, # Auto-enabled if Redis is configured
batch_size: 100, # Flush after 100 updates
flush_interval: 60 # Or flush every 60 seconds
)
end
Performance: Metrics are recorded asynchronously in a background thread, ensuring zero overhead on feature checks. The enabled? method remains lightning-fast even with metrics enabled.
Note: When redis_tracking: true is set, usage counts are persisted to Redis and aggregated across all processes, giving you total usage statistics. Metrics are automatically flushed in batches to minimize Redis overhead.
Audit Logging
# View audit log entries
entries = Magick.audit_log.entries(feature_name: :my_feature, limit: 100)
entries.each do |entry|
puts "#{entry.timestamp}: #{entry.action} by #{entry.user_id}"
end
Architecture
Adapters
Magick uses a dual-adapter strategy:
- Memory Adapter: Fast, in-memory storage with TTL support
- Redis Adapter: Persistent storage for distributed systems (optional)
The registry automatically falls back from memory to Redis if a feature isn't found in memory. When features are updated:
- Both adapters are updated simultaneously
- Cache invalidation messages are published via Redis Pub/Sub to notify other processes
- Targeting updates trigger immediate cache invalidation to ensure consistency
Memory-Only Mode
If Redis is not configured, Magick works in memory-only mode:
- ✅ Fast, zero external dependencies
- ✅ Perfect for single-process applications or development
- ⚠️ No cross-process cache invalidation - each process has isolated cache
- ⚠️ Changes in one process won't be reflected in other processes
Redis Mode (Recommended for Production)
With Redis configured:
- ✅ Cross-process cache invalidation via Redis Pub/Sub
- ✅ Persistent storage across restarts
- ✅ Zero Redis calls on feature checks (only memory lookups)
- ✅ Automatic cache invalidation when features change in any process
- ✅ Isolated from Rails cache - Use
db: 1to store feature toggles in a separate Redis database, ensuring they persist even when Rails cache is cleared
Important: By default, Magick uses Redis database 1 to avoid conflicts with Rails cache (which typically uses database 0). This ensures that clearing Rails cache (Rails.cache.clear) won't affect your feature toggle states.
ActiveRecord Adapter (Optional)
The ActiveRecord adapter provides database-backed persistent storage for feature flags. It's useful when you want to:
- Store feature flags in your application database
- Use ActiveRecord models for feature management
- Have a fallback storage layer
- Work with PostgreSQL, MySQL, SQLite, or any ActiveRecord-supported database
Setup:
- Generate and run the migration (required):
bash rails generate magick:active_record rails db:migrate
With UUID primary keys:
rails generate magick:active_record --uuid
rails db:migrate
Important: The adapter will not auto-create the table. You must run migrations before using the ActiveRecord adapter. If the table doesn't exist, the adapter will raise a clear error with instructions.
- Configure in
config/initializers/magick.rb:ruby Magick.configure do active_record # Uses default MagickFeature model # Or specify a custom model: # active_record model_class: YourCustomModel end
PostgreSQL Support:
The generator automatically detects PostgreSQL and uses jsonb for the data column, providing:
- Better performance with native JSON queries
- Native JSON indexing and querying capabilities
- Type-safe JSON storage
For other databases (MySQL, SQLite, etc.), it uses text with serialized JSON.
UUID Primary Keys:
When using the --uuid flag:
- Creates table with
id: :uuidinstead of integer primary key - Enables
pgcryptoextension for PostgreSQL (required for UUID generation) - Works with other databases using their native UUID support
Note: The ActiveRecord adapter works as a fallback in the adapter chain: Memory → Redis → ActiveRecord. It's automatically included if ActiveRecord is available and configured.
Adapter Chain:
The adapter registry uses a fallback strategy:
- Memory Adapter (first) - Fast, in-memory lookups
- Redis Adapter (second) - Persistent, distributed storage
- ActiveRecord Adapter (third) - Database-backed fallback
When a feature is requested:
- First checks memory cache (fastest)
- Falls back to Redis if not in memory
- Falls back to ActiveRecord if Redis is unavailable or returns nil
- Updates all adapters when features are modified
This ensures maximum performance while maintaining persistence and reliability.
Admin UI
Magick includes a web-based Admin UI for managing feature flags. It's a Rails Engine that provides a user-friendly interface for viewing, enabling, disabling, and configuring features.
Setup:
- Configure roles (optional) for targeting management in
config/initializers/magick.rb:
Rails.application.config.after_initialize do
Magick::AdminUI.configure do |config|
config.available_roles = ['admin', 'user', 'manager', 'guest']
end
end
- Mount the engine in
config/routes.rb:
Rails.application.routes.draw do
# ... your other routes ...
# With authentication (recommended for production)
authenticate :admin_user do
mount Magick::AdminUI::Engine, at: '/magick'
end
# Or without authentication (development only)
# mount Magick::AdminUI::Engine, at: '/magick'
end
Access:
Once mounted, visit /magick in your browser to access the Admin UI.
Features:
- Feature List: View all registered features with their current status, type, and description
- Feature Details: View detailed information about each feature including:
- Current value/status
- Targeting rules (users, groups, roles, percentages, etc.)
- Performance statistics (usage count, average duration)
- Feature metadata (type, default value, dependencies)
- Enable/Disable: Quickly enable or disable features globally
- Targeting Management: Configure targeting rules through a user-friendly interface:
- Role Targeting: Select roles from a configured list (checkboxes)
- User Targeting: Enter user IDs (comma-separated)
- Visual Display: See all active targeting rules with badges
- Edit Features: Update feature values (boolean, string, number) directly from the UI
- Statistics: View performance metrics and usage statistics for each feature
- Feature Grouping: Organize features into groups for easier management and filtering
- Filtering: Filter features by group, name, or description
Feature Grouping:
Features can be organized into groups for easier management and filtering:
Setting Groups:
- Set a group when registering a feature in code:
ruby Magick.register_feature(:new_payment_flow, type: :boolean, default_value: false, group: 'Payment', description: "New payment processing flow" ) - Or set/update groups via the Admin UI when editing a feature
- Set a group when registering a feature in code:
Filtering by Group:
- Use the group dropdown in the Admin UI to filter features by group
- Combine group filtering with search to find specific features quickly
Benefits:
- Organize features by functional area (e.g., "Authentication", "Payment", "UI")
- Quickly find related features
- Better organization for large feature flag sets
Targeting Management:
The Admin UI provides a comprehensive targeting interface:
Role Targeting:
- Configure available roles via
Magick::AdminUI.configure - Select multiple roles using checkboxes
- Roles are automatically added/removed when checkboxes are toggled
- Configure available roles via
User Targeting:
- Enter user IDs as comma-separated values (e.g.,
123, 456, 789) - Add or remove users dynamically
- Clear all user targeting by leaving the field empty
- Enter user IDs as comma-separated values (e.g.,
Visual Feedback:
- All targeting rules are displayed as badges in the feature details view
- Easy to see which roles/users have access to each feature
Routes:
The Admin UI provides the following routes:
GET /magick- Feature list (index)GET /magick/features/:id- Feature detailsGET /magick/features/:id/edit- Edit featurePUT /magick/features/:id- Update feature valuePUT /magick/features/:id/enable- Enable feature globallyPUT /magick/features/:id/disable- Disable feature globallyPUT /magick/features/:id/enable_for_user- Enable feature for specific userPUT /magick/features/:id/enable_for_role- Enable feature for specific rolePUT /magick/features/:id/disable_for_role- Disable feature for specific rolePUT /magick/features/:id/update_targeting- Update targeting rules (roles and users)GET /magick/stats/:id- View feature statistics
Security:
The Admin UI is a basic Rails Engine without built-in authentication. You should add authentication/authorization before mounting it in production. For example:
# config/routes.rb
Rails.application.routes.draw do
# Using Devise
authenticate :admin_user do
mount Magick::AdminUI::Engine, at: '/magick'
end
# Or using session-based authentication
constraints(->(request) { request.session[:user_id].present? && request.session[:admin] }) do
mount Magick::AdminUI::Engine, at: '/magick'
end
end
Or use a before_action in your ApplicationController if you mount it at the application level.
Note: The Admin UI is optional and only loaded when explicitly enabled in configuration. It requires Rails to be available.
Feature Types
:boolean- True/false flags:string- String values:number- Numeric values
Feature Status
:active- Feature is active and can be enabled:inactive- Feature is disabled for everyone:deprecated- Feature is deprecated (can be enabled withallow_deprecated: truein context)
Testing
Use the testing helpers in your RSpec tests:
RSpec.describe MyFeature do
it 'works with feature enabled' do
with_feature_enabled(:new_feature) do
# Test code here
end
end
it 'works with feature disabled' do
with_feature_disabled(:new_feature) do
# Test code here
end
end
end
Development
After checking out the repo, run:
bundle install
rspec
Contributing
Bug reports and pull requests are welcome on GitHub.
License
The gem is available as open source under the terms of the MIT License.