How it’s built
For the software nerd who also brews.
If you’ve ever opened the network tab on a brewing app, this page is for you. I build user interfaces for a living, and I wanted Beerwright to feel like the best software I use at work: instant, keyboard-friendly, and still there when the Wi-Fi in the garage isn’t.
It works like Linear
Linear showed that a web app can feel faster than a native one if the client holds the data and the network catches up in the background. Beerwright is built the same way. Your whole workspace, every recipe, batch, profile and bottle of yeast in the fridge, lives on your device in IndexedDB. Reads never wait on the network, so the style bars recalculate on every keystroke and there are no spinners in the editor.
Edits apply locally first, go into an outbox, and sync over a WebSocket when there’s a connection. In a cellar with no signal you can still brew, and it all catches up when you’re back upstairs.
you type ──▶ command ──▶ local store ──▶ screen updates
│ (IndexedDB)
▼
outbox ──WebSocket──▶ your Durable Object
│ same command code
│ SQLite, versioned
▼
your other devices and co-brewers
Every change is a command
There’s exactly one way to change your data. Adding a hop, moving a dry-hop day or scaling a batch is a named, typed, serializable command, and there are about a hundred of them. The same command code runs twice: once in your browser so the change is instant, and once on the server, which owns the real copy, gives it a version number and sends it to your other devices.
Because everything is a command, a lot comes almost for free. Undo is replaying a command’s inverse. Version history is the command log. Co-brewing on one recipe is just two people’s commands landing in the same place. And the same log always produces the same state, on the client or the server, which is something the tests check directly.
The brewing math is its own package
Gravity, IBU, color, mash pH, carbonation, blending and decoctions live in one pure TypeScript engine with no DOM and no I/O. It takes a recipe and returns numbers, so it runs in the browser, on the server and inside the assistant’s tools without a single change.
Being pure makes it easy to test hard. Results are checked against published figures and reference recipes, with property tests for things like scaling, and the app can show its work: open the explanation on any number and you’ll see how it was reached.
The engine is open source. It’s on GitHub as @beerwright/core under the Apache 2.0 license, the same code the app runs, with guides and an API reference. If you’re building brewing software of your own, you’re welcome to use it.
The assistant comes in the same door as you
Most AI features are a chat box that writes text. Here the assistant only gets typed tools, and they’re the same ones the app uses:
- It reads your recipe from the same store you’re looking at.
- It can only name an ingredient by searching the catalog, so it can’t invent a malt.
- It calculates with the same engine as the editor, so every number it quotes is real.
- It can’t write anything. It proposes a batch of the same commands you’d dispatch yourself, and nothing happens until you accept. Then undo, sync and history all just work.
None of that depends on a particular AI model. The guarantees come from the architecture, so they hold whichever model is behind it.
What it runs on
- Cloudflare Workers for the API, with a Hono router.
- A Durable Object per brewer, backed by SQLite, as the sync authority, and one more for each recipe you co-brew. It’s single-threaded, so writes never race, and it sleeps when nobody is connected.
- D1 for accounts, plans and share links.
- React 19, TanStack Router and Vite for the app, with TypeScript in strict mode everywhere.
- The catalog ships as versioned static data, cached at the edge.
How it’s tested
There are a little under five hundred test files. The Worker and the Durable Objects are tested in workerd over real WebSockets. A sync chaos harness throws dropped connections, duplicate and out-of-order messages, multiple tabs and co-brewers at the sync engine and checks it always settles in the same place. Performance budgets are measured in a real browser on every run.
Why I built it this way
One write path means one place for bugs to hide. A local-first client means speed and offline brewing aren’t features I have to keep adding, they’re what happens by default. A pure engine means the math can be tested on its own. And because the assistant can only act through commands, it can’t change your beer without you seeing it.
If you want to talk shop, about sync, the IBU model or anything else, email me at hello@beerwright.com. I’m always happy to.