Skip to content

Sometimes a flag decides not what a handler does but whether it runs at all: an endpoint that should only exist while a feature is on. Gate it declaratively with BridgeFlagGuard + @RequireFlag, so a request is rejected before your handler is ever entered.

Apply BridgeFlagGuard (at the controller or route level) and mark the handlers that require a flag:

import { Controller, Get, UseGuards } from '@nestjs/common';
import { RequireFlag, BridgeFlagGuard } from '@nebulr-group/bridge-nestjs/flags';

@Controller('exports')
@UseGuards(BridgeFlagGuard)
export class ExportsController {
  @Get()
  @RequireFlag('exports_enabled')
  list() {
    // only reachable when exports_enabled is on
  }
}

When the flag is off, the guard refuses and your handler never runs. The answer says why, so the frontend can react to it:

Why the flag is offStatuscodefix
The workspace’s plan doesn’t include it: an upgrade alone would turn it on402FEATURE_NOT_IN_PLANwhere to upgrade (manageRoute, default /subscription)
The person’s role or privileges keep it off403FEATURE_NOT_PERMITTEDask a workspace admin
Switched off, another condition, or outside the rollout403FEATURE_OFFnone
{ "statusCode": 402, "code": "FEATURE_NOT_IN_PLAN", "flag": "exports_enabled",
  "feature": "exports", "reason": "plan", "fix": "/subscription",
  "message": "Your plan does not include 'exports'. Upgrade to use it." }

feature is there when the flag’s rule targets a plan feature (bridge:billing.entitlement.<feature>). The 403s are still ForbiddenExceptions and keep the old message (Feature flag 'exports_enabled' is not enabled). The Svelte plugin opens its upgrade dialog on a 402 FEATURE_NOT_IN_PLAN, as it does for a plan limit. A handler with no @RequireFlag passes the guard untouched.

@RequireFlag(key, defaultValue = false, options?). For a non-boolean flag, use options.equals to require a particular value:

@Get('beta')
@RequireFlag('export_mode', 'v1', { equals: 'v2' })
betaExport() {
  // only reachable when export_mode evaluates to 'v2'
}

The second argument is the default the guard evaluates with when the flag is unconfigured or Bridge is unreachable.

options.optional: true makes the guard skip the handler instead of refusing with the flag-specific 402 / 403. This is the pattern for a kill switch that should quietly disable a route rather than surface a flag error:

@Get('legacy-report')
@RequireFlag('legacy_report_enabled', false, { optional: true })
legacyReport() {
  // guarded off silently when the flag is off
}

BridgeFlagGuard only reads the flag; it never authenticates the caller. If the flag has a percentage rollout or targets a user attribute, the request needs an identity to bucket against. Compose the flag guard with BridgeAuthGuard (or your own auth) so req.bridgeUser (or req.user) is populated first: the guard evaluates for that verified caller, and a request without one is evaluated anonymously. Nothing the client sends, including the x-bridge-context header, changes the guard’s decision. See Per-request context and Gate features with flags.

When you want the flag value in the handler rather than a hard gate, the @Flag param decorator injects it directly:

import { Controller, Get } from '@nestjs/common';
import { Flag } from '@nebulr-group/bridge-nestjs/flags';

@Controller('home')
export class HomeController {
  @Get()
  home(@Flag({ key: 'show_new_home', defaultValue: false }) showNew: boolean) {
    return showNew ? this.newHome() : this.classicHome();
  }
}

@Flag reads the same per-request context (req.bridgeFlagsContext) that the guard uses, so it buckets against the same identity. For a typed, reusable param, wrap it with useFlagParam<T>(key, defaultValue).