npm package: @iziad/feature-flags
Description: A comprehensive, type-safe feature flag library for NestJS with Redis caching and automatic DB sync
Author: Ziad Saber
Latest version: 2.1.1

# 🚩 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](https://img.shields.io/npm/v/@iziad/feature-flags.svg)](https://www.npmjs.com/package/@iziad/feature-flags)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/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

```bash
# 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

```typescript
// 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)

```typescript
// 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:**

```typescript
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:
1. ✅ Import `PrismaModule` in your `AppModule` imports array
2. ✅ Pass `prismaService: PrismaService` (the class reference)
3. ✅ Pass `imports: [PrismaModule]` in the options (helps with dependency resolution)

---

## 💡 Usage

### Check Feature Flags

```typescript
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

```typescript
async doSomethingForUser(userId: string) {
  const hasBetaAccess = await this.featureFlags.isEnabled(
    'beta',
    'beta_features',
    userId
  );
  
  if (hasBetaAccess) {
    // Show beta features
  }
}
```

### Check Flag for Multiple Users

```typescript
// 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

```typescript
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

```typescript
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

```typescript
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.

```typescript
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)

```typescript
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

```typescript
FeatureFlagModule.forRoot({
  flags: ALL_FLAGS,
  database: {
    schema: 'feature_flags', // PostgreSQL: separate schema
                              // MySQL: separate database
  },
})
```

---

## 📝 Environment Variables

Override configuration using environment variables (highest priority):

```bash
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):

```bash
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

```typescript
FeatureFlagModule.forRoot({
  flags: ALL_FLAGS,
  module: {
    flushEndpoint: false,
  },
})
```

---

## 🧪 Testing

```typescript
import { FeatureFlagTestingModule } from '@iziad/feature-flags/testing';

const module = await Test.createTestingModule({
  imports: [
    FeatureFlagTestingModule.forRoot({
      flags: ALL_FLAGS,
    }),
  ],
}).compile();
```

---

## 📚 API Reference

### FeatureFlagService

```typescript
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

```typescript
// 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

1. **Use Flag Objects** - Type-safe with autocomplete
2. **Define Flags Centrally** - Single source of truth
3. **Enable Redis in Production** - Better performance
4. **Use Sync Cooldown** - Prevent excessive syncs
5. **Protect Critical Flags** - Mark as `protected: true`
6. **Environment-Aware Configuration** - Different states per environment

---

## 🐛 Troubleshooting

### Prisma: "Can't resolve dependencies of FEATURE_FLAG_REPOSITORY"

**Solution:** You must do THREE things:

```typescript
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:
1. ✅ Import `PrismaModule` in your `AppModule` imports array
2. ✅ Pass `prismaService: PrismaService` (the class reference, not a string)
3. ✅ Pass `imports: [PrismaModule]` in the options (ensures proper dependency resolution)

### Tables Not Created

**Solution:** Enable `autoCreateTables`:

```typescript
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:

```typescript
@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:

```typescript
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):**

```typescript
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`:

```typescript
// 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:

```bash
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

- **[All Options Reference](./docs/ALL_OPTIONS_REFERENCE.md)** - Complete configuration reference
- **[Adding a New ORM](./docs/ADDING_NEW_ORM.md)** - Guide for ORM integration

---

## 🤝 Contributing

Contributions are welcome! Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for details.

---

## 📄 License

MIT © [Ziad Saber](https://github.com/iziad)

---

## 🔗 Links

- [NPM Package](https://www.npmjs.com/package/@iziad/feature-flags)
- [GitHub Repository](https://github.com/iziad/nestjs-feature-flags)
- [Issues](https://github.com/iziad/nestjs-feature-flags/issues)

---

**Built with ❤️ for the NestJS community**