This ticket has sixty-four commits over five weeks. The title says tournament 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 teams, 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 tournament code itself had been working for weeks.

what I expected versus what the commits say

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

The feature arc happened. The tail is what I did not expect. I 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.

An old product has a lot of doorways.

what first-time state means when the product is this old

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 a league or association import. It might arrive through a registration page that was never quite deprecated, which I 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. Tournament event folders expects setup teams to be complete. Setup teams expects locations to exist. The personal team 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.

I did not know which paths were broken until I walked staging accounts through the full flow by hand. That is how I 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 go quiet, 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

Tournament 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 teams 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 tournament 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 team dashboard was the messiest. The production app has a dashboard surface for team 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. Tournament 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 I 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.

I 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, I picked up a follow-on about first-time tracking for the onboarding checklist. Its fix landed the same day as the locations and setup teams 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 roster” and “create your first event.” Completion tracking is stored per page. Complete the roster through the roster page, the checklist marks it done. Complete roster 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 but honest, 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 teams, 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 I found by hand.

I do not have a clean answer for how to introduce that contract into a codebase this old without a rewrite larger than the business can absorb right now. What I have is the certainty that every new feature touching tournament management or onboarding will be a scavenger hunt through legacy doorways until the contract shows up. This was the third or fourth time I 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 I have not walked yet are still out there, holding whatever state they wrote years ago.