Macrop
Own product
A nutrition planner where no macro value is ever stored: every kcal, protein, carb and fat figure is derived from ingredient macros × quantity, so the math can’t drift.
Meal plans go stale because real life swaps meals, and every swap breaks the math. Macrop makes flexibility free: swap whole meals, rotate single ingredients, and daily targets and the weekly shopping list stay correct, instantly.
Demo account available on request.
- role
- Sole engineer
- scale
- ~148k lines · 3 workspaces
- stack
- TypeScript · Hono · Drizzle
- status
- Live at macrop.de
Architecture
One engine, two runtimes
The same pure TypeScript engine runs on the server as the authoritative source and in the browser for optimistic previews: the UI updates instantly and is guaranteed to agree with what the server persists. The boundary is enforced, not intended: lint rules block any import across it, and CI runs a dedicated architecture gate on every push.
packages/core pure macro engine: framework-free, no I/O
│
├── apps/api Hono · Drizzle · PostgreSQL authoritative
└── apps/web React · TanStack Query optimistic previews
│
hard HTTP boundary: cross-imports fail the lintAI boundary
The model proposes, the engine derives
Meals can be drafted from a sentence or from a photo, and none of that machinery is allowed near the arithmetic. The model returns ingredients and quantities (no prompt’s output schema has a total in it), so every kcal, protein, carb and fat figure on screen is still the same pure engine multiplying macros by quantity.
The provenance rule that governs the staple catalog governs model output too: a row the model estimated stays private to the household that imported it, its micronutrients left null rather than invented, and only a hit against a real nutrition source is promoted into the global catalog. A guessed number never becomes everyone’s number.
- 7 managed prompt slots, text and image: the I/O contract is code, while the prompt text, the provider and the model are edited and versioned in the database
- 6 providers behind one interface, capability-gated per slot, so a prompt moves from a hosted frontier model to a local one without a call site changing
- Every response is parsed by a bounded schema, pinned by a test: a hallucinated quantity is the one number that could inflate a plan, so it has a ceiling
- User text reaches the model fenced and labelled as data, never as instructions
- Per-call cost and rate-limit logging, alerting on failure, and auto-disable when a provider’s credits run out
- Every AI surface hides itself when unconfigured: the product runs with the whole platform switched off
Deep dives
Three hard problems
Optimistic UI that recomputes
Optimistic mutations don’t just flip a flag: the browser re-runs the macro engine client-side, shows the recomputed result immediately, reconciles it against the server’s authoritative response and rolls back on failure. Because both sides run the identical pure functions, preview and persisted state can’t disagree.
Shared meals in a household
One household-owned meal definition can be referenced from many members' plan slots, with per-member portion scaling, edit propagation to every reference and cleanup of orphaned definitions. Aliasing, ownership and lifecycle in a single feature: a real data-modeling problem, not a CRUD form.
Ingredient identity & tenancy
Ingredients live in a global catalog or scoped privately to a household, and every read is filtered by the authenticated scope: ownership of nested entities is verified by joining to the owning plan, so a forged id can never cross tenants. Multi-tenancy beyond "add a userId column".
Process
Guardrails a team would have
Solo doesn’t mean unreviewed: I built the guardrails a team would have, then automated them.
- A three-branch promotion flow (dev → staging → main), PR-only with tagged releases: merging staging deploys the staging box, merging main deploys production
- CI gates every push: lint, typecheck, the architecture guardrail, unit tests and integration tests against a dedicated PostgreSQL database
- Pre-commit and pre-push hooks fix lint issues before CI sees them and keep protected branches PR-only
- A self-maintaining audit suite of Claude Code commands that checks the repo against its own committed standards, grown into its own project, the Heal Suite
- lines of TypeScript
- ~148k
- workspaces
- 3
- test files
- 236
- external nutrition APIs
- 3
- releases
- CI-gated
Provenance
Born as a specification
This repository’s first commit is not code; it is a 1,315-line specification: the domain model, the decision defaults, the version pins with their reasons, and a standing rule to ask before anything consequential or irreversible. The second commit is the application generated from it, 155 files in one pass. Everything after follows the same split, and the history records it precisely: as of August 2026, 96% of the non-merge commits carry a Claude co-author trailer, and not one is recorded as hand-written. I did not type this codebase. I specified it, constrained it, reviewed it and merged it.
That is what the ownership claim on this page means. The decisions live in written artifacts the agents had to obey: the genesis spec (deleted from the tree, still readable in the first commit), plan documents that precede their implementation commits in the log, eleven path-scoped rule files, and a test policy with teeth: an agent may add tests freely but cannot change an existing one without my explicit approval in that session, enforced by a hook that intercepts the edit. And the merges are the signature: every one of the repository’s 1,315 merges is mine, by pure coincidence exactly as many as the genesis spec has lines; both counts are real, and the history landed on the same number twice. The repository itself is private (deliberately: the code is the product), so the history behind these numbers is shown on request, the same way the demo account works.
The record also shows the discipline earning its keep on the app itself: English consent copy once shipped still in German, because the translation gate guarded key parity but not whether a value was still the source string: caught, fixed and pinned with a test so it cannot recur. What the record cannot do is reconstruct a per-line human/agent split, and I will not pretend it can. The claim is ownership of the specification, the merges and the verdicts; the audit machinery that patrolled all of it grew into the Heal Suite, whose page carries the other half of this disclosure.
Retrospective
What I’d do differently
A sample, not the full list: four decisions that were reasonable at the time, and what each one cost.
- Pick the ownership model on day one. I built Macrop for one person and later made it work for a household, which meant repointing every record that says who owns what.
- Never match on a name a person reads. A food was recognised by its German name, so the first path that wrote English stopped matching: the same food quietly became two foods, with two sets of macros and two lines on the shopping list.
- Generate the two runtimes from one description. The macro arithmetic is a shared package, but the code feeding it (which meal variant is picked, what the response carries) is written twice, once per side. A lint rule stops the two halves importing each other; keeping them in step is a paired test fixture and a written rule, not a compiler.
- A fix is not a prevention. I closed findings one at a time, which held until the same classes kept coming back and each one cost me twice. A mistake worth the name now ends as a test, a lint rule or a pre-commit check that fails the next time, and automating that loop is where the Heal Suite came from.
Contact
Want the full tour?
The app is live at macrop.de, and I will walk you through the parts that are behind the login: the household model, the optimistic path and the release pipeline.
Ask for the walkthrough
Contact
A few lines are plenty. These three are what let me give you a straight answer instead of a follow-up questionnaire:
- What you are building. The product and roughly where it is: an idea, a codebase someone left behind, something live and struggling.
- What is in the way. The part that made you open a contact form. That is usually the whole brief.
- When it matters. A date, a launch, a funding round: anything that tells me whether I have the capacity.