The 50 iterations
retrospective.
Fifty build iterations. 348+ automated tests. 36 journal posts. Four locations served from a single codebase. Roughly 900 mug-club members on the rack. Here is the honest milestone read on the most technically-loaded BBQ operation we could figure out how to build — what worked, what we don’t yet know, and what’s next.
Figures are operator estimates rounded to honest precision. Mug-club count is approximate — the D1 portal’s the source of truth, this read is roughly current. Test count is the iter-50 figure committed to CI.
We set out to build the most technically-advanced BBQ operation we could.
The pitch, fifty iterations ago, was simple and a little naive. Brisket and post oak on one side. Cloudflare Workers, D1, edge-rendered everything on the other. The hypothesis: a regional BBQ operation that’s actually pleasant to read — by humans, by Google, by ChatGPT, by the agent that’s going to plan somebody’s Saturday in two years — wins the compounding game over a decade. We picked the long compounder.
Fifty iterations later, the operation is four locations, one codebase, a ~900-person mug club, a catering pipeline that quotes itself, and a journal with thirty-six posts in it (this one’s thirty-six). The site reads as well to a screen reader as it does to a search bot. The D1 schema is the source of truth for what’s on the menu at every location at every hour. The smoker still runs on post oak. Both halves of the original pitch survived contact with the build.
This post is the honest read on what fifty iterations produced. Numbers where they exist. Hedges where they don’t. A short list of the hard parts at the end — because every milestone post that doesn’t have a hard-parts section is a sales pitch dressed up as a retrospective. The longer arc — the Cantina, the brewery partners, the thirty-Saturday accounting — lives at our first thirty Saturdays.
Cloudflare Workers, D1, and a 7-minute Lighthouse rebuild.
The site runs on Cloudflare Workers via OpenNext. D1 is the database — SQLite at the edge, replicated, with the admin portal and the catering booking system reading and writing from the same schema as the public surfaces. Everything that can edge-render does. The pages a first- time visitor hits paint from the closest data center to their browser, not from a US-east origin and back. That shape matters more than the marketing language around it implies — Lighthouse scores on a cold cache are the first thing every honest infrastructure post should quote, and we lean on that.
The test suite is 348-plus assertions across unit, integration, and route-level checks. Every push runs the suite plus a Lighthouse rebuild — the full cycle lands around seven minutes end-to-end. We treat the suite as the safety net, not the spec. The spec is the menu and the cambro discipline; the suite catches the regressions. Fifty iterations into the build, the suite has caught a wood-doctrine drift that would have shipped (more on that in §07), a JSON-LD violation on three pages, several broken canonical paths, and one D1 migration that would have nuked a column the admin portal needed. Each one of those is worth more than the seven minutes the suite costs.
The honest trade-offs are worth naming. Workers don’t have a filesystem in the Node-fs sense, so anything that wanted to read a local PDF at runtime had to move to inline buffers or live as a static asset. D1 is SQLite, which means no PostgreSQL extensions and no transactional isolation guarantees the way a Postgres backend would offer. Next 16’s typegen has a quirk around folders that combine a dynamic segment with a literal dot-suffix — see AGENTS.md — which we hit on the voice-friendly menu route and worked around with a two-segment shape. And schema-dts has its own opinions about which Schema.org subtypes it will let you author cleanly. None of these are deal- breakers. All of them cost real iteration time. The next operator who builds on this stack should know.
llms.txt, /openapi.json, voice-friendly menus, structured data everywhere.
The thesis on the AI side: in five years a meaningful share of restaurant discovery is going to flow through LLM agents — somebody saying “find me a brisket spot in Colorado Springs that’s open Wednesday night and serves beer” and the agent reading actual structured data, not screen-scraping a Yelp card. We don’t know if that’s right. We bet it is. The site ships a /llms.txt at the root, a /openapi.json describing every JSON endpoint, a /api/menu/voice- friendly.json that returns the menu in a shape an audio agent can read out loud without choking on em-dashes, and structured-data JSON-LD on every page that warrants it (LocalBusiness, MenuItem, Article, BreadcrumbList, JobPosting, Recipe — the whole expected set).
The hedge: we don’t yet have numbers on AI-driven traffic. Too early to claim that worked. Server-side log parsing tells us a handful of GPTBot, ClaudeBot, and PerplexityBot user agents are pulling pages on the regular — they show up in the daily access logs — but attributing real restaurant visits back to an LLM agent query is genuinely hard and we’re not going to pretend the attribution is solved. What we can say is the operation is structurally prepared for that traffic. When somebody asks ChatGPT about brisket in Colorado Springs and it pulls a real answer back, the answer should be accurate, because the source is the same D1 row the floor staff reads off the iPad.
The lesson we did learn — and we’ll fight anyone on this — is that being LLM-readable is the same discipline as being screen-reader-readable, which is the same discipline as being indexable. There aren’t three separate stacks. There’s one stack — semantic HTML, honest structured data, alt text on every image, JSON for the things JSON’s good at — and you either ship it or you don’t. The AIO work made the rest of the SEO work better. It did not cost the SEO work.
~900 members and an SMS magic-link portal.
The mug club is the program that holds Tuesday through Thursday up at the Cantina. Numbered ceramic mug, $99/yr, perks at four locations and three brewery partners. Roughly 900 members on the rack today — operator estimate, the portal’s the source of truth and the number rolls a little week to week as renewals cycle. The honest read is that the floor under midweek service is a real, measurable thing and the program made it real.
The portal lives at /mug-club and runs on D1 with an SMS magic-link sign-in (Twilio on the send side, D1 on the verify side, no password ever stored). Members tap a link, get a six-digit code, see their mug number, their renewal date, and the four-location perk grid. No email-and-password reset funnel, no password manager dance — the lowest-friction shape we could build for a customer who’s standing at the bar in a brewery on a Tuesday. Total auth surface area, in security terms, is roughly “your phone number plus a code that expires.” That’s the right trade for this surface. The catering admin portal uses a different, tighter auth shape — different threat model, different answer.
We will not pretend the mug club is the marketing event the program-launch posts implied it would be. It’s an operational tool. It produces a predictable Tuesday- Thursday floor. The dollar value of that floor — versus the membership revenue itself — is the number that actually matters and we’ll write that math up properly when we have the second twelve months of data to anchor it.
Four locations from one codebase. Location-aware everything.
The Cantina runs the full menu. The three brewery- parked trucks run an abbreviated sandwich-and-sides format. Each location has its own hours (the brewery partners post hours that drift week to week with their brewery’s schedule), its own menu subset, its own FAQ, its own structured-data block. All of that comes out of a single codebase. The map from location → menu items lives in one TypeScript file. The hours feed lives in D1. The AI-agent queries that ask “what does Stage Stop serve at the Star Ranch location on a Wednesday” pull the answer from the same source the floor staff reads off the iPad.
The single-codebase posture is the thing we’d fight for hardest if a future operator came in wanting to fork per-location. Forking is fast at iteration five and a disaster at iteration fifty. We caught a wood- doctrine drift in §07 precisely because every location page was reading from the same canonical wood list — the drift was visible because one place said “hickory” on a page that should have said “post oak.” A forked-per-location codebase never surfaces that bug; it just lets the wrong wood go quietly into production and confuse a customer who came in asking about hickory because the website promised it.
The economics of the brewery-truck shape are unchanged from the economics of running a brewery truck: no rent, ~12% gross to the brewery, four staff per truck against eighteen at the flagship on a Saturday. What’s new at iteration fifty is the digital shape — the per-location menu feeds, the per-location hours feeds, the per-location structured data. The physical operation was easy enough to spin up at the first brewery partner. The codebase that scales it to the fourth and the fifth was the work.
From a PDF to a calculator, a calendar, and an admin dashboard.
Catering started as a single endpoint that emailed back a PDF quote. Twelve iterations later it’s a calendar at /catering/calendar (read-only, paints confirmed bookings, ISR five minutes), a smart-quote calculator that prices headcount against the occasion landing pages, and an admin dashboard at /admin/catering where the operator sees pending quotes, confirms or declines them, and watches the booking calendar fill up in real time. The D1 schema for catering_bookings is the spine — the public calendar reads from it, the quote calculator writes to it, the admin dashboard edits it, and the operator’s confirmation flips the row from pending to confirmed.
The thing that took twelve iterations was not the code. It was figuring out which decisions the calculator should make automatically and which decisions the operator should make in the admin. Headcount-times- price-per-plate is a calculator decision. Whether to accept a 200-person wedding the same weekend as a brewery beer-release party is an operator decision. The calculator that tries to be the operator ships an over-quoted brisket plate to a customer who’s never coming back. The operator who tries to be the calculator answers a quote at 11pm on a Wednesday and gets the math wrong. Twelve iterations of separating those two things produced the version that’s live today.
The four months of under-priced catering we wrote about in our first thirty Saturdays would not have happened if the calculator had existed earlier. It didn’t. We were quoting catering off a trailer-era spreadsheet for the first four months after the Cantina opened. The calculator now reflects the flagship-era floor. Future iterations of this business eat that mistake exactly once.
Wood doctrine drift, schema violations, and a competitor post we noindex'd.
Three hard things, named honestly, in the order they hurt the most.
First: wood doctrine drift across nine pages, caught only by adversarial verification. The Stage Stop pit doctrine is post oak on brisket, apple-and-cherry on pork, hickory-and-pecan on ribs. That doctrine is written in the AGENTS.md voice rules. Around iteration thirty-six, an adversarial sweep — running the published site against the canonical wood list — caught nine pages that had quietly drifted. Some said hickory where they should have said post oak. One winter-smoking-tips post recommended a wood we do not actually use. Those drift bugs do not get caught by a type checker. They get caught by an operator who reads every page out loud against the doctrine, or by a test that does the same thing. We added the second kind. We still do the first.
Second: structured-data violations that would have shipped. Schema.org JSON-LD is not enforced by the browser. You can ship a Recipe block missing the required prepTime and nothing breaks visibly — until Google’s rich-results tester quietly drops the page out of the carousel and you don’t know why your recipes stopped showing up in search for a month. Our suite now validates every JSON-LD block against its declared @type. We’ve caught at least three would-have-shipped violations that way. The cost of the check is a few hundred milliseconds in CI. The cost of the bug is a month of lost rich-results eligibility. Easy math.
Third: the operator-confirmation hedge on the competitor post. We wrote comparing BBQ in Colorado Springs — a piece that names Front Range BBQ, Bird Dog BBQ, and Chuckwagon 719 and compares them to us. It ships today with robots:noindex and a draft banner because Chef Eve has not yet hand-verified each hedge-cited claim with each named operator. That post will not come off noindex until those phone calls happen. That is the right posture. Naming a competing business in print, even with hedges, without the operator’s confirmation that the claims are accurate, is how you end up in a small-town restaurant-industry argument that nobody wins. We are willing to delay the post. We are not willing to ship an unverified comparison.
Pass-with-notes, parallel pipelines, and cron as the single source of truth.
Three things we learned about building this kind of operation autonomously, in honest order of how load-bearing they turned out to be.
First: pass-with-notes verification beats pass-fail. A test that says “this route loads and emits valid JSON-LD, but the wood-doctrine sweep found two places that say hickory where we expected post oak” is more useful than a test that just passes. Pass-fail catches the regressions you wrote the test for. Pass-with-notes catches the drift you didn’t. We carry notes through CI even when the suite is green. The next iteration reads the notes first.
Second: parallel pipelines. The site, the admin portal, the JSON feeds, the structured-data layer, and the OG image generation are five separate pipelines that share data but ship on independent cadences. The structured-data layer can land a fix without the OG images regenerating. The admin portal can ship an iteration without the public site rebuilding. The shape kept iteration speed up past iteration thirty when a monolithic build would have started to drag.
Third: cron is the canonical cadence. The single source of truth for “how often does this thing run” lives in wrangler.jsonc, not in the route docblock and not in a separate scheduling doc. The docblock can lie. The cron line cannot — if the cron says every six hours, the route runs every six hours, and any docblock that disagrees is wrong. We have caught at least two stale docblocks by going to wrangler.jsonc as the source of truth and writing the docblock from it.
Operator surface, sourdough pilot, a Pueblo conversation, Google for Jobs.
Four things on the next-fifty horizon, in honest order of certainty.
First: onboarding the operator surface. The admin portal exists. Chef Eve uses it. The next step is making it usable by a second operator — a sous chef at the Cantina, a truck lead at one of the brewery partners — without a thirty-minute onboarding from the build side. That’s a UX problem more than a code problem, and the UX problem is the work of the next ten iterations.
Second: the sourdough-night pilot starts the first Wednesday in July. Eight-week run. Sourdough loaves baked overnight in the brisket smoker, Wed/Thu only, paired with smoked-tomato bisque. The hypothesis is that midweek wants a draw the mug club alone can’t deliver. The code side ships the menu surface, the pilot result ships the food side.
Third: a Pueblo truck partnership. We are exploring — no announcement yet, no contract signed, no brewery named. Pueblo customers driving up for Friday tacos was one of the surprises of the first thirty Saturdays. The model extends south naturally. Two conversations have happened. Both went well. We will not name the brewery until there’s something to name.
Fourth: getting on Google for Jobs. We ship JobPosting structured data on every open role at /jobs today. The roles are real. The structured data is valid. The remaining work is the indexation feedback loop — confirming Google is actually picking up the postings, watching the Search Console for the Google-for-Jobs panel, and iterating if it doesn’t land. This is boring and unglamorous and the kind of thing every regional operator should be doing. We’ll write up the result when there’s a result.
Beyond those four, the longer arc holds. More brewery partners where the cadence fits. A possible second flagship up the Front Range. The catering side becoming its own vertical. None of that is news in the next fifty iterations. It is the horizon, not the calendar. We are fifty iterations in. We are not done.
The journal, or the menu.
Thirty-six posts in. The brisket math, the brewery economics, the cambro discipline. Or skip the read and look at what's on the smoker.