Training, sleep, nutrition and recovery — built on your own Apple
Health, Whoop or Amazfit data, and computed on your own iPhone. Every score names
the paper it came from, and every one of them will tell you when it does not have
enough of your data yet.
Not on the App Store yet — the developer is mid-enrolment in
the Apple Developer Program. There is no waiting list, because there is no server
to keep one on.
Every install opens fully unlocked for 30 days from
first launch — no card, nothing to cancel. The price is shown in the app before
you're ever asked to pay.
0 of 3 inputs · fresh install · -startTab today3 of 3 inputs · with history · preview data
Real captures, iPhone 17 Pro simulator:
-startTab today and -startTab today -showFixtures. The populated
frame is the app's own test fixtures, not a real person's numbers.
The whole idea
What Velisa does when it does not know
“Not enough to call it yet. Needs recovery markers or 28 days of
training history — neither exists yet.”
Today's call, on a fresh install · Velisa/UI/Today/TodayView.swift
That is the product, on the first screen a new
install shows. Three commitments hold it up, and they are enforced by tests rather than
intentions: a missing input contributes exactly zero and is shown as missing, never
imputed; no screen anywhere says “no data available”, because a calibrating card has to
name what it is waiting for; and where there is uncertainty, it is on the screen.
One of those tests asserts that no
locked-feature copy may contain a numeral — so nobody can slip a
plausible-looking preview behind a paywall. The rule applies to us too.
Sourcedocs/STATE.md — “The three commitments
everything else follows from”. Engines return nil or
.calibrating below their minimum window, and the metrics engine writes
no row rather than a zero.
What it replaces
Three jobs that are usually three apps.
The log
4471
exercises. Bodyweight counts toward volume load, scaled per exercise — a standard
push-up carries 0.64 of you2, a pull-up all of it. Twelve
pull-ups at 78 kg is 936 kg of work, not zero. Deltoids split front, side
and rear.
1 · 2README.md, docs/STATE.md;
Velisa/Workouts/WorkoutModels.swift — the 0.64 is Ebben et al.
2011, a kinetic analysis of push-up variations. Where no published figure exists the
factor stays 1.0, an explicit upper bound, rather than a guess.
The food
14,5513
foods bundled offline in 12.3 MB, including 1,014 Indian cooked dishes. A barcode
scanner and a nutrition-panel reader for everything else — built that way because of
what the research actually found: no Indian barcode registry publishes nutrition at
all.4
3 · 4README.md food-database table (INDB, CC BY 4.0;
Open Food Facts India kept in a separate file under ODbL 1.0).
docs/STATE.md — “Barcode coverage”: gtin appears zero times in
ONDC's entire API contract.
The layer neither has
185
explained metrics over 716
cited references. Every one states its own minimum data window and renders nothing below
it. Tap any number and the arithmetic that produced it opens underneath.
5 · 618 = the ExplainedMetric cases in
Velisa/Scoring/MetricExplainer.swift. 71 = distinct Citation
records across Velisa/Scoring/. Both counted from source on
2026-08-01.
The actual app
Four tabs, four real captures.
Today
One card decides the day, and it is allowed to decline. Today's call
reads three inputs — recovery markers, acute:chronic load, and fitness against
fatigue — names which of them it actually had, and lists the reasons it landed
where it did.
With none of the three, it makes no recommendation at all.
3 inputs · confidence printed on the card · every reason listed
Train
A full strength and cardio logger. The acute:chronic workload ratio
is computed the uncoupled way — the 7-day window is not inside the 28-day one — so
it cannot manufacture its own correlation, which also means it will not agree with
an app that couples.
Volume load counts bodyweight for calisthenics, scaled per exercise
rather than all-or-nothing.
Calorie and macro targets measured from what you actually ate and
what your weight actually did — not predicted from a formula, once there are two
weeks of your own data to read instead. A wearable's active-energy figure is shown
for context and never paid out as a calorie credit.
Unlogged days are skipped, never read as zero. One blank day treated
as a fast would invent a deficit that never happened.
Recovery Score, Velisa Age, Today's Ledger, sleep regularity,
HRV-guided training, illness detection, energy availability. Each one is gated behind
its own minimum data window, and each one refuses to answer when the literature does
not sign the answer.
Velisa Age leads with the direction it is moving, not the level —
because the level's uncertainty is wider than most of the differences people care
about.
18 explained metrics · each gated on its own window
Train, Eat and Body above are captured with
-showFixtures — the app's own test data, so the design is visible on a fresh
install. Nothing in them is a real person's numbers.
Nothing in the scoring layer is fitted, tuned, or chosen because it looked
good. Every threshold is a number printed in a paper, and the paper is attached to it.
18
metrics with an explainer sheet
71
cited references in Velisa/Scoring
0
weights fitted to data
0
third-party dependencies
First, what “your normal” means
A 57 ms HRV reading is not good or bad in the abstract. It is above or
below your own recent middle. Every engine builds that baseline the same way — the
median, not the mean, and the median absolute deviation,
not the standard deviation. One three-hour night should not be allowed to redefine what is
normal for you.
z = 0.6745 × (value − median) / MAD
Velisa/Scoring/Baseline.swift:24 — 0.6745 rescales
MAD so it is comparable to a standard deviation. isReliable is
count >= 14: below fourteen days of history, z() returns nothing
at all and every engine that depends on it declines to render a verdict.
Two rules govern the published tables underneath that. Thresholds are
health-optimised, not population averages — anchoring to a population mean is
exactly why other effective-age calculators tell the average adult they are biologically
younger than they are, because the mean is dragged down by the unwell. And a missing
interval is never invented: where a paper publishes only a point estimate, that
contributor is recorded as carrying no disclosed error, and the output states how many
contributors were in that position — so “no uncertainty shown” is never quietly confused
with “no uncertainty exists”.
SourceVelisa/Scoring/References.swift,
file header — “Nothing in this file is fitted, tuned or guessed.”
Velisa Age
two layers, kept separate on purpose
“The age of a person whose all-cause mortality risk matches yours, given
your fitness and a short list of measured behaviours.” The spine is your fitness age: a
published VO₂max reference table is inverted, so instead of asking what you should
hit, it asks at what age the reference value equals what you actually have. That is a purely
descriptive statement and needs no assumption about how fast anything declines.
The ledger is then added on top. For each behaviour with a published hazard
ratio — sleep regularity, sleep duration, steps, moderate and vigorous activity, strength
training, resting heart rate, body fat — the ratio converts to years through
Spiegelhalter's effective-age transform. All-cause mortality rises about 10% per year of
age after 25, so:
Δ years = ln(HR) ÷ ln(1.10) ≈ 10 × ln(HR)
HR 1.22 → +2.0 years
HR 0.85 → −1.6 years
Each Δ is shrunk by how well Velisa actually measured that
exposure, capped at ±5 years per contributor and ±15 in total. A contributor you have no
data for contributes exactly zero years and widens the ± — it is never filled in
with a population average.
Shows nothing until
60 days of wear, 45 nights of sleep, and at least 4 ledger
contributors present.
a rendering of the markers, never a replacement for them
Four overnight markers — HRV (log-transformed, because raw HRV is
right-skewed), resting heart rate, respiratory rate and sleep adequacy — each turned into a
personal robust z-score against a 28-night baseline, then combined with equal
weights.
composite Z = mean(z₁, z₂, z₃, z₄)
unit-weighted — no component is favoured
“The weights are equal, and that is a choice
with a paper behind it rather than a shrug: unit-weighted models match or beat
optimally-weighted ones when the criterion is noisy and the true weights are unknown
(Dawes 1979). Polar and WHOOP publish contradictory orderings and no numbers.”
Shown inside the app, in the Recovery Score explainer ·
Velisa/Scoring/RecoveryScore.swift:838
The ± around it is the wider of two things: how far the number
would move under Polar's stated ordering or WHOOP's stated ordering instead of unit
weights, or the device's own published overnight measurement error. Whichever is larger is
named as the reason for the band's width — a false sentence about why a number is
uncertain is still a false sentence.
And four conditions under which it refuses
to answer at all. Each one is a case where the arithmetic would still run and the
answer would still be wrong, which is exactly when a health app is most dangerous.
Two readings of the same night. HRV below baseline and resting heart
rate also below baseline. Both the fatigue reading and the parasympathetic-saturation
reading are live, and the literature does not sign that quadrant.
Illness signals own today. A recovery score is answering the wrong question, and
“you're at 22%” reads as “train less” when the correct statement is “three overnight
signals moved together”.
Your HRV baseline restarted. The source or convention changed inside the
comparison window — Apple stores SDNN, Whoop reports RMSSD. The step is a device swap,
not you.
The weighting decides this number. If the sensitivity band spans 20 percentile
points or more, the four components are shown instead of a single figure.
Shows nothing until
14 nights of baseline per signal (28 preferred) · 3 valid nights a
week · 3 of the 4 signals present · and 30 days of composite history before any 0–100
percentile is shown at all.
Below that last one the app says so, in these words:
“A percentile computed from fewer is a number pretending to be one, so this shows the band
and the four signals instead.”
“Energy” is not a measured quantity. It is four different real things
wearing one word — metabolic energy, sleep pressure, autonomic arousal, and perceived
fatigability — and only the first is genuinely conserved. There is no physiological
reservoir that drains from 100 to 0 over a day.
So a published alertness curve carries the spine, and three ledgers sit
beside it. They are never summed, with the curve or with each other:
lane 1 minutes above your resting range
lane 2 training load added today
lane 3 kilocalories in versus out
──────────────────────────────────────────
total — different quantities, different
units, and no published factor
converts between them
The heart-rate lane is labelled “time above your resting range”, not
“stress”, on purpose: the classifier it would be imitating reads beat-to-beat HRV, and
Velisa has heart-rate bins. Calling it the same thing would be the false-precision this
metric exists to avoid.
The spine is the Three-Process Model, and
checking it found an error. Velisa's own research memo prints the two homeostatic
equations with their asymptotes swapped. As written, the wake process rises and the
sleep process diverges. The memo's own definition of the recovery rate settles
which way round they go, because that expression is the algebraic solution of one equation
and not of the other:
g = ln((ha − 14.0) / (ha − 7.96)) / 8
= ln(0.3 / 6.34) / 8
= −0.38136
used: S (t_aw) = la + (s_w − la)·e^(d·t_aw)
→ decays toward la = 2.4
S′(t_as) = ha − (ha − s_s)·e^(g·t_as)
→ recovers toward ha = 14.3
Every constant is printed in Ingre et al. 2014 — 136 aircrew,
964 sleep logs. None is fitted here. The correction is not merely written down: the test
suite re-derives the published 7.96 → 14.0-in-eight-hours anchor from the implementation, so
it is asserted rather than asserted-about. Where a form is not published — Ingre's
two brake constants are, the algebraic form that consumes them is not — that gap ships as a
visible disclosure on the lane itself.
Shows nothing until
48 hours of sleep and wake history for the alertness curve. Each
other lane gates independently on its own data.
Every metric carries its own minimumWindow string, and the type
will not compile a metric without one. These are those strings, unedited.
Ten of Velisa's eighteen explained metrics, the minimum amount of
your own data each needs before it will show a number, and the references behind it.
Metric
Shows nothing until
Sources
Today's call
At least one of: recovery markers with a 14-day baseline behind them, or 28 days of training history. With none of the three inputs no recommendation is made at all.
Ten of eighteen. The remaining eight — volume load,
estimated one-rep max, weekly sets per muscle, Velisa Trajectory, sleep debt, chronotype,
trend weight and macro targets — carry the same field and the same rule.
And what it refuses to compute
the restraint is the feature
Things the literature or the data genuinely does not support, listed on
purpose rather than quietly shipped anyway.
Your circadian phase, as a clock time. Published wearable phase estimators carry
roughly ±2 hours of error and depend on light exposure, which no connected device
measures. “Your melatonin onset is 21:47” is exactly the kind of claim that turns an
honest layer into a wrapper with extra confidence. Velisa shows your sleep-timing
midpoint instead, disclosed as not the same thing.
A simulated Process S. The physiology is real, but its standard time constants
are calibrated against EEG slow-wave activity no wearable provides, and the commonly
quoted values could not be traced to a primary source. A transparent decaying sleep-debt
heuristic ships instead, labelled a heuristic everywhere it appears.
The coupled acute:chronic workload ratio most training apps ship, where the last
7 days sit inside the 28-day average. Mathematical coupling manufactures an apparent
injury association even in randomly generated data, because the numerator is part of the
denominator (Impellizzeri 2020). Velisa computes the uncoupled version — which is why its
number will not match an app that couples.
A body-battery figure. Beat-to-beat data across a whole day is not available from
any source Velisa reads, and the minutes-to-points mapping is unpublished everywhere. It
would be a reimplementation of an unpublished algorithm on worse data than the original.
A precise South Asian body-fat threshold. The literature states the
direction the correction should move and publishes no magnitude. Waist
cut-offs are published and are implemented — 90 cm male, 80 cm female, against
102 and 88 for the global set — but the body-fat offset returns
nil in code, and the gap is disclosed in the output rather than filled with
an invented coefficient.
SourceVelisa/Scoring/References.swift,
ReferencePopulation.unverifiedBodyFatOffsetPoints — a documented
nil with a comment saying what would have to be published for it to become a
number.
Six rules, on all eighteen
Every explainer answers the same six things, in the order a curious
person wants them: what it is, one plain sentence · how it is
worked out · what your number is, with the arithmetic spelled
out · what counts as good, with your position marked on it ·
what moves it · and when it is wrong.
That last one is not a convention. In the type
that carries an explainer, five of the six fields have defaults and
limitations does not — the comment beside it reads “Required. No default,
so it cannot be forgotten.” A metric with an empty limitations list fails
isWellFormed, and the test suite holds all eighteen against it.
“Until then Velisa leaves this blank rather
than showing a number it cannot stand behind. Everything below still applies — it is
how the figure will be worked out once the data is there.”
The explainer sheet, calibrating · Velisa/Design/ExplainerSheet.swift
The explainer sheet behind every metric in the app,
shown here still calibrating.
The ± is the honest part
28, plus or minus 28.
This is the app's own headline number from the capture beside it, drawn to
scale. The point estimate is 28. The band around it runs from 0 to 56 — wider than the
claim, and the app says so on the card rather than in a footnote.
28
← ± 28 years →
014284256
A scale from 0 to 56 years. The point estimate sits at 28, and the
uncertainty band spans the entire scale, from 0 to 56 — a width of plus or minus 28
years. Drawn beneath it at the same scale, a second bar three years wide runs from 28 to
31, the calendar age printed on the same card.
The short white bar underneath is drawn at the same scale. It is
three years — the distance from 28 to the calendar age of 31 printed on the same card.
That is the part a person reacts to. The amber band is the part that is honest, and it is
nearly nineteen times wider.
A competitor would ship “Fitness Age: 28” and
stop. The reason the band is that wide on this reading is printed on the card too:
VO₂max was estimated rather than measured, and the app tags it
VO₂MAX ESTIMATED so you know which half of the uncertainty is yours to
fix.
NoteVelisa Age's ± is measurement
error — how precisely your own VO₂max is known — not the spread between people of
your age, which would have produced a useless ±33-year band on every reading. Those are
two different questions, and the second is stated separately, in the caveat quoted
here.
Body · Velisa Age · preview data. The caveat on the card,
verbatim: “Fitness age is a low-resolution measure — the spread between people of the same
age is over a decade. Track the direction it moves, not the number.”
Appearance
Three palettes, all dark.
Settings → Appearance. Three palettes, all dark, all shipped —
the app has no light theme to match.
Verifiable, not asserted
What Velisa does not collect.
Every line below is quoted from the
privacy policy already published at velisa.app/privacy — written
before this page existed, and required by App Store Connect for a HealthKit app.
No account
Velisa never asks for or stores an email address, a name, or a password of its own.
No server
There is nothing to sell from. Your data lives in a database on your phone, and
on-device StoreKit verification means even the subscription needs no backend.
No analytics SDK
No crash reporter and no third-party tracking library of any kind. This is a fact about
the app's code — there is nothing to point to — not a policy about how one is
configured.
No ad identifier
And no advertising anywhere in the app.
No location, microphone or contacts
None of the three is requested, at any point, for any feature.
Nothing sold, ever
There is no server to sell from and no buyer this app is built to serve besides you.
Zero third-party dependencies in the app itself, and
zero external requests on this page — enforced in the browser by a content-security policy,
not just intended.
Thirty days, no card
You see your own numbers before anyone asks you for anything.
“Velisa opens with 30 days of everything, no card
needed. Then one subscription keeps it.” Not an App Store introductory offer — that
kind requires a card, a password and a “you will be charged on…” before you have seen a
single one of your own numbers, which is the opposite of an app whose whole argument is
that the synthesis only earns its keep once there is real history behind it.
If you decide not to subscribe:
“Nothing you logged is deleted, and exporting it stays open either way.” Data export is
the one permanent member of the always-available set — it does not lock, even when the
rest of the app does.
Apple is the merchant of record. Velisa never sees
a card number, a billing address, or any payment detail at all.
The price is shown in the app before you are ever
asked to pay. It is not on this page, because it is not final — and publishing a
placeholder would be inventing a number, which is the one thing this app is built not to
do.
SourceBoth sentences are the app's own paywall copy.
Trial length is read from LocalTrial.days everywhere it appears, and a test
pins every sentence about it to that constant — so no string can drift out of sync with
the code.
AlsoEntitlement.alwaysAvailable contains exactly one
member: .dataExport.
Not on the App Store yet
Available on the App Store
Velisa isn't shipping yet.Thirty days of everything. No card.
The developer is mid-enrolment in the Apple Developer
Program. There is no waiting list, because there is no server to keep one on — so this is a
plain email to a real inbox.
Every install opens fully unlocked for 30 days from first
launch — no card, nothing to cancel. The price is shown in the app before you're ever
asked to pay.