← BACK

A music library of my own, running for nothing

Cloudflare Workers · R2 · D1 Summer 2026 Source →

The library and the player, running on iPhone

I wanted my own music on my phone, playable on a plane, without paying a subscription for the privilege. That part is ordinary. The constraint I set myself was the interesting bit: it had to run for nothing, and not the kind of nothing that turns into a bill in three months.

The downloader pulls, it is not pushed to

Fetching and remuxing audio needs real binaries, and a Workers isolate cannot run those. The obvious answer is Cloudflare Containers. It is the wrong answer twice: Containers have no free tier at all, and they leave through Cloudflare IP ranges, which is exactly what the source platform's bot filtering watches for.

A Worker also cannot call a machine sitting behind a home router. No fixed address, no open port. Instead of tunnelling through that, I turned the call around.

Push Worker my machine needs a tunnel and an open port
Pull Worker my machine nothing exposed at all

Every connection is now outbound, so there is nothing to open on the router. Nothing runs on Cloudflare, so nothing leaves the free tier. And the request to the platform leaves from a home connection rather than a datacenter range, which turns out to be the difference between working and being filtered. The trade-off is real and I will not dress it up: my machine has to be on for a job to run. Jobs sit in the queue and drain at the next boot. Playback never waits on it, since the files already live in object storage.

Claiming a job without racing

Pulling only works if two workers cannot grab the same job. The claim is a conditional update, and the second one to arrive sees zero rows changed and walks away empty handed:

UPDATE tracks SET status = 'downloading', claimed_at = ?
  WHERE id = ? AND status = 'pending'

Claims also carry a lease. Anything reserved but silent for fifteen minutes goes back in the queue, which is what saves a job from a power cut, a container stopped mid-download, or a laptop lid closing. Progress reports push the lease forward, so a slow job is never preempted by its own slowness.

Offline was the point, not a feature

Tracks are stored through the Cache API and played from a blob: URL. The service worker is deliberately kept away from audio: Safari probes every <audio> element with a one-byte range request, and a worker that helpfully answers with the whole file breaks playback without raising anything you can catch.

The part I did not see coming is that the list has to be local too. Every file can be on the device and the app will still open to an empty library, because knowing which tracks exist is an API call, and that call is the one thing a plane guarantees will fail. The last good response is now kept locally and used as the fallback.

The service worker was deleting every cache absent from its keep-list, and the audio cache is created by the page. So each update quietly wiped every downloaded file. The app looked perfect and was empty at precisely the moment it was supposed to earn its keep.

Small things Safari and Chrome insist on

Opus only plays inside an Ogg container, so everything is remuxed into one, stream-copied, with no re-encoding loss. Chrome caps every cookie at 400 days no matter what the server asks for, so sessions quietly re-issue themselves past thirty days and a device is never locked out. iOS ignores the orientation field in the web app manifest, and Safari has no API to lock it, so the portrait lock lives in CSS. None of these are hard. All of them fail silently, which is worse.

Adding a track without leaving the page

Pasting links by hand got old within a week, so there is a small Chrome extension that puts a button in the page. The content script never talks to my Worker directly. It messages the extension's own service worker, which is what lets the extension avoid asking for permission to read the pages you visit.

An Add to Music button sitting next to Subscribe
One click, from the page, with no host permission on the site.

What it costs

Workers
~2,000 of 100,000 requests a day
D1
a few thousand of 5M reads a day
R2
~1,500 tracks inside 10 GB, egress free
The downloader
about 3 W on a Raspberry Pi
Per month$0

The first line that could ever tip into paid is storage past ten gigabytes, which on my listening habits is a problem for another year.

mazeasdamien/dmzs-music on GitHub