nucleic-updates
A tiny redirect Worker layered on the Sparkle update host. See docs/CLOUD_INFRA.md §1 for the
full update-hosting design.
updates.nucleic.blakeslee.xyz is served directly by the nucleic-updates R2 bucket (custom
domain, no Worker in the path — fully CDN-cacheable). This Worker exists for one job: resolve a
channel's latest alias to the current versioned DMG, so anything elsewhere — the website's
"Download" button, docs, a chat link — can point at one stable URL that never needs updating:
GET https://updates.nucleic.blakeslee.xyz/canary/latest → 302 → …/canary/Nucleic-Canary-<ver>.dmg
GET https://updates.nucleic.blakeslee.xyz/beta/latest → 302 → …/beta/Nucleic-Beta-<ver>.dmg
GET https://updates.nucleic.blakeslee.xyz/rc/latest → 302 → …/rc/Nucleic-RC-<ver>.dmg
GET https://updates.nucleic.blakeslee.xyz/stable/latest → 302 → …/stable/Nucleic-<ver>.dmg
DMGs are foldered by channel (<channel>/<file>.dmg) for tidy, predictable download links; the
appcast feeds stay at the bucket root (appcast-<channel>.xml), where each build's baked-in
SUFeedURL points them.
The Worker also fronts the iOS companion (NucleicRemote), which ships via TestFlight rather than Sparkle/R2. Two more aliases are static redirects to the fixed TestFlight invite links:
GET https://updates.nucleic.blakeslee.xyz/testflight/canary → 302 → https://testflight.apple.com/join/nFnHdmAU
GET https://updates.nucleic.blakeslee.xyz/testflight/beta → 302 → https://testflight.apple.com/join/AaBZpbkp
How it works
- Routing. Path-scoped Worker routes (
/<channel>/latestand/testflight/<channel>) sit on the updates hostname. Worker routes take precedence over an R2 custom domain on the same host, so only those paths reach the Worker; the rootappcast-*.xmland the<channel>/*.dmgobjects keep being served straight from R2. - Latest resolution. The Worker reads the channel's appcast (
appcast-<channel>.xml) from the bound R2 bucket and redirects to the enclosure with the highestsparkle:version(the monotonic build numberrelease-macos.shbumps every release). It's the same XML the in-app updater reads, so the alias and the updater always agree — and there is no per-release config to update. - TestFlight aliases. The two
/testflight/<channel>paths are static redirects to fixed TestFlight invite links (TESTFLIGHTinsrc/index.ts) — no appcast, no R2 read. Edit the map and redeploy if a link is ever rotated. - 302, short TTL. The
latestredirect is a temporary302(the target changes each release) cached for 5 min, matching the appcast's ownmax-age(scripts/upload-r2.sh). The TestFlight redirects are also302, cached an hour since they only change on a manual link rotation.
Develop
npm install
npm run typecheck # tsc --noEmit
npm test # node:test — unit tests for the appcast parser
npm run check # wrangler deploy --dry-run (bundles, no upload, no auth)
npm run dev # local server at http://localhost:8787
Local smoke test against npm run dev (needs a real appcast in the bound bucket, or use
--remote):
curl -sI http://localhost:8787/beta/latest # → 302, Location: …/Nucleic-Beta-<ver>.dmg
Deploy (one-time + on demand)
Prereqs: the blakeslee.xyz zone and the nucleic-updates R2 bucket (with its
updates.nucleic.blakeslee.xyz custom domain) already exist on this account — both are set up for
Sparkle hosting in docs/CLOUD_INFRA.md §1.
npm run deploy # wrangler deploy — registers the /<channel>/latest + /testflight/<channel> routes
After deploy, verify (a published beta release must exist in the bucket):
curl -sI https://updates.nucleic.blakeslee.xyz/beta/latest
# HTTP/2 302 ; location: https://updates.nucleic.blakeslee.xyz/beta/Nucleic-Beta-<ver>.dmg
curl -sIL https://updates.nucleic.blakeslee.xyz/beta/latest | tail -1 # follows → 200 DMG
No secrets, no KV, no cron — the only binding is the UPDATES R2 bucket (read-only use).