On April 20, an organization on the production app could show three different member counts on three different screens, and they all disagreed. The package tier said one number. The per-event controls said another. The credit logic, when it ran at all, said a third. Each number had been correct at some point in the product’s history. None of them were correct together.
That was the problem the ticket was supposed to fix. We had written it up as a Packages UI redesign, pulled in a Figma mockup, and planned to spend a focused day on it. The day went somewhere else.
The model was wrong before the UI was
The organization feature had been bolted onto a system built for individual member registrations. The original design assumed a single account type: a member pays, a member registers. Organizations are different. They buy credit pools, spin up multiple child events, and allocate members across those events from a shared balance.
That model existed in the codebase. It just had no name. Whether an account was an organization, a standalone event, or a child event of a parent organization was decided at runtime by checking which flags were set and which were not, in an order nobody had written down. Organization behavior was inferred from a cluster of flags, fallback conditions, and package tier strings that had piled up over several product iterations.
Claude helped us map it. We fed it the relevant legacy application files and asked it to trace how the available member count was computed in an organization create flow. It produced a call graph with five decision branches. Two of them ended in the same fallback, and one silently returned zero. That zero had been in production for months. Nobody noticed because zero triggered a “contact us” fallback in the UI, which looked intentional.
It was not intentional, and we had not noticed.
The first wrong turn: patch the three places
When we first described the problem to Claude as “fix the available member count discrepancy,” it proposed patching the three places where the counts diverged. It would have worked: the counts would have agreed.
The underlying model would still have been inferring type from flags, and the next developer to add a fourth account type would have created a fourth divergence.
So we reframed the problem as “the account type taxonomy is implicit, make it explicit.” After that, Claude’s output was better on every pass. The call graph became readable. The handler drafts were clean. The constants file suggestion came from Claude, not from us.
The output was only ever as honest as the problem statement we gave it.
Account types were inferred, not declared
The first real commit did not touch the UI at all. It standardized the account type taxonomy so that organizations, child events, and legacy standalone events each had an explicit type constant instead of being inferred from flag combinations.
The new constants live in a shared file that server-side code and the client-side package configuration both pull from. One file, read by both sides, instead of two files that were supposed to agree.
Before this change, adding a new account type meant updating three separate places and hoping they stayed synchronized. After, it means adding one constant and updating the routing logic that branches on it. The old way produced the silent zero. The new way makes a missing branch a visible gap.
The economics were scattered too
Organization credits are the economic primitive for organizations. A new organization starts with a credit balance. Each event it creates costs credits. Adding members costs credits. The per-pack size and prices are configuration, not logic.
None of that was true in the code. Starting credits were a hardcoded integer in the organization creation handler. Event costs were a different hardcoded integer in the event creation handler. Pack sizes for purchasing additional member slots were a third set of strings in the Packages UI template itself, formatted for display but never referenced by the engine that calculated what a purchase actually bought.
The fix was not clever. We moved all three into the constants file: a single starting-credit constant, an event cost per creation, and pack sizes as an array that the UI and the engine both read. When pricing changes, one file changes, and the number means the same thing everywhere it appears.
The UI fix came last
The Packages page redesign from Figma was the last thing we touched. The original design had per-event member controls, spinners that let organization admins set member counts at the individual event level. The redesign replaced them with a single global add-members counter tied to the organization credit pool.
That sounds like a simplification. It was the UI finally telling the truth that the model had been telling underneath since the taxonomy and constants changes landed. The per-event spinners encoded a lie, that member allocations were event-level decisions. They are organization-level decisions drawn from a shared pool. The spinner had survived from before the credit model existed.
Removing it was straightforward once the model was honest. The new counter reads the organization’s current credit balance, shows available members as a derived number, and posts a single add-members action. Claude generated the first draft of the handler for that action in about three minutes. It got the credit deduction logic right on the first pass because the constants were now in one place and it could read them.
What changed in production
New organizations now start with a fixed credit allocation. That number appears once in the codebase, in the constants file, and it flows through organization creation, the Packages display, and the credit ledger. Event creation costs a known amount. Adding a pack of members costs a known amount. The Packages page shows the real credit pool instead of a derived guess from three separate event-level tallies.
A second ticket also shipped on April 20. An orientation-flow refactor compiled and released to staging while the first was in review. Smaller change, same principle. The orientation flow had been inferring account type from the same flag cluster. It now reads the type constant directly.
Two separate tickets ended up making the same correction: read the declared type instead of inferring it.
Claude traced the call graph, drafted handlers, and caught two places where we had not updated all the references to the old flag-cluster pattern. It was useful for the mechanical work. It could not have found the root cause on its own, because the root cause was not a bug. It was a missing abstraction.
We have shipped plenty of pricing tweaks. Add a column here, change a string there, deploy, move on. This was not one of those. It was the day we admitted the organization model had never been named, and named it.
If a system shows different numbers on different screens, check whether the underlying model is declared anywhere or only inferred from flags and fallbacks. Then find where each price, credit amount and account type is defined, and whether every screen and calculation reads from that one place.