Skip to main content

TL;DR

This guide replaces scattered environment-variable, hard-coded, JSON, and database flags with GrowthBook without a big-bang cutover. The agent inventories the legacy system, writes its semantic contract, puts a typed provider boundary in front of every read, compares GrowthBook decisions in shadow mode, transfers authority one flag at a time, and deletes the old paths only after the observation window closes. The key safety property is reversibility. Legacy mode remains available during migration, per-key cutover is allowlisted, mismatches are observable without leaking identities, and original cohort behavior is preserved or consciously changed. The finished state is one authoritative provider, tested fallbacks, documented ownership, and no hidden legacy path serving traffic. This guide is optimized for AI coding agents, and it is recommended that you hand it off to your agent of choice for implementation.

Guide map

Give this guide to your coding agent

Copy this page’s URL and the prompt below into a coding agent that can inspect your repository. Replace REPLACE_WITH_REPOSITORY with the repository path or name and REPLACE_WITH_GUIDE_URL with this page’s public URL. Start in a branch, worktree, or disposable clone, and keep the legacy provider authoritative during the first pass.

Task

Replace an application-specific feature flag system with GrowthBook without changing production behavior in one large cutover. The legacy system may combine environment variables, hard-coded booleans, JSON files, a database table, account allowlists, or homegrown percentage rollouts. This guide performs the migration as observable, reversible changes. It inventories and classifies each switch, puts one typed interface in front of reads, compares GrowthBook in shadow mode, transfers control one flag at a time, and removes the old system only after it stops serving traffic. The reference Node.js/TypeScript service uses one GrowthBookClient for feature definitions and request-scoped evaluators for isolated user attributes. Browser applications should use the JavaScript SDK or their framework SDK.

Use this guide when

  • Your application already has feature-like conditions spread across environment variables, source files, configuration files, or database rows.
  • Changing a runtime flag currently requires a deployment, a database edit, or a custom admin endpoint.
  • You need targeting, gradual rollout, environment separation, a kill switch, an experiment, or a visible change history.
  • You cannot accept a big-bang provider replacement.
  • You can observe both legacy and GrowthBook decisions before GrowthBook serves them.
  • You have a stable user, device, or account identifier for any percentage-based rule.

Do not use this guide when

  • The value is a build-time constant that should change only when you build a new artifact.
  • The value is a secret, credential, connection string, or cryptographic key.
  • The condition grants authorization, enforces a billing entitlement, or protects data. A feature flag may hide a control or stage a feature, but the server must still enforce authorization and entitlement independently.
  • The value is permanent application configuration with no release lifecycle. Keep it in typed configuration unless runtime targeting is a real requirement.
  • A single static Boolean, owned by one developer, with no runtime changes or targeting, is genuinely the complete requirement. A small DIY switch may be the simpler system.
  • The old implementation cannot be read safely in parallel. First create a read-only snapshot or a test environment where comparison cannot mutate state.

Tested stack

The complete TypeScript examples were checked against GrowthBook commit e44a15af063860c7118f52508746356d55e5a91d, including @growthbook/growthbook 1.7.0 evaluation behavior and the SDK’s own feature, typed-feature, and multi-user tests. The reference commands assume Node.js 22, TypeScript 5.7, npm, and Jest 29.7. Pin the SDK and CLI versions in your lockfile. Re-run the contract tests in this guide before adopting a newer major version or changing GrowthBook rule semantics.

Required access

You need:
  • Read access to the application repository, deployment configuration, and legacy flag table or configuration store.
  • Permission to create a GrowthBook project, feature flags, attributes, and SDK connections for the environments in scope.
  • A GrowthBook SDK client key for each deployed environment. SDK connection endpoints are read-only and intended for SDK delivery; do not confuse the client key with a REST API secret.
  • A Secret Key or Personal Access Token only if you enable CLI type generation in CI. Store it as GBCLI_BEARER_AUTH; never commit it or pass it to application code.
  • Access to production logs or metrics so you can measure shadow mismatches and fallback use.
  • An owner who can decide whether assignment changes are acceptable for existing percentage rollouts.

Files this guide creates

Adapt the paths to your repository, but keep the provider boundary intact:
Do not create every file before you understand the old behavior. The inventory and semantic contract come first.

End state

The migration is complete only when all of these statements are true:
  1. Every legacy switch has a recorded owner, type, source, purpose, safe fallback, environment scope, and removal decision.
  2. Authorization, billing entitlements, secrets, and permanent configuration remain outside the feature flag system.
  3. All application reads go through one typed provider interface.
  4. GrowthBook uses the same attribute names, types, rule order, environment mapping, and intended randomization unit as the accepted migration contract.
  5. Shadow telemetry shows no unexplained decision mismatch for the contexts that matter.
  6. A known assignment change in a percentage rollout is either prevented or explicitly accepted before cutover.
  7. Production can return to the legacy provider through a deployment-level switch during the cutover window.
  8. A GrowthBook fetch failure produces the documented code fallback or temporary legacy fallback. It does not produce an accidental truthy value.
  9. Unit, targeting, failure, and parity tests pass in CI.
  10. The legacy mutation path is disabled before its data and code are deleted.
  11. Code references, stale review, ownership, and cleanup dates are part of the ongoing flag lifecycle.

Rollback

Keep FLAG_PROVIDER_MODE=legacy available until every migrated flag has completed its observation window. If a GrowthBook-served decision causes an incident:
  1. Set FLAG_PROVIDER_MODE=legacy in the deployment configuration.
  2. Restart or redeploy the affected service so the startup-only mode changes.
  3. Confirm the startup receipt reports "mode":"legacy".
  4. Confirm decision telemetry reports "servedBy":"legacy" for the affected key.
  5. Revert the GrowthBook feature revision if the issue is a rule or value error.
  6. Preserve mismatch and incident evidence before editing either implementation.
After the legacy code has been deleted, rollback means redeploying the last release that still contains it. Record that release or image digest before deletion. A GrowthBook flag cannot restore code that is no longer in the application.

Understand what you are replacing

A DIY feature flag often starts as one line:
That line may be the correct solution. The migration becomes worthwhile when the surrounding requirements have already appeared: runtime changes, environment separation, stable percentage assignment, user or account targeting, review, diagnostics, experiments, and cleanup. The decision is not “one Boolean versus a platform.” It is “the system you currently operate, including its undocumented behavior, versus a maintained control plane.” Do not erase working behavior just to make the architecture look cleaner. GrowthBook feature evaluation normally happens locally in the SDK after the feature payload has loaded. A flag check does not need a control-plane request every time. The migration still adds operational dependencies: payload delivery, SDK initialization, identity quality, and a publishing workflow. This guide makes those dependencies visible instead of treating them as magic.

Phase 0: Freeze unsafe changes

Do not migrate while the old system is changing underneath you. Establish a short change freeze for the first flag or require every legacy change to be mirrored in GrowthBook during shadow mode. Before editing code:
Record the commit and deployment revision that the inventory describes. If the worktree is dirty, determine which changes belong to the migration before proceeding. Do not overwrite unrelated edits. Create a migration issue or runbook with these global decisions:
  • Cutover owner: The person who can change FLAG_PROVIDER_MODE and redeploy.
  • Observation window: At least enough traffic to exercise the important targeting branches. Use time and traffic, not time alone.
  • Mismatch budget: Zero unexplained mismatches for kill switches and high-risk release flags. A documented cohort change may be acceptable for low-risk rollouts.
  • Fallback policy: The safe code value for each key and whether a temporary legacy fallback is allowed.
  • Identity policy: The canonical user, anonymous device, and account identifiers.
  • Mutation freeze: Who can change legacy values and how those changes are mirrored.
  • Deletion gate: The exact evidence required before legacy code, rows, or admin endpoints can be removed.

Inventory every possible flag source

Start broad. Search results are candidates, not confirmed flags. Terms such as enabled, beta, or rollout also appear in tests, library code, and ordinary domain logic. If rg is installed, run these searches from the repository root:
Search deployment and infrastructure files too. A flag that does not appear in source may still enter through Kubernetes, Terraform, a CI variable, or a platform dashboard:

Add a repeatable inventory script

The following dependency-free script emits JSON Lines so you can diff findings across commits. It does not claim that every match is a flag. It gives the agent and reviewer a bounded queue for classification. Create scripts/inventory-feature-flags.mjs:
Run it and keep the result as a CI artifact or migration attachment, not as permanent application configuration:
An expected receipt looks like this:
The number is only a search result count. Manually review every line, follow helper functions to their callers, inspect defaults in deployment configuration, and query any legacy table read-only.

Inventory the database without changing it

Adapt this query to your schema. Select metadata and values only if your security policy allows them:
Also inspect override and assignment tables:
Counts expose complexity without copying user or account identifiers into the migration document. If the old system persists assignments, inventory that table separately. Persistent assignments require a different continuity plan from deterministic hashing.

Classify before you migrate

Put each confirmed candidate into exactly one primary class. Record secondary concerns in notes rather than inventing overlapping categories.

Never migrate these as ordinary flags

Authorization: A client-visible value such as is-admin must not grant server access. Check the authenticated principal and permission on every protected operation. Billing entitlements: A flag can control a rollout within the set of entitled accounts. It must not replace the billing or entitlement service. A user changing local attributes must not unlock a paid capability. Secrets: SDK feature payloads are configuration delivery. Do not put API tokens, database passwords, encryption keys, private prompts, or credentials into a flag value. Unreviewed executable content: Prefer a flag that selects a versioned implementation, such as recommendation-v2, over remotely editable JavaScript, SQL, or shell commands. Safety invariants: Validation, rate limits, data retention, and destructive-operation safeguards belong in code and policy. Do not make them removable through an ordinary release flag.

Create the migration manifest

Use one row per flag. A spreadsheet, issue table, or version-controlled YAML file works during the migration. Do not include raw user or account IDs.
The manifest is a work queue, not a source of runtime truth. Once GrowthBook is authoritative, remove or archive the migration-only fields so nobody operates 2 control planes indefinitely.

Write the semantic contract

Two providers can both return true to a happy-path test while disagreeing for production users. Before creating GrowthBook rules, document how the legacy system makes a decision. For every key, answer these questions:
  1. What is the exact value type? Distinguish the Boolean false, string "false", number 0, empty string, null, and a missing key.
  2. What is the code fallback? This is the value compiled into the call site and used when no provider has a usable value.
  3. What is the legacy default? It may differ from the code fallback because a database or environment parser supplies another default.
  4. Which source wins? Write the precedence of hard-coded override, account override, environment variable, JSON file, database row, and default.
  5. Which condition wins? GrowthBook evaluates rules top to bottom and the first matching rule wins. Confirm whether the old system does the same.
  6. What is the environment? Use a deployment environment such as dev, staging, test, or production, not an ambiguous build mode.
  7. What attributes are used? Record exact names, types, casing, and missing-value behavior.
  8. What is the randomization unit? User, anonymous device, account, organization, session, or request are not interchangeable.
  9. How is a rollout bucket calculated? Record the seed, hash function, hash version, normalization, and range boundaries.
  10. How fresh is the value? Record cache duration, polling interval, or deployment requirement.
  11. What happens when the source fails? A timeout, malformed JSON, missing row, or database outage must have an observed behavior.
  12. Does evaluation have side effects? Some custom systems persist assignments or emit exposure events during reads.

Normalize truthiness before comparing

Do not preserve accidental JavaScript truthiness. Decode each legacy source once at startup and reject ambiguous values:
This prevents Boolean("false") from becoming true. Make parser changes in a separate commit if they alter current behavior. A migration is easier to review when cleanup and provider replacement are not hidden in the same diff.

Prove cohort continuity instead of assuming it

A 10% legacy rollout and a 10% GrowthBook rollout usually include the same amount of traffic, not the same people. Different seeds, algorithms, attribute names, string normalization, or range boundaries change membership. Use one of these strategies:
  • Preserve a small explicit cohort: Import the existing account or user IDs into a GrowthBook Saved Group and target that group during migration. Keep ID lists concise because they increase the SDK payload.
  • Pass a temporary legacy-cohort attribute: Continue calculating legacy_invoice_preview=true and target it in GrowthBook. Remove the attribute after the rollout completes. This preserves behavior but intentionally retains part of the legacy evaluator for a limited time.
  • Preserve persisted assignments: Export only the assignment data required by an approved migration design. Do not assume deterministic hashing can reproduce a stored assignment table.
  • Accept re-bucketing: Do this only for a low-risk release, outside an active experiment, with written approval. Tell support and analytics when assignment changes.
  • Wait for 0% or 100%: If the rollout can pause or finish safely, migrate after there is no partial cohort to preserve.
Setting the same hashAttribute is necessary, but it is not proof of cohort parity. In GrowthBook, a percentage rule deterministically samples on the configured attribute. Missing the attribute causes the percentage rule to be skipped and evaluation falls through to the next rule or default. Test empty and missing identifiers explicitly.

Put one typed boundary in front of every read

Do this before GrowthBook controls a single production decision. The boundary lets you switch providers without editing business logic again. Install the SDK and test dependencies. Use the versions already accepted by your repository if they differ:

Define feature types and code fallbacks

Create src/feature-flags/app-features.ts:
Every key has a compile-time fallback. For release flags, default to the known production path. For an operational flag, ask what state is safest during a control-plane and cache failure. “Off” is not universally safe: a flag named disable-payments has inverted semantics. Prefer positive names such as payments-enabled and document the fallback. Remote payloads also need runtime validation. These validators preserve the legacy page-size choices and recommendation schema below; adapt them to your accepted semantic contract. Never coerce the string "false" into a Boolean or use a TypeScript cast as validation. Keep the validators when generating SDK types with the CLI. During initial migration, maintain this interface manually. After all intended keys exist in GrowthBook, replace it with CLI-generated AppFeatures types and retain CODE_FALLBACKS as a checked application policy.

Define a request context

Create src/feature-flags/context.ts:
Do not generate a random identifier inside this function. A request ID is useful for tracing but is usually wrong for rollout assignment. Persist an anonymous device ID before login if anonymous users require stable treatment. Decide whether a user should keep the anonymous assignment after login; do not switch silently halfway through a workflow. The actual attribute values remain local to normal SDK evaluation. GrowthBook receives feature definitions; the SDK evaluates them against the attributes in memory. If you use a sensitive attribute for targeting, follow the secure attribute guidance and the relevant SDK instructions. Hashing an identifier for telemetry is a separate concern and does not automatically make it a GrowthBook secure attribute.

Define provider and decision receipts

Create src/feature-flags/provider.ts:
FeatureProvider creates a request scope. That matters on servers. A singleton GrowthBookClient reuses the loaded feature payload, while createScopedInstance keeps one request’s attributes and evaluation de-duplication separate from another request. The 4 modes have deliberate meanings:
growthbook-preferred is transitional. Do not leave it forever. A missing GrowthBook key would silently keep legacy behavior and prevent the old system from being removed.

Snapshot the legacy system

Do not perform a database query for every feature read just because the adapter is new. Load the old configuration using its established freshness contract, validate it, and expose a synchronous snapshot. Create src/feature-flags/legacy-loader.ts:
Wire loadRows to a parameterized, read-only query in your own database layer. Do not put a database client or SQL dialect into the flag interface. If parsing fails, prefer failing application startup over silently changing a known production value. If the current application already tolerates malformed config, record that behavior and decide explicitly whether to preserve or fix it in a separate change.

Implement the legacy provider exactly once

Create src/feature-flags/legacy-provider.ts:
The hashing function represents the application’s existing algorithm, not GrowthBook’s algorithm. Keep the old implementation byte-for-byte if possible. The point of shadow mode is to expose differences, not make 2 unrelated algorithms look equivalent. At this stage, change call sites to use LegacyFlagProvider only. Deploy and verify that decisions are unchanged before adding GrowthBook. This isolates adapter mistakes from provider differences.

Create environment-scoped GrowthBook configuration

Use GrowthBook environments for where the code runs. Use projects for ownership and organization, not as a substitute for deployment environments. For the reference service:
  1. Create or use the built-in dev, test, staging, and production environments.
  2. Create one SDK connection for each environment that the service uses.
  3. Put that environment’s client key in GROWTHBOOK_CLIENT_KEY at deployment time.
  4. Set GROWTHBOOK_API_HOST=https://cdn.growthbook.io for GrowthBook Cloud, or the documented endpoint for your self-hosted deployment.
  5. Keep DEPLOY_ENV separate from NODE_ENV. A staging server commonly runs a production Node build.
  6. Do not place a production SDK client key in a local .env example.
The GrowthBook SDK connection endpoint is scoped to one environment and may also be scoped to one project. A production service should receive only production feature definitions. Reusing a development key in production defeats that separation. Create the 4 reference features with the exact keys and types from AppFeatures. For the first shadow deployment:
  • Match the accepted legacy default in every environment.
  • Recreate deterministic targeting rules in the same top-to-bottom precedence.
  • Do not create an experiment rule yet. Shadow evaluation should compare release behavior without emitting experiment exposures or contaminating analysis.
  • Leave any partial rollout at 0% until the cohort-continuity strategy is resolved.
  • Add a description that links to the migration issue, owner, safe fallback, and expected cleanup condition.
Feature keys cannot be renamed after creation. Review spelling and casing before publishing. A disabled feature is excluded from the SDK payload and evaluates as unknown, so shadow tests must distinguish “disabled intentionally” from “forgot to deliver the feature.” See feature fundamentals, rule order, and targeting attributes before translating complex rules.

Map legacy semantics explicitly

Use a table like this during review: Case matters. If the legacy system stores country="us" and GrowthBook targets "US", either normalize the application attribute or configure the intended comparison. Do not “fix” casing only for some services.

Implement the GrowthBook provider

Create src/feature-flags/feature-refresh.ts. SDK 1.7.0 does not have the newer init({ pollingInterval }) option, so this example uses its supported refreshFeatures method. Start one timer per long-lived client, not per request.
src/feature-flags/feature-refresh.ts
The timer bypasses the cache, skips overlapping refresh calls, and does not keep Node running by itself. Call its stop function before destroying the client. In this SDK version, refreshFeatures() resolves without a success receipt on ordinary network failures and retains the last payload; promise resolution is not proof of freshness. Monitor delivery and use a staging publish-to-evaluation check to prove propagation. Expect up to the polling interval plus delivery time while the endpoint is healthy, not an instantaneous kill switch. Define an operational staleness limit and escalation policy before production cutover. Create src/feature-flags/growthbook-provider.ts:
The SDK’s init method reports failures in its return value instead of throwing for normal network and timeout failures. Until a usable payload loads, feature evaluation returns null; this adapter converts that state to the explicit call-site fallback and marks it unusable. A non-null value that fails its runtime contract is also unusable, with reason: "invalid-value". In growthbook-preferred mode that allows the legacy fallback; in growthbook-only mode it uses the code fallback. fromPayload is for deterministic tests. It uses the same payload shape as the SDK connection endpoint without making a network request. connect starts polling even if the first fetch fails, so later delivery can recover without restarting the service. fromPayload starts no timer. The Node.js SDK documentation also covers streaming; if you need faster propagation, configure the required EventSource support and test it before replacing this polling implementation.

Emit comparison evidence without leaking identities

Shadow mode can generate high-volume logs. Emit every mismatch, sample matches, and hash values before logging them. Do not send raw user IDs, account IDs, emails, feature JSON, or SDK payloads to ordinary logs. Create src/feature-flags/decision-telemetry.ts:
The stable serializer prevents object key order from producing a false JSON mismatch. It does not make arbitrary objects safe for feature flags. Keep GrowthBook JSON values JSON-compatible and validate their application shape before use. Use a telemetry salt from your secret manager. It is not a GrowthBook SDK decryption key. Rotate it according to your log-correlation policy; rotation intentionally breaks cross-period subject correlation.

Serve one provider and compare the other

Create src/feature-flags/migrating-provider.ts:
Shadow evaluation must be side-effect free. Do not let the legacy provider persist a new assignment during comparison. Do not attach an experiment rule or exposure callback merely to compare release values. If either provider’s evaluation has unavoidable side effects, create a pure comparison endpoint or snapshot first.

Bootstrap once at process startup

Create src/feature-flags/bootstrap.ts:
This bootstrap intentionally loads the legacy snapshot in every mode until legacy removal. That guarantees growthbook-preferred can fall back. In growthbook-only, the snapshot is no longer needed for serving, so remove that dependency in a later cleanup commit after the observation window. The SDK receipt may report source: "cache" and still be usable. Alert on success: false, fallback use, and a prolonged absence of fresh payloads according to your refresh design. Do not log the client key or CLI bearer token.

Change call sites without changing behavior

Before:
After, while FLAG_PROVIDER_MODE=legacy:
In a request handler, create one scope and reuse it:
Do not call buildFeatureFlags inside the handler. SDK initialization and legacy snapshot loading belong at application startup. The request scope is cheap and isolates attributes; the control-plane payload remains shared.

Cut over one flag at a time

Use GROWTHBOOK_SERVE_KEYS as a comma-separated, startup-validated allowlist while the mode is growthbook-preferred. Selected keys prefer GrowthBook and temporarily fall back to legacy when the GrowthBook decision is unusable. Unselected keys continue serving legacy while comparison telemetry runs. The complete sequence is:

Stage 1: Legacy through the adapter

Deploy:
Verify application behavior and latency are unchanged. This stage should not require a GrowthBook client key. If behavior changes, fix the legacy adapter before adding another provider.

Stage 2: Shadow in development and staging

Deploy the correct non-production SDK client key:
Exercise a matrix of users and accounts. Use the GrowthBook Simulation page to test the same attributes and inspect which rule wins. For each flag, include:
  • No optional attributes.
  • An internal user.
  • A normal user.
  • An allowlisted account.
  • An excluded account.
  • A context matching 2 ordered rules.
  • Missing and empty hash attributes.
  • Every environment in which the flag is delivered.

Stage 3: Shadow in production

Production shadow mode still serves legacy decisions. Roll it out like ordinary observability code and watch its resource cost. Do not log raw values or identities. An expected matching receipt is sampled:
An actionable mismatch looks like this:
The digest tells you that values differ without exposing them. Reproduce the subject through an approved internal workflow using the known fixture or support context. Do not attempt to reverse the hash from logs.

Stage 4: Serve one low-risk key

After the key’s mismatch queue is empty or explained, deploy:
Confirm telemetry reports servedBy: "growthbook" for search-page-size and servedBy: "legacy" for every unselected key. Change the GrowthBook value in a non-production environment, publish it, and verify the service receives the change within the documented refresh window. Then select the first production flag. Prefer a reversible, low-risk release flag with no partial cohort and no experiment attached. Observe:
  • Error rate and latency on both behavior paths.
  • GrowthBook startup and refresh success.
  • Count of servedBy: "legacy" fallbacks for a selected key.
  • Decision mismatch rate.
  • Missing identity and missing account_id rates.
  • Support or business symptoms specific to the feature.
Add keys gradually:
An empty allowlist in growthbook-preferred serves no keys from GrowthBook. That is a safe configuration, not an instruction to migrate everything.

Stage 5: Remove the temporary legacy fallback

When every key is selected and stable, deploy:
Now a missing or unavailable GrowthBook value uses CODE_FALLBACKS, not the legacy system. Run a controlled failure test in staging by using an invalid client key or blocking the SDK endpoint. Confirm startup reports failure and each flag takes its documented code fallback. Do not simulate this by disabling a production feature without understanding the result. A disabled feature is removed from the payload and therefore exercises the fallback path for every user.

Explain mismatches systematically

Do not average all flags into one mismatch percentage. A 0.1% mismatch in a payment kill switch can matter more than a 20% documented re-bucketing in a cosmetic rollout. Classify every mismatch into one of these causes:
  • Missing feature: The GrowthBook SDK payload does not contain the key. Check feature state, environment, project delivery, SDK connection scope, and spelling.
  • Wrong default: The feature exists, but its default differs from the legacy value.
  • Rule order: A context matches multiple rules and the first match differs.
  • Attribute name: accountId and account_id are different keys.
  • Attribute type: The old system compares a number while the new attribute is a string.
  • Normalization: Casing, whitespace, Unicode, date formatting, or semantic version handling differs.
  • Missing identity: The percentage rule has no value for its hash attribute and falls through.
  • Cohort re-bucketing: Both providers include the intended percentage but assign different subjects.
  • Staleness: One provider has a newer value than the other because update and cache timing differ.
  • JSON representation: Object key order differs but values are equivalent, or a genuinely different nested value exists.
  • Legacy side effect: A read created or changed an assignment.
  • Concurrent mutation: Someone changed one control plane without mirroring the other.
Use a key-level review record:
Do not put real account IDs in this record. Store sensitive reproduction details in the approved incident or support system.

Test the provider contract

Tests should use explicit feature payloads. Do not make unit tests depend on the current state of a shared GrowthBook organization.

Test GrowthBook targeting and fallbacks

Create tests/feature-flags/growthbook-provider.test.ts:
The last test protects against a server bug where one request mutates attributes shared by another request.

Test serving mode and the per-key allowlist

Create tests/feature-flags/migrating-provider.test.ts:

Add parity fixtures for real production shapes

The previous tests prove adapter mechanics. A parity contract proves your rules. Build fixtures from synthetic examples that cover actual branches; do not copy production PII into the repository. Create tests/feature-flags/parity-contract.test.ts:
Do not add a partial percentage rollout to this test and expect identical membership unless you have implemented one of the cohort-preservation strategies. Instead, test each provider’s stability separately and record the accepted assignment difference. Run the suite:
Expected receipt:
Your exact count may differ. The acceptance signal is zero failed tests, a successful type check, and no network dependency in unit tests.

Generate strict feature types in CI

Once the GrowthBook project contains the intended keys and value types, generate AppFeatures from GrowthBook instead of maintaining it twice. The current GrowthBook CLI is installed from the growthbook npm package and its generate-types command writes a TypeScript AppFeatures definition. Install and authenticate interactively on a workstation:
For a self-hosted instance, configure a profile whose server URL includes the documented /api suffix. In CI, use a narrowly scoped Secret Key or Personal Access Token in GBCLI_BEARER_AUTH and disable interactive prompts. Never use the public SDK client key as REST API authentication. After generation, make app-features.ts re-export the generated interface and keep fallbacks checked against it:
The satisfies constraint makes CI fail when GrowthBook adds a key without an application fallback, removes a key still used by the application, or changes a value type incompatibly. Create .github/workflows/feature-flag-contract.yml:
Pin the CLI version to the one you have tested. Current CLI command groups follow the newest API version, and a command group can advance in a major CLI release. Review the changelog before upgrading the pin. If pull requests from forks cannot access secrets, split the job: run checked-in type compilation for every pull request and run authenticated regeneration on trusted branches or a scheduled workflow. Do not expose the token to untrusted code.

Add operational diagnostics

Shadow telemetry answers “do the providers agree?” GrowthBook Feature Evaluation Diagnostics can answer “which values and rules are the live SDKs evaluating?” when you send evaluation events to your warehouse and configure a Feature Usage Query. The JavaScript/Node SDK supports onFeatureUsage. The callback includes the feature key, evaluation result, and, for the multi-user client, the user context. It is de-duplicated for repeated evaluation of the same value within a scoped context. That makes it useful for recent decision diagnostics, but it is not a request-count metric. Add a warehouse-bound callback when your privacy and event-volume policies permit it:
Treat unit_id according to your data policy. Do not send email, raw secure attributes, roles, or the entire user context by default. Partition a high-volume evaluation table by timestamp and, where supported, cluster by feature key so recent diagnostic queries do not scan unbounded history. Track these service-level metrics independently of warehouse diagnostics:
  • SDK initialization successes and failures by deployment environment.
  • Payload source: network, cache, timeout, or error.
  • Refresh age and refresh failures.
  • GrowthBook code-fallback count by key.
  • Transitional legacy-fallback count by selected key.
  • Shadow comparison count and mismatch count by key and reason.
  • Missing randomization-attribute count.
  • Decision latency around the provider call.
Do not report “local evaluation adds no latency.” Measure your adapter. Normal SDK evaluation avoids a network request for each flag check, but serialization, logging, a slow legacy comparison, or remote evaluation can still add latency.

Handle failures deliberately

The code fallback must be safe before the incident occurs. During an outage, nobody should debate whether false disables the new checkout or disables checkout entirely.

Govern the system you actually have

GrowthBook supports drafts and published revisions for feature changes. Optional approval flows and some advanced governance features depend on plan and organization configuration. Build a minimum operational policy that works on your current plan, then add product-enforced controls where available.

Minimum policy on any plan

  • Give every flag an owner and a plain-language purpose.
  • Record the safe fallback and incident action.
  • Separate development, test, staging, and production SDK connections.
  • Require a change record or pull request for production rule changes, even when the product does not enforce an approval.
  • Use a second-person review for high-risk operational and release changes.
  • Put an expected cleanup condition and date on temporary flags.
  • Test the proposed attributes in Simulation before publishing.
  • Verify the published revision through application telemetry after change.
  • Keep permissions least-privileged.

Product-enforced controls when available

Approval flows can require a different user to review a draft before publishing. Publishing creates immutable revisions, and a prior revision can be selected and reverted through a new change. Configure approval requirements for production environments according to your plan and risk model. Do not claim that every GrowthBook plan enforces the same workflow. If approvals, custom roles, custom environments, schedules, or another control is required, confirm its current availability before making it a migration dependency.

Naming and ownership

Feature keys are durable API names. Use lowercase kebab case, avoid team names that may change, and encode the behavior rather than the implementation ticket:
Keep descriptions operational:

Add code references and stale review

Code References scans a checked-out repository and sends the locations of feature keys to GrowthBook. Run the scanner in CI on the branch or branches your team uses for cleanup decisions. It lets reviewers see where a flag remains in code and improves the evidence available during stale review. The current gb-find-code-refs tool can run directly or through its Docker image. Its exact invocation and credentials depend on the integration you configure, so use the official repository and pin a reviewed release or image digest rather than copying an unpinned latest command into production CI. Code references are evidence, not deletion authorization. Dynamic keys, aliases, generated clients, another repository, mobile releases, scheduled jobs, and old deployed versions may not appear in one scan. GrowthBook marks a feature stale after it has not been updated for 2 weeks and either has no active environment or has one-sided rules that send 100% of traffic to one variation. Use that as a review queue. It does not prove the flag can be deleted automatically. For each stale candidate:
  1. Confirm the production value and rule state.
  2. Check code references on the protected default branch.
  3. Search all repositories and clients that consume the SDK connection.
  4. Check support, mobile, worker, and backward-compatibility requirements.
  5. Decide which behavior becomes permanent.
  6. Remove the losing branch from code.
  7. Deploy and observe the cleanup.
  8. Remove the flag read.
  9. Re-run code references.
  10. Archive or delete the GrowthBook feature according to team policy.
Never ask an agent to delete all flags marked stale. Ask it to prepare a cleanup plan with references, owners, deployed-version evidence, and rollback impact.

Remove the legacy system in the correct order

The final cleanup is another migration, not housekeeping to squeeze into the cutover deployment.

1. Stop legacy mutations

Disable the old admin endpoint, UI, scheduled updater, and manual database write procedure. Return a clear error that points operators to GrowthBook. Keep reads intact temporarily. Verify no writes for the observation window:
Adapt interval syntax to your warehouse or database. The expected result is zero writes after the mutation cutoff, except an explicitly documented rollback test.

2. Remove legacy fallback reads

Run growthbook-only long enough to exercise an SDK refresh and normal production traffic. Remove growthbook-preferred, GROWTHBOOK_SERVE_KEYS, and the legacy provider dependency. Simplify the runtime interface to GrowthBook plus code fallbacks. Search for old keys again:
The only remaining matches should be migration history, tests you are deleting in the same reviewed change, or intentionally preserved documentation.

3. Remove data after backup and retention review

Export the legacy schema and row counts to the approved backup location. Do not place raw overrides in a pull request artifact. Verify restore access before dropping a table. Prefer this sequence:
Renaming or restricting the table can catch unknown readers before deletion. Do not drop a shared table solely because one service no longer reads it.

4. Delete migration-only telemetry

Remove pairwise comparison logs and the telemetry salt after the migration window. Retain normal GrowthBook diagnostics and fallback alerts. High-volume shadow instrumentation is not the permanent operating model.

5. Close the migration record

Record:
  • Final application commit and deployment revision.
  • GrowthBook feature keys and owning project.
  • Date legacy writes stopped.
  • Date legacy reads stopped.
  • Backup and retention location.
  • Any accepted cohort discontinuity.
  • Rollback image or release expiration.
  • Remaining temporary flags and their cleanup dates.

Optional OpenFeature branch

OpenFeature is useful when a team has already standardized on its vendor-neutral evaluation API across multiple services, or portability is a stated architecture requirement. Do not add it merely to make this migration look more abstract. At the commit verified for this guide, GrowthBook documents OpenFeature providers for Python, Go, .NET, and Java. The native GrowthBook SDKs expose capabilities outside the OpenFeature interface, and the documented provider list does not include an official JavaScript/TypeScript provider. Therefore, the reference Node implementation uses the native SDK behind an application-owned interface. Use an official GrowthBook OpenFeature provider in a supported language when all of these are true:
  • The service already uses OpenFeature or has an approved cross-language standardization plan.
  • The provider supports the GrowthBook capabilities you need.
  • Your evaluation context maps the stable targetingKey and additional attributes correctly.
  • Provider initialization, shutdown, cache, and failure behavior are covered by contract tests.
  • The team accepts that native-only capabilities may require an escape hatch.
Do not write an ad hoc JavaScript OpenFeature provider during a feature flag migration unless maintaining that adapter is itself an explicit project goal. The application-owned FeatureProvider in this guide already limits vendor-specific code to one file without claiming standards compliance.

DIY versus GrowthBook: make the decision honestly

Keep or improve DIY when all of these remain true:
  • The switch is static or deploy-time by design.
  • One application and one team own it.
  • No user, account, or percentage targeting is needed.
  • A deployment rollback meets the operational requirement.
  • No experiment exposure or outcome analysis is planned.
  • The switch will not multiply into a shared control plane.
  • A typed configuration parser and tests solve the actual problem.
Migrate when the application is already rebuilding platform responsibilities:
GrowthBook reduces the amount of custom control-plane and evaluation machinery the team owns. It does not remove the need to choose safe fallbacks, model identity, protect authorization, validate behavior, monitor delivery, or delete temporary code. The strongest reason to migrate is not that GrowthBook can return a Boolean. It is that the team needs consistent behavior from creation through rollout, diagnosis, measurement, and removal, and no longer wants to maintain each of those systems independently.

Definition of done

Inventory and classification

  • Repository, deployment, configuration, and database searches are complete.
  • Every confirmed switch has a manifest row and owner.
  • Every candidate is classified as release, experiment, operational, permission/entitlement, permanent configuration, build-time configuration, or dead code.
  • Authorization, billing entitlements, secrets, executable content, and safety invariants remain outside ordinary flags.
  • Dead flags are deleted rather than migrated.

Semantic parity

  • Value type, code fallback, legacy default, precedence, rule order, environment, attributes, randomization unit, hashing, freshness, and failure behavior are documented per key.
  • Ambiguous string and Boolean coercion is removed or explicitly preserved.
  • Partial rollouts have a cohort-preservation or accepted re-bucketing decision.
  • Missing identity and missing account attributes are tested.
  • Synthetic parity fixtures cover every meaningful branch.

Runtime migration

  • Every call site uses the typed provider boundary.
  • Legacy-only adapter deployment preserved behavior.
  • Correct environment-scoped SDK client keys are installed.
  • Shadow evaluation has no side effects.
  • Every unexplained production mismatch is resolved.
  • GrowthBook serves only allowlisted keys during incremental cutover.
  • Selected-key legacy fallback count is zero in the acceptance window.
  • GrowthBook-only failure tests produce documented code fallbacks.
  • Application shutdown closes the SDK provider cleanly.

Tests and operations

  • Unit, multi-user isolation, targeting, mode, parity, and failure tests pass.
  • Generated feature types match the checked-in file.
  • CI uses a pinned CLI and least-privileged secret.
  • Startup, refresh, fallback, mismatch, missing-attribute, and latency signals exist.
  • Production change review matches the current plan’s available controls.
  • Operators can revert a feature revision and can redeploy FLAG_PROVIDER_MODE=legacy during the rollback window.

Cleanup

  • Legacy mutation paths are disabled and observed unused.
  • GrowthBook-only mode runs before legacy reads are removed.
  • Legacy credentials are revoked.
  • Legacy data is backed up according to retention policy before deletion.
  • Code References run on protected branches.
  • Stale flags are reviewed with code and deployment evidence, never deleted automatically.
  • Migration-only telemetry and secrets are removed.
  • Every remaining temporary flag has a cleanup owner and condition.

Source map and freshness

This guide was verified on 2026-08-12 against GrowthBook repository commit e44a15af063860c7118f52508746356d55e5a91d and these source areas: The highest-drift details are package versions, CLI commands, plan availability, environment limits, provider coverage, and stale-detection criteria. Recheck them before publishing this guide or using it as an automated migration instruction. The application-specific details drift faster than the product docs. Re-run the inventory, regenerate types, rebuild parity fixtures, and capture a new source commit whenever the legacy implementation or target GrowthBook configuration changes.

Final migration receipt

End the migration with a machine-readable summary that another coding agent can verify:
Replace every placeholder with evidence. If a field is unknown, leave the migration open. The end state is not “GrowthBook returns the expected value once.” It is a single operational system, a tested fallback, an explainable assignment model, and a credible path to delete each temporary flag.