My Cycle Safe: Technical Diligence

Technical diligence · mycyclesafe.ai

Two engines, one boundary that cannot be crossed: one generates the statistics, the other only explains them.

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.

My Cycle Safe

System context

The system as one box, its one optional cloud hop, and everything else it touches.

HMAC-SHA256 signed HTTPS
each an action she took

Her, logging
30 second daily check-in
optional voice, optional photo

My Cycle Safe
React UI, Engine 1 on-device statistics
encrypted local stores, native plugins
resident on the handset

Her clinician
receives a PDF she chooses to share
no account, no portal, no device access

App stores
distribution and payment

Cloud AI layer, Engine 2
mycyclesafe-api: Cloudflare Worker + D1
reached only for a document scan,
an engine-assisted question, or phrasing

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.

My Cycle Safe

Architecture

Four layers on the device, one process boundary, and one signed seam out to the cloud.

UI layer

React 18 and TypeScript, more than 30 screens.

Engine 1, statistics

The correlation engine, running in a Web Worker, off the UI thread.

Persistence

Two Dexie databases, AES-GCM applied at field level.

Native

Kotlin plugins for optical capture, keystore and speech. The one on-device process boundary.

Nothing in these four layers writes to the network directly.

Why two databases

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.

What that costs us

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.

The one signed seam

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.

My Cycle Safe

Architecture · two engines, one boundary

Everything Engine 2 can receive, and nothing it is ever allowed to decide.

parse-intent: question + allowed variable names

phrase-finding: one de-identified finding

classify-document: a document image

companion-notify: a coarse relationship label only

CLOUD, mycyclesafe-api (Cloudflare Worker) + D1

config flip

Worker routes
parse-intent, phrase-finding, classify-document
companion-notify, entitlement

Engine 2, gemini-2.5-flash-lite via OpenRouter
understands & phrases, never generates a finding

native Gemini
selectable fallback

D1
accounts, entitlement, companion log
no health columns

DEVICE, her Android phone

wraps key

wraps key

React / Capacitor UI

Engine 1, on-device n-of-1 statistics
TypeScript, generates every insight

Encrypted health store
Dexie/IndexedDB, AES-256-GCM per field

Encrypted documents store
separate DB

Android Keystore
non-extractable KEK wraps the data key

The one crossing: HMAC-SHA256 signed HTTPS, carrying a question, a de-identified finding, or a document image, never her raw log.

RouteWhat crossesWhat never does
parse-intentA typed question and the closed list of allowed dimension namesThe rest of her check-in history
phrase-findingOne already-computed finding: lag, direction, strength, nRaw symptom rows, dates, free text
classify-documentA document image, base64, signedAnything stored server-side beyond the model call
companion notifyA 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.

My Cycle Safe

Critical path · Engine 1

Check in to defended finding, entirely on the device.

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.

My Cycle Safe

Critical path · Engine 2, a signed round trip

The only path that leaves the device, and what stops it if the signature is wrong.

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.

My Cycle Safe

The hard part

Find a real lagged effect in sparse, irregular, self reported data. Report nothing when there is nothing.

The search space

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 floor

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 budget

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.

My Cycle Safe

Why naive approaches fail

924 hypotheses go in. Almost always, nothing comes out.

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.

Trap 1 · the winner's curse

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.

Trap 2 · rank transform tie breaking

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.

My Cycle Safe

Engine 1, step by step

How the statistician decides: every check-in run through the same gates before it may speak.

No

Yes

No

Yes

Her daily
check-ins

Per-dimension series
own baseline subtracted

66 pairs x 14 lags
924 hypotheses

Spearman rank
cross-correlation
strongest lag kept

n >= 12
paired obs?

Still learning
report nothing,
invent nothing

Benjamini-Hochberg
FDR, all pairs
together

clears q = 0.05
threshold?

Bootstrap CI
on correlation

Report a finding
direction, lag,
strength band, n

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.

The observation floor

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.

The correction, once

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.

What ships after

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.

My Cycle Safe

Approach and evaluation · Engine 1

The statistics engine must pass two opposite tests on every build, or it does not ship.

The baseline it is measured against

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.

Eval set and method

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.

My Cycle Safe

Model selection · Engine 2

Six candidates, about 48 golden cases, graded by the production checkers. One model won on every axis that mattered.

ModelFully correct, parse-intentUnresolved recall / precisionInjection resistanceGuard-pass, phrasing
gemini-2.5-flash-lite · winner97.6%100% / 100%100%87.5%
gpt-oss-120b · incumbent85.7%86.7% / 92.9%66.7%62.5%

What was scored

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.

What decided it

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.

The incumbent's specific failure

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.

My Cycle Safe

Data

Zero rows of her health data anywhere off the device. The control plane holds only what a subscription needs.

What we have

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.

Document scanning, honestly

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.

Does it compound

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.

My Cycle Safe

Performance envelope

Two targets are measured. The rest need hardware or traffic we do not have yet.

Bundle size
3.63 MB of 60 MB
Engine 2 control-plane cost
$0.085 per 1,000 calls, blended
Pattern reveal, 18 months of data
target 5 s, unmeasured on device
Cold start, ₹10,000 handset
target 2.5 s, unmeasured
Doctor PDF render
target 10 s, unmeasured
Check in, human task time
target 30 s median, needs users

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.

My Cycle Safe

Security architecture · trust boundaries

Three trust boundaries. Only one of them carries data off the device, and only by her action.

Boundary 1 · JS sandbox
React UI, Engine 1 Worker, Dexie stores

Ciphertext at rest. Plaintext exists only in memory and is never re persisted.

crossing 1 · Capacitor bridge, wrapped key only

Boundary 2 · Android keystore
Non extractable KEK, hardware backed

It cannot export its own bytes. That is the entire point of the design.

crossing 2 · HMAC-signed HTTPS, three routes only

Boundary 3 · signed Worker seam
mycyclesafe-api, request-signed on every call

Companion notify accepts only a coarse relationship label. No message text, mood or symptom is a valid field on that route.

Key management

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.

Consent, not just encryption

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.

Enforced, not documented

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.

My Cycle Safe

Compliance and reliability posture

Nothing here is certified. This is the exact status of each item a buyer would ask about.

ItemStatusDetail
SOC 2Nonemycyclesafe-api handles no health data, only entitlement and rate-limit rows. Relevant chiefly if a clinician side backend is ever built.
ISO 27001NoneSame reasoning.
Penetration testNever runScope would be the app package, the Worker's three signed routes and the native bridge.
CDSCO classificationUnresolvedCadenced 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 18EnforcedA 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, MTTRNot applicable yetNo 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.

My Cycle Safe

Failure modes · blast radius per dependency

Everything her body's finding depends on keeps working with the radio off.

Everything inside keeps working with the radio off

Engine 1

on her device

Optical capture

degraded

Speech

degraded

Keystore

hard down

Quota

degraded

Worker / OpenRouter

Engine 2 down

DependencyBehaviour on failure
Optical capture (photo)Capability probe fails closed. The document-scan entry point is not rendered without it.
On device speech recogniserVoice capture is absent rather than disabled. Tap entry is the full path, never a fallback.
Keystore or master keyHard down. Records become unreadable by design, the intended consequence of one tap erase.
IndexedDB quotaThe write path surfaces a specific quota error rather than dropping the check in.
Worker or OpenRouter outageEngine 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 paymentsHard 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.

My Cycle Safe

Alternatives considered and rejected

Including the one decision that keeps her raw log off the network entirely.

DecisionRejected alternativeThe specific reason it lost
Capacitor shellProgressive web appNo web API exists for Health Connect or HealthKit, and getUserMedia is unreliable inside WKWebView.
Capacitor shellReact NativeWorkable, 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-deviceCloud-computed statisticsKeeping 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 OpenRouterDirect per-vendor SDKsA 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 defaultAlways-on generative phrasingBuilt, 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 sensingCGM or wearable integrationDexcom 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.

My Cycle Safe

Limitations

What this system does badly, and what we would rebuild given six months.

Not yet established

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.

Known weak points

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.

My Cycle Safe

Unit cost of serving

Two of three cost lines are zero. The third is a fraction of a cent, not a rounding error we hid.

Engine 1

₹0. Every rank transform, correlation and bootstrap resample executes on hardware the user already owns.

Storage and bandwidth

₹0. Health records are not transmitted or held off-device. Bandwidth is the install download, paid for by the store.

Engine 2

$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.

Store commission

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.

My Cycle Safe

Delivery and vendor exposure

Four engineers, one narrow vendor path for the AI-assisted features, none on the safety path.

Delivery

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.

Build versus buy, and concentration

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.

My Cycle Safe

Roadmap 1 of 2 · capability ordered, dependencies flagged

No dates. Each item names what blocks it, starting with the four that need something from outside the codebase.

CapabilityBlocked byNote
Real hardware validationOne Android handsetKeystore attestation and radio off voice. None of this is a code change.
AI phrasing, on by defaultProduction trafficBuilt 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 deliveryA pairing code designThe 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 pilotCDSCO pre submission opinionClassification 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.

My Cycle Safe · mycyclesafe.ai

Roadmap 2 of 2 · nothing external, a sequencing choice

The other four are not blocked on anyone else. They are ordered by when the team chooses to pick them up.

CapabilityBlocked byNote
Play Integrity attestationNothing externalReplaces HMAC-only signing with real device attestation on the Worker routes.
Condition-specific RAG for Engine 2Production mileage on today's three jobsA 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 unificationNothing externalOur own top tech debt item. It reduces the surface on which an export or erase can miss data.
iOS parityA Mac, and a second native engineerEvery 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.

My Cycle Safe · mycyclesafe.ai