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 four-layer cascade. The first matching layer wins:
| Priority | Layer | Description |
|---|---|---|
| 1 | Archived | If the flag has archivedAt set, evaluation always returns false |
| 2 | Attribute override | Best override whose attributes all match the evaluation context |
| 3 | Percentage rollout | Deterministic hash of flagKey + targetingKey modulo 100 |
| 4 | Global default | The flag's enabled field |
Top-level userId, tenantId, and environment values are merged into targeting attributes. If several overrides match, the evaluator prefers more attributes, then higher priority, earlier createdAt, and lower id.
CRUD Operations
FeatureFlagService also exposes methods for managing flags programmatically:
// 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:
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
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 constant | Event string | Payload type |
|---|---|---|
FeatureFlagEvents.EVALUATED | feature-flag.evaluated | FlagEvaluatedEvent |
FeatureFlagEvents.CREATED | feature-flag.created | FlagMutationEvent |
FeatureFlagEvents.UPDATED | feature-flag.updated | FlagMutationEvent |
FeatureFlagEvents.ARCHIVED | feature-flag.archived | FlagMutationEvent |
FeatureFlagEvents.OVERRIDE_SET | feature-flag.override.set | FlagOverrideEvent |
FeatureFlagEvents.OVERRIDE_REMOVED | feature-flag.override.removed | FlagOverrideEvent |
FeatureFlagEvents.CACHE_INVALIDATED | feature-flag.cache.invalidated | {} |
Listening to events
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})`);
}
}