Notifio is a desktop app that watches rental search pages and tells you when a new listing appears. It is not in any store. The whole distribution mechanism is one page with three buttons on it, each of which is a 302 to a presigned Cloudflare R2 URL that stops working sixty seconds later.
I have written about both halves of that. The download route hands out three fixed paths because the browser cannot tell you which Mac you are on, and the object keys deliberately carry no version number, because reinstalling is the update mechanism and a versioned key would mean the page has to know which version is current.
This post is what that decision actually produced, measured this morning by following our own redirects.
Reading our own headers back
The route answers 302. The Location is signed, expires in 60 seconds, and points at the object in R2. A one-byte range request gets the headers without pulling a quarter of a gigabyte:
for p in mac-arm64 mac-x64 win; do
loc=$(curl -s -i "https://notifio.app/api/download/$p" | tr -d '\r' | sed -n 's/^location: //p')
curl -s -r 0-0 -D - -o /dev/null "$loc" | tr -d '\r' | grep -iE '^(content-range|last-modified|etag)'
done
--- mac-arm64 ---
Content-Range: bytes 0-0/291511009
ETag: "d5fa339f86808351b935744a702725a6-35"
Last-Modified: Tue, 22 Sep 2026 12:24:46 GMT
--- mac-x64 ---
Content-Range: bytes 0-0/301712899
ETag: "5d5cb2ae0f58e13b42634f457d036d71-36"
Last-Modified: Tue, 22 Sep 2026 12:25:03 GMT
--- win ---
Content-Range: bytes 0-0/237616310
ETag: "a4d73a851c8fb664801c10e741fbf0b2-29"
Last-Modified: Tue, 28 Jul 2026 11:35:55 GMT
Two of those were built seventeen seconds apart. The third was built 56 days earlier.
| Button | Bytes | Size | Built | Age today |
|---|---|---|---|---|
| Apple Silicon | 291,511,009 | 278.0 MiB | 22 Sep 2026 | 34 days |
| Intel Mac | 301,712,899 | 287.7 MiB | 22 Sep 2026 | 34 days |
| Windows | 237,616,310 | 226.6 MiB | 28 Jul 2026 | 90 days |
The -35, -36 and -29 on the ETags are the multipart chunk counts from the upload, which is a pleasant way of being told the Mac builds went up in roughly 8MB pieces. The dates are the part I did not know.
Why one host cannot ship the other host's installer
The release script explains itself in its own header comment:
/**
* One-command release: bump version -> build installer(s) -> upload to Cloudflare R2.
*
* Runs on both Windows and macOS. Each host builds its own targets:
* Windows -> release/notifio-setup.exe
* macOS -> release/notifio-arm64.dmg + release/notifio-x64.dmg
*
* R2 object keys are fixed (the download API serves them by name), so a release
* overwrites the previous one. See RELEASES.md.
*/
Every sentence there is true and the combination is the problem. pnpm release defaults to --platform auto, which means "whatever this machine can build". On a Mac that is two DMGs. The NSIS installer needs a Windows machine, and I do not habitually have one in front of me.
So the September release was real. It bumped the version, built both DMGs, uploaded both, and finished successfully. It did not fail to ship Windows. It was never asked to.
And because the keys are fixed, there is nothing in the bucket that disagrees. notifio-setup.exe is still there, still served, still 302'd to by the Windows button, with no marker anywhere that it is three months behind the thing next to it.
Two versions behind one set of buttons
app/package.json carries the version, and the release script bumps it. Checking it out at the commit each build came from:
$ git cat-file -p 6103cd7:app/package.json | grep version # the 28 Jul era
"version": "1.0.3",
$ git cat-file -p bf3824b:app/package.json | grep version # the 22 Sep release
"version": "1.1.0",
$ grep version app/package.json # HEAD today
"version": "1.1.0",
A Mac user downloads 1.1.0. A Windows user downloads 1.0.3. There is no third place that records this, because:
$ grep -rn "getVersion\|APP_VERSION" app/src | wc -l
0
Nothing in the app reads app.getVersion(). The version is not in the window title, not in a settings panel, not in the licence check the app makes against the server on startup, not in the alert emails the server sends on its behalf. The number exists to be passed to electron-builder and then it stops existing. There is no telemetry, which I am happy about, and also no answer to "what are you running", which I am not.
What the gap actually contains
Three commits have touched app/ since 28 July:
$ git log --format='%ad %s' --date=short --since=2026-07-28 -- app/
2026-10-18 Charge each visitor the local-currency price shown on the page
2026-10-02 Remove em dashes and identify Daniel Pertu as founder of apps
2026-09-22 Add desktop notifications and live per-search monitoring state
The Mac DMGs were cut on 22 September, hours after the third of those. So the Mac build has the native banner and the live per-search state, and does not have the other two. The Windows build has none of them.
The desktop notification is the one that stings. The app's whole claim is that it tells you first, and for three months the fastest channel it has, the local one that costs no network round trip at all, has existed only on macOS. I wrote a post about one banner per search rather than one per listing and spent a paragraph of it on which Electron notification options apply on Windows. They apply on a build nobody can download.
Worse, yesterday I published a post about the price string the server hands the desktop app. That is the 18 October commit. No installer currently on the download page contains it. A reader who went to look would have found an app that does not do the thing the post describes, on either platform.
The documented sizes are wrong too
app/RELEASES.md opens with a table:
| File | Platform | Size |
|---|---|---|
notifio-arm64.dmg |
macOS Apple Silicon | ~600MB |
notifio-x64.dmg |
macOS Intel | ~600MB |
notifio-setup.exe |
Windows 10+ 64-bit | ~300MB |
The real figures are 278.0 MiB, 287.7 MiB and 226.6 MiB. The Mac entries are off by a factor of two. That one is innocent: 600MB is what the unpacked .app weighs, and the file in the table is the compressed DMG. The breakdown underneath it ("~350MB Electron, ~180MB Chromium, ~70MB everything else") is describing the bundle, not the download, and nothing relabelled them when they got written into a column headed Size.
So three numbers in the internal docs are wrong in the harmless direction, and the one number that would have mattered, the build date, is not in the docs at all. Nor is it on the download page, which quotes no size and no version either:
$ curl -s https://notifio.app/download | grep -ciE "MB|version|1\.1\.0"
0
What I am changing, in order
The fix is not "remember to build on Windows". That is the same class of thing as the gap itself.
-
Put the build date on the page. The download route already talks to R2 to sign a URL. A
HeadObjecton the same key, cached, getsLastModifiedfor free, and the card can say "built 22 September". A date is enough for a reader to notice what I did not. -
Make the app report its version on the licence check. One field in the existing
POST /api/validatebody. No new endpoint, no new telemetry surface, and it answers "which build is in the wild" without the user having to know. -
Fail the release when a platform falls behind.
--platform autoshould warn when the key it is not uploading is older than the one it is.
I am deliberately not adding auto-update. Reinstalling being the update mechanism is still the right call for an app that ships a few times a quarter, and a Squirrel or an electron-updater feed is a lot of machinery to maintain for that cadence. But "reinstalling is the update mechanism" only works if a reinstall gets you something newer, and for Windows, since July, it has not.
You can check whether I have got to this. The commands at the top of this post work from any machine, and the one header worth reading is Last-Modified.
Top comments (0)