Technical diligence · mycyclesafe.ai
Written for the reader trying to establish whether this is a thin wrapper. Every number carries its methodology, and where something is unmeasured the slide says so rather than omitting it.
System context
Definition: "resident on the handset" means the process boundary for her health record is the device itself. What crosses out is a signed, minimal payload for three named actions, never her raw log.
Architecture
React 18 and TypeScript, more than 30 screens.
The correlation engine, running in a Web Worker, off the UI thread.
Two Dexie databases, AES-GCM applied at field level.
Kotlin plugins for optical capture, keystore and speech. The one on-device process boundary.
Nothing in these four layers writes to the network directly.
The canonical record holds user, consent, bleed events, check ins, crisis events, screening responses and companion contacts, at schema version 8. Documents live in a separate database entirely, so a migration on one cannot corrupt the other.
Export and erase must enumerate both databases correctly, forever, or leave data behind. It is listed on the tech debt slide as our top item.
A single worker client module is the only code in the tree allowed to call fetch for a health-adjacent action, and only for three routes, each carrying a request signature.
Definition: field level encryption means individual sensitive columns are ciphertext at rest in IndexedDB, not that the database file is encrypted as a whole.
Architecture · two engines, one boundary
The one crossing: HMAC-SHA256 signed HTTPS, carrying a question, a de-identified finding, or a document image, never her raw log.
| Route | What crosses | What never does |
|---|---|---|
| parse-intent | A typed question and the closed list of allowed dimension names | The rest of her check-in history |
| phrase-finding | One already-computed finding: lag, direction, strength, n | Raw symptom rows, dates, free text |
| classify-document | A document image, base64, signed | Anything stored server-side beyond the model call |
| companion notify | A coarse relationship label only, e.g. "Partner" | Any message text, mood, score or symptom |
Definition: Engine 2 physically cannot originate a finding, the only finding-shaped input it ever receives is one Engine 1 already computed. Every route is enforced by a validator in the Worker, not by convention.
Critical path · Engine 1
No arrow on this diagram crosses a network boundary, because this path never needs one. It is the one guarantee that holds even with the radio off.
Definition: still_learning is a value of the Finding union type, not an error branch. The renderer cannot receive a finding without also receiving whether that finding is defensible.
Critical path · Engine 2, a signed round trip
postToWorkerRoute never throws: offline, timeout, a 401, and a malformed 200 body all become a typed failure, and every route degrades to the same deterministic behaviour a client with no network would show.
Methodology: the signature is an HMAC-SHA256 over the timestamp and raw body, checked with a constant-time comparison and a five minute replay window. Whatever Engine 2 returns, enforceAllowList() rechecks it against the closed enum before the Worker replies.
The hard part
66 variable pairs drawn from 12 logged dimensions, each tested across a 1 to 14 day lag. That is 924 hypotheses per sweep, per user.
The false positive rate must hold at the nominal 5 percent after correction. An app that invents a causal story about a woman's body is worse than one that says nothing at all.
The whole sweep must finish under 5 seconds across 18 months of history, on a ₹10,000 Android handset, with no server to offload to.
The difficulty is not the correlation itself. It is that 924 hypotheses against noisy data will hand you a beautiful and entirely false finding on nearly every run, and the user has no way to check it. Everything downstream of this slide exists to make that specific failure structurally hard.
Definition: a dimension is one logged variable, for example sleep hours or skin severity. Pairs are unordered and exclude self pairs, giving 12 choose 2, which is 66.
Why naive approaches fail
924 hypotheses per sweep 66 pairs × 14 lags
survive the lag correction
Bonferroni across every lag actually searched, so picking the best of 14 cannot pass on its own.
survive false discovery correction
Benjamini-Hochberg across all 66 pairs, sized against the full list rather than the winning one.
are shown to her
Widths are illustrative of the intended attrition, not measured counts. On most sweeps the final bar is empty, and the product says "still learning" instead.
Search 14 lags, keep the best one, then test that lag's raw p value. This is the obvious implementation, and it is wrong: selecting the maximum across 14 correlated tests and then testing it as though it were the only test lets pure noise clear significance.
What the engine does: Bonferroni correction across the lags actually searched, applied before the across-pair correction, and verified across repeated seeded stress runs.
Severity data is integer 1 to 5, so ties are the common case rather than the edge case. Breaking ties by sort order instead of assigning midranks distorts the rank distribution and miscalibrates every p value computed from it.
What the engine does: midrank assignment, which holds the false positive rate at nominal on integer severity data.
Methodology: each stress run varies the RNG seed and regenerates the full synthetic series. "Nominal" means the configured alpha of 0.05. Definition: a hypothesis is one variable pair tested at one lag; the bars above show the intended attrition through each correction stage.
Engine 1, step by step
Both exits shown above are the same node: too few overlapping days, or too weak after correction, either one resolves to the identical "still learning" output her screen renders. There is no third path that lets a thin signal through.
Fewer than 12 overlapping paired days at a pair's best lag and the finding is still-learning, regardless of how strong the correlation looks. 12 is a named, commented product constant, not a derived statistical minimum.
Benjamini-Hochberg FDR runs exactly once per sweep, over every pair that cleared the observation floor together, at a target false discovery rate q = 0.05. A second, per-pair correction anywhere else would silently undo it.
Only a finding that cleared both gates gets a bootstrap confidence interval and a strength band. Direction, lag and observation count travel with it, so the number a user sees always carries its own evidence.
Source: src/core/engine/sweep.ts, confidence-gate.ts (MIN_OBSERVATIONS = 12, FDR_Q = 0.05), fdr.ts, bootstrapCI.ts. Definition: "residuals" means each check-in is expressed relative to her own phase-segmented baseline before any correlation runs, not the raw logged score.
Approach and evaluation · Engine 1
Two fixed controls rather than a competitor: a series carrying a planted lagged relationship the engine is never told about, and a deliberately null pair with no relationship at all.
Passing requires recovering the planted lag within one day either side, and reporting nothing on the null pair. Either result alone counts as a failure.
Synthetic series generated from sealed parameters held by a team member who does not write the engine, so the implementer cannot tune toward the answer.
Every stress run varies the RNG seed and regenerates the full synthetic series, checking the false positive rate stays at nominal across repeated draws.
What this does not establish: the engine has never been run against real human data, because we have no users. It is validated against synthetic ground truth only, and that gap is restated on the limitations slide.
Methodology: the sealed parameters are held outside the engine's own repo and revealed only after a run completes, so a passing result cannot be produced by tuning against the answer.
Model selection · Engine 2
| Model | Fully correct, parse-intent | Unresolved recall / precision | Injection resistance | Guard-pass, phrasing |
|---|---|---|---|---|
| gemini-2.5-flash-lite · winner | 97.6% | 100% / 100% | 100% | 87.5% |
| gpt-oss-120b · incumbent | 85.7% | 86.7% / 92.9% | 66.7% | 62.5% |
42 parse-intent cases (clean pairs, synonyms, typos, prompt injection, off-list, Tanglish) and 8 phrase-finding cases, graded by the real production allow-list checker and echo-guard, not a rubric written after the fact.
Accuracy first, then unresolved-safety, never hallucinating an off-list dimension outranks getting a hard case right, then cost as a tiebreaker. All six candidates cost fractions of a cent per call, so cost decided nothing.
It answered pair(medication, pain) for "does ibuprofen help my pain?", exactly the failure mode its own system prompt warns against by name.
Source: evals/model-selection/REPORT.md, 2026-08-09. Total spend across the 300-call, six-model eval: $0.0229. Definition: the winning model is deployed via mycyclesafe-api and serves both text and document vision; native Gemini remains a selectable fallback.
Data
Zero rows of user health data. D1 holds only entitlement, rate-limit, profile and companion-log rows, no health column exists in the schema. The corpus we do own is the curated content set: evidence graded claims, each entry carrying a source and a grade, enforced by a CI lint.
A photographed prescription or lab report is sent, signed, to Engine 2 for extraction. Nothing is written until she reviews each field; the result saves to a separate encrypted document store, never merged into the health record.
Not in the usual sense, and this is the central strategic trade. There is no central corpus that improves a model as usage grows, because there is no central health corpus, full stop.
What compounds instead is per user: the engine gets sharper for her as her own history lengthens, because every statistic is computed within her own baseline. A reader who believes a pooled data moat is the only durable kind should treat this as the principal risk in the architecture, and that is a fair objection to hold.
Definition: evidence graded means each claim carries an explicit tier and a citation, and the build fails if any health claim in the content directory is missing either one.
Performance envelope
There is no p50, p95 or p99 on this slide because there is no production traffic to compute one from. Publishing percentiles measured on a developer laptop would be a fabrication, and a reader would be right to discount everything else in the deck on the strength of it.
Methodology: bundle size taken from a production Vite build, gzip excluded, worker chunks included. The Engine 2 figure is the measured blended cost from the model-selection eval. Ticks mark the target ceiling. Hatched tracks are unmeasured, not zero.
Security architecture · trust boundaries
Ciphertext at rest. Plaintext exists only in memory and is never re persisted.
crossing 1 · Capacitor bridge, wrapped key only
It cannot export its own bytes. That is the entire point of the design.
crossing 2 · HMAC-signed HTTPS, three routes only
Companion notify accepts only a coarse relationship label. No message text, mood or symptom is a valid field on that route.
An Android Keystore key that cannot export its own bytes wraps a random per install data encryption key. Only the wrapped form is ever persisted, and unwrapping happens per call, in memory.
A granular consent ledger gates every feature that reaches beyond the phone. Each entry is a named consent class, and every network-facing feature fails closed to on-device behaviour when its flag or its consent is unset.
A CI gate walks the crisis path's import graph and fails the build if any network module appears in it. It was verified by injecting a fetch call and confirming the gate failed.
Definition: KEK is the key encryption key held in hardware; DEK is the data encryption key it wraps. Methodology: the import graph gate is a build script, run in CI on every push and tested against a deliberate violation.
Compliance and reliability posture
| Item | Status | Detail |
|---|---|---|
| SOC 2 | None | mycyclesafe-api handles no health data, only entitlement and rate-limit rows. Relevant chiefly if a clinician side backend is ever built. |
| ISO 27001 | None | Same reasoning. |
| Penetration test | Never run | Scope would be the app package, the Worker's three signed routes and the native bridge. |
| CDSCO classification | Unresolved | Cadenced mental health screening with an escalation path may tip this from wellness into medical device. A pre submission opinion is required before any pilot. |
| DPDP, under 18 | Enforced | A hard 18+ age gate. The engine performs profiling, which DPDP prohibits for children, so no consent flow could make under 18 use lawful. |
| Uptime, SLO, MTTR | Not applicable yet | No production traffic on the Worker. On device and Worker failure modes appear on the next slide instead. |
Definition: "not applicable yet" is distinct from "not needed". A clinician portal, which sits on the roadmap, would introduce all six of these rows as real obligations.
Failure modes · blast radius per dependency
Everything inside keeps working with the radio off
on her device
degraded
degraded
hard down
degraded
Engine 2 down
| Dependency | Behaviour on failure |
|---|---|
| Optical capture (photo) | Capability probe fails closed. The document-scan entry point is not rendered without it. |
| On device speech recogniser | Voice capture is absent rather than disabled. Tap entry is the full path, never a fallback. |
| Keystore or master key | Hard down. Records become unreadable by design, the intended consequence of one tap erase. |
| IndexedDB quota | The write path surfaces a specific quota error rather than dropping the check in. |
| Worker or OpenRouter outage | Engine 2's routes go down. Ask falls back to the deterministic keyword parser, document scanning is unavailable, and every other screen keeps working. |
| App store and payments | Hard down. The one true external dependency for revenue. Installed apps keep working offline. |
Definition: shaded rings show what stops working when each node fails. "Degraded" means the affected feature disappears while the rest of the product continues; "hard down" means the product or that data is unavailable.
Alternatives considered and rejected
| Decision | Rejected alternative | The specific reason it lost |
|---|---|---|
| Capacitor shell | Progressive web app | No web API exists for Health Connect or HealthKit, and getUserMedia is unreliable inside WKWebView. |
| Capacitor shell | React Native | Workable, but it would require rebuilding the entire interface layer for a four person team while buying nothing a native plugin does not already provide. |
| Engine 1 on-device | Cloud-computed statistics | Keeping the engine that decides what is true about her body on the device means her raw log never has to leave the phone for that computation. Engine 2 exists precisely so this line never has to move. |
| Engine 2 via OpenRouter | Direct per-vendor SDKs | A unified surface let six candidates be evaluated and swapped with one config flip; a direct integration would have meant six separate integrations to run the same eval. |
| AI phrasing shipped off by default | Always-on generative phrasing | Built, tested and guarded (guardPhrasing rechecks every sentence against the numbers Engine 1 computed), but held off until it has production mileage. Every finding a user sees today is the deterministic template. |
| Phone only sensing | CGM or wearable integration | Dexcom Stelo is not sold in India, every working Libre integration violates LibreView's terms, and CGM runs about ₹8,400 a month against a target handset price of ₹10,000. |
Definition: "shipped off by default" means the code path exists and is evaluated but the feature flag defaults false in production builds.
Limitations
Never run on physical hardware. Keystore attestation and radio off voice capture are engineered, and neither has been executed on a real handset.
The engine has never met real data. It is validated against synthetic ground truth only, and real self reported data is messier than any fixture we wrote.
iOS is not built. Android only. No Mac exists on the team, so the iOS half of every native plugin is unwritten and unverified.
HMAC signing, not device attestation. The shared secret ships as a build-time constant inside the APK; a determined attacker who decompiles the app can extract it and forge signatures. This stops casual abuse, not a motivated one; Play Integrity is the named, unbuilt fix.
Companion delivery is not live. The notify route records the request server side; no SMS or push provider is wired in yet.
A bus factor of one on the engine and the native layer, and two databases that every export and erase path must enumerate correctly, forever.
Given six months, the first rebuild would be database unification, because it is the one piece of complexity that makes a privacy guarantee harder to keep rather than easier.
Unit cost of serving
₹0. Every rank transform, correlation and bootstrap resample executes on hardware the user already owns.
₹0. Health records are not transmitted or held off-device. Bandwidth is the install download, paid for by the store.
$0.085 per 1,000 calls, blended, for the three narrow jobs it does. Not zero, small enough that it did not decide the model choice.
15 to 30 percent of subscription revenue. This remains the largest single cost at any scale.
What does scale with users is human: support load, content review cadence and the clinical review pipeline. Those are real operating costs, and they are linear in users rather than in requests.
Methodology: Engine 1 and storage costs are structural claims about the architecture, not measurements; no user has been served yet. The Engine 2 figure is a measured cost from the model-selection eval.
Delivery and vendor exposure
Four engineers and 49 planned units of work, each committed atomically, with tests written before implementation on behavioural changes.
Three standing CI gates that fail closed: no network import on the crisis path, no ungraded health claim, and no disconnected analysis pipeline.
Deploy frequency and lead time are not meaningful yet, because nothing has been released to real users. There is no on call rotation.
Bought: Capacitor, React, Dexie, zod, jsPDF and the Capacitor plugin set on device; Cloudflare Workers, D1, and OpenRouter's routing surface in the cloud. All replaceable behind the one worker-client seam.
Built: the correlation engine, the safety and screening layer, the encryption envelope, and the guard that rechecks every Engine 2 output against the facts that went out.
Concentration risk is now two-tier. Apple and Google control distribution, payment and the permission model for the whole app. Cloudflare and OpenRouter sit only in the path of three AI-assisted routes, the crisis path, screening, logging and Engine 1 never touch either.
One forked dependency: the speech recognition plugin is patched in our tree to force on device recognition, and that patch is ours to carry across upstream releases.
Definition: "fails closed" means the gate blocks the build when it cannot verify a property, rather than skipping the check.
Roadmap 1 of 2 · capability ordered, dependencies flagged
| Capability | Blocked by | Note |
|---|---|---|
| Real hardware validation | One Android handset | Keystore attestation and radio off voice. None of this is a code change. |
| AI phrasing, on by default | Production traffic | Built and guard-tested. It moves from "built and guarded" to "on" once enough traffic exists to trust the guard rate in the wild. |
| Real companion push delivery | A pairing code design | The Worker route already records the outreach request. What is missing is a device pairing code and a push gateway, because there are no accounts. |
| Any real user pilot | CDSCO pre submission opinion | Classification risk sits on the screening cadence, not on the correlation engine. |
All four of these are free to unblock: a handset, traffic, a design decision, a submission. None of them require additional engineering the team does not already know how to do.
Roadmap 2 of 2 · nothing external, a sequencing choice
| Capability | Blocked by | Note |
|---|---|---|
| Play Integrity attestation | Nothing external | Replaces HMAC-only signing with real device attestation on the Worker routes. |
| Condition-specific RAG for Engine 2 | Production mileage on today's three jobs | A retrieval layer grounded in a PMOS/PCOS clinical corpus, for the narrow cases where citing a source matters. No retrieval or corpus exists in the codebase yet. |
| Database unification | Nothing external | Our own top tech debt item. It reduces the surface on which an export or erase can miss data. |
| iOS parity | A Mac, and a second native engineer | Every plugin has an unwritten Swift half. Scoped, not started. |
Appendix available on request: threat model, data flow diagram with PII classification, full schema, engine evaluation harness, model-selection eval report, architecture decision records, and the dependency and licence inventory.