Halyard
A progressive delivery console that can answer "who is seeing this?"
A concept study in developer tooling — a feature-flag and rollout platform designed around the one question nobody can answer during an incident, plus a kill switch that is fast without being accidental.

- Project type
- Developer platform design & information architecture
- Industry
- Developer Tools
- Timeline
- 12-week scope · 2026
- Platform
- Web consoleCLISDK surface
- Services
- Product DesignDesign SystemsFrontend Engineering
- Technologies
- FigmaVue 3TypeScriptGoOpenTelemetry
What made this hard
Halyard is a scenario about a category of tool that is powerful, widely adopted, and consistently unhelpful at the exact moment it matters most. Feature-flag platforms are excellent at storing rules. During an incident nobody wants to read rules — they want an answer, and deriving the answer from four separate rule surfaces under pressure is where these tools fail.
The state is distributed across four surfaces
A flag's effect is the product of its default, its environment overrides, its cohort targeting and its individual overrides. Each is stored and displayed separately, and the reader has to combine them mentally to reach the only answer they came for.
The critical question is asked under the worst conditions
"Who is seeing this?" is almost always asked at 3am, by someone who did not configure the rollout, while a graph is going the wrong way. Any interface requiring careful reading has failed at that moment.
The kill switch is either slow or dangerous
Guarded by a confirmation dialog it costs seconds nobody has, and the dialog is clicked through unread anyway. Unguarded it gets hit by accident. Both designs are common and both are wrong.
Temporary flags are permanent
A flag created for a two-week rollout is still evaluated three years later, and every one is a live branch in production code. Nothing in the tool makes that accumulation visible.
Staging and production look identical
The same console, the same layout, one small dropdown between them. The resulting mistake is rare, severe, and entirely predictable from the design.
The SDK and the console must agree
If the console says a user is in the rollout and the SDK disagrees, the console is worse than useless — it is confidently wrong, and it will be trusted during an incident.
The situation we designed against
The premises focus on the incident path rather than the configuration path, because configuration is done calmly by someone who understands the system and diagnosis is done urgently by someone who does not. Tools in this category are almost always designed for the first case.
- The reader is not the author
- The person diagnosing a rollout at 3am did not set it up and has no context on why the cohorts are shaped as they are. Every surface must be readable cold.
- Rules are stored; answers are wanted
- The gap between "here are four rule sets" and "here is who is affected" is small for a calm expert and enormous under pressure.
- Confirmation dialogs do not prevent mistakes
- We assume any dialog in a hot path is clicked through unread. Safety has to come from reversibility and scope rather than from a speed bump.
- Flag debt accumulates silently
- We assume no team voluntarily audits flags. If cleanup is not surfaced as state in the tool, it does not happen.
- Environment mistakes are a design failure
- We assume that if staging and production are visually identical, someone will eventually act on the wrong one, and the design owns that outcome.
- Trust in the console is binary
- One occasion where the console disagreed with observed behaviour, and engineers go back to reading the SDK logs permanently.
The people we designed for
Sam Okafor
On-call backend engineer
Paged at 3am about an error rate on a service they do not own. They suspect a rollout, have never opened this flag before, and need to know within a minute whether to kill it.
“I do not care how the targeting is configured. I need to know who is getting the bad code and how to make that stop.”
Goals
- Find out who is currently affected, immediately
- Stop the bleeding without taking down anything unrelated
- Leave a record so the owning team knows what was done
Frustrations
- Four screens to combine before an answer exists
- No way to check a specific reported user against the rules
- A kill switch guarded by a dialog, or one that kills more than intended
Priya Venkatesan
Platform engineer, developer experience
Owns the flag system across forty services. Spends a recurring part of every quarter chasing teams to remove flags that finished rolling out long ago and are now permanent conditionals.
“Every flag nobody removed is an `if` statement in production that somebody will trip over in two years.”
Goals
- See which flags are dead without auditing by hand
- Give teams a reason to clean up that is not her asking
- Keep the console and the SDK provably in agreement
Frustrations
- No lifecycle state, so a finished flag looks like an active one
- Cleanup is a spreadsheet she maintains personally
- No way to prove the console's answer matches the SDK's
Where the current experience loses them
Scroll the map horizontally to see every stage.
Designing for the reader, not the author
Feature-flag tools are built by people configuring rollouts and used, at the moments that matter, by people diagnosing them. Those are different jobs with opposite requirements, and the category consistently optimises for the first.
Configuration is calm. The person doing it understands the cohorts, has time to read, and benefits from a faithful, complete representation of every rule. That is what these consoles provide, and they provide it well.
Diagnosis is not calm. It happens at 3am, to someone who did not configure the rollout and has no idea why the cohorts are shaped as they are, while a graph goes the wrong way. That person does not want a faithful representation of the rules. They want one sentence: here is who is affected.
Halyard is designed around the second reader, and the first reader is treated as the easier case — they get the rules, one level down, where someone with time will happily click.
Computing the answer changes what the tool is
Leading with a computed audience sounds like a presentation choice. It is actually the architectural decision the whole project turns on.
A console that displays rules is unfalsifiable. It shows you what is stored, and what is stored is by definition correct. There is nothing to check.
A console that computes an audience is making a claim, and a claim can be wrong. That is uncomfortable and it is the point: the computed answer can be compared against what the SDK actually does on the same inputs, and any divergence between the two becomes visible immediately rather than during an outage. The evaluation explainer shows both answers side by side for exactly this reason.
It costs something real. Computing an audience over a large user base is expensive and sometimes only approximate, so the figure carries its freshness and a visible marker when it is an estimate. An approximate number presented with confidence would be worse than the rule list it replaced — trust in a console of this kind is binary, and one confidently wrong answer ends it permanently.
The kill switch argument
The most contested decision here is removing the confirmation dialog, so the reasoning is worth setting out fully.
There are two conventional designs. A guarded kill switch puts a modal in front of the action; an unguarded one does not. The guarded version costs seconds in the one flow where seconds are the entire value of the product, and it prevents nothing, because a dialog in a hot path is clicked through unread by everyone, every time. The unguarded version gets hit by accident.
Both designs assume the failure is pressing the button unintentionally. That is not the common failure. The common failure is pressing it intentionally without knowing what it covers — killing a flag that turns out to be attached to three other services, at 3am, while trying to help.
So the design addresses that instead. The blast radius is stated beside the control, before the press: which services, which environments, how many users. The action is reversible with one click for five minutes. Every kill writes an audit entry naming the person, which matters more for the follow-up than for the prevention.
This is a risk transfer, not a risk elimination, and it should be said plainly. Accidental kills will happen and will briefly affect production. A team whose rollback path is slow, or whose flags routinely have wide blast radii, should keep the dialog — the trade only works if reversal is genuinely instant.
Flag debt is a design problem
The hygiene work is the least glamorous part of this and possibly the most valuable.
Every flag is created as temporary. A large share become permanent, and each survivor is a live conditional in production that nobody remembers the reason for. The usual remedy is a platform engineer with a spreadsheet and persistence, which works exactly as well as one person's persistence.
Mandatory expiry makes the debt a state the tool displays rather than something someone tracks. But the expiry date alone would only produce a queue of notifications, and notifications about other people's cleanup are the easiest thing in the world to ignore. What makes it work is generating the removal pull request once a flag's evaluation has been constant long enough to be safe — because the obstacle was never willingness. It was that removing a flag means finding every call site, and nobody wants to do that on a Thursday.
The part that does not work is expiry churn. Rollouts legitimately run long, extensions become reflexive, and by the third snooze the date means nothing. Requiring a reason and showing extensions in the hygiene view slows the reflex a little. It does not fix it, and this design does not have a good answer.
What we decided before drawing anything
Show the answer, not the rules that imply it
The console computes the current audience and presents it as the primary content. Rules remain available, one level down, for the person changing them rather than the person reading them.
Design for the reader who did not write it
Every surface is built for someone encountering this flag for the first time, at speed, under stress. The author is the easier case and gets the second-priority treatment.
Safety through reversibility, not friction
A confirmation dialog in a hot path is a speed bump that gets clicked through unread. Making the action instantly and obviously undoable is what actually prevents harm.
Every flag has an expiry
Set at creation, no exceptions. A permanent flag is a branch in production code, and it should have to be named as one.
Environments are never confusable
Production carries persistent chrome, a distinct colour and an explicit word. Colour alone is insufficient and is never used alone.
The console must be provably right
Its computed answer is checked against the SDK's evaluation on the same inputs. A console that is confidently wrong once is never trusted again.
How the product was reorganised
Changes
The landing surface. Ordered by recency, because that is the incident question.
Recent changes
Every rollout state change in the last 24 hours, across all flags
Correlation
Change timeline overlaid on service error rates
Flag
One surface holding the computed audience, the lifecycle state and the controls.
Audience
The computed answer — who is currently affected, and how many
Lifecycle
One of six states, with the expiry date and its owner
Rules
Default, environment, cohort and individual overrides, one level down
Kill
Scoped disable with the blast radius stated before the action
Explain
Evaluate any identifier against live rules and show the path taken.
Rule path
Every rule considered, in order, and which one short-circuited
SDK parity
The console's answer checked against the SDK's on identical inputs
Hygiene
Flag debt as a visible state rather than a spreadsheet somebody maintains.
Expiring
Flags past or approaching their declared expiry, with owners
Dead
Fully rolled out or fully off for a sustained period
Removal pull requests
Generated for flags whose evaluation is now constant
Before and after, step for step
Answering "who is seeing this?"
The flow the entire product is judged on. The before column is not badly designed — each of those screens is fine on its own. The failure is that the answer exists in none of them.
6 to1steps, −5
Before
- Open the flag
- Read the default
- Open environment overrides
- Open cohort targeting
- Open individual overrides
- Combine mentally
After
- Open the flag
Checking whether a specific user is affected
Impossible in the current state, which is why incident tickets contain screenshots of SDK debug logs.
4 to1steps, −3
Before
- Read the cohort definitions
- Work out whether the user matches
- Ask the owning team
- Read SDK logs
After
- Paste the identifier into Explain
Stopping a bad rollout
The blast radius being stated before the action is what allows the scoped kill to be chosen over the blunt one.
5 to2steps, −3
Before
- Find the flag
- Guess the blast radius
- Click disable
- Dismiss a confirmation dialog
- Hope nothing unrelated was attached
After
- Read the stated blast radius
- Kill, scoped
Retiring a finished flag
Currently driven entirely by one platform engineer with a spreadsheet and persistence.
4 to2steps, −2
Before
- Maintain a spreadsheet
- Chase the owning team
- Wait
- Chase again
After
- Flag enters the expiring state automatically
- Accept the generated removal pull request
The choices that shaped the product
Each of these could reasonably have gone the other way. What follows is the argument for the direction taken, and what it gave up.
Compute the audience instead of displaying the rules
Flags store a default, environment overrides, cohort targeting and individual overrides. Every one of those is displayed faithfully, and the answer the reader wants is in none of them — it is the product of all four, computed in the reader's head under time pressure.
What we did
The flag surface leads with the computed audience — who is currently affected, by segment, with a count. Rules move one level down, where the person editing them will still find them.
Why
It moves the computation from the reader to the machine, which is the whole job. It also makes the console checkable: a computed answer can be compared against the SDK's evaluation, whereas a list of rules can only be agreed with.
What it cost
Computing an audience over a large user base is expensive and sometimes approximate, so the figure carries its own freshness and a clear marker when it is an estimate. An approximate answer presented confidently would be worse than the rule list it replaced.
A kill switch with no confirmation dialog
The two conventional designs both fail. A confirmation dialog costs seconds during an incident and is clicked through unread, so it prevents nothing while slowing the one action that matters. No guard at all means accidental kills.
What we did
One action, no modal. Safety comes from three other places: the blast radius is stated beside the control before it is pressed, the action is reversible with one click for a sustained window, and every kill writes an audit entry naming the person.
Why
Dialogs defend against the wrong failure. The real risk is not that someone presses the button without meaning to — it is that they press it not knowing what it covers. Stating the scope addresses that; instant reversibility handles the rest.
What it cost
Accidental kills will happen and will briefly affect production. We judged a rare, immediately reversible accident better than seconds added to every genuine incident, but this is a real risk transfer rather than a risk elimination, and a team with a slow rollback path should not accept it.
Mandatory expiry at flag creation
Every flag is created as temporary and a large share become permanent. Each survivor is a live conditional in production that nobody remembers the reason for, and no team voluntarily audits them.
What we did
Creation requires an expiry date. Past it the flag enters an expiring state, appears in the hygiene view with its owner, and — once its evaluation has been constant for long enough — generates a removal pull request.
Why
It converts flag debt from an invisible accumulation into a state the tool displays, and the generated pull request removes the actual obstacle, which was never willingness but the tedium of finding every call site.
What it cost
Expiry churn. Rollouts legitimately run long, and every extension is a small tax that becomes a reflex — after the third snooze the date is meaningless. Extensions therefore require a reason and are shown in the hygiene view, which helps a little and does not solve it.
Production carries persistent chrome, not a dropdown
Staging and production share a console, a layout and a small environment selector. The resulting mistake is rare and severe, and it is entirely predictable from the design.
What we did
Production gets a persistent bordered frame, a distinct accent, the word PRODUCTION in the header, and the environment name inside every destructive control's label rather than only in the selector.
Why
The signal needs to be present in peripheral vision at the moment of action, not available for checking beforehand. Putting the environment in the button label means the last thing read before clicking states which environment is about to change.
What it cost
Persistent chrome costs screen space on every production view, which is most views, and the effect fades with familiarity as all such warnings do. Putting the environment in the control label is the part that keeps working after the frame becomes invisible.
An evaluation explainer as a first-class surface
"Is this specific user affected?" cannot be answered from rule definitions without simulating the evaluation by hand, which is why incident tickets end up containing screenshots of SDK debug output.
What we did
Paste an identifier and see the exact rule path — every rule considered in order, which one short-circuited, and the resulting variant, alongside the SDK's answer on the same inputs.
Why
It answers the question directly and, by showing both answers together, makes any console-versus-SDK divergence immediately visible rather than discovered during an outage.
What it cost
It exposes internal evaluation order, which constrains future changes to it — people will build habits around what they see. It is also a surface that handles user identifiers, so it needs access control and audit that the rest of the console does not.
Key screens






The system underneath the screens
A dense console system tuned for reading under stress. Tight radii and compact density, monospace for anything an engineer will copy, a status ramp held apart from the categorical chart hues, and environment identity treated as a system-level concern rather than a page-level one.
Colour
- Terminal#0B0B10Primary surface, dark theme
- Halyard#818CF8Primary action and the subject series in charts, dark theme
- Halyard (light)#4F46E5The same role restepped for the light theme
- Reference#71717ASecondary text, and the comparison series in every chart
- Production#DC2626Environment identity only. Never a series colour, never a status.
Typography
- CountAudience sizes and affected-user counts, tabular1.75rem · 600
- HeadingFlag names and panel titles1.0625rem · 600
- BodyDescriptions, rule text and console copy0.875rem · 400
- CodeFlag keys, identifiers and rule paths — anything that gets copied0.8125rem · 400
- ChromePRODUCTION — environment identity in the persistent frame0.6875rem · 600
Tokens
- --env-chrome
- persistent
- Production identity is frame, colour and word. Never colour alone.
- --destructive-label
- includes environment
- The last text read before a destructive click names the environment.
- --undo-window
- 5min
- One-click reversal on any kill. Replaces the confirmation dialog.
- --audience-freshness
- required
- Every computed audience states its age and whether it is an estimate.
- --expiry
- required
- Enforced at creation. There is no path to a flag without one.
- --row-height
- 28px
- Compact. A console is read in bulk, not admired.
Components
6 components · 31 variants
- AudiencePanel5The computed answer, with segment breakdown, freshness and estimate marker.
- KillControl4Scoped disable stating its blast radius, with a five-minute one-click undo.
- LifecycleBadge6Draft, rolling, held, complete, expiring, archived — icon and word, never colour alone.
- RulePath7Ordered evaluation trace showing which rule short-circuited and why.
- EnvironmentFrame3Persistent chrome carrying environment identity across every view.
- ChangeRow6A rollout state change with actor, time and correlated service impact.
Rendered specimens
What shipped
Computed audience panel
One authoritative answer to who is currently affected, with a segment breakdown, its own freshness and an explicit marker when it is an estimate.
Evaluation explainer
Any identifier evaluated against live rules, showing the ordered rule path and the SDK's answer on the same inputs.
Reversible scoped kill
Blast radius stated before the action, no confirmation dialog, one-click reversal for five minutes, and an audit entry naming the actor.
Enforced flag lifecycle
Mandatory expiry at creation, six lifecycle states, and generated removal pull requests once evaluation has been constant.
Unmistakable environments
Persistent production chrome plus the environment name inside every destructive control label, so the signal survives familiarity.
Change-first landing view
Recent rollout changes across all flags, correlated with service error rates, because recency is what an incident asks about.
What the work changed
These describe the design itself. Because this is a concept project with no users, there are no adoption, retention or revenue figures on this page — those would have to be invented, and an invented number is worth less than none.
- 1
- Authoritative answer to "who sees this"
- 5 → 1
- Screens to diagnose a rollout
- 0
- Confirmation dialogs on the kill switch
- 100%
- Flags carrying an expiry
- 6
- Lifecycle states specified
- 1
- Action to explain any evaluation
A computed audience, replacing four rule surfaces a reader had to combine mentally to reach an answer.
Flag state, environment, cohort membership, overrides and evaluation history consolidated onto one surface.
One action, no modal. Safety comes from scope, reversibility and an audit entry, not from a dialog nobody reads at 3am.
Creation requires one. A flag without an expiry is permanent branching disguised as a temporary experiment.
Draft, rolling, held, complete, expiring and archived — making flag debt visible as a state rather than a spreadsheet.
Paste an identifier, get the exact rule path that produced that user's variant, including which rule short-circuited.
Next case study
Northwind Cloud
One design system across four acquired products
SaaSYour product
Have a problem shaped like one of these?
Halyard is a concept. If you are working on something with the same kind of complexity, tell us what is not working and we will tell you how we would approach it.
Or email us directly at hello@uxatom.com