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:
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: