SkinAtlas, part 1: explainability over magic
I wanted to know which product was making it worse.
That is a harder question than it sounds, because the answer is spread across a shelf of bottles, an ingredients list nobody reads, and a memory of six bad weeks that I do not actually have. So SkinAtlas keeps the record instead of me, and the founding rule is written into every design doc: explainability over magic. Every benefit, risk and conflict it shows traces back to a concrete record in an ingredient knowledge base I curated by hand. Nothing is a black-box model opinion, including the AI copilot, which is only allowed to answer from your own data and cites the records it used.
It’s the youngest of my projects (first commit June 21) and the fastest moving: 762 commits in under two weeks, through five roadmap phases. Paste an ingredient list and get an analysis; build AM/PM routines and get conflict detection; keep a skin journal and get correlations; photograph your skin and get a vision-model read, grounded in the same knowledge base.
This note is about that founding rule. Two more are coming: how the knowledge base grows by a hundred ingredients a night, and the memory system that lets a new session pick up where the last one died.

The discover page. Every card on it traces back to an ingredient record.
Boring choices, on purpose
The architecture decisions are recorded as ADRs, and most of them choose
the boring option. SQLite (via Turso) instead of Postgres, which forces
every list field to be serialized JSON and produced a small in-house
rule: all serialization goes through one helper module, no raw
JSON.parse in feature code. Hand-written CSS, no UI framework.
Anthropic models as the single intelligence layer, with one chokepoint
file so cost and behavior are tunable in one place; the skin analyzer
runs on a mid-tier model after a commit titled “swap Opus → Sonnet” made
the cost math work.
One decision was legal rather than technical: the obvious data source for ingredient information is a popular ingredient-decoding site, and SkinAtlas deliberately never scrapes it. The decision doc names the terms-of-service risk to a future paid tier and lists the alternatives I built instead: open product databases, the EU’s 24,000-entry cosmetic ingredient registry, retailer pages verified case by case.
The outage that improved the build
Three days after launch, production went down. It took three stacked causes to do it: a missing auth env var, a hosting quota, and a database schema that had never been migrated for the new auth tables. Each was invisible locally because dev-mode fallbacks masked them. The fix that mattered wasn’t any of the three patches; it was a build-time schema-drift check that now fails the deploy if the live database doesn’t match the code’s expectations. Fail at build, not at 2am.