Managing Feature Flag Versions and Migrations in JavaScript

Feature flags aren’t just about toggles — they evolve over time. A flag might start as a boolean, later switch to an expression, and eventually get removed. Without a versioning strategy, things break: stale configs, incompatible logic, and orphaned code paths. In this article, you’ll learn how to build a versioned feature flag system in JavaScript that can safely evolve — without breaking your app. Step 1: Version Your Flags Explicitly Start by defining each flag with a version field: const fallbackFlags = { newUI: { version: 1, expr: "user.plan === 'pro'", }, searchBoost: { version: 2, expr: "getBucket(user.id) < 30", }, }; This allows your app to know what format or logic it's dealing with. Step 2: Handle Version Migrations at Load Time When loading remote or cached flags, normalize and upgrade them if needed: function migrateFlag(flag, currentVersion) { const upgraded = { ...flag }; if (!flag.version || flag.version < currentVersion) { // Example: older flags used plain booleans if (typeof flag === "boolean") { upgraded.expr = flag ? "true" : "false"; } upgraded.version = currentVersion; } return upgraded; } Wrap this into your loading logic so all flags are upgraded before evaluation. Step 3: Avoid Breaking Changes by Supporting Legacy Versions If your app may receive old snapshots (e.g. from offline clients), support evaluation of multiple versions: function evaluateFlag(flag, user, context = {}) { try { if (flag.version === 1) { const fn = new Function('user', `return (${flag.expr})`); return !!fn(user); } if (flag.version === 2) { const fn = new Function('user', 'context', 'getBucket', `return (${flag.expr})`); return !!fn(user, context, getBucket); } return false; } catch { return false; } } You can remove support for old versions gradually, once they’re no longer needed. Step 4: Track Flag Cleanup with Metadata Add optional metadata to track rollout state: { searchBoost: { version: 2, expr: "getBucket(user.id) < 30", deprecated: false, rolloutStatus: "active" } } This helps you audit flags later and safely remove them when they're no longer needed. Step 5: Persist and Load Flag Snapshots with Version Checks When storing snapshots (e.g. in localStorage or IndexedDB), include a schema or timestamp: function storeFlags(flags) { localStorage.setItem('flagSnapshot', JSON.stringify({ schemaVersion: 2, timestamp: Date.now(), flags, })); } This ensures you don’t accidentally load outdated or incompatible formats later. ✅ Pros:

May 2, 2025 - 04:13
 0
Managing Feature Flag Versions and Migrations in JavaScript

Feature flags aren’t just about toggles — they evolve over time. A flag might start as a boolean, later switch to an expression, and eventually get removed. Without a versioning strategy, things break: stale configs, incompatible logic, and orphaned code paths.

In this article, you’ll learn how to build a versioned feature flag system in JavaScript that can safely evolve — without breaking your app.

Step 1: Version Your Flags Explicitly

Start by defining each flag with a version field:


const fallbackFlags = {
  newUI: {
    version: 1,
    expr: "user.plan === 'pro'",
  },
  searchBoost: {
    version: 2,
    expr: "getBucket(user.id) < 30",
  },
};

This allows your app to know what format or logic it's dealing with.

Step 2: Handle Version Migrations at Load Time

When loading remote or cached flags, normalize and upgrade them if needed:


function migrateFlag(flag, currentVersion) {
  const upgraded = { ...flag };

  if (!flag.version || flag.version < currentVersion) {
    // Example: older flags used plain booleans
    if (typeof flag === "boolean") {
      upgraded.expr = flag ? "true" : "false";
    }

    upgraded.version = currentVersion;
  }

  return upgraded;
}

Wrap this into your loading logic so all flags are upgraded before evaluation.

Step 3: Avoid Breaking Changes by Supporting Legacy Versions

If your app may receive old snapshots (e.g. from offline clients), support evaluation of multiple versions:


function evaluateFlag(flag, user, context = {}) {
  try {
    if (flag.version === 1) {
      const fn = new Function('user', `return (${flag.expr})`);
      return !!fn(user);
    }

    if (flag.version === 2) {
      const fn = new Function('user', 'context', 'getBucket', `return (${flag.expr})`);
      return !!fn(user, context, getBucket);
    }

    return false;
  } catch {
    return false;
  }
}

You can remove support for old versions gradually, once they’re no longer needed.

Step 4: Track Flag Cleanup with Metadata

Add optional metadata to track rollout state:


{
  searchBoost: {
    version: 2,
    expr: "getBucket(user.id) < 30",
    deprecated: false,
    rolloutStatus: "active"
  }
}

This helps you audit flags later and safely remove them when they're no longer needed.

Step 5: Persist and Load Flag Snapshots with Version Checks

When storing snapshots (e.g. in localStorage or IndexedDB), include a schema or timestamp:


function storeFlags(flags) {
  localStorage.setItem('flagSnapshot', JSON.stringify({
    schemaVersion: 2,
    timestamp: Date.now(),
    flags,
  }));
}

This ensures you don’t accidentally load outdated or incompatible formats later.

Pros: