Day 9: Feature Flags – Shipping Without Fear
Hey_Techys!! Day 9 of the PostHog series!
You know that feeling when you build a shiny new feature, gather your team, pour resources into it, push it to production... and then watch it flop?
Or worse—it drives users AWAY?
I know a product manager who got sacked for this exact thing.
PostHog says: "I've got you covered."
With feature flags, you can release features to specific users, test them safely, and roll back instantly if things go wrong.
Let me show you what I did.
What Are Feature Flags?
Imagine this:
You build a new feature. Instead of showing it to everyone at once, you show it to:
10% of users first (test the waters)
Then 50% (if it's working well)
Then 100% (full rollout)
And if something breaks? You flip a switch in PostHog and the feature disappears. No redeployment. No emergency code changes. Just... gone.
That's a feature flag.
My Feature: Inline Task Editing
If you've been following the series, you know I'm building TaskList (or TaskFlow, as I keep accidentally calling it 😅).
Current problem: Users can't edit tasks. They have to delete and recreate them.
My solution: Add inline editing. Click a task → Edit it → Save.
My fear: What if users hate it? What if it breaks something?
Feature flag to the rescue: Release it to 50% of users. See what happens. Roll back if needed.
GitHub repo: https://github.com/LizzyKate/Posthog/tree/feature-flags
Website link: https://posthog-blue.vercel.app/
Step 1: Create the Feature Flag (2 Minutes)
Go to PostHog → Feature Flags tab
(Pro tip: I couldn't find it at first. Had to search through their menu like I was hunting for treasure. My guess? PostHog is playing hard to get 🤣)

Click "New feature flag"
You'll see a form. Probably the simplest form PostHog has (finally!).
What I filled out:
Key: inline-task-editing
(This is what your code checks for. Keep it simple, lowercase, hyphenated.)
Description: "Allows user to edit task"
(Just a reminder for future-me.)
Release condition: 50% of users
(Half see it, half don't. Safe testing!)
Enable feature flag: ✅ Checked

That's it. Click Save.
2 minutes. Done.
Step 2: Build the Feature
I went to my code and built the inline editing feature:
Click the ⋮ menu on a task
Click "Edit"
Task becomes editable inline
Save or cancel
The key part: Wrapping it in a feature flag check.
const canEditTasks = posthogClient.isFeatureEnabled("inline-task-editing");
const showEdit = canEditTasks === true; // Only show when explicitly true (not undefined or false)
return (
<DropdownMenu>
<DropdownMenuContent>
{/* Only show Edit if flag is enabled */}
{showEdit && (
<DropdownMenuItem onClick={handleStartEdit}
data-ph-capture-attribute="task-edit-menu-item">
<Pencil className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
);
Notice the key: "inline-task-editing" matches what I set in PostHog. ✅
If the flag is true → Show Edit button
If the flag is false → Hide Edit button
Simple conditional rendering.
Step 3: Track Usage (So I Can See What Happens)
I didn't just implement the flag. I tracked HOW people use it.
Three events:
1. User starts editing:
const handleStartEdit = () => {
setIsEditing(true);
setEditedTitle(task.title);
setEditedDescription(task.description || "");
// Track when user starts editing (FEATURE FLAG EVENT)
posthog.capture("task_edit_started", {
task_id: task.id,
priority: task.priority,
category: task.category,
feature_flag: "inline-task-editing",
has_description: !!task.description,
});
};
2. User saves changes:
const handleSaveEdit = () => {
if (editedTitle.trim()) {
const updates: Partial<Task> = {
title: editedTitle.trim(),
description: editedDescription.trim() || undefined,
};
updateTask(task.id, updates);
// Track successful edit (FEATURE FLAG EVENT)
posthog.capture("task_edited", {
task_id: task.id,
old_title: task.title,
new_title: editedTitle.trim(),
title_changed: task.title !== editedTitle.trim(),
description_changed: task.description !== editedDescription.trim(),
feature_flag: "inline-task-editing",
});
}
setIsEditing(false);
};
3. User cancels:
const handleCancelEdit = () => {
setEditedTitle(task.title);
setEditedDescription(task.description || "");
setIsEditing(false);
// Track edit cancellation (FEATURE FLAG EVENT)
posthog.capture("task_edit_cancelled", {
task_id: task.id,
feature_flag: "inline-task-editing",
});
};
Why track this?
So I can answer:
How many people clicked Edit?
Do they finish editing or cancel?
Is this feature actually useful?
Data > guessing. ✅
Step 4: Testing (The Part That Confused Me)
I set the flag to 50% rollout.
I had 4 identified users in PostHog.
My expectation:
2 users see it, 2 users don't. Right?
What actually happened:
User 1: No Edit button ❌
User 2: No Edit button ❌
User 3: No Edit button ❌
User 4: Edit button appears! ✅
Me: "Wait... only 1 out of 4?!"
PostHog even showed: "Will match approximately 50% (2/4) of total users."
So why did only 1 user get it?
The Answer: It's Truly Random
50% rollout doesn't mean "exactly half your users."
It means each user has a 50% chance.
Think of it like flipping a coin 4 times:
Heads = see feature
Tails = don't see feature
You COULD get:
2 heads, 2 tails (perfect 50/50)
3 tails, 1 head (what I got)
4 tails, 0 heads (bad luck!)
PostHog uses deterministic hashing:
Takes your user's distinct_id
Hashes it to a number (0-100%)
Compares it to the rollout percentage
Same user = always same result
Why this matters:
User A always sees the feature (or always doesn't)
They don't flip-flop between sessions
This creates a consistent experience. ✅

Testing Both States
To see both the "Edit button visible" and "Edit button hidden" states, I had to:
Option 1: Create more users
Keep signing up with different emails until I got both states.
Option 2: Use incognito mode
Different browser = different distinct_id = different flag value
Option 3: Change rollout to 100%
Everyone sees it. Easy testing.
I eventually got one user with true. Finally! 🎉
What they saw:
Click ⋮ menu
"Edit" option appears
Click it
Task becomes editable
Save or cancel

What the other 3 users saw:
Click ⋮ menu
Only "Delete" option
No Edit button at all

Same app. Two different experiences.
This is how you safely ship features. ✅
N/B: For email targeting, users must be identified (Day 7 covered this). But percentage rollout works for both anonymous and identified users - PostHog assigns anonymous users a distinct_id automatically.
The "Persist flag across authentication steps" checkbox helps anonymous users keep the same flag value after they log in (so they don't suddenly see a different experience).

Targeting Specific Users (And The Problem I Hit)
After testing random rollout, I thought:
"What if I want to show this to SPECIFIC users who fulfill certain conditions?"
Conditions like:
Regular users of my app
Users subscribed to a certain plan
Users that use a certain browser
Users with a certain type of device
I went back to PostHog → Edit flag → Release conditions → Add condition

I saw options:
Email address
Browser
Device type
Country
Custom properties, etc.
I picked Email address and entered: onyenekwelizabeth@gmail.com
Saved it.
Logged in as that user.
Result: No Edit button. 🤔
The Real Problem: undefined
I opened the console and saw:
Inline Task Editing Enabled: undefined
Not false. Not true. undefined.
This happened EVERY TIME after login—whether I used email targeting or random rollout.
Why undefined?
When you call posthog.isFeatureEnabled() immediately after login, PostHog hasn't finished loading the flags yet.
Here's the timeline:
User logs in →
posthog.identify()is called ✅User redirects to
/tasks→ Page loads immediatelyTaskItem renders → Checks flag
PostHog is still loading flags → Returns
undefined❌No Edit button shows (because
undefinedis falsy)
If I refreshed the page:
Inline Task Editing Enabled: true
Now it works! Because PostHog cached the flags.
The Fix: posthog.reloadFeatureFlags()
I found this method in PostHog's docs. It forces PostHog to reload flags immediately after identifying a user.
What reloadFeatureFlags() does:
Tells PostHog: "Hey, I just identified a user. Go fetch their flags NOW." By the time the /tasks page renders, the flags are ready.
I moved flag loading into the login flow, and only redirected after the flags finished loading.
Updated LoginForm:
The key change: Making the function async and using await so the redirect happens ONLY after flags are loaded.
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
if (!email) return;
setIsLoading(true);
try {
// 1. Save user to localStorage
const user = auth.login(email, name || undefined);
// 2. Identify user in PostHog
posthog.identify(email, {
email,
name: name || undefined,
identified_at: new Date().toISOString(),
});
// 3. Capture login event
posthog.capture("user_logged_in", {
email,
});
// 🎯 4. RELOAD FEATURE FLAGS - Wait for them to load!
await posthog.reloadFeatureFlags(); // ← The "await" is critical!
console.log("Feature flags loaded!");
// 5. Redirect to tasks (only after flags are ready)
router.push("/tasks");
} catch (error) {
console.error("Login error:", error);
} finally {
setIsLoading(false);
}
};
Result after adding this:
Edit button appears immediately. No refresh needed!
No Flicker Problem
I expected to see the Edit button "pop in" after a delay (the famous flicker problem).
But I didn't see any flicker.
The flag loaded so fast (~100-200ms) that by the time I clicked the dropdown menu, the Edit button was already there.
PostHog is FAST. The flag loaded before I could even interact with the UI.
This is why reloadFeatureFlags() works so well. It kicks off the flag loading immediately during login, so by the time you're navigating the app, flags are ready.
The Other Mistake I Made
I also combined TWO things without realising:
Target specific user (email = onyenekwelizabeth@gmail.com)
50% rollout

What PostHog did:
First check: Does email match? ✅
Second check: Is user in the random 50%? ❌
So even though the email matched, the rollout percentage still applied!
PostHog showed: "50% of 👤 Email address = ...beth@gmail.com"
That means:
Only users with that email are eligible
Of those users, only 50% see it
Since I only had ONE user with that email, they had a 50/50 shot.
They lost the coin flip. 😅
PostHog even warned me: “Will match approximately 13% (0/4) of total users"
But I didn't notice. 🤦♀️
What I should have done:
Email = onyenekwelizabeth@gmail.com
Rollout = 100%

Then that user ALWAYS sees it.
I felt silly. But I learned! 🎓
The Three Served Value Options
While creating the flag, I saw three options for how flags work:

1. Release toggle (boolean)
Returns true or false.
What I used: This one. ✅
Example:
const canEdit = posthog.isFeatureEnabled("inline-task-editing");
// Returns: true or false
When to use: Simple on/off features.
2. Multiple variants with rollout percentages (A/B testing)
Returns a variant key (like "control", "variantA", "variantB").
Example use case:
Testing two different UI designs.
Setup in PostHog:
Variant A (50%): "blue-button"
Variant B (50%): "green-button"
Code:
const variant = posthog.getFeatureFlag("button-color-test");
if (variant === "blue-button") {
return <Button color="blue">Click Me</Button>;
} else if (variant === "green-button") {
return <Button color="green">Click Me</Button>;
}
When to use: A/B testing different versions of a feature.
3. Remote config (single payload)
Returns a JSON payload with configuration values.
Example use case:
You want to change text or settings without redeploying code.
Setup in PostHog:
{
"welcome_message": "Welcome to TaskFlow!",
"max_tasks": 100,
"theme": "dark"
}
Code:
const config = posthog.getRemoteConfigPayload("app-config");
console.log(config.welcome_message); // "Welcome to TaskFlow!"
console.log(config.max_tasks); // 100
When to use:
Changing text/copy without code changes
Updating configuration values remotely
Emergency tweaks (like disabling a buggy feature)
Note: Remote config flags require server-side setup with your feature flags secure API key. They're meant for backend use, not frontend (because the key needs to stay secret).
For Day 9, I stuck with simple boolean flags. Remote config is more advanced.
What I Learned (Day 9 Takeaways)
1. Feature flags = safety net
Ship features without fear. If something breaks, flip the switch. Done.
2. 50% rollout is truly random
Don't expect exactly half your users. It's probability, not precision.
3. Always track usage
Don't just implement the flag. Track if people USE it.
Events = data = decisions. ✅
4. Flags return undefined on first load
Whether using random rollout or email targeting, flags aren't ready immediately after login.
isFeatureEnabled() returns undefined until PostHog finishes loading flags.
The fix: await posthog.reloadFeatureFlags() in your login handler.
This ensures flags are ready before the page renders. ✅
5. Start small, then expand
Real rollout strategy:
10% (you + team)
50% (if no bugs)
100% (full release)
Remove flag from code (it's permanent now)
6. Feature flags aren't just for new features
Use them for:
Kill switches (turn off buggy features)
Beta access (give early adopters a preview)
A/B testing (compare two versions)
Gradual migrations (old code → new code safely)
What Worked Great
✅ Creating the flag (2 minutes)
✅ Implementing it in code (simple conditional)
✅ Tracking events (see actual usage)
✅ Rolling out safely (no production disasters)
✅ Testing both states (incognito mode trick)
My Recommendations
Use feature flags when:
Releasing risky changes
Building experimental features
Testing new UI designs
Gradually migrating code
You need a kill switch
Don't use feature flags for:
Simple bug fixes (just deploy them)
Features you're 100% confident about
Things that don't need gradual rollout
Best practices:
Start with percentage rollout (10% → 50% → 100%)
Always track usage events
Remove flags after full rollout (clean up your code!)
Document what each flag does
Set auto-deletion in PostHog (don't keep them forever)
Up Next: Day 10
So far:
Day 1: What is PostHog? ✅
Day 2: Setup (SSR bug) ✅
Day 3: Empty dashboard ✅
Day 4: Toolbar (Actions struggle) ✅
Day 5: Custom events (AI magic) ✅
Day 6: Session replay (UX goldmine) ✅
Day 7: User identification ✅
Day 8: Funnels (settings overload) ✅
Day 9: Feature flags (shipping without fear) ✅
Coming up:
Day 10: A/B Testing – What PostHog Does Well (And What It Doesn’t)
Day 11+: More features...
Stay tuned!
Follow along with the series:
- 🎥 TikTok: [https://www.tiktok.com/@hey_techys?is_from_webapp=1&sender_device=pc]
- 📸 Instagram: [https://www.instagram.com/hey_techys]
- 💼 LinkedIn: [https://www.linkedin.com/in/onyenekwe-elizabeth-46a467183/]
- 🐦 Twitter/X: [https://x.com/ElizabethOnyen6]
Byeeeeeeee!!!!
- Lizzy