My movie discovery site (galaxymovies.app) needed a feature that sounds simple: "email me when something on my watchlist lands on Netflix." Users save titles to a local watchlist, pick their streaming services, and want a nudge the day a title actually arrives.
That feature wants an account system. Passwords, sessions, email verification, password resets, the works. I really did not want to build that. Every line of auth code is a liability I have to maintain forever, and the payoff for users is typing a password into a movie site.
So I asked a different question: what's the minimum credential that makes this work? The answer turned out to be one email address and two tokens. Here's the whole design.
The insight: the confirmation link is already proof
When someone submits the waitlist form, they get a confirmation email with a token link. Clicking it proves they control that inbox. That's it. That's the entire identity system.
But the confirm token can't be the API credential. It's a link secret, meant to be clicked once and done. Syncing a watchlist needs a durable bearer credential the client sends on every edit. So confirm mints a second, separate token: a UUID sync key stored in a Redis hash keyed by email.
export const PENDING_KEY = 'gm:waitlist:pending' // email => confirm token
export const CONFIRMED_KEY = 'gm:waitlist:confirmed' // email => unsub token
export const SYNCKEY_KEY = 'gm:alerts:synckey' // email => sync UUID
export const ALERTS_PREFS_KEY = 'gm:alerts:prefs' // email => {watchlist, services, region}
export const ALERTS_NOTIFIED_KEY = 'gm:alerts:notified' // email => sent alert keys
Two credentials, two jobs. Leaking or rotating the sync key never touches the unsubscribe token, and vice versa.
Handing out the key through a URL fragment
The interesting part is delivery. The sync key has to get from the confirmation click to the user's browser without ever appearing in my server logs. Query strings get logged; fragments don't.
So the confirm endpoint 302-redirects to a status page with the credentials in the hash:
// Fragment never reaches the server on redirect follow, so the sync
// credential stays out of access logs from this point on.
const syncKey = await getOrCreateSyncKey(kv, email)
return redirect('confirmed', `email=${encodeURIComponent(email)}&key=${syncKey}`)
Which produces /email-status?status=confirmed#email=you%40x.com&key=abc-123. The status query param is logged; the fragment is not, because browsers don't send it. The page reads window.location.hash, writes {email, key} to localStorage, and the credential now exists in exactly two places: the user's browser and one Redis hash field.
Syncing without sessions
The client posts a snapshot of the watchlist and chosen providers whenever they change. Two details keep this cheap:
export async function syncAlertsNow(data, creds = getAlertsCredentials()) {
if (!creds) return
const payload = JSON.stringify(buildSnapshot(data))
if (localStorage.getItem(LAST_SYNCED_KEY) === payload) return
const ok = await postPrefs(creds, snapshot)
if (ok) localStorage.setItem(LAST_SYNCED_KEY, payload)
}
A 4 second debounce collapses edit bursts, and the LAST_SYNCED_KEY check skips the request entirely if nothing changed since the last successful sync. Any watchlist edit re-renders the app, so without the diff check every idle re-render would fire a POST.
On the server side, POST /api/alerts-prefs validates the email and key formats, rate limits per IP and per email (30/hour each, keyed by a sha256 of the address rather than the raw email), then safeEquals the key against the hash:
const stored = await kv.hget<string>(SYNCKEY_KEY, email)
if (!stored || !safeEqual(stored, key)) {
return res.status(403).json({ error: 'Invalid sync credentials' })
}
await kv.hset(ALERTS_PREFS_KEY, { [email]: { watchlist, services, region, updatedAt: Date.now() } })
Notice what's missing: no session, no cookie, no JWT. The sync key is just a bearer token with a 36 character format check.
The cron does the matching
A daily job diffs each tracked provider's catalog (added and coming soon lists are already computed by an earlier cron and cached). Then it loads all three hashes in three HGETALLs and walks subscribers:
for (const item of change[kind]) {
if (!item.tmdbId || !watchlistKeys.has(`${item.tmdbType}:${item.tmdbId}`)) continue
const notifiedKey = `${kind}:${item.tmdbType}:${item.tmdbId}@${providerKey}`
if (notified.has(notifiedKey)) continue
out.push({ kind, notifiedKey, tmdbId: item.tmdbId, ... })
}
Each subscriber's watchlist becomes a mediaType:id set matched against their chosen providers' diffs. The notifiedKey dedupe (added:movie:550@netflix) means nobody gets the same alert twice, and each user gets at most one email per day with up to 20 matches. The email carries a List-Unsubscribe header and a one-click link.
One subtlety: the cron reads provider diffs with fresh: true to skip the per-instance memo I added to keep pageviews cheap. A memoized diff is exactly wrong here, the whole point is "what changed today."
Syncing another device needs no new UI. If an already confirmed email resubmits the waitlist form, the server mails them an "alerts-link" URL that reissues the fragment flow on whatever device opened the email:
if (await kv.hget(CONFIRMED_KEY, email)) {
const key = await getOrCreateSyncKey(kv, email)
await sendLinkDeviceEmail(email, key)
return res.status(200).json({ ok: true, pending: true })
}
Same pending: true response as a fresh signup, deliberately. Returning "you're already subscribed" would make the form an oracle for checking who's on the list.
Unsubscribing deletes everything
The unsubscribe link uses the original confirm token and wipes all five hash fields: confirmed status, pending, sync key, stored prefs, and the notified set:
await kv.hdel(CONFIRMED_KEY, email)
await kv.hdel(PENDING_KEY, email)
await kv.hdel(SYNCKEY_KEY, email)
await kv.hdel(ALERTS_PREFS_KEY, email)
await kv.hdel(ALERTS_NOTIFIED_KEY, email)
Any client still holding the sync key starts getting 403s immediately, and there's no residual data to leak.
What I gave up, honestly
This is not authentication, and I want to be straight about the limits:
- Anyone with the sync key can overwrite that email's watchlist prefs. It's a write only vandalism risk bounded to preference data, not an impersonation risk. There's nothing sensitive to read back, the prefs endpoint never returns stored data.
- The confirm and link endpoints skip rate limiting on purpose, because mail scanners prefetch links. The token is the auth. That trade means those endpoints are cheap GETs that do one hash lookup.
- If someone loses their browser profile, the fix is resubmitting the form and getting a fresh link. Annoying, but not a support ticket.
The actual lesson
"Needs alerts" does not mean "needs accounts." If the identity proof you're already doing (email confirmation) is strong enough, a second bearer credential for the chatty sync traffic gets you the whole feature. Fragment delivery keeps it out of logs, format checks plus rate limits keep it boring, and the unsubscribe path deleting every trace keeps it honest.
Total auth surface: zero passwords, zero sessions, zero cookies. One email field and a UUID.
The site is galaxymovies.app if you want to see the flow. Sign up, confirm, and watch your browser's localStorage for gm.alerts.v1, that's the whole credential.