Bespoke, Not Recolored: Starting the Industry-Specific Design System
Five demo tenants run on this codebase now, each with its own goal, its own vocabulary, its own staff title, its own confirmation formats — and every one of them still rendered the exact same layout, just recolored. A new epic, separate from both the Path C moldability track and the Monetization roadmap, started from a stakeholder ask that drew a real line: each industry should look and feel genuinely different, not just wear a different logo over the same shell.
The decision made before writing any code
The architectural question came first, deliberately, through a brainstorming pass rather than jumping straight to a plan. The choice was between a shared component that branches its styling by industry, and a genuinely separate component per industry per page, dispatched through a lookup keyed on industry. The second one costs more — five real components instead of one branching one, eventually — but it's the only version that can actually produce a "bespoke" result rather than a themed one, since a single component branching on a prop tends to converge back toward one shared layout no matter how much conditional styling gets layered onto it. That became the spec, with a shared TenantThemeContext doing the one job every one of those bespoke components needs done identically: knowing which tenant it's rendering for and what that tenant's branding actually is.
Two real gaps showed up during that investigation, and they got handled differently on purpose. TavernSettings had carried an Industry field since late July, but nothing had ever read it past the backend — a straightforward gap to close. The other gap wasn't fixable this phase at all: there's no mechanism yet for an anonymous, not-yet-logged-in visitor to be identified as belonging to any particular tenant. That's a real, open question — how does a new customer even land on the right business's login screen — and it got written down as an explicit, documented deferral rather than quietly worked around. Every industry's pre-login pages fall back to the Bar design for now, on purpose, until that prerequisite gets solved.
Catching the spec's own contradiction before any code existed
A self-review pass over the finished spec, before implementation started, found something the spec had gotten backwards about itself: the dispatcher example it used to illustrate the pattern fell back to the new Bar-specific design whenever an industry couldn't be resolved — directly contradicting the same document's own promise, one paragraph earlier, that the other four tenants would see zero regression. A fallback to the newest bespoke design is exactly the opposite of a safe default. Catching that in the spec itself, rather than three implementation tasks later once a fallback path had actually been built that way, is the cheapest place that class of bug can be found.
Scoping one page instead of nine
The spec's real Phase 1 scope was nine pages across the customer and admin experience. Writing a single implementation plan with complete, placeholder-free code for all nine at once would have produced something nobody could actually review — not because the pattern is complicated, but because a plan that long stops being legible as a plan. The scope that shipped instead was deliberately narrower: the one backend change every one of the nine pages would eventually need, plus exactly one page, BeerList, chosen because it was the stakeholder's explicitly stated top priority. The other eight get their own follow-up plans later, reusing the same pattern once it's proven on the first one.
An assembly line that still caught two real gaps mid-flight
Seven tasks, each with a fresh implementer working from nothing but that task's own brief, and a separate reviewer for each — the same isolation pattern that closed the multi-tenancy gaps back in Sprint 15, because a reviewer who's only seen one task's diff reviews the diff, not their memory of the whole plan. Two of those seven caught something the original investigation had actually missed.
Task 2 was supposed to be a single, contained change — MeController's two customer-facing endpoints returning the tenant's industry for the first time. The task's own implementer, not a later reviewer, spotted mid-flight that ConfirmationsController.cs had an identical private helper with the exact same broken constructor call that the first task's new field had introduced — missed by the original investigation because it lived somewhere the initial grep hadn't looked. The task's reviewer confirmed it with a targeted search of their own before it closed, and the fix landed inside the same task rather than spawning a separate one.
Task 4 — wiring the new theme context into App.jsx — shipped with a real design gap its own task reviewer caught: the original version only fetched a tenant's theme once, at mount. Anyone who logged in mid-session, without a hard page refresh, would keep seeing whatever theme had resolved before they authenticated, never the real one. The fix reused the exact event-listener pattern App.jsx already had wired up for its own progress state, rather than inventing a new one.
Task 6 — the actual new Bar design — carried its own pair of defects, both caught before the task closed rather than after. A migrated test asserted on the text "On Tap," which turned out to be ambiguous between an availability-filter button and a beer's own status badge; scoping the query to the specific beer's row fixed the false positive it had been silently producing. Separately, a leftover reorder of two JSX blocks — an artifact of an abandoned debugging attempt that never got reverted — had put the beer list visually above the filter controls, a real layout and accessibility regression the reviewer only caught by directly comparing the new component against the generic one it was built from.
What seven individually-clean reviews still couldn't see
Every one of the seven tasks passed its own review. A separate whole-branch review, dispatched only once every task-level review had already signed off, still found three real problems, and all three share the same shape: none of them are visible from inside any single task's diff, because they're about how the pieces behave once assembled, not whether any one piece is correct on its own.
The theme context's logout handling spread the previous, stale state into its reset instead of actually clearing it, meaning a visitor who signed out on the public /beers route kept seeing whichever tenant's branding had been active before they logged out. Nothing guarded against two overlapping theme fetches resolving out of order, so a fast login-then-logout could let a slower, now-stale authenticated fetch land after the logout had already reset things, silently restoring branding that should have been gone. And the page dispatcher itself ignored its own loading flag entirely, which meant a cold load of /beers would briefly mount the generic design, then immediately swap to the Bar design once the theme resolved — a visible flash, plus a duplicate network call neither version needed. That last one got fixed by gating the dispatcher on loading and rendering nothing until the real theme is known, a decision worth making carefully here specifically because eight more page dispatchers are about to copy this exact file as their starting point. All three landed in one consolidated commit, re-reviewed once, confirmed clean.
The PM angle
This is the same lesson Sprint 15 already taught the project once, showing up again in a completely different part of the codebase: task-scoped review is genuinely good at catching whether one piece is correct, and genuinely blind to whether the assembled whole behaves correctly, because integration behavior by definition doesn't exist inside any single task's diff. The fix isn't reviewing each task harder — two of these seven tasks already caught real gaps through unusually careful task-level review, which is worth crediting. The fix is treating a final, whole-branch look as a required step with its own distinct value, not a formality after the tasks are "done."
The decision I'd defend most from this stretch is scoping the plan down to one page instead of writing all nine at once. It would have been possible to plan the whole epic in one pass and call that thoroughness. It would also have produced a plan too long to actually review carefully, which defeats the entire point of writing one down in the first place. Proving the pattern once, on the page that mattered most, is a better trade than describing all nine and reviewing none of them properly.
Where the project stands
Merged to master as a fast-forward, twelve commits from the initial spec through the final cross-task fixes, full suites green — 576 backend, 359 frontend — plus a live Docker and browser check: a Bar tenant customer gets the real dark-wood-and-gold tap-list design, confirmed against actual computed styles, while a Coffee Haven customer still sees the exact same generic list that existed before any of this started. The zero-regression promise holds against a running app, not just a test suite.
What's explicitly not built yet: the other eight pages this same pattern needs to reach, and bespoke designs for the four industries that aren't Bar. Anonymous pre-login tenant identification remains the one open prerequisite blocking a second industry from ever getting its own pre-login experience — written down as a known gap, not a silent one.
The next session paused this epic entirely rather than continuing it, for a reason that had nothing to do with the design system itself — a demo drawing closer meant the Bar app specifically needed to be in working order first, covered next.
Comments
Loading comments...