ADR-009: Feature Flag System

ADR-009: Custom Feature Flag System

Status: Accepted Date: 2026-02-21 Deciders: John (AI Director) Category: Backend


Context

Drop needs feature flags for several reasons:

  1. Gradual rollout: Cards feature requires a card issuing partner before activation -- must be gated
  2. Kill switches: Ability to disable features instantly in production if compliance or operational issues arise
  3. Development: Feature-in-progress code can be merged to main without being user-visible
  4. A/B testing: Future capability for comparing payment flows

Feature flag approaches considered:

Approach Cost Complexity Server+Client Targeting Audit
Custom (env vars) Free Low Yes (NEXT_PUBLIC_) No Via deploy history
LaunchDarkly $10/seat/mo Medium Yes Yes (per-user) Yes
Unleash (self-hosted) Free (OSS) High (infra) Yes Yes Yes
ConfigCat Free tier Low Yes Yes Yes

At Drop's current stage (pre-launch, no production users), a custom system based on environment variables provides exactly what is needed: server+client flag availability, zero operational overhead, and type-safe TypeScript integration. User-level targeting is not needed until there are users to target.

Decision

Implement a custom feature flag system using NEXT_PUBLIC_FF_* environment variables, with type-safe TypeScript wrappers for server and client access.

Architecture:

graph TB
    subgraph env["Environment Variables"]
        vars["NEXT_PUBLIC_FF_VIRTUAL_CARDS=false<br/>NEXT_PUBLIC_FF_PHYSICAL_CARDS=false<br/>NEXT_PUBLIC_FF_NOTIFICATIONS=true<br/>..."]
    end

    subgraph server["Server-Side (API Routes)"]
        isEnabled["isEnabled('virtualCards')"]
        featureGate["featureGate('physicalCards')"]
        getAllFlags["getAllFlags()"]
    end

    subgraph client["Client-Side (React)"]
        useFlag["useFeatureFlag('notifications')"]
        useFlags["useFeatureFlags()"]
    end

    vars -->|"Build-time inline<br/>(NEXT_PUBLIC_ prefix)"| server
    vars -->|"Build-time inline<br/>(NEXT_PUBLIC_ prefix)"| client

    featureGate -->|"Returns 404<br/>if disabled"| api_route["API Route<br/>(e.g., POST /api/cards/[id]/physical)"]

Flag registry (feature-flags.ts:27-36):

Flag Env Var Default Purpose
virtualCards NEXT_PUBLIC_FF_VIRTUAL_CARDS false Virtual card issuance
physicalCards NEXT_PUBLIC_FF_PHYSICAL_CARDS false Physical card ordering
cardDetails NEXT_PUBLIC_FF_CARD_DETAILS false Card detail view
cardFreeze NEXT_PUBLIC_FF_CARD_FREEZE false Card freeze/unfreeze
cardPin NEXT_PUBLIC_FF_CARD_PIN false Card PIN management
spendingLimits NEXT_PUBLIC_FF_SPENDING_LIMITS false Spending limit controls
notifications NEXT_PUBLIC_FF_NOTIFICATIONS true Push notifications
merchantDashboard NEXT_PUBLIC_FF_MERCHANT_DASHBOARD true Merchant dashboard

Server-side API route protection via featureGate():

const gate = featureGate("physicalCards");
if (gate) return gate;  // Returns 404: "Feature not available"

Client-side conditional rendering via useFeatureFlag():

const cardsEnabled = useFeatureFlag("virtualCards");
if (!cardsEnabled) return null;

Consequences

Positive

Negative

Risks

References


Revision #4
Created 2026-02-21 05:59:00 UTC by John
Updated 2026-05-23 10:56:49 UTC by John