Skip to content

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.

  1. 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).
  2. Navigate to Project → Operate → Feature Flags.
  3. Find the flag by name. The names mirror the FeatureFlags enum entries.
  4. Edit the strategies. Per environment (development, staging, production), set:
  5. Toggle on/off — global enable.
  6. Percentage rollout — gradual ramp.
  7. User constraint — match by userId, country, etc. (constraint shape per Unleash docs).
  8. Operator-specific — for white-labels, gate by a tenant identifier passed in the Unleash context.
  9. Save. Propagation is near-real-time ({{TBD}}-second poll on the backend; specifies in the SDK init — confirm in feature-flag.module.ts providers).

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:

FEATURE_FLAGS_USE_LOCAL=true

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

npm test -- apps/api/src/system/feature-flag/

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:

  1. Remove the FS.isEnabled(...) check from the use site, keeping only the enabled branch.
  2. Remove the enum entry.
  3. Remove the flag from GitLab (or archive it — keep the record).
  4. Append a docs/api/changelog.md entry 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

  1. Toggle the flag on in GitLab development → confirm FS.isEnabled returns true from a test.
  2. Toggle the flag off → confirm FS.isEnabled returns false. Wait one poll interval between flips.
  3. For public flags: GET /feature-flags reflects the toggle in the response.
  4. Frontend: the gated UI element appears / disappears across a hard refresh.
  5. 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 useLocal was accidentally true, the backend ignores GitLab. Always re-confirm FEATURE_FLAGS_USE_LOCAL=false in 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.