Skip to content

Percentage Rollouts

Percentage rollout uses murmurhash3 for deterministic bucketing: the same user always gets the same result for a given flag, ensuring a consistent experience across requests.

Evaluation Priority

When isEnabled() is called, flags are evaluated through a 6-layer cascade. The first matching layer wins:

PriorityLayerDescription
1ArchivedIf the flag has archivedAt set, evaluation always returns false
2User overrideOverride matching the current userId (most specific)
3Tenant overrideOverride matching the current tenantId
4Environment overrideOverride matching the current environment
5Percentage rolloutDeterministic hash of flagKey + userId (or tenantId) mod 100
6Global defaultThe flag's enabled field

CRUD Operations

FeatureFlagService also exposes methods for managing flags programmatically:

typescript
// Create a flag
const flag = await this.flags.create({
  key: 'NEW_FEATURE',
  description: 'Enables the new feature',
  enabled: false,
  percentage: 0,
});

// Update a flag
await this.flags.update('NEW_FEATURE', {
  enabled: true,
  percentage: 50,
});

// Archive a flag (soft delete -- evaluations return false)
await this.flags.archive('OLD_FEATURE');

// List all active (non-archived) flags
const allFlags = await this.flags.findAll();

// Manually invalidate the cache
this.flags.invalidateCache();

Caching

Caching is handled by pluggable adapters (see Cache Adapters). The default MemoryCacheAdapter stores flags in an in-memory Map. For multi-instance deployments, use RedisCacheAdapter with Pub/Sub cross-instance invalidation.

Cache TTL is controlled by the cacheTtlMs option (default 30000 ms). Set to 0 to disable caching. You can manually invalidate the cache at any time:

typescript
await this.flags.invalidateCache();

TIP

In v0.2.0, invalidateCache() is async. If you are upgrading from v0.1.0, add await to all invalidateCache() calls.

Events

Enable event emission to observe flag lifecycle changes. Requires @nestjs/event-emitter as an optional peer dependency.

Important: You must import EventEmitterModule.forRoot() in your app module. The feature-flag module reuses the same EventEmitter2 singleton that NestJS manages, so @OnEvent() listeners work out of the box.

Setup

typescript
import { EventEmitterModule } from '@nestjs/event-emitter';

@Module({
  imports: [
    EventEmitterModule.forRoot(),   // must be imported
    FeatureFlagModule.forRoot({
      environment: 'production',
      prisma: prismaService,
      emitEvents: true,
    }),
  ],
})
export class AppModule {}

Event types

Event constantEvent stringPayload type
FeatureFlagEvents.EVALUATEDfeature-flag.evaluatedFlagEvaluatedEvent
FeatureFlagEvents.CREATEDfeature-flag.createdFlagMutationEvent
FeatureFlagEvents.UPDATEDfeature-flag.updatedFlagMutationEvent
FeatureFlagEvents.ARCHIVEDfeature-flag.archivedFlagMutationEvent
FeatureFlagEvents.OVERRIDE_SETfeature-flag.override.setFlagOverrideEvent
FeatureFlagEvents.OVERRIDE_REMOVEDfeature-flag.override.removedFlagOverrideEvent
FeatureFlagEvents.CACHE_INVALIDATEDfeature-flag.cache.invalidated{}

Listening to events

typescript
import { OnEvent } from '@nestjs/event-emitter';
import { FeatureFlagEvents, FlagEvaluatedEvent } from '@nestarc/feature-flag';

@Injectable()
export class FlagAuditListener {
  @OnEvent(FeatureFlagEvents.EVALUATED)
  handleEvaluation(event: FlagEvaluatedEvent) {
    console.log(`Flag ${event.flagKey} = ${event.result} (source: ${event.source})`);
  }
}

Released under the MIT License.