---
title: "The feature was fine. First-time state was the bug."
canonical: https://dxdev.com/blog/first-time-state/
datePublished: 2026-03-31
---
This ticket has sixty-four commits over five weeks. The title says event folders. For most of the trail, the log agrees. Then one day near the end reads like a different ticket entirely: signup flow fixes, locations, setup groups, the personal dashboard, the loading path for brand-new sites. None of that is the feature.

That day is the story. By the time it arrived, the event code itself had been working for weeks.

## what we expected versus what the commits say

The production app has had event management for a long time. The feature surface is not new. This ticket was specifically about event folders, a UI for organizing sub-events inside an event. We expected the usual arc: build the feature, walk it on staging, ship.

The feature arc happened. The tail is what we did not expect. We went in thinking any remaining bugs would be in the feature. That was wrong. The feature was fine. The bugs were in everything that led to the feature, or could lead to it, depending on which of the old doorways a user arrived through.

A legacy product has a lot of doorways.

## what first-time state means in a legacy product

The legacy platform's signup and site creation flow has never been a single clean path. A new site might be created by staff in the admin panel. It might come through an organization or association import. It might arrive through a registration page that was never quite deprecated, which we will call the legacy registration path. Each route creates a site. Each leaves slightly different initial state behind.

Slightly different is fine until a feature downstream expects the state to be uniform. Event folders expects setup groups to be complete. Setup groups expects locations to exist. The personal dashboard expects the account to have cleared a first-run check. If a site arrived through the legacy registration path in 2021, it may have skipped one or more of those steps. The system never complained. The feature just silently misbehaved when someone tried to use it.

We did not know which paths were broken until we [walked staging accounts through the full flow](/blog/walking-the-funnel-beats-telemetry/) by hand. That is how we found the legacy registration path. It does not appear in the happy-path test matrix. It barely shows up in support tickets, because users who hit a confusing setup flow tend to assume setup is complicated and say nothing, which is the worst failure mode for an onboarding bug: the user absorbs the friction and never files a ticket.

## the fixes, one by one

Event folders itself needed tighter precondition checks. The feature assumed setup was clean. The fix was to verify before rendering.

That closed the happy path. The staging walk started immediately after.

Setup groups was the first real find. Sites created through the staff-creation path had a missing flag. Not a false flag, a missing one. The feature code treated absent and false differently. The defensive check that should have caught the absent case had been written assuming false was the only alternative. The two checks looked interchangeable in the code. The database treated them as different values.

Locations had a different shape of the same problem. Sites from the association import path had a location record structure that was one field short of what event folders expected. Nothing surfaced at import time. Nothing crashed at query time. The UI just skipped a configuration step, leaving the user somewhere that looked complete but was not.

The personal dashboard was the messiest. The production app has a dashboard surface for group managers and first-run state for it is supposed to be set during site creation. For sites that came through the old registration path, it was not being set at all. Event features that link back through the dashboard linked to nothing. No 404, no error. A dead link that looked like a working link until you clicked it.

The legacy registration path was last and the most interesting. Sites created through it had their first-run flag set to a value we will call optimistic. The system believed they had completed onboarding when they had only started it. No single line of code was wrong. The sequence of events at registration time had drifted from what every downstream consumer assumed was the invariant. The bug was distributed across a causal chain spanning three modules and one legacy migration.

We fixed each of these in sequence, with a staging pass after each one to find the next. That is how a feature ticket ends its log reading like a tour of the signup system.

## a second ticket, same day, same shape

While the first ticket was still open, we picked up a follow-on about first-time tracking for the onboarding checklist. Its fix landed the same day as the locations and setup groups commits. Different ticket, different surface, same architecture.

The Getting Started Checklist is an onboarding guide for new sites. A list of steps like "set up your member list" and "create your first event." Completion tracking is stored per page. Complete the member list through the member list page, the checklist marks it done. Complete the member list through the import flow, the checklist does not know. The checklist watches pages, so the underlying actions are invisible to it.

That is structurally identical to the original problem. The feature works. The state tracking is page-local. Any path that bypasses the page leaves the tracker behind.

Fixing it meant moving the hook from the page layer to the action layer. The action fires regardless of which surface the user came through. The checklist reads from the action. A small architectural shift, but the logic had been in the wrong place from the start.

Two tickets in the same week with the same root cause meant the product was pointing at the thin part of its architecture.

## what multiple staging environments surface

The production app runs across a fleet of long-lived git clones. Changes develop in a feature branch, get merged to develop, then get pulled across the environments.

The original ticket's branch stayed open for five weeks, long enough for a full release and a string of hotfixes to land underneath it. Each staging pass found something new. The branch spent most of that stretch live across multiple environments, and the multi-environment testing was not optional.

The multi-clone structure means a fix has to survive environments with real divergent data state. A fix that closes the gap on one clone might reveal a different initial-state variant on another, because that clone has accounts created through a path the others do not have in recent data. That is tedious, since production data carries every one of those variants and more.

## the contract that does not exist yet

The legacy platform's first-time and onboarding behavior is not in one place. A grep for a single first-run flag touches more than twenty source files, and that is one flag; the checklist, setup groups, and import paths each keep their own state. Flags set by one module, checked by another, sometimes not set at all depending on which entry path fired at creation time. There is no document, no code-level contract, no test assertion that says: here is the canonical initial state for a newly created site, here is every path that creates a site, and here is verification that every path produces that state.

That contract does not exist. What exists is sixty-four commits and a growing list of known broken paths we found by hand.

We do not have a clean answer for how to introduce that contract into a codebase like this without a rewrite larger than the business can absorb right now. What we have is the certainty that every new feature touching event management or onboarding will be a scavenger hunt through legacy doorways until the contract shows up. This was the third or fourth time we have run that hunt. It will not be the last.

The commits stopped at the end of March. The feature took a month. The doorways took a day near the end, and the ones we have not walked yet are still out there, holding whatever state they wrote years ago.

## Takeaway

Ask your team or vendor to list every path that creates a new site and to show that each one leaves the same first-run state behind. In this case the feature itself worked, and the bugs were in the entry paths that led to it. Until that state is written down and tested, each new feature that touches onboarding or event management risks the same search through old entry paths.
