Most personal sites end with "say hi" and an email address. I wanted the end of mine to be something you could do on the page itself. The result is a melody guestbook: you tap out eight notes, press save, and your tune joins the wall for the next visitor to play. There's no text field anywhere.
Why no words
A guestbook that accepts text is a moderation job. Spam, links, abuse — anything public with a text box needs someone watching it. Eight notes from a fixed scale can't contain a link or an insult. The constraint removed the moderation problem entirely, and it also made the guestbook fit the site, which is mostly about music.
It turns out eight notes is enough to say something. People leave little fanfares, descending sighs, the first bar of songs they like.
The format
A tune is an array of eight integers. Each is either -1 (a rest) or 0 to 9, a step up the D minor pentatonic scale starting at D4 — the same scale the playable letters in the hero use. A pentatonic scale has no notes that clash with each other, so any combination sounds at least pleasant. There are no wrong answers, which matters when the person writing is a visitor who may never have written music.
{ "n": [0, 2, 4, -1, 5, 4, 2, 0] }
The server checks exactly that shape: eight steps, each an integer from -1 to 9, and at least three actual notes so an all-rest tune doesn't count. The request body is capped at 400 bytes before it's even parsed.
The server
The site is static files on Cloudflare, plus one small Worker that answers two routes:
GET /api/tunes → newest tunes first
POST /api/tunes {n: [8 ints]} → the saved tune
Everything else is passed through to the static files. Tunes are stored in Cloudflare's KV store, and I used one trick to avoid a database: the tune itself goes in the key's metadata, not its value. KV's list operation returns metadata along with keys, so a single list call returns the latest 48 tunes, notes included, with no follow-up reads.
KV lists keys in sorted order, and I want newest first. So each key starts with a number that counts down over time: ten trillion minus the current time in milliseconds, zero-padded to 13 digits, plus a few random characters so two tunes in the same millisecond don't collide. Sorting ascending then gives newest first for free.
Rate limiting without storing IPs
Without text there's nothing to moderate, but someone could still flood the wall. The limit is one tune per visitor per minute. To enforce it, the Worker hashes the visitor's IP address (with a site-specific prefix) using SHA-256, keeps the first 12 bytes, and stores that hash in KV with a 60-second expiry. If the hash is already there, the post is refused with a friendly "one tune a minute".
The raw IP is never written anywhere, and the hash disappears after a minute on its own — no cleanup job needed. The only other thing stored with a tune is the two-letter country code Cloudflare provides, so the wall can show that a tune came from Japan or Brazil. Never the city, never the address.
What I'd tell someone building one
- Pick a constraint that removes a problem. "No text" wasn't a limitation I worked around; it's the reason the feature was cheap to run.
- Validate the exact shape. The server accepts one precise format and rejects everything else. That's the whole security model, and it's easy to reason about.
- Let storage do the bookkeeping. Expiring keys handled the rate limit, and a reversed timestamp handled the sort order. Neither needed code that runs on a schedule.
Try it at the bottom of the home page. What's stored is described on the privacy page.