A lightweight, flexible feature flagging library for Node.js applications
Fanion is a modern feature flagging library that allows you to control feature rollouts, conduct A/B tests, and manage application behavior dynamically. Built with TypeScript and designed for simplicity and performance.
- 🚀 Lightweight & Fast - Minimal overhead with maximum performance
- 🔧 TypeScript First - Full type safety and IntelliSense support
- 🏪 Pluggable Storage - Memory, database, or custom storage providers
- 🎯 Context-Aware - Dynamic feature evaluation based on user context
- 🔄 Async Support - Built for modern async/await patterns
- 📦 Zero Dependencies - No external dependencies in core package
npm install fanion
# or
yarn add fanion
# or
pnpm add fanion
For database support, also install the appropriate database driver:
# For SQL databases (SQLite, PostgreSQL, MySQL)
npm install knex sqlite3 # or pg, mysql2
# For DynamoDB
npm install @aws-sdk/client-dynamodb @aws-sdk/util-dynamodb
import { featureManager } from 'fanion';
// Create a feature manager instance
const features = featureManager();
// Define a simple feature flag
features.define('new-dashboard', () => true);
// Check if feature is active
const isActive = await features.active('new-dashboard');
console.log(isActive); // true
// Define a feature flag with user context
features.define('premium-features', (context: { user: { plan: string } }) => {
return context.user.plan === 'premium';
});
// Check feature with context
const hasAccess = await features.active('premium-features', {
user: { plan: 'premium' }
});
import { featureManager, createInMemoryDriver } from 'fanion';
// Create feature manager with storage
const features = featureManager({
store: createInMemoryDriver()
});
// Define and store a feature flag
features.defineAndStore('beta-feature', true);
// Feature flags stored in the provider are automatically retrieved
const isEnabled = await features.active('beta-feature');
import { featureManagerWithDatabase, createKnexDatabaseDriver } from 'fanion';
import knex from 'knex';
// Create database connection
const db = knex({
client: 'sqlite3',
connection: {
filename: './features.db'
},
useNullAsDefault: true
});
// Create feature manager with database storage
const features = await featureManagerWithDatabase({
store: createKnexDatabaseDriver({
connection: db,
tableName: 'feature_flags', // optional, defaults to 'feature_flags'
featureNameColumn: 'feature_name', // optional, defaults to 'feature_name'
valueColumn: 'value' // optional, defaults to 'value'
})
});
// The database table is automatically created
// Feature flags are now persisted in the database
features.defineAndStore('persistent-feature', true);
import { featureManagerWithDatabase, createDynamoDBDatabaseDriver } from 'fanion';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
// Create DynamoDB client
const dynamoClient = new DynamoDBClient({
region: 'us-east-1',
// Add other configuration as needed (credentials, endpoint, etc.)
});
// Create feature manager with DynamoDB storage
const features = await featureManagerWithDatabase({
store: createDynamoDBDatabaseDriver({
client: dynamoClient,
tableName: 'feature_flags', // optional, defaults to 'feature_flags'
featureNameAttribute: 'feature_name', // optional, defaults to 'feature_name'
valueAttribute: 'value' // optional, defaults to 'value'
})
});
// The DynamoDB table is automatically created with pay-per-request billing
// Feature flags are now persisted in DynamoDB
features.defineAndStore('persistent-feature', true);
The main class for managing feature flags.
Define a feature flag with an optional check function.
// Simple flag (always true when no check function provided)
features.define('simple-flag');
// Flag with synchronous check
features.define('admin-feature', (ctx: { isAdmin: boolean }) => ctx.isAdmin);
// Flag with asynchronous check
features.define('database-feature', async (ctx) => {
return await checkDatabaseFeature(ctx.userId);
});
Define a feature flag and store it in the configured storage provider.
features.defineAndStore('new-ui', false);
Create a feature manager instance with database initialization. This function automatically initializes the database connection and creates necessary tables.
const features = await featureManagerWithDatabase({
store: createKnexDatabaseDriver({ connection: db })
});
Initialize the storage provider (useful for database connections). This is called automatically when using featureManagerWithDatabase()
.
const features = featureManager({ store: dbDriver });
await features.initStore(); // Initialize database tables
Check if a feature flag is active for the given context.
const isActive = await features.active('feature-name');
const isActiveWithContext = await features.active('feature-name', { userId: 123 });
import { createInMemoryDriver } from 'fanion';
const memoryStore = createInMemoryDriver();
const features = featureManager({ store: memoryStore });
The Knex database driver supports all databases that Knex supports (PostgreSQL, MySQL, SQLite, etc.).
import { createKnexDatabaseDriver, featureManagerWithDatabase, KnexDatabaseDriver } from 'fanion';
import knex from 'knex';
// Setup database connection
const db = knex({
client: 'postgresql', // or 'mysql', 'sqlite3', etc.
connection: {
host: 'localhost',
user: 'username',
password: 'password',
database: 'myapp'
}
});
// Create database driver with custom configuration
const dbDriver = createKnexDatabaseDriver({
connection: db,
tableName: 'my_feature_flags', // defaults to 'feature_flags'
featureNameColumn: 'flag_name', // defaults to 'feature_name'
valueColumn: 'is_enabled' // defaults to 'value'
});
// Initialize feature manager with database
const features = await featureManagerWithDatabase({
store: dbDriver
});
Configuration Options:
connection
: Knex database connection instance (required)tableName
: Name of the database table (optional, defaults to 'feature_flags')featureNameColumn
: Name of the feature name column (optional, defaults to 'feature_name')valueColumn
: Name of the value column (optional, defaults to 'value')
The DynamoDB driver uses AWS SDK v3 to connect to Amazon DynamoDB.
import { createDynamoDBDatabaseDriver, featureManagerWithDatabase } from 'fanion';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
// Setup DynamoDB client
const dynamoClient = new DynamoDBClient({
region: 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
// For local development with DynamoDB Local
// endpoint: 'http://localhost:8000',
});
// Create DynamoDB driver with custom configuration
const dynamoDriver = createDynamoDBDatabaseDriver({
client: dynamoClient,
tableName: 'my_feature_flags', // defaults to 'feature_flags'
featureNameAttribute: 'flag_name', // defaults to 'feature_name'
valueAttribute: 'is_enabled' // defaults to 'value'
});
// Initialize feature manager with DynamoDB
const features = await featureManagerWithDatabase({
store: dynamoDriver
});
Configuration Options:
client
: DynamoDB client instance from AWS SDK v3 (required)tableName
: Name of the DynamoDB table (optional, defaults to 'feature_flags')featureNameAttribute
: Name of the partition key attribute (optional, defaults to 'feature_name')valueAttribute
: Name of the value attribute (optional, defaults to 'value')
DynamoDB Table Structure: The driver automatically creates a table with:
- Partition key:
feature_name
(or custom attribute name) - Pay-per-request billing mode
- No sort key (simple key-value storage)
Implement the FeatureStorageProvider
interface:
interface FeatureStorageProvider {
set(flag: string, value: boolean): Promise<void>;
get(flag: string): Promise<boolean | undefined>;
delete(featureName: string): Promise<void>;
initStore?(): Promise<void>; // Optional initialization method
}
class RedisStorage implements FeatureStorageProvider {
async set(flag: string, value: boolean): Promise<void> {
// Implement Redis storage
}
async get(flag: string): Promise<boolean | undefined> {
// Implement Redis retrieval
}
async delete(featureName: string): Promise<void> {
// Implement Redis deletion
}
async initStore(): Promise<void> {
// Optional: Initialize Redis connection
}
}
For database providers, implement the DatabaseStorageProvider
interface:
import { DatabaseStorageProvider } from 'fanion';
interface DatabaseStorageProvider extends FeatureStorageProvider {
createTableIfNotExists(): Promise<void>;
isDatabaseDriver(): boolean;
initStore(): Promise<void>;
}
class CustomDatabaseDriver implements DatabaseStorageProvider {
async set(flag: string, value: boolean): Promise<void> {
// Implement database upsert
}
async get(flag: string): Promise<boolean | undefined> {
// Implement database retrieval
}
async delete(featureName: string): Promise<void> {
// Implement database deletion
}
async createTableIfNotExists(): Promise<void> {
// Create the feature flags table if it doesn't exist
}
isDatabaseDriver(): boolean {
return true;
}
async initStore(): Promise<void> {
await this.createTableIfNotExists();
}
}
Generate consistent feature flag names with namespacing.
import { generateFeatureName } from 'fanion';
const flagName = generateFeatureName('auth', 'oauth', 'google');
// Returns: "auth:oauth.google"
features.define('homepage-variant', (context: { userId: number }) => {
// Simple hash-based A/B test
return context.userId % 2 === 0;
});
const showVariantA = await features.active('homepage-variant', { userId: 123 });
features.define('new-feature', (context: { userId: number }) => {
// Roll out to 20% of users
return (context.userId % 100) < 20;
});
features.define('debug-mode', () => {
return process.env.NODE_ENV === 'development';
});
features.define('enterprise-features', async (context: {
user: { id: number; plan: string; createdAt: Date }
}) => {
const { user } = context;
// Multiple conditions
if (user.plan !== 'enterprise') return false;
// Check account age
const accountAge = Date.now() - user.createdAt.getTime();
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
return accountAge > thirtyDays;
});
Fanion provides specific error types for better error handling:
import { FeatureNotExistsError } from 'fanion';
try {
await features.active('non-existent-flag');
} catch (error) {
if (error instanceof FeatureNotExistsError) {
console.log('Feature flag not found');
}
}
// Good
features.define('checkout-express-shipping');
// Avoid
features.define('flag1');
features.define('external-api-feature', async (context) => {
try {
return await checkExternalAPI(context);
} catch {
// Fallback to safe default
return false;
}
});
interface UserContext {
userId: number;
plan: 'free' | 'premium' | 'enterprise';
features: string[];
}
features.define<{ user: UserContext }>('premium-dashboard', (context) => {
return context.user.plan !== 'free';
});
// Good - simple and fast
features.define('new-ui', (ctx: { version: string }) => {
return ctx.version >= '2.0.0';
});
// Avoid - complex logic that might slow down requests
features.define('complex-feature', async (ctx) => {
// Avoid heavy database queries or API calls
const result = await heavyDatabaseQuery(ctx);
return processComplexLogic(result);
});
import express from 'express';
import { featureManager } from 'fanion';
const app = express();
const features = featureManager();
features.define('rate-limiting', (ctx: { ip: string }) => {
// Enable rate limiting for specific IPs
return ctx.ip !== '127.0.0.1';
});
app.use(async (req, res, next) => {
const shouldRateLimit = await features.active('rate-limiting', {
ip: req.ip
});
if (shouldRateLimit) {
// Apply rate limiting middleware
}
next();
});
// lib/features.ts
import { featureManagerWithDatabase, createKnexDatabaseDriver } from 'fanion';
import knex from 'knex';
const db = knex({
client: 'postgresql',
connection: process.env.DATABASE_URL
});
export const features = await featureManagerWithDatabase({
store: createKnexDatabaseDriver({ connection: db })
});
features.define('beta-features', (ctx: { user?: { isBetaTester: boolean } }) => {
return ctx.user?.isBetaTester ?? false;
});
// pages/dashboard.tsx
import { features } from '../lib/features';
export async function getServerSideProps(context) {
const showBetaFeatures = await features.active('beta-features', {
user: context.user
});
return {
props: { showBetaFeatures }
};
}
When using SQL database storage providers, Fanion automatically creates a table with the following structure:
CREATE TABLE feature_flags (
feature_name VARCHAR PRIMARY KEY,
value BOOLEAN NOT NULL
);
The table and column names are configurable through the driver configuration.
When using the DynamoDB driver, Fanion automatically creates a table with:
- Partition Key:
feature_name
(String) - Attributes:
value
(Boolean) - Billing Mode: Pay-per-request
- No sort key: Simple key-value storage
The table and attribute names are configurable through the driver configuration.
If you're migrating from in-memory storage to database storage, here's how to transition smoothly:
import { featureManager, featureManagerWithDatabase, createKnexDatabaseDriver, createDynamoDBDatabaseDriver } from 'fanion';
// Old in-memory setup
const oldFeatures = featureManager();
oldFeatures.define('feature-a', () => true);
oldFeatures.define('feature-b', (ctx) => ctx.user.isPremium);
// New SQL database setup
const sqlFeatures = await featureManagerWithDatabase({
store: createKnexDatabaseDriver({ connection: db })
});
// Or new DynamoDB setup
const dynamoFeatures = await featureManagerWithDatabase({
store: createDynamoDBDatabaseDriver({ client: dynamoClient })
});
// Migrate stored flags to database
sqlFeatures.defineAndStore('feature-a', true);
// Keep dynamic flags as code-based
sqlFeatures.define('feature-b', (ctx) => ctx.user.isPremium);
// Good: Use database for flags controlled by admins/operators
features.defineAndStore('maintenance-mode', false);
features.defineAndStore('new-ui-rollout', true);
// Good: Keep complex logic in code
features.define('premium-features', (ctx) => {
return ctx.user.plan === 'premium' && ctx.user.verified;
});
For SQL Databases:
// Good: Reuse database connections
const db = knex({
client: 'postgresql',
connection: process.env.DATABASE_URL,
pool: { min: 2, max: 10 } // Configure connection pooling
});
// Use the same connection for your app and feature flags
const features = await featureManagerWithDatabase({
store: createKnexDatabaseDriver({ connection: db })
});
For DynamoDB:
// Good: Reuse DynamoDB client
const dynamoClient = new DynamoDBClient({
region: process.env.AWS_REGION,
// Configure connection settings, retries, etc.
maxAttempts: 3,
});
// Use the same client for your app and feature flags
const features = await featureManagerWithDatabase({
store: createDynamoDBDatabaseDriver({ client: dynamoClient })
});
// Good: Handle database errors gracefully
try {
const isEnabled = await features.active('new-feature');
if (isEnabled) {
// Feature logic
}
} catch (error) {
// Log error and use safe default
console.error('Feature flag error:', error);
// Fall back to safe behavior
}
- Feature checks are designed to be fast and lightweight
- Use in-memory storage for high-performance scenarios
- Database providers automatically create tables and handle upserts efficiently
- Consider connection pooling for SQL database-backed storage in production
- DynamoDB provides automatic scaling and low-latency access
- Implement caching for frequently accessed database-backed feature flags
- Consider the frequency of feature flag evaluations in hot code paths
- DynamoDB pay-per-request billing is cost-effective for most feature flag workloads
We welcome contributions! Please see our Contributing Guide for details.
MIT © Maxence Guyonvarho
See CHANGELOG.md for a detailed list of changes.
Made with ❤️ by Maxence Guyonvarho