New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

@iziad/feature-flags

Package Overview
Dependencies
Maintainers
1
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@iziad/feature-flags

A comprehensive, type-safe feature flag library for NestJS with Redis caching and automatic DB sync

latest
npmnpm
Version
2.1.1
Version published
Weekly downloads
7
250%
Maintainers
1
Weekly downloads
 
Created
Source

🚩 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.

npm version License: MIT

✨ 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 PrismaModule in your AppModule imports 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
  }
}

Check Flag for Multiple Users

// Check flag state for multiple users at once
const result = await this.featureFlags.isEnabledForUsers(
  'beta',
  'ai_assistant',
  ['user1', 'user2', 'user3']
);

// Result contains:
// - userStates: { user1: true, user2: false, user3: true }
// - globalEnabled: false // Global flag state (before user overrides)
// - anyEnabled: true  // true if ANY user has it enabled (OR logic)
// - allEnabled: false // true only if ALL users have it enabled (AND logic)

if (result.anyEnabled) {
  // At least one user has access
}

if (result.allEnabled) {
  // All users have access
}

// Using FlagObject
const result2 = await this.featureFlags.isEnabledForUsers(
  MyFlags.BETA_FEATURES,
  ['user1', 'user2']
);

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=true

Priority: 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 override

Disable 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>
  
  // Check flag for multiple users
  async isEnabledForUsers(
    categoryOrFlagObject: string | FlagObject,
    flagOrUserIds?: string | string[],
    userIds?: string[]
  ): Promise<MultipleUsersResult>
}

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 PrismaModule in your AppModule imports 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.

TypeORM: "Nest can't resolve dependencies of the FEATURE_FLAG_REPOSITORY - DataSource at index [1] is not available"

The Issue: When using TypeOrmModule.forRootAsync() with a @Global() DatabaseModule, the DataSource provider might not be accessible to FeatureFlagTypeOrmModule.

Solution: Ensure your DatabaseModule is @Global() and import it in your app module BEFORE FeatureFlagModule:

// database.module.ts
import { Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Global()  // ✅ Must be @Global()
@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      useFactory: async () => buildTypeOrmOptions(),
      dataSourceFactory: async (options) => {
        const ds = new DataSource(options);
        return await ds.initialize();
      },
    }),
  ],
  exports: [TypeOrmModule],  // ✅ Export TypeOrmModule
})
export class DatabaseModule {}

// app.module.ts
@Module({
  imports: [
    DatabaseModule,  // ✅ Import FIRST
    // ... other modules
    FeatureFlagModule.forRoot({
      flags: ALL_FLAGS,
      database: { type: 'typeorm' },
      imports: [DatabaseModule],  // ✅ Also pass via imports option
    }),
  ],
})
export class AppModule {}

Why? The FeatureFlagTypeOrmModule needs access to the DataSource to create repositories via TypeOrmModule.forFeature(). Even though DatabaseModule is @Global(), explicitly importing it ensures proper module resolution.

Note: This issue was fixed in version 2.1.1+ by getting DataSource from repository's connection instead of injecting directly, but the above setup is still recommended for best compatibility.

TypeORM: "column UserFeatureFlagOverrideModel.flagId does not exist"

The Issue: This error occurs when TypeORM tries to access a foreign key column that doesn't exist in your database schema.

Solution: This has been fixed in version 2.1.1+. The entities no longer use foreign key relationships - they use flagKey string directly. If you're using an older version, upgrade to 2.1.1 or later:

npm install @iziad/feature-flags@latest

Note: The entities use flagKey string to reference flags, not a foreign key relationship. This is by design and matches how the repository queries work. The relationship was removed because it wasn't used and caused schema issues.

📖 Documentation

🤝 Contributing

Contributions are welcome! Please read CONTRIBUTING.md for details.

📄 License

MIT © Ziad Saber

Built with ❤️ for the NestJS community

Keywords

nestjs

FAQs

Package last updated on 25 Dec 2025

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts