← Back to Work

Personal Project · 2026

Brew Log: A Homebrewing Batch Tracker

I homebrew mead. I wanted a more focused tracker than Google Docs, but didn't want to pay for it. So I built one — with no framework, no dependencies, and nothing stored anywhere but your own device.

Type Personal Project
Stack Vanilla JS (ES modules), localStorage, IndexedDB — zero dependencies
Status Live
LIVE PROJECT Open Brew Log → View source on GitHub →

How It Started

I homebrew mead. For a while I tracked batches in Gdocs, which... worked... but quickly grew out of control and sloppy as my notes merged in with the instructions.

This didn't start as a product idea. I wasn't trying to build something for other brewers. I just wanted a tool that fit the actual workflow of making a batch — recipe first, then tracking, then notes — without forcing me to switch between different files or apps.

What I Built

Each batch gets a card with a photo thumbnail, a recipe editor with an ingredients list and step-by-step instructions (markdown supported), and a gravity log that estimates ABV as you add readings. Tasting notes have attribute tags so you can characterize a batch in a consistent way across batches without free-form text getting hard to compare.

Brew mode turns the recipe steps into a sequential checklist designed for use at the kettle — big tap targets, minimal chrome, usable with wet hands. When you work through the last step, the batch auto-marks as "It's done!" — small thing, but satisfying in practice.

The architecture is localStorage for structured metadata and IndexedDB for photos, keeping everything on-device. There's no backend, no account, no framework. A full JSON export/import lets you back up or move data. Recipes can also be shared as a URL with the recipe encoded entirely in the URL hash — no server needed, just a link.

Cross-device sync is available as a personal feature flag, powered by a separate tool I built called Syncer. Public visitors get a clean, self-contained app; I get seamless multi-device sync. One codebase, two experiences.

A Few Design Decisions

No framework. Intentional. The constraint of vanilla JS forces DOM fluency and keeps the app fast, portable, and free of build tooling. There's nothing to update, nothing to break. It's also a useful demonstration that "no dependencies" doesn't mean "no polish."

localStorage as primary store. Working within localStorage's 5MB cap forces discipline around data shape and makes the import/export contract explicit rather than implicit. The constraint is a feature: it keeps the data model lean and the backup story simple. Photos go to IndexedDB — something I'd been aware of in the abstract, but the async API took real getting used to before the split felt clean.

Manual save over autosave. I started with autosave, which seemed like the obvious choice. But it was causing focus issues on text fields — fire too often and it'd yank the cursor mid-sentence. Switched to manual save with an unsaved-changes warning on close. Simpler and less surprising.

Feature-flagged sync. Adding sync via Syncer later in the project didn't require a new codebase or a separate deploy. A single flag separates the "personal power tool" version from the "public-facing product" version. The flag keeps the public experience intentionally minimal — no auth prompts, no sync UI — while I get the feature I actually wanted.

What I'd Do Differently

The shareable recipe links were added late, as a nice-to-have. They work, but they're bolted on rather than designed in — the URL is longer than it needs to be because the entire recipe object gets encoded rather than a leaner representation. If I'd thought about sharing earlier I'd have built a minimal data model for it from the start. Not a real problem in practice, but a clear sign of where the architecture was added versus planned.

Cross-device sync turned out to be the bigger missing piece — less for sharing recipes and more for just being able to pull up a batch on my phone while standing at the kettle. I did eventually address it with Syncer, but it would have been cleaner to think about sync boundaries earlier rather than retrofitting them. The feature flag makes the seam invisible to users, but it's still a seam.

All said, it gets the job done and looks decent. Go ahead and try it — just remember that the data is stored locally, so make sure to save, import, and export accordingly.

Open Brew Log → View source on GitHub →
← Back to Work