Apple Music Edge API
A Cloudflare Worker that signs ES256 JWTs with the Web Crypto API, so my site can read the Apple Music API without shipping my developer private key to a browser.
The problem
I wanted the homepage to show what I'm actually listening to, live. Every Apple Music API
request needs a developer token — a JWT you sign yourself with a private key Apple issues as
a .p8 file. Signing that in client-side JavaScript means handing the key to
every visitor, so it has to happen somewhere I control.
My site is static and already behind Cloudflare, so a Worker adds a server-side step without adding a server to maintain.
Signing the token
Workers don't run Node, so jsonwebtoken isn't available — the ES256 signature is
built against the Web Crypto API instead.
const cryptoKey = await crypto.subtle.importKey(
'pkcs8', keyBuffer,
{ name: 'ECDSA', namedCurve: 'P-256' },
false, // non-extractable
['sign']
);
const payload = { iss: teamId, iat: now, exp: now + 3600 };
Two details matter. The key is imported non-extractable, so even code inside
the Worker can't read the raw bytes back out. And JWS expects the raw r‖s
signature, which is what crypto.subtle.sign returns for ECDSA — the DER-wrapped
form some libraries produce is silently rejected by Apple.
The part that took longest
Not the cryptography — the key formatting. importKey needs raw PKCS#8 bytes, but
a .p8 arrives as PEM text, and passing through an environment variable mangles
it in several different ways: newlines as literal \n pairs, the key collapsed to
one line, Windows line endings, or the markers stripped entirely. Every variant fails in the
same place with the same unhelpful error.
So the Worker normalises before it parses — unescape, re-wrap the base64 body at 64 characters, restore the markers, then decode. Unglamorous, and it's most of what makes this work across deploys instead of only on the machine where it was first set up.
Reliability
- Apple's status code is passed through rather than flattened, so a rate limit reads as a rate limit.
- Errors return JSON with CORS headers, same as success — one shape for the client to handle.
- Responses cache for an hour at the edge, which keeps almost all traffic off Apple's API and off my rate limit.
Tradeoffs
A Worker over a backend: no idle cost and nothing to patch, at the price of being coupled to the Workers runtime — which is exactly why the signing is hand-rolled.
The token is valid for an hour but a new one is minted per uncached request. Caching it would save a cheap signature and add cross-invocation state; with an hour of response caching in front, that hasn't been worth it.