Feature Flags and the Login Boundary: Don't Let Sign-Up Reshuffle Your Variants
A visitor lands on the new pricing page, likes it enough to sign up — and the first thing they see after creating an account is the old one.
Nothing is broken. This is deterministic bucketing working exactly as designed. A percentage rollout hashes the user key you give it, so the cohort a person lands in is a pure function of that key. Before login, the key is an anonymous id the SDK generated. After login, it's your real user id. Different key, different hash, possibly different variant.
For a boolean rollout that's a jarring flicker. For an experiment it's worse: one human is counted as two subjects, the pre-login impression is attributed to variant B, and the conversion that variant B earned gets credited to variant A. Your signup funnel — the exact place you most want to test — is the place where the measurement quietly falls apart.
Carry the anonymous id across the boundary
The fix is not to stop bucketing anonymous users — it's to stop throwing their key away. Featureflow's JavaScript client generates an anonymous id, persists it in localStorage, and writes it to an ff-anonymous-id cookie so your backend can evaluate against the same key the browser used. Read it with getAnonymousId(), or pass it explicitly via the X-Featureflow-Anonymous-Idheader when your API lives on a different subdomain and the cookie won't travel.
Three rules follow from that:
- Call
updateUser()on login, don't re-init. Re-initialising the client drops the session context and forces a full reload of feature config. Updating the user re-evaluates in place and gives you back the new feature map. - Keep the anonymous id as an attribute. Pass it through as a targeting attribute after login so you can still join the two halves of the session when you look at results.
- Reset it on logout, not on login. On a shared device, the next visitor should not inherit the last one's cohort.
resetAnonymousId()is for the sign-out path.
import { init, type FeatureflowClient } from 'featureflow-client';
// Anonymous visitor: the SDK generates and persists a key
const featureflow: FeatureflowClient = await init(FF_JS_API_KEY);
// ... they browse the flagged pricing page ...
async function onSignIn(account: Account) {
// Re-evaluate in place, keeping the anonymous key as an attribute
await featureflow.updateUser({
id: account.id,
attributes: {
plan: account.plan,
anonymousId: featureflow.getAnonymousId(),
},
});
}
function onSignOut() {
// Next visitor on this device starts clean
featureflow.resetAnonymousId();
}For funnels, pin the key until the funnel ends
Attributes let you reconcile the data afterwards, but they don't stop the variant itself from flipping mid-flow. When a rollout spans the signup boundary — onboarding sequences, checkout, trial activation — evaluate that particular feature against the anonymous id for the whole journey and switch to the account id only once the flow completes. It costs one extra field on the evaluation call and removes an entire class of “the page changed while I was filling it in” bug reports.
The same key discipline applies server-side. If your backend renders any part of that funnel, it needs the browser's anonymous id too — otherwise the server and client disagree about which variant the visitor is in, and you ship a page that argues with itself. The Featureflow docs cover the cookie and header handoff between the client and server SDKs.
Bucketing is only as stable as the key you feed it. Decide deliberately what identifies a user before they have an account, and login stops being the place your rollouts and experiments come apart.
#FeatureFlags#ABTesting#ProgressiveDelivery#ContinuousDelivery#JavaScript
Keep one visitor in one cohort
Start free with Featureflow — deterministic bucketing, anonymous id handoff between client and server SDKs, and targeting on any attribute you pass.
Start Now (Free)Related Articles
Feature Flags and SSR: Stop the Flash of the Wrong Variant
The server renders one variant, the browser boots without flag state and repaints another. Evaluate on the server, bootstrap the client with the same answers, and first paint stops lying.
Feature Flags in Serverless Functions: The Poll That Never Fires
Flag SDKs refresh config on a timer. A frozen container doesn't run timers, so it thaws holding hour-old config. Build the client at module scope, let evaluation drive the refresh, and pre-register failover variants for the cold path.
Feature Flags in Background Jobs: Evaluate at Execution, Not Enqueue
Jobs run minutes or days after they're enqueued — and workers live for weeks. Evaluate flags per execution, key them on the job's subject so bucketing matches your web tier, and give every consumer a kill switch.