Using feature flags in Angular
How to implement feature flags in Angular the idiomatic way — a service, an APP_INITIALIZER, a custom structural directive, route guards, and per-user targeting.
Angular’s dependency injection and structural directives make feature flags fit the framework cleanly — far more so than dropping raw if statements through your components. This guide shows the idiomatic way to wire feature flags into an Angular app: a single service, initialization at bootstrap, a *featureFlag directive for templates, route guards for gating whole pages, and per-user targeting.
If you’re new to the concept itself, start with what are feature flags and come back. This guide assumes you know what a flag is and want to use one in Angular.
The core service
flaggy() returns a global singleton, so wrap it in an injectable service to give the rest of your app a typed handle through Angular’s DI. Create the client once and expose a check.
// feature-flag.service.ts
import { Injectable } from '@angular/core';
import { flaggy } from '@flaggy.io/sdk-js';
import { environment } from '../environments/environment';
@Injectable({ providedIn: 'root' })
export class FeatureFlagService {
private client = flaggy({
apiKey: environment.flaggyApiKey,
environment: environment.production ? 'production' : 'development',
});
async init(): Promise<void> {
await this.client.initialize();
}
isEnabled(key: string, context?: Record<string, unknown>): boolean {
return this.client.isEnabled(key, context);
}
}
Put the API key in your environment.ts files so it differs per build target. The SDK’s environment option accepts "production", "staging", or "development" and defaults to "production". A client-side key is safe to expose — it only authorizes reading flag rules, not changing them.
Initialize at bootstrap with APP_INITIALIZER
Flags need to be ready before your first component renders, or you’ll get a flash of the wrong UI. APP_INITIALIZER runs a factory during bootstrap and waits for the returned promise before the app starts.
// app.config.ts (standalone)
import { ApplicationConfig, APP_INITIALIZER } from '@angular/core';
import { FeatureFlagService } from './feature-flag.service';
export const appConfig: ApplicationConfig = {
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [FeatureFlagService],
useFactory: (flags: FeatureFlagService) => () => flags.init(),
},
],
};
For an older NgModule app, register the same provider in app.module.ts. The effect is identical: the SDK downloads its ruleset before any route activates, so every isEnabled call after this point is a synchronous in-memory lookup.
A structural directive for templates
The cleanest way to gate markup is a custom structural directive that works just like *ngIf. This keeps flag checks declarative and out of your component classes.
// feature-flag.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
import { FeatureFlagService } from './feature-flag.service';
@Directive({ selector: '[featureFlag]', standalone: true })
export class FeatureFlagDirective {
constructor(
private tpl: TemplateRef<unknown>,
private vcr: ViewContainerRef,
private flags: FeatureFlagService,
) {}
@Input() set featureFlag(key: string) {
const negate = key.startsWith('!');
const flag = negate ? key.slice(1) : key;
const show = this.flags.isEnabled(flag) !== negate;
this.vcr.clear();
if (show) this.vcr.createEmbeddedView(this.tpl);
}
}
Now gate any element, and use a leading ! for the fallback branch:
<app-new-dashboard *featureFlag="'release-new-dashboard'"></app-new-dashboard>
<app-old-dashboard *featureFlag="'!release-new-dashboard'"></app-old-dashboard>
Import the standalone directive into any component that needs it (or your shared module). Because it reads from the already-initialized service, there’s no async flicker.
Gating routes with a guard
For features that are entire pages, gate the route instead of the template. A functional CanActivateFn keeps unfinished pages unreachable until the flag is on.
// feature-flag.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { FeatureFlagService } from './feature-flag.service';
export function requireFlag(key: string): CanActivateFn {
return () => {
const flags = inject(FeatureFlagService);
const router = inject(Router);
return flags.isEnabled(key) ? true : router.parseUrl('/');
};
}
// app.routes.ts
import { Routes } from '@angular/router';
import { requireFlag } from './feature-flag.guard';
export const routes: Routes = [
{
path: 'new-reports',
loadComponent: () => import('./reports/reports.component'),
canActivate: [requireFlag('release-new-reports')],
},
];
Combined with loadComponent, the route’s code is also lazy-loaded, so a gated-off feature isn’t even downloaded.
How flag changes reach the client
The SDK refreshes its cached rules in the background — every 60 seconds by default, backing off on failure up to 15 minutes. When you flip a flag in the dashboard, clients pick it up on their next refresh, so a change propagates within about a minute rather than instantly. isEnabled always reads the latest cached value.
There’s no push event to subscribe to. For a long-lived view that must reflect a mid-session change (a kill switch, an experiment ending) without waiting for navigation, expose the flag as a signal and refresh it on an interval:
// in FeatureFlagService — add the signal and extend the init() from earlier
import { signal } from '@angular/core';
readonly maintenanceBanner = signal(false);
async init(): Promise<void> {
await this.client.initialize();
const read = () => this.maintenanceBanner.set(this.isEnabled('ops-maintenance-banner'));
read();
setInterval(read, 60_000);
}
@if (flags.maintenanceBanner()) {
<app-maintenance-banner />
}
For most release flags a synchronous check is fine — you read it once at render. Reserve polling for the rare flag that must visibly change mid-session.
Per-user targeting
Percentage rollouts, beta cohorts, and plan-tier gating all rely on passing a user context to the check. The targeting rule lives in the dashboard, not in your Angular code — that’s what lets a product manager change who sees a feature without a deploy.
const showEditor = this.flags.isEnabled('experiment-new-editor', {
key: this.user.id,
plan: this.user.plan,
country: this.user.countryCode,
});
You define the rule once — “users where plan equals team AND country equals US” — and adjust it from the dashboard whenever you want.
Keeping Angular flags tidy
The Angular wiring above is the easy part. The harder discipline is removing flags once a feature is fully rolled out, so your templates don’t fill up with *featureFlag directives that are permanently true. A naming convention and a removal habit are what prevent that — covered in feature flag management.
The same model — service, idiomatic primitive, dashboard-side targeting — applies to React, Vue, and Svelte. See our guides on using feature flags in React and feature flags in JavaScript.
Flaggy gives you local-evaluation SDKs, segmentation, and a full audit log on a flat $99/month plan — no per-user metering.