Twenty-one days. Seventy-four commits. One production release.
Blood went from “I need to understand my blood test” to v2.0.0 in production in three weeks. This post covers where we are, what changed between the initial build and the production release, and what I learned along the way.
Current State: v2.0.0
Code:
- ~14,900 lines of code (frontend + backend, including the curated marker knowledge base)
- 74 commits
- 21 backend tests (unit + regression + E2E)
- 0 critical security vulnerabilities
Infrastructure:
- AWS eu-west-3 (Paris)
- CloudFront CDN (test + prod)
- Lambda (Python 3.12)
- S3 (frontend hosting + job state)
- API Gateway (throttled: burst 30, rate 10/s)
Costs:
- €22/month during development (includes testing overhead)
- Projected €30–50/month at 1,000 tests/month
- AI costs: ~€0.003 per test (Bedrock Nova 2 Lite — one conclusion call per analysis)
The v2.0.0 Story
The biggest change between the first working version and the production release wasn’t a new feature. It was removing something.
The original architecture used Bedrock to explain every outlier individually. A test with 10 outliers triggered 10 AI explanation calls plus one conclusion call. That’s expensive, slow, and inconsistent. The explanations were fine — but they weren’t better than what a properly curated knowledge base could do.
So I built markerInfo.js: 84 canonical markers, each with a plain-language description, “too high” and “too low” explanations, practical diet and supplement tips, and verified source links from NHS and MedlinePlus. No AI writes these. I wrote them, verified the sources, and they don’t hallucinate.
The result: one AI call per analysis instead of 11. Faster responses. More consistent explanations. Better sources. Lower cost. DATA_VERSION bumped to 2 — any result stored under v1 falls back gracefully.
This is what the AI role looks like now:
| Task | v1 approach | v2 approach |
|---|---|---|
| Extract markers | Bedrock | pdfplumber → Bedrock for JSON structuring |
| Explain outliers | Bedrock (one call per outlier) | Curated knowledge base |
| Overall conclusion | Bedrock | Bedrock (unchanged) |
One AI call. One holistic conclusion. Everything else is deterministic.
Features Complete
- PDF upload (drag-drop + click-to-browse)
- Password-protected PDF support
- Photo upload (JPEG, PNG, WebP, GIF, HEIC/HEIF — canvas resize, Bedrock vision path)
- AI analysis (extraction + conclusion)
- Curated per-marker explanations (84 markers, verified sources, diet/supplement tips)
- Marker Guide page (full browsable knowledge base with sidebar, sparklines, sources)
- Results visualization (range bars, donut charts, trend lines)
- IndexedDB storage (persistent across sessions)
- Test history (list, delete, navigate)
- Trends page (worsening/improving/stable classification)
- Manual entry mode (JSON paste or form)
- BYOK flow (bring your own AI — extract text, generate prompt, paste result)
- Edit existing results (inline marker editing, add missing markers, edit date/lab)
- Privacy notice (full GDPR compliance)
- Consent banner (AI as recommended option)
- Feedback widget (Discord notifications)
- Mobile responsive (iOS Safari + Android Chrome tested)
- tinycount analytics (fire-and-forget, privacy-respecting)
- Re-analyse flow (version check, inline password prompt)
Technical Debt Inventory
Intentional Debt (Will Refactor Later)
- Test coverage: 21 backend tests, minimal frontend tests. Adequate for MVP, needs expansion before scale.
- Error messages: User-facing errors are generic (“Analysis failed”). Should be more specific without leaking implementation details.
- Marker knowledge base editorial workflow: No formal process for adding or improving marker content in
markerInfo.js. Currently: edit and ship.
Lessons Learned
What Went Well
- Constraints enabled speed: “Privacy by architecture” eliminated entire classes of decisions — no database design, no auth system, no server-side state to reason about.
- Async job pattern: Beat the 30-second API Gateway timeout cleanly. Reusable pattern for any slow AI workflow.
- Curated knowledge beats generated knowledge: Removing per-marker AI explanations and replacing them with a verified knowledge base made the product faster, cheaper, and more consistent. The instinct to “use AI for everything” was wrong.
- Bedrock Nova 2 Lite: Right balance of cost, speed, and EU residency for the one job it still does: the holistic conclusion.
- Svelte 5: Small bundles, fast dev loop, no runtime bloat.
- GitLab CI/CD: Auto-deploy to test on every commit caught bugs early.
What I’d Do Differently
Mobile testing was an afterthought and that was embarrassing. I tested on desktop. Desktop looked great. My friend opened Blood on his phone, couldn’t read half the analysis cards, and had to tell me about it. I’m a Product Owner — I should have caught that before a user did. Classic “I am the target audience” trap.
The progress indicator was a known problem I chose to ignore. I shipped the infinite spinner knowing it was bad. Two days later, my first user thought the app had crashed. It cost me one conversation and thirty minutes to fix something I’d already identified as wrong. Wrong priority order.
I almost shipped AI explanations that didn’t need to be AI. The per-marker explanation architecture felt clever. Bedrock explains each outlier with context, sources, tips. But the explanations were generic — they said the same thing for every user with elevated LDL. A curated knowledge base does that better, faster, and without hallucinating URLs. I should have started with curated data and only used AI where it genuinely adds value (personalized conclusions, holistic pattern recognition).
The pattern in all three: I optimized for technical correctness instead of user experience and product correctness.
The Philosophy Holds
Three principles guided every decision:
- Privacy by Architecture: No server-side storage. No accounts. No cookies. IndexedDB only.
- EU Data Residency: Everything in eu-west-3. AWS DPA for GDPR Art. 9 compliance.
- AI as Translator: Explains context, not prescriptions. Amplifies human judgment.
v2.0.0 pushed this further: by removing AI from per-marker explanations, the product became more predictable, not less useful. The AI translates the overall picture. The curated knowledge base explains the individual markers. Humans decide what to do next.
These weren’t constraints that slowed us down. They were the product.
What’s Next
Post-launch priorities:
- Rate limiting — Before any public sharing beyond closed beta
- Google Drive sync — Most requested feature (cross-device access)
- Multi-language — Dutch and French for Belgian market
- Payment integration — Freemium: full analysis free, Trends paid
And I’ll keep writing DevLogs. If something interesting happens — a lab reaches out, the pricing model turns out to be wrong, the curated knowledge base needs a rethink — it’ll be here.
Twenty-One Days
I started this project because a PDF sat on my desktop for three days.
Twenty-one days later: v2.0.0 in production. Seventy-four commits. A product that processes blood tests, stores them privately on your device, explains what’s out of range from a curated knowledge base, generates one personalized AI conclusion, and tracks trends across every test you’ve uploaded.
The gaps are real. p95 latency is 34 seconds when the target is 30. Rate limiting isn’t done. Test coverage is thin in the frontend. But the core idea — that the gap between “here’s your data” and “here’s what it means” is solvable without giving your health data to a server — held through every decision.
The AI does less than I originally designed. The product is better for it.
This is post #6 (final) in the Blood Development Log series. Read post #5 → | Series index →
Blood is built by DGTL bv. Privacy-first. EU-hosted. AI-powered.
🀄