Tazzina
Reducing the waste and guesswork of dialing in coffee.
My favorite interactions
I like making products that feel alive by approximating tangible, real-world objects and experiences. Tazzina, my coffee dial-in app, is where I leaned into that hardest.
Context
Tazzina is the first app I've made in years. I built it because getting into espresso exposed a practical problem: every new bag starts from a guess. Grind size, yield, time, and sometimes temperature all affect the result, and changing the wrong variable can make the next shot worse.
At first I only wrote down grind size on masking tape and stuck it to the bag. It proved the need, but not the solution. Once I had multiple beans in rotation, the variables became harder to remember, the settings changed as the beans aged, and buying the same bean again meant starting from memory.
The bet
The obvious solution was to use the Notes app. That solved part of the problem, but only as a generic place to write things down. Espresso recipes have a shape: dose, grind, yield, time, and a few variables that only make sense in relation to each other.
Therefore I built Tazzina around the espresso workflow. A user saves the recipe that worked for a bean, archives the bean when it runs out, and sees the recipe again when they buy that bean later.
The bigger bet is this: every saved recipe can make the next user's first shot better. If enough people scan and save the same bean, Tazzina should be able to say "start here" instead of leaving every person to waste the same two to five shots finding the right variables.
Technical decisions
I started with a lightweight web app, but the workflow kept pointing back to native iOS. Scanning a bag, checking a widget, or glancing at a watch are small interactions but they matter when the user is standing next to their machine.
I chose Convex instead of iCloud because Tazzina is not only a private utility. It needs sync, a real backend for data aggregation, and a path to other platforms without rebuilding the product around a different data model.
The guided dial-in flow also had two jobs. It helps beginners by changing one variable at a time and avoiding overcorrection. Strategically, it also produces cleaner recipes, which makes future suggestions more accurate.
Strategic direction
The most interesting technical problem was hidden inside the simplest field: grind size. A recipe that says "grind 6" only makes sense if I know which grinder produced that number. A Breville might go from 1 to 16, a Niche from 0 to 100, another grinder from 0.0 to 10.0, and another from 1A to 10F.
The first version of the recommendation loop is already valuable. Tazzina can say that five people with the same grinder tend to use grind size 7 for this bean. The recommendation is not generic. It is anchored to the grinder the user actually owns.
The big ambition is cross-grinder translation. As more recipes connect the same beans to different grinders, Tazzina can start to infer how a setting on grinder A maps to a setting on grinder B. That means the app can suggest a starting point even when no one has used that exact bean with that exact grinder yet.
What did not work
Bean recognition through scanning was a convoluted problem. I first tried extracting only the roaster and bean name from OCR, with blocked words like roast levels or tasting notes that yielded unreliable results. I tested LLM-based extraction which was more reliable but slower and more expensive than the job warranted.
The simpler approach proved to be the best: save all OCR-extracted words as signals to match, and ask users to enter roaster and bean names at bean-creation time. Those fields can be verified later, while the broader bag text compounds future scanning accuracy.
Managing the library
One thing I underestimated was how much workflow there would be around bean verification. Convex was a good backend, but the console was not made for this kind of management. To verify one bean, I might need the submission open in one tab, the existing bean record in another, Google in a third, a color picker somewhere else, and a contrast checker after that. Even simple decisions like "is this the same roaster?" were awkward when I was cross-checking names inside JSON rows.
So I built a small admin console for myself. It puts the review queue, bean details, merge options, web search, color selection, and contrast checking into one place, shaped around the actual steps I take when cleaning up submissions.
It is not a public feature, but it is one of the things that makes the public feature work.
Scope control
Some of the most interesting product decisions were not about what to add, but about what Tazzina should refuse to become.
I tried designing a dialing in mode through a tasting compass, but the classic espresso compass has always felt almost unintelligible to me. It assumes users can have enough taste vocabulary to describe what is wrong. I wanted the app to guide judgment without making users pretend to be professional tasters.
I also decided against puck prep guidance and shot logging. Both were useful adjacent ideas, but they would make Tazzina an espresso teacher or a daily tracking app. I only care about the recipe that worked. The app should help users get there, save it, and then get out of the way.
There were other cuts. Official bean photos had copyright implications, temperature tracking was too advanced for v1, sharing was useful for distribution but premature, and broader brew methods like V60 or Aeropress belonged to a later version.
That later version has since arrived. Once the bean and recipe model proved solid, Tazzina expanded beyond espresso to pour-over, Aeropress, moka pot, and more. The core loop did not change. You still save the recipe that worked and start the next person closer to it. It just stopped being about a single way to make coffee.
Design
I wanted the beans to be the hero of the app. Coffee bags already have a strong visual language, so I wanted that to define the visual identity of the product. Bean cards use user photos presented like small polaroids, colors identified from the bag, and subtle 3D motion to make the objects feel tactile.
Impact
- Before
- A new bag started from a guess. My best system was writing grind size on masking tape, and dialing one in cost two to five wasted shots.
- After
- Each bean keeps its own recipe, the guided flow moves one variable at a time, and rebuying a bag brings the recipe back. A new bag now takes one to two shots, sometimes none when a suggestion exists.
I designed and built Tazzina end to end on my own: the SwiftUI app, backend and sync, OCR bag scanning, the recipe-suggestion engine, analytics, the marketing site, and an internal admin console for curating the catalog. Owning the whole stack kept the product and the data model honest, since every scope cut had to survive what I could actually build and maintain.
The cross-grinder translation engine is live. It maps a setting on one grinder to a setting on another, so it can propose a starting point even for a bean and grinder pairing no one has logged yet. Every saved recipe sharpens it, and the recommendations compound as more people dial in the same beans on different equipment.
I built in analytics around the moments that mattered most: whether people use suggestions, how many attempts a bean takes, where scan or search fails, and how often people return. My own use has already validated the shape. I ignored a grind nudge and later found it was right, reached for the watch app when my phone was elsewhere, and rescanned a bean I had forgotten buying, which brought back grind 6, 36g yield, 93°C. As the catalog grows, the same loop turns two-to-five wasted shots into a credible first shot for the next person who scans that bag.