EONIA — An app that doesn’t recommend: it decides
The missing layer of biological decision between measuring and intervening · Own product · Health · Local-first + AI
- Client
- RBT Studio’s own product
- Role
- Product, system design and full-stack + AI development — sole author
- Timeline
- June 2026 → ongoing (MVP v1.0 + server increment v1.5)
- Stack
- TypeScript, React Native · Expo, Skia + Reanimated, NestJS, PostgreSQL · pgvector, LangGraph + Claude, Render · EAS
The high-performance user has more biological data than any previous generation — and still takes the same supplements every morning, however they are doing that day. The market solved measurement (Oura, WHOOP) and intervention (evidence-based supplementation), and left the middle empty: the decision. A system that recommends shifts the cognitive load onto the user; one that decides absorbs it. EONIA turns a 30-second check-in into a calculated biological state and a capsule architecture adapted to that state, on the device.
The problem: The gap is in the decision, not the data
Measurement and intervention are well solved by the market; between them there is nothing. The difference isn’t incremental but structural: the EONIA user doesn’t need to understand the HPA axis to benefit from it. Every screen answers a single question — what do I need to do right now — with no charts and no jargon up front.
How it was approached
- Specify before programming, and decide in writing. The project started with a complete document set (SPEC · ARCHITECTURE · SCAFFOLD · DESIGN · HANDOFF) before the first line of product code, and every structural decision was recorded as an ADR with its discarded alternative. It isn’t bureaucracy: in a product that touches health, traceability of why the system decides what it decides is part of the product.
- The decision engine is pure and deterministic. @eonia/engine has no I/O, no React, no network, no language model. A check-in goes in, a state and an architecture come out. It is the only place where decisions are made. A deliberate consequence: the same package runs on the device and on the server and produces exactly the same verdict — which means offline mode isn’t a degraded version, but the same system.
- The Orb is an entity, not a chart. The central visualisation (Skia + Reanimated, a sumi-e brush stroke) has to communicate the biological state pre-cognitively: in under a second, without reading or interpreting. A donut chart would have been cheaper and would have failed the only requirement that mattered. The three layers of information — what to do, why, and the physiological mechanism — are an architectural boundary enforced in the code, not a design convention.
- Where AI comes in. The narrative layer generates the personalised report with LangGraph and Claude, but under a hard constraint: the engine decides, the LLM only explains. The preparation node is pure code — normalisation, trend arithmetic, label resolution — and the model receives a grounded context it can’t step outside of. Three specialists analyse in parallel, a node synthesises and a safety gate decides between delivering or escalating. A hallucination in the normalisation node would have propagated to every branch; that’s why that node isn’t a model call.
- The clinical catalogue had to come out of the code. The formulas lived as constants in the engine, which meant updating a protocol was a deployment. They were moved to the database and a back-office console was built with three non-hierarchical roles: admin, specialist and provider. Editing the catalogue invalidates the clinical signature and bumps the version, and the report cache key includes that version. It is the only way a pharmacist — not a commit — can sign off a protocol.
- An incident that deserves to be in the case study. With the beta already in testers’ hands, every user got locked out after verifying their email. The cause: force_organization_selection enabled in the identity provider. Every session received a “choose organisation” task that a B2C user can never satisfy, the session stayed pending, the client counted it anyway and single-session mode rejected any new login. It was fixed on two fronts: the configuration and the client, because a production app can’t depend on a third party’s console being configured correctly.
check-in → protocol: Check-in · 5 dimensions → Individual baseline → @eonia/engine · pure → 6 biological states → 3-day hysteresis → 6 architectures · circadian → LangGraph · grounded narrative → Safety gate → Optional sync · Postgres
Solution: A closed loop of signal, decision and execution
A 5-dimension check-in in ~30 seconds → EONIA Score against your personal baseline → one of 6 biological states → one of 6 capsule architectures with circadian windows per compound. The architecture only changes after 3 consecutive coherent days: deliberate hysteresis against noise-driven oscillation. The clinical catalogue lives in the database with professional sign-off and versioning, not in the code.
Product decisions
- my role: Product, system design and full-stack + AI development — sole author
- key architecture: A pure, deterministic engine — no I/O, no network, no language model. The same package runs on device and server and produces the same verdict: offline mode is not a degraded version.
- key AI decision: The engine decides, the LLM only explains. The narrative layer receives a grounded context it can’t step outside of — it doesn’t derive a state, an architecture or a number of its own.
- status: MVP v1.0 deployed (Render + APK via EAS) and server increment v1.5 in production. Closed beta.
Results
No user metrics yet — the beta is closed and no figures have been published. What is verifiable: a complete, deployed MVP v1.0, with a dockerised backend on Render via Blueprint and an Android APK distributed to testers via EAS. Server increment v1.5 in production, with optional authenticated sync, narrative reports and a back-office console with role-based access control. And an editable, signed clinical catalogue: protocols are no longer code, and the clinical signature is a state of the data that only a professional can set.
- 35 — tests on the decision core, with multi-day integration
- 5 — workspaces with clear boundaries: engine, mobile, backend, admin, narrative
- 46 — commits since kickoff, with the SDD set kept in sync with the code
"Putting the LLM in the wrong place is easy and expensive. Restricting it to explaining a decision already made by verifiable code is what makes the system auditable — and what lets offline mode lose nothing essential."
What was learned
- The ADR that delivered the most value wasn’t technical. Reconciling an inherited multitenant backend with a B2C product didn’t solve a code problem: it settled whether the product was B2C or B2B2C, and stopped the backend from dragging the product along.
- Whatever a professional must be able to change can’t live in the code. Moving the clinical catalogue to a signed, versioned database turned an app into an operable system — a pharmacist signs off a protocol, not a commit.
- A third party’s configuration is production surface. One misset flag in the identity provider locked out 100% of testers after email verification. It was fixed in the configuration and in the client: a production app can’t depend on a third party’s console being right.
Demo and beta on request.