Skip to content

Feature Flags

CruzJS includes a feature flag system for toggling functionality at runtime without redeploying. Flags are org-scoped, so each organization can have independent flag states.

Register the FeatureFlagModule in your application:

import { FeatureFlagModule } from '@cruzjs/core/feature-flags';
export default createCruzApp({
modules: [FeatureFlagModule],
});

Feature flags are created via the tRPC featureFlag.create procedure or directly through the FeatureFlagService:

// Via tRPC (from the client)
trpc.featureFlag.create.useMutation().mutate({
key: 'new-dashboard',
name: 'New Dashboard',
description: 'Enables the redesigned dashboard experience',
enabled: false,
});
PropertyTypeDescription
keystringUnique slug identifier (e.g. new-dashboard)
namestringHuman-readable name
descriptionstringOptional description of what the flag controls
enabledbooleanWhether the flag is active
conditionsJSONOptional JSON conditions for advanced targeting

Inject FeatureFlagService and call isEnabled():

import { Injectable, Inject } from '@cruzjs/core/di';
import { FeatureFlagService } from '@cruzjs/core/feature-flags';
@Injectable()
export class DashboardService {
constructor(
@Inject(FeatureFlagService) private readonly flags: FeatureFlagService,
) {}
async getDashboardData(orgId: string) {
const useNewDashboard = await this.flags.isEnabled('new-dashboard', orgId);
if (useNewDashboard) {
return this.getNewDashboardData(orgId);
}
return this.getLegacyDashboardData(orgId);
}
}
  • isEnabled(key, orgId) returns a simple boolean
  • evaluate(key, orgId) returns the full flag object including conditions, useful for complex targeting logic
const flag = await this.flags.evaluate('beta-features', orgId);
// flag: { key, name, enabled, conditions, ... }

The same pattern works inside tRPC procedures:

import { FeatureFlagService } from '@cruzjs/core/feature-flags';
const dashboardRouter = router({
getData: orgProcedure.query(async ({ ctx }) => {
const flags = ctx.container.get(FeatureFlagService);
const isNew = await flags.isEnabled('new-dashboard', ctx.org.id);
// ...
}),
});

All procedures are org-scoped and require appropriate permissions.

ProcedureTypeDescription
featureFlag.listqueryList all flags for the current org
featureFlag.createmutationCreate a new feature flag
featureFlag.evaluatequeryEvaluate a flag by key
featureFlag.updatemutationUpdate flag properties (name, enabled, conditions)
featureFlag.deletemutationDelete a feature flag
function Dashboard() {
const { data: flag } = trpc.featureFlag.evaluate.useQuery({
key: 'new-dashboard',
});
if (flag?.enabled) {
return <NewDashboard />;
}
return <LegacyDashboard />;
}

Use the conditions field for percentage-based rollouts:

trpc.featureFlag.create.useMutation().mutate({
key: 'new-editor',
name: 'New Editor',
enabled: true,
conditions: {
rolloutPercentage: 25, // Enable for 25% of users
},
});

Then evaluate with conditions in your service:

const flag = await this.flags.evaluate('new-editor', orgId);
if (flag?.enabled && flag.conditions?.rolloutPercentage) {
const hash = simpleHash(userId);
const inRollout = (hash % 100) < flag.conditions.rolloutPercentage;
return inRollout;
}