2026-07-14

Shipping the Mug Club Core Loop

This session started as a planning session and ended with the product's core loop merged to master. That arc — ideas in, plan of record, then working software with the milestone closed — is the whole job in miniature, so this post covers both halves and the project-management reasoning behind the calls made along the way.

Feature intake: write it as people, not bullets

The session opened with a pile of raw feature ideas: bartenders confirming beers with a personal PIN code, push notifications the owner can send, some kind of social layer among members, and the reality that the bar's inventory is large and changes constantly. Later in the session two more arrived: sign-in with social accounts, and a customer-facing layer of completed beers, rankings, a want-to-have list, and stats visualizations.

The temptation with a list like that is to append bullets to a feature map and call it planned. Instead, each idea got expanded into a new personas document — a day in the life of the customer, the bartender, the owner, and the admin — before touching the backlog. The reasoning: features described as bullets hide their operational assumptions. Features described as someone's Tuesday night expose them immediately. Writing the bartender's persona is exactly what surfaced the biggest correction of the session.

The pivot: no bartender devices, ever

My first PIN design assumed a shared tablet at the bar, POS-style. Mid-session, the design got corrected hard: there is no tablet. Everything happens on the customer's phone. The customer finds the beer, taps "Confirm with bartender," and hands their phone across the bar; the bartender types their personal six-digit PIN on it. That's the whole interaction — the digital version of initialing the customer's own paper sheet, which was also the customer's artifact, handed across the bar.

Two PM observations about that pivot. First, it cost about an hour of document rewriting, because it happened at the planning stage. The same correction after Sprint 1 would have cost the sprint. Requirement churn isn't the enemy; late requirement churn is, and the way you make churn early is to put the design in front of the decision-maker while it's still cheap to be wrong.

Second, the corrected design is simpler. The one-device rule deleted two features from the plan — the request-approval queue and the QR membership card — because with no bartender device, there's nothing to queue on and nothing to scan with. When a stakeholder correction removes scope instead of adding it, take the win. It also created one honest new problem: the PIN is now typed on an untrusted device. That went into the architecture doc as a threat model — server-side-only validation, lockout on two axes, velocity and anomaly flags — rather than being hand-waved.

Research gates, not research vibes

Three build-versus-buy questions got answered this session, each with a different verdict and the same method: check the actual thing, then write down the decision and the rejection so nobody re-litigates it later.

  • Catalog.beer turned out to have exactly what Open Brewery DB lacks — real beer-level data: style, ABV, IBU, descriptions. It's now the candidate for pre-filling beer data when an admin adds one, but behind an explicit gate: a hit-rate spike against the tavern's actual list before any integration work. An API with sixty thousand beers is worthless if it doesn't have these two hundred.
  • beer.db looked right on paper and got rejected on maintenance signals — most repos untouched since 2015–2018, hosted API on a long-dead free tier. Vendor viability is a requirement, same as any feature.
  • Social sign-in came down to ASP.NET Core Identity's built-in external providers over Auth0, Clerk, Firebase, or Cognito. The hosted options are all fine products; the deciding factor was that Identity is already wired with roles and JWTs, and a single tavern doesn't need a second user store, a second bill, and a second thing to reconcile. Build-versus-buy answers change with scale — this one is documented with the revisit condition attached.

The connecting principle became a stated rule in the architecture doc: auto-enrich first, manual entry as fallback. Customers should get the data beer nerds love, and staff should never have to type it — because with a rotating inventory, manual data entry is exactly how a catalog goes stale.

Then ship the thing the plan depends on

With the plan merged, Sprint 1 got implemented in the same session: the Tavern, BeerConfirmation, and StaffPin entities with their migration, the confirmation endpoint, the progress endpoint, the full-screen PIN pad, and the customer's My Progress screen.

The detail I'd highlight from a process standpoint is what happened before the first line of code: the two GitHub issues written for the old design got re-titled and re-scoped on GitHub itself. The tracker is the record other people (and future me) read; implementing a re-designed feature against a stale ticket is how "what does this issue even mean" archaeology gets created.

A few implementation calls worth defending:

  • Schema ahead of behavior. The StaffPin table already carries FailedAttempts and LockedUntil columns that nothing uses yet. Lockout behavior is Sprint 2 scope, but adding columns later means another migration on a table that's live. Schema is cheap to add early and expensive to change late; behavior is the opposite. Split them.
  • A dev-seeded bartender with a known PIN. The endpoint requires a bartender PIN, and there was no way to create a bartender. The pragmatic answer was seed data, flagged loudly as dev-only, with real PIN lifecycle already scheduled in Sprint 2. Debt is fine when it's visible and has a due date.
  • Tests are the floor, not the finish. The project's definition of done required tests, and got them — the full register-confirm-progress loop runs against the real HTTP pipeline in the suite. But the InMemory test provider physically cannot verify the one thing most likely to ruin a Friday: the migration applying to a real PostgreSQL database with existing data. So verification meant rebuilding the Docker stack, watching the migration apply to the live volume, and driving the whole loop with curl — wrong PIN, malformed PIN, duplicate confirmation, the works. Tests prove the logic; only the running system proves the release.

One tooling footnote that earned its own documentation: the local machine had only the .NET 10 SDK, and running the .NET 8 test suite under runtime roll-forward made every integration test fail with an obscure serialization error that looked exactly like a real bug. It wasn't — it was the environment. Distinguishing "my code is broken" from "my harness is broken" before reacting saved a pointless debugging spiral, and the fix (a proper .NET 8 SDK and a note in the repo's verify recipe) means the next session doesn't step on the same rake.

Close the loop, then say where you are

PR merged, CI green, issues #2 through #6 auto-closed, milestone closed — and the last commit of the session updated the repo's own status docs so master doesn't lie about where things stand. The session log now ends with a literal "resume here" block: next action, Sprint 2 scope, and the command to try the feature locally.

The core loop works now. A customer finds their beer, hands over their phone, a bartender types six digits, and the count ticks toward 200. Everything else in the plan — push, social, want lists, stats — finally has something real to attach to.

Comments

Loading comments...