Toggle a feature flag¶
Goal: add a new feature flag, or toggle an existing one for a specific environment / customer / cohort. Audience: customer engineering team — and SRE / on-call who need to flip a flag at 02:00. Time: minutes (toggle existing) — 1 day (introduce a new flag end-to-end). Friction: low — the integration is in place and battle-tested.
What you'll change¶
| Layer | Path | Action |
|---|---|---|
| Flag definition | GitLab Unleash project (UI) | Create / edit / toggle the flag. |
| Backend reader | apps/api/src/system/feature-flag/ (feature-flag.module.ts, feature-flag.controller.ts) |
Read flag value at runtime. |
| Flag namespace constants | libs/integrations/feature-flag/features (per apps/api/src/system/feature-flag/feature-flag.module.ts:2) |
Add the new flag name to the enum. |
| Frontend reader | ebit-fe/src/ (search featureFlag, useFeatureFlag) |
Conditional rendering. |
| Doppler | FEATURE_FLAGS_API_URL, FEATURE_FLAGS_API_KEY, FEATURE_FLAGS_USE_LOCAL |
Already configured. Verify per env. |
Canonical example¶
The integration uses GitLab Unleash as the flag service. Wiring lives at apps/api/src/system/feature-flag/feature-flag.module.ts:
// apps/api/src/system/feature-flag/feature-flag.module.ts:2-18 (verified)
import { FeatureFlags } from '@app/integrations/feature-flag/features';
…
{
unleashUrl: config.get('FEATURE_FLAGS_API_URL'), // GitLab Unleash endpoint
unleashApiKey: config.get('FEATURE_FLAGS_API_KEY'),
useLocal: config.get('FEATURE_FLAGS_USE_LOCAL'), // dev-time bypass
}
Doppler example (per .example.env):
FEATURE_FLAGS_USE_LOCAL=false
FEATURE_FLAGS_API_URL="https://gitlab.com/api/v4/feature_flags/unleash/123"
FEATURE_FLAGS_API_KEY="glffct-token"
Flag names are typed via FeatureFlags (TypeScript enum from @app/integrations/feature-flag/features). New flags must be added to that enum to be readable by the typed accessor.
Steps¶
A — Toggle an existing flag for a customer / environment¶
The lowest-friction case. No code changes.
- Sign in to GitLab. The customer's GitLab project is the host of the Unleash instance — find it via
FEATURE_FLAGS_API_URL(the URL contains the project + flag ID). - Navigate to Project → Operate → Feature Flags.
- Find the flag by name. The names mirror the
FeatureFlagsenum entries. - Edit the strategies. Per environment (
development,staging,production), set: - Toggle on/off — global enable.
- Percentage rollout — gradual ramp.
- User constraint — match by
userId,country, etc. (constraint shape per Unleash docs). - Operator-specific — for white-labels, gate by a tenant identifier passed in the Unleash context.
- Save. Propagation is near-real-time (
{{TBD}}-second poll on the backend; specifies in the SDK init — confirm infeature-flag.module.tsproviders).
Document the change in the customer's release / change log. No PR.
B — Add a new feature flag end-to-end¶
Use this when you're introducing a feature that needs a kill-switch or a gradual rollout.
B.1 Define the flag in GitLab¶
Go to GitLab → Project → Operate → Feature Flags → New feature flag.
- Name — kebab-case, prefixed by feature area:
casino-hi-lo-enabled,payments-yourprovider-enabled,experimental-new-bet-validator. - Description — what does the flag gate? Who's the owner? When can it be removed?
- Default strategy — start with off everywhere for safety.
B.2 Add the flag name to the enum¶
Edit libs/integrations/feature-flag/features (the file imported by apps/api/src/system/feature-flag/feature-flag.module.ts:2):
// libs/integrations/feature-flag/features/index.ts (or wherever FeatureFlags lives)
export enum FeatureFlags {
…existing flags…
CASINO_HI_LO_ENABLED = 'casino-hi-lo-enabled',
}
The string value MUST match the GitLab name exactly, character for character — Unleash matches by string.
If the flag is public (read on the frontend), also add it to PublicFeatureFlags (per feature-flag.controller.ts:4). The controller exposes only PublicFeatureFlags to the FE; everything else stays server-side.
B.3 Read the flag in backend code¶
Inject the feature-flag service into your service / controller and check the flag at the use site:
import { FS } from '@api/system/feature-flag/feature-flag.module'; // singleton accessor
import { FeatureFlags } from '@app/integrations/feature-flag/features';
if (FS.isEnabled(FeatureFlags.CASINO_HI_LO_ENABLED, { userId: user.id })) {
// gated path
}
Pass the user / context fields the GitLab strategy will evaluate against — userId, country, etc.
If the flag is OFF, fall back to the safe default (e.g. throw FeatureNotAvailable, hide the route from the catalog).
B.4 Read the flag on the frontend (only for PublicFeatureFlags)¶
The FE fetches public flags from GET /feature-flags (or the equivalent route — verify in docs/api-reference/api.md "Feature Flag" tag, if exposed).
const { isEnabled } = useFeatureFlag('casino-hi-lo-enabled');
return isEnabled ? <HiLoBoard /> : null;
Do not evaluate non-public flags on the FE — they leak gating logic to the client.
B.5 Local-dev override¶
For a dev box that doesn't have GitLab access, set:
This switches the SDK to a local stub. The stub typically returns true for everything (or matches a JSON file — verify implementation in feature-flag.module.ts providers). Useful for unblocking local dev when the network is down.
B.6 Tests¶
Cover both branches of every gated flow — when the flag is on and when it's off.
B.7 Document the flag¶
Append a row to a docs/engineering/feature-flags.md registry ({{TBD}} — registry doc not yet written; track flags in a comment block at libs/integrations/feature-flag/features/index.ts until the registry exists). Each entry: name, owner, purpose, sunset date.
C — Sunset / remove a flag¶
Once a flag has been on at 100% in production for two minor releases:
- Remove the
FS.isEnabled(...)check from the use site, keeping only the enabled branch. - Remove the enum entry.
- Remove the flag from GitLab (or archive it — keep the record).
- Append a
docs/api/changelog.mdentry under "Removed gating".
A long-lived flag is a smell. Sunset-or-promote is the right outcome.
Cache TTL / propagation lag¶
The Unleash SDK polls GitLab for flag definitions. The poll interval is configured in the SDK setup at apps/api/src/system/feature-flag/feature-flag.module.ts (search the unleash config — likely refreshInterval in seconds). Default is {{TBD: confirm by reading the wired SDK config}} — typically 15 seconds in Unleash's defaults.
Implication: a flag flip in GitLab takes up to one poll-interval to propagate to running pods. For a true "instant" flip during incident, restart the pods — they fetch flag state at boot.
Verification¶
- Toggle the flag on in GitLab development → confirm
FS.isEnabledreturnstruefrom a test. - Toggle the flag off → confirm
FS.isEnabledreturnsfalse. Wait one poll interval between flips. - For public flags:
GET /feature-flagsreflects the toggle in the response. - Frontend: the gated UI element appears / disappears across a hard refresh.
- Loki: with the flag flipped, you should see the gated code path executing in logs (and not when off).
Notes¶
- Cache poisoning risk: if the SDK caches a flag value at a stale state and
useLocalwas accidentallytrue, the backend ignores GitLab. Always re-confirmFEATURE_FLAGS_USE_LOCAL=falsein production. - Performance: flag evaluation is in-process after first poll — no per-call HTTP round-trip. Adding flags is essentially free at runtime.
- Auditability: GitLab logs every flag change (who, when, what strategy). Use it for post-incident.
Cross-links¶
integration-cookbook.md— index.docs/api-reference/api.md→ public feature-flag endpoint (verify tag).docs/env-reference.md—FEATURE_FLAGS_*keys.docs/runbooks/— incident-time flag flip procedures.- GitLab Unleash docs: https://docs.gitlab.com/ee/operations/feature_flags.html.