Official FeatureHub Ruby SDK
Overview
To control the feature flags from the FeatureHub Admin console, either use our demo version for evaluation or install the app using our guide here.
SDK installation
Add the featurehub-sdk gem to your Gemfile:
gem 'featurehub-sdk'
To use it in your code:
require 'featurehub-sdk'
Options to get feature updates
There are 2 ways to request feature updates via this SDK:
- SSE (Server Sent Events) realtime updates
Makes a persistent connection to the FeatureHub Edge server. Any updates to features come through in near-realtime, automatically updating the repository. Recommended for long-running server applications.
- Polling client (GET request)
Requests updates at a configurable interval (0 = once only). Useful for short-lived processes such as CLI tools or batch jobs.
Both options use concurrent-ruby to keep the connection open and update state in the background.
Quick start
1. Copy your API Key
Find and copy your API Key from the FeatureHub Admin Console on the API Keys page. It will look similar to:
default/71ed3c04-122b-4312-9ea8-06b2b8d6ceac/fsTmCrcZZoGyl56kPHxfKAkbHrJ7xZMKO3dlBiab5IqUXjgKvqpjxYdI8zdXiJqYCpv92Jrki0jY5taE
There are two key types — Server Evaluated and Client Evaluated. More detail here.
- Client Evaluated keys (contain
*) send full rollout strategy data to the SDK and evaluate strategies locally, per request. Intended for secure server-side environments such as microservices. - Server Evaluated keys evaluate on the server side. Suitable for insecure clients or environments where you evaluate one user per connection.
2. Create FeatureHub config
config = FeatureHub::Sdk::FeatureHubConfig.new(
ENV.fetch("FEATUREHUB_EDGE_URL"),
[ENV.fetch("FEATUREHUB_CLIENT_API_KEY")]
)
config.init
You only ever need to do this once. A FeatureHubConfig holds a FeatureHubRepository (state) and an edge service (updates). In Rails, create an initializer:
Rails.configuration.fh_client = FeatureHub::Sdk::FeatureHubConfig.new(
ENV.fetch("FEATUREHUB_EDGE_URL"),
[ENV.fetch("FEATUREHUB_CLIENT_API_KEY")]
).init
In Sinatra:
class App < Sinatra::Base
configure do
set :fh_config, FeatureHub::Sdk::FeatureHubConfig.new(
ENV.fetch("FEATUREHUB_EDGE_URL"),
[ENV.fetch("FEATUREHUB_CLIENT_API_KEY")]
)
end
end
To use the polling client instead of SSE:
config.use_polling_edge_service(30)
# OR — reads FEATUREHUB_POLL_INTERVAL env var, defaults to 30 seconds
config.use_polling_edge_service
3. Check readiness and request feature state
if config.repository.ready?
# safe to evaluate features
end
See Readiness below for details on incorporating this into health checks.
Evaluating features
Without a context (no rollout strategies)
if config.new_context.build.feature("FEATURE_TITLE_TO_UPPERCASE").flag
"HELLO WORLD"
else
"hello world"
end
With a context (rollout strategies)
Build a context with the attributes you want to use for strategy evaluation, then call build to push them to the server (server-evaluated keys) or trigger a poll (client-evaluated keys):
ctx = config.new_context
.user_key(current_user.id)
.country("australia")
.platform("ios")
.version("2.3.1")
.attribute_value("plan", "premium")
.build
if ctx.feature("FEATURE_TITLE_TO_UPPERCASE").flag
# ...
end
Well-known context attributes
| Method | ContextKey |
|---|---|
user_key(value) |
:userkey |
session_key(value) |
:session |
country(value) |
:country |
platform(value) |
:platform |
device(value) |
:device |
version(value) |
:version |
Custom attributes
ctx.attribute_value("contract_ids", [2, 17, 45])
assign — bulk-set attributes from a hash
assign accepts a hash, maps well-known keys to their dedicated setters, and merges anything else as a custom attribute:
ctx.assign(
userkey: current_user.id,
country: "nz",
plan: "enterprise"
)
String keys are also accepted ("userkey" and :userkey are equivalent).
Construct a context with initial attributes
Pass a hash directly to new_context via the repository, or pre-populate at construction time:
ctx = FeatureHub::Sdk::ClientContext.new(repository, { userkey: "u1", country: "nz" })
One-off feature evaluation with inline attributes
If you only need to check one feature and do not want to build a context, you can pass attributes directly to feature:
# On the config (delegates to the repository)
config.feature("SUBMIT_COLOR_BUTTON", { country: "nz" }).string
# Or directly on the repository
config.repository.feature("SUBMIT_COLOR_BUTTON", { country: "nz", userkey: "u1" }).string
This creates a temporary ClientContext internally and evaluates the feature through it.
value — get a raw value with a default
value(key, default_value = nil, attrs = nil) returns the feature's value directly, or default_value if the feature does not exist:
# Simple lookup with a fallback
color = config.value("SUBMIT_COLOR_BUTTON", "blue")
# With inline attributes for strategy evaluation
color = config.value("SUBMIT_COLOR_BUTTON", "blue", { country: "nz" })
# Also available on the repository directly
color = config.repository.value("SUBMIT_COLOR_BUTTON", "blue")
Feature value accessors
| Method | Returns |
|---|---|
.flag / .boolean |
bool? |
.string |
String? |
.number |
Float? |
.raw_json |
String? (raw JSON string) |
.json |
Hash? (parsed JSON) |
.enabled? |
bool (true if flag is on) |
.set? |
bool (true if a value has been set) |
.exists? |
bool (true if the feature exists in the repository) |
.present? |
bool (alias for exists?) |
Feature interceptors
Interceptors let you override feature values at runtime without changing the repository. They are evaluated before rollout strategies.
Environment variable interceptor
Override any feature at runtime using environment variables:
FEATUREHUB_OVERRIDE_FEATURES=true
FEATUREHUB_MY_FEATURE=true
FEATUREHUB_SUBMIT_COLOR_BUTTON=green
config.repository.register_interceptor(FeatureHub::Sdk::EnvironmentInterceptor.new)
Local YAML interceptor
Override features from a YAML file. Useful during development or testing:
# featurehub-overrides.yaml
flagValues:
MY_FEATURE: true
SUBMIT_COLOR_BUTTON: green
MAX_RETRIES: 3
All options are passed as a single hash:
# Default file path (featurehub-features.yaml or FEATUREHUB_LOCAL_YAML env var)
config.repository.register_interceptor(FeatureHub::Sdk::LocalYamlValueInterceptor.new)
# Explicit file path
config.repository.register_interceptor(
FeatureHub::Sdk::LocalYamlValueInterceptor.new(filename: "path/to/overrides.yaml")
)
# Watch for file changes and reload automatically
config.repository.register_interceptor(
FeatureHub::Sdk::LocalYamlValueInterceptor.new(watch: true, watch_interval: 5)
)
# With a custom logger
config.repository.register_interceptor(
FeatureHub::Sdk::LocalYamlValueInterceptor.new(filename: "overrides.yaml", logger: my_logger)
)
Supported options: :filename, :watch (default: false), :watch_interval (seconds, default: 5), :logger.
Offline / local-only mode with LocalYamlStore
LocalYamlStore loads features from a YAML file directly into the repository, with no Edge connection required. It uses the same file format as LocalYamlValueInterceptor. This is useful for tests, CI environments, or services that manage their own feature state.
# features.yaml
flagValues:
MY_FLAG: true
SUBMIT_COLOR_BUTTON: green
MAX_RETRIES: 3
PRICING_CONFIG:
base: 9.99
tiers: [19.99, 49.99]
repository = FeatureHub::Sdk::FeatureHubRepository.new
# Default file path (featurehub-features.yaml or FEATUREHUB_LOCAL_YAML env var)
store = FeatureHub::Sdk::LocalYamlStore.new(repository)
# Explicit file path
store = FeatureHub::Sdk::LocalYamlStore.new(repository, filename: "features.yaml")
repository.feature("MY_FLAG").flag # => true
repository.value("SUBMIT_COLOR_BUTTON") # => "green"
The file path defaults to featurehub-overrides.yaml or the FEATUREHUB_LOCAL_YAML environment variable. Complex values (hashes, arrays) are serialised to a JSON string and stored as a JSON feature type.
Caching feature state in Redis
RedisSessionStore persists feature values from a FeatureHubRepository to Redis. On startup it replays cached features into the repository, then listens for live updates and writes newer versions back. A background timer periodically re-reads a SHA key so that updates published by other processes are picked up automatically.
Warning: Do not use
RedisSessionStorewith server-evaluated features. Each server-evaluated context resolves to different values; sharing a single Redis key across processes will cause them to overwrite each other's state.
Multi-process writes are safe: the store uses SHA256-based change detection and Redis WATCH/MULTI/EXEC to atomically update both keys and prevent races between concurrent writers.
Pass a FeatureHubConfig as the second argument — the store reads repository and environment_id from it and registers itself as a raw update listener automatically.
# Requires the 'redis' gem: gem 'redis', '~> 5'
store = FeatureHub::Sdk::RedisSessionStore.new(
"redis://localhost:6379",
config, # FeatureHubConfig — NOT config.repository
{
prefix: "myapp", # Redis key prefix (default: "featurehub")
db: 0, # Redis DB index (default: 0)
refresh_timeout: 300, # Seconds between periodic SHA checks (default: 300)
backoff_timeout: 500, # Milliseconds to wait between WATCH retries (default: 500)
retry_update_count: 10, # Maximum WATCH retry attempts per write (default: 10)
logger: my_logger # Optional logger
}
)
# Shut down cleanly
store.close
You can also pass an existing Redis client instead of a connection string (e.g. a RedisCluster client or a pre-configured Redis instance):
redis = Redis.new(url: "redis://localhost:6379", db: 1)
store = FeatureHub::Sdk::RedisSessionStore.new(redis, config)
You can also pass a RedisSessionStoreOptions object directly:
opts = FeatureHub::Sdk::RedisSessionStoreOptions.new(prefix: "myapp", db: 2)
store = FeatureHub::Sdk::RedisSessionStore.new("redis://localhost:6379", config, opts)
Redis keys used:
{prefix}_{environment_id}— JSON-encoded array of all feature states{prefix}_{environment_id}_sha— SHA256 fingerprint used for cross-process change detection
Caching feature state in Memcache
MemcacheSessionStore persists feature values from a FeatureHubRepository to Memcache. On startup it reads any previously saved features from Memcache and replays them into the repository, then listens for live updates and writes newer versions back. A background timer periodically re-reads a SHA key so that updates published by other processes are picked up automatically.
Warning: Do not use
MemcacheSessionStorewith server-evaluated features. Each server-evaluated context resolves to different values; sharing a single Memcache key across processes will cause them to overwrite each other's state.
Multi-process writes are safe: the store uses SHA256-based change detection and Dalli's compare-and-set (cas) to prevent races between concurrent writers.
# Requires the 'dalli' gem: gem 'dalli', '~> 5'
store = FeatureHub::Sdk::MemcacheSessionStore.new(
"localhost:11211",
config,
{
prefix: "myapp", # Key prefix (default: "featurehub")
refresh_timeout: 300, # Seconds between periodic SHA checks (default: 300)
backoff_timeout: 500, # Milliseconds to wait between CAS retries (default: 500)
retry_update_count: 10, # Maximum CAS retry attempts per write (default: 10)
logger: my_logger # Optional logger (default: SDK default logger)
}
)
# Shut down cleanly
store.close
You can also pass an existing Dalli::Client instead of a connection string:
dalli = Dalli::Client.new("localhost:11211", serializer: JSON)
store = FeatureHub::Sdk::MemcacheSessionStore.new(dalli, config)
Memcache keys used:
{prefix}_{environment_id}— JSON-encoded array of all feature states{prefix}_{environment_id}_sha— SHA256 fingerprint used for cross-process change detection
Custom raw update listeners
RawUpdateFeatureListener is a base class you can subclass to observe every raw feature update that flows through the repository, regardless of source. Register an instance with the repository (or config) and override only the callbacks you need:
class MyAuditListener < FeatureHub::Sdk::RawUpdateFeatureListener
def process_updates(features, source)
features.each { |f| Rails.logger.info("bulk update from #{source}: #{f["key"]}") }
end
def process_update(feature, source)
Rails.logger.info("single update from #{source}: #{feature["key"]}")
end
def delete_feature(feature, source)
Rails.logger.warn("deleted from #{source}: #{feature["key"]}")
end
end
config.register_raw_update_listener(MyAuditListener.new)
Callbacks are dispatched asynchronously via Concurrent::Future. The source parameter will be "streaming", "polling", "local-yaml", "redis-store", "memcache-store", or "unknown".
All listeners are closed automatically when config.close or repository.close is called.
Using inside popular web servers
Most popular web servers fork processes to handle traffic. Forking kills the Edge connection but preserves the cached repository. Call force_new_edge_service in your framework's post-fork hook to restart the connection:
config.force_new_edge_service
Passenger
In config.ru:
if defined?(PhusionPassenger)
PhusionPassenger.on_event(:starting_worker_process) do |forked|
App.settings.fh_config.force_new_edge_service if forked
end
end
Puma
on_worker_boot do
App.settings.fh_config.force_new_edge_service
end
Unicorn
after_fork do |_server, _worker|
App.settings.fh_config.force_new_edge_service
end
Spring
Spring.after_fork do
App.settings.fh_config.force_new_edge_service
end
Extracting and restoring state
You can snapshot the repository state and reload it later (e.g. as a warm-start cache):
require 'json'
# Snapshot
state = config.repository.extract_feature_state
save(state.to_json)
# Restore
config.repository.notify(:features, JSON.parse(read_state))
Readiness
It is recommended to include the repository's ready state in your health/readiness check. The repository becomes ready once it has received its first successful update, and stays ready even through temporary connection loss. It is only not ready if the API key is invalid or no state has ever been received:
config.repository.ready?
Examples
Check our example Sinatra app here.