2026-07-13

Refactor Milestone: Hardening the Foundation

The last update left the beer app with a working scaffold: a React frontend, an ASP.NET Core API, Postgres, Docker, and basic login. That was enough to feel like progress, but it also masked a few gaps I hadn't actually checked.

Before starting on the app's real feature, a bartender-confirmed "mug club" progress tracker, I asked for a fresh audit of the whole project instead of just diving in. That audit turned into a CLAUDE.md file at the root of the repo documenting what's actually built versus what only exists in the planning docs. The gap was bigger than expected: the docs described roles, a confirmation flow, and a progress view. The code had none of it, and three foundational pieces were quietly broken underneath the parts that did work.

Three gaps hiding under a working demo

  1. No migrations. Program.cs called db.Database.Migrate() on every startup, but there was no Migrations folder anywhere in the project. That call was a no-op. A fresh database got zero tables.
  2. No seed data. Every new environment started with an empty beer list. Fine for a demo where I click "Add Beer" myself, not fine for anyone else trying to run the project.
  3. Roles that existed only in name. Program.cs already wired up AddIdentity<IdentityUser, IdentityRole>(), so the role tables existed in the data model. But nothing created the actual roles, assigned them to a user, put them in the JWT, or checked them anywhere. Every write endpoint was just [Authorize], meaning any logged-in user, not any particular kind of user, could create or delete a beer.

The fix

The migration was the easy part once I noticed it was missing:

dotnet ef migrations add InitialCreate

Seeding the three roles (Admin, Bartender, Customer) and a handful of sample beers went into a small SeedData.InitializeAsync() call, run right after the migration on startup, checking first so it's safe to run on every restart.

Role-based auth took a bit more care. New self-registrations now get assigned to Customer automatically. The JWT gets a role claim added at login. And the catalog's write endpoints changed from [Authorize] to [Authorize(Roles = "Admin")], so managing the beer list is now actually an admin action, not just a "logged in" action.

A bug hiding in plain sight

While wiring this up, I found the frontend's saveBeer() call never attached the JWT to its request at all. It just posted the form data with no Authorization header. Every "authorized" write had been failing silently the whole time, roles or no roles. That one was a two-line fix, but it wouldn't have surfaced without actually testing the auth flow end to end instead of trusting that [Authorize] on the backend meant the feature worked.

I verified all of it against a live Docker stack rather than just trusting the code: registered a plain customer, confirmed they got a 403 trying to create a beer, promoted them to Admin directly in Postgres, logged back in, and confirmed the same request now returned 201.

The PM angle

The tempting move here was to skip straight to the mug club feature. It's the actual point of the app, and it's the part that's fun to build. But shipping a bartender-confirmation flow on top of a database that doesn't reliably create its own schema, with an auth system that only pretends to check roles, just moves the bug further downstream and makes it harder to find.

This is the same call I'd make on a real project: before committing to new scope, confirm the foundation underneath it actually does what the previous status report said it did. The audit that produced CLAUDE.md wasn't extra work bolted onto the task. It was the thing that turned "add role-based auth" from a vague ask into three specific, verifiable gaps. Documentation that describes features nobody built yet isn't just an inconvenience. It's a risk that shows up as a surprise later, usually right when you're depending on the thing it claims already works.

Comments

Loading comments...