@iziad/feature-flags
v2.0.2
Published
A comprehensive, type-safe feature flag library for NestJS with Redis caching and automatic DB sync
Downloads
24
Maintainers
Readme
🚩 NestJS Feature Flags
A production-ready, type-safe feature flag library for NestJS with dynamic ORM support (TypeORM & Prisma), Redis caching, and automatic database synchronization.
✨ Features
- 🎯 Dynamic ORM Support - Single module automatically selects TypeORM or Prisma
- 🔒 Type-Safe API - Full TypeScript support with autocomplete
- 👤 User Overrides - Per-user feature flag customization
- 🌍 Environment-Aware - Different flag states per environment
- ⚡ Redis Caching - Optional high-performance caching
- 🔄 Auto-Sync - Automatic database synchronization with multi-container safety
- 🛡️ Protected Flags - Prevent critical flags from deletion
- 📦 Zero Config - Works out of the box with sensible defaults
📦 Installation
# Install the library
npm install @iziad/feature-flags
# Install your ORM (choose one)
npm install @nestjs/typeorm typeorm # For TypeORM
npm install @prisma/client # For Prisma
# Optional: Redis for caching and distributed locking
npm install ioredis🚀 Quick Start
Step 1: Define Your Flags
// src/feature-flags/flags.ts
import { flags, extractDefinitions } from '@iziad/feature-flags';
export const MyFlags = flags('ui', {
DARK_MODE: 'Enable dark mode theme',
NEW_DASHBOARD: 'New dashboard design',
});
export const ALL_FLAGS = extractDefinitions(MyFlags);Step 2: Setup Module (Unified API)
// app.module.ts
import { Module } from '@nestjs/common';
import { FeatureFlagModule } from '@iziad/feature-flags';
import { ALL_FLAGS } from './feature-flags/flags';
// For TypeORM
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'user',
password: 'pass',
database: 'mydb',
}),
// Unified module - automatically uses TypeORM
FeatureFlagModule.forRoot({
flags: ALL_FLAGS,
database: {
type: 'typeorm', // or 'prisma'
},
autoCreateTables: true,
autoSync: true,
}),
],
})
export class AppModule {}Or with Prisma:
import { PrismaService } from './prisma/prisma.service';
import { PrismaModule } from './prisma/prisma.module'; // Your Prisma module
@Module({
imports: [
PrismaModule, // ← MUST import this! Provides PrismaService
FeatureFlagModule.forRoot({
flags: ALL_FLAGS,
database: {
type: 'prisma',
schema: 'feature_flags', // Optional: separate schema
},
prismaService: PrismaService, // ← Required! Pass the class reference
imports: [PrismaModule], // ← Also pass via imports option
autoCreateTables: true,
autoSync: true,
}),
],
})
export class AppModule {}Important: You must do BOTH:
- ✅ Import
PrismaModulein yourAppModuleimports array - ✅ Pass
prismaService: PrismaService(the class reference) - ✅ Pass
imports: [PrismaModule]in the options (helps with dependency resolution)
💡 Usage
Check Feature Flags
import { Injectable } from '@nestjs/common';
import { FeatureFlagService } from '@iziad/feature-flags';
import { MyFlags } from './feature-flags/flags';
@Injectable()
export class MyService {
constructor(private featureFlags: FeatureFlagService) {}
async doSomething() {
// Check if flag is enabled
const isDarkMode = await this.featureFlags.isEnabled(
MyFlags.DARK_MODE.category,
MyFlags.DARK_MODE.key
);
if (isDarkMode) {
// Dark mode logic
}
}
}With User Overrides
async doSomethingForUser(userId: string) {
const hasBetaAccess = await this.featureFlags.isEnabled(
'beta',
'beta_features',
userId
);
if (hasBetaAccess) {
// Show beta features
}
}Route Guards
import { Controller, Get, UseGuards } from '@nestjs/common';
import { FeatureFlagGuard, FeatureFlag } from '@iziad/feature-flags';
import { MyFlags } from './feature-flags/flags';
@Controller('dashboard')
@UseGuards(FeatureFlagGuard)
export class DashboardController {
@Get('new')
@FeatureFlag(MyFlags.NEW_DASHBOARD.key, MyFlags.NEW_DASHBOARD.category)
getNewDashboard() {
return { version: 'new' };
}
}Multiple Flags
import { RequireAllFeatures, RequireAnyFeature } from '@iziad/feature-flags';
@Controller('beta')
@UseGuards(FeatureFlagGuard)
export class BetaController {
// Require ALL flags to be enabled
@Get('feature-a')
@RequireAllFeatures([
{ flag: 'beta_features', category: 'beta' },
{ flag: 'new_dashboard', category: 'ui' }
])
getFeatureA() {
return { feature: 'A' };
}
// Require ANY flag to be enabled
@Get('feature-b')
@RequireAnyFeature([
{ flag: 'beta_features', category: 'beta' },
{ flag: 'dark_mode', category: 'ui' }
])
getFeatureB() {
return { feature: 'B' };
}
}🔧 Configuration
Complete Configuration Example
FeatureFlagModule.forRoot({
// Required
flags: ALL_FLAGS,
// Database
database: {
type: 'typeorm', // or 'prisma'
schema: 'feature_flags', // Optional: separate schema/database
tablePrefix: '_', // Table prefix (default: '_')
},
// For Prisma only
prismaService: PrismaService,
// For TypeORM only (optional - uses defaults if not provided)
entities: [FeatureFlagEntityModel, UserFeatureFlagOverrideModel],
connectionName: 'default',
// Table creation
autoCreateTables: true, // Default: false
// Synchronization
autoSync: true, // Default: true
syncOnlyIfNoLock: true, // Default: true
syncCooldown: 300, // Default: 300 seconds
// Environment
environment: {
current: process.env.NODE_ENV || 'development',
},
// Redis (optional)
redis: {
host: 'localhost',
port: 6379,
password: 'secret',
db: 0,
keyPrefix: 'ff:',
},
cache: {
ttl: 3600, // Cache TTL in seconds
},
// Admin API
module: {
flushEndpoint: true, // Enable admin endpoints
},
// Logging
logging: {
level: 'log', // 'error' | 'warn' | 'log' | 'debug' | 'verbose'
contexts: {
'FeatureFlagService': 'debug',
},
},
})🌍 Environment-Aware Flags
Feature flags are environment-aware by default. Each environment has its own flag states.
FeatureFlagModule.forRoot({
flags: ALL_FLAGS,
environment: {
current: process.env.NODE_ENV || 'development',
},
})🔐 Multi-Container Safety
The library ensures only one container syncs at a time using distributed locking:
- Redis Lock (preferred): Uses Redis if configured
- Database Lock (fallback): Uses database-based locks
- Sync Cooldown: Prevents redundant syncs (default: 5 minutes)
FeatureFlagModule.forRoot({
flags: ALL_FLAGS,
syncOnlyIfNoLock: true,
syncCooldown: 300, // 5 minutes
// Redis for distributed locking
redis: {
host: 'redis',
port: 6379,
},
})🗄️ Database Tables
The library automatically creates prefixed tables:
_feature_flags- Main feature flags_user_feature_flag_overrides- User-specific overrides_feature_flag_locks- Distributed locking_feature_flag_last_sync- Sync timestamp tracking
Separate Schema/Database
FeatureFlagModule.forRoot({
flags: ALL_FLAGS,
database: {
schema: 'feature_flags', // PostgreSQL: separate schema
// MySQL: separate database
},
})📝 Environment Variables
Override configuration using environment variables (highest priority):
FEATURE_FLAG_AUTO_SYNC=true
FEATURE_FLAG_SYNC_ONLY_IF_NO_LOCK=true
FEATURE_FLAG_SYNC_COOLDOWN=300
FEATURE_FLAG_SMART_CACHE_INVALIDATION=truePriority: Environment Variables > Module Configuration > Defaults
🔌 Admin API
Built-in admin endpoints (disabled by default in production):
POST /admin/feature-flags/flush # Flush cache
POST /admin/feature-flags/sync # Manual sync
GET /admin/feature-flags/status # Get status
PATCH /admin/feature-flags/override # Set user overrideDisable Admin API
FeatureFlagModule.forRoot({
flags: ALL_FLAGS,
module: {
flushEndpoint: false,
},
})🧪 Testing
import { FeatureFlagTestingModule } from '@iziad/feature-flags/testing';
const module = await Test.createTestingModule({
imports: [
FeatureFlagTestingModule.forRoot({
flags: ALL_FLAGS,
}),
],
}).compile();📚 API Reference
FeatureFlagService
class FeatureFlagService {
// Check if flag is enabled
async isEnabled(
category: string,
flag: string,
userId?: string
): Promise<boolean>
// Get all flags for a category
async getFlags(
category: string,
userId?: string
): Promise<Record<string, boolean>>
// Get detailed evaluation
async getFlagEvaluation(
category: string,
flag: string,
userId?: string
): Promise<FeatureFlagEvaluation>
// Sync flags to database
async syncFlags(): Promise<void>
// Flush cache
async flushCache(options?: FlushCacheOptions): Promise<FlushCacheResult>
// User overrides
async setUserOverride(userId: string, flagKey: string, enabled: boolean): Promise<void>
async removeUserOverride(userId: string, flagKey: string): Promise<void>
}Decorators
// Single flag
@FeatureFlag(flagKey, category)
// Optional flag (doesn't block if disabled)
@OptionalFeature(flagKey, category)
// All flags required
@RequireAllFeatures([{ flag, category }, ...])
// Any flag required
@RequireAnyFeature([{ flag, category }, ...])🎯 Best Practices
- Use Flag Objects - Type-safe with autocomplete
- Define Flags Centrally - Single source of truth
- Enable Redis in Production - Better performance
- Use Sync Cooldown - Prevent excessive syncs
- Protect Critical Flags - Mark as
protected: true - Environment-Aware Configuration - Different states per environment
🐛 Troubleshooting
Prisma: "Can't resolve dependencies of FEATURE_FLAG_REPOSITORY"
Solution: You must do THREE things:
import { PrismaService } from './prisma/prisma.service';
import { PrismaModule } from './prisma/prisma.module';
@Module({
imports: [
PrismaModule, // ← 1. Import PrismaModule in AppModule
FeatureFlagModule.forRoot({
flags: ALL_FLAGS,
database: { type: 'prisma' },
prismaService: PrismaService, // ← 2. Pass the class reference
imports: [PrismaModule], // ← 3. Also pass via imports option
}),
],
})
export class AppModule {}Why? Prisma doesn't have official NestJS integration like TypeORM, so the library can't auto-detect your PrismaService. You must:
- ✅ Import
PrismaModulein yourAppModuleimports array - ✅ Pass
prismaService: PrismaService(the class reference, not a string) - ✅ Pass
imports: [PrismaModule]in the options (ensures proper dependency resolution)
Tables Not Created
Solution: Enable autoCreateTables:
FeatureFlagModule.forRoot({
flags: ALL_FLAGS,
autoCreateTables: true,
})TypeORM: "No metadata for FeatureFlagEntityModel was found"
The Issue: When you explicitly list entities in TypeOrmModule.forRoot(), TypeORM builds metadata at initialization time. Our module uses TypeOrmModule.forFeature() which should work, but if your DataSource is configured with an explicit entities array, you need to include our entities.
Solution 1 (Recommended): Use entity auto-loading with path patterns - TypeORM discovers entities automatically:
@Module({
imports: [
TypeOrmModule.forRoot({
// ... your database config
// Use path patterns - TypeORM will scan and load all .entity.ts files
entities: [__dirname + '/**/*.entity{.ts,.js}'],
// This automatically discovers:
// - Your app entities in src/**/*.entity.ts
// - Our entities via TypeOrmModule.forFeature() in FeatureFlagModule
}),
FeatureFlagModule.forRoot({
flags: ALL_FLAGS,
// No need to manually add entities - forFeature() handles registration!
}),
],
})
export class AppModule {}How it works: When you use path patterns, TypeORM scans directories for entity files. Our module calls TypeOrmModule.forFeature([FeatureFlagEntityModel, ...]) which registers our entities with the DataSource. This works seamlessly because TypeORM supports dynamic entity registration when using path patterns.
Solution 2: If you use explicit entity arrays (from objects/maps), merge our entities array:
import { FEATURE_FLAG_ENTITIES } from '@iziad/feature-flags/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
// ... your database config
entities: [
// Your entities (from object/map)
...Object.values(entities),
// Dynamically include feature flag entities
...FEATURE_FLAG_ENTITIES,
],
}),
FeatureFlagModule.forRoot({
flags: ALL_FLAGS,
}),
],
})
export class AppModule {}Alternative - Manual import (if you prefer):
import {
FeatureFlagEntityModel,
UserFeatureFlagOverrideModel
} from '@iziad/feature-flags/typeorm';
entities: [
...Object.values(entities),
FeatureFlagEntityModel,
UserFeatureFlagOverrideModel,
]Why? TypeORM builds entity metadata when the DataSource initializes. If you use an explicit entities array, all entities must be listed there. TypeOrmModule.forFeature() registers repositories for DI, but TypeORM still needs the entities in the DataSource metadata. When using explicit arrays, you must include our entities.
📖 Documentation
- All Options Reference - Complete configuration reference
- Adding a New ORM - Guide for ORM integration
🤝 Contributing
Contributions are welcome! Please read CONTRIBUTING.md for details.
📄 License
MIT © Ziad Saber
🔗 Links
Built with ❤️ for the NestJS community
