A self-built conversational gift picker for Mom (79). Hand-curated venues, accessible UX, AI-personalized result, full phone-deploy infrastructure built alongside.
A warm, chat-style web app that walks Mom through picking her own Mother's Day pampering gift, then sends Jason a pre-filled iMessage with the venue she chose so he can buy the gift cert.
Two Cloudflare Workers on Jason's account. The whole app is ~70 KB of HTML/CSS/JS bundled into one worker response — no build step, no backend, no database.
The live app. Self-contained HTML/CSS/JS. State is in-memory with a manual nav stack for back-button support.
~70 KB · single worker
Three jobs in one worker: (1) AI proxy to Anthropic API (origin-allowlisted). (2) /deploy hardcoded to MDP. (3) /deploy/<worker_name> general allowlisted deploy.
v3 · 3ed9a5c0-aa42-491a
Built alongside the picker so updates can ship from anywhere — phone, browser, terminal — without wrangler. One curl pushes new HTML to any allowlisted worker.
curl -X POST https://mdp-api-proxy-may2026.jason-8ce.workers.dev/deploy/<worker> \
-H "Authorization: Bearer <RELAY_SHARED_SECRET>" \
-H "Content-Type: application/json" \
-d '{"url":"https://paste.rs/<id>"}'
Two-token model with separated roles. The shared secret is low-blast-radius — it can only deploy to allowlisted build workers (bt-*, bosstorque-*, sperry-*, mdp-*, mothers-day-*, or anything ending in -mayYYYY etc.). Critical workers like bosstorque-rules and the relay itself are hard-blocked. The CF API token (broader scope) lives only inside the worker secret and never reaches the caller.
Ten bugs surfaced during build and QA. Documenting them here as a record and so the patterns aren't relearned next time.
Two near-identical labels back to back. Fix: removed the duplicate, kept the one that didn't echo "I'd love" already used nearby.
The opening message and closing question both anchored to Mother's Day specifically. Mom might open the link later, not on the day itself. Fix: rewrote as timeless. Closing now reads "Take your time — there's no rush, and your gift will keep."
Original SMS body read as Jason talking to Mom and signed by Jason. But Mom is the sender — when she'd hit send, Jason would receive a message that looked like he'd sent it to himself. Fix: flipped to Mom's voice. "Jason — I picked: [venue] ... Thank you, sweetheart. Love you! 💕 — Mom"
The relay was deploying to workers-day-picker-may2026 (a stale duplicate) instead of the live mothers-day-picker-may2026. Phone deploys would have updated the wrong app. Fix: patched the proxy to target the right worker. Stale worker deleted.
Original design used the same CF_API_TOKEN for both caller-auth and internal CF API auth. Anyone with the caller token would have full CF API access — could nuke any worker on the account. Fix: split into RELAY_SHARED_SECRET (caller, narrow allowlist) and CF_API_TOKEN (internal only).
Originally deploys required wrangler CLI on the Mac — a non-starter when Jason wants to ship from his phone. Fix: added a generalized /deploy/<worker_name> endpoint to the proxy with allowlist + hard-block rules. Saved the secret to 1Password and Claude mobile memory.
The salt-spa description includes "people consistently fall asleep" — meaning brief in-chair relaxation. The AI extrapolated to "You'll sleep so peacefully there," which sounds like an overnight stay. Fix: tightened the system prompt. Explicit constraint: never echo sleep-adjacent words; these are day visits, period. Banned word list expanded.
After tapping "Show me everything," the page scrolled past all 20 venues to land on the nav row. Fix: removed the forced scroll-to-bottom. Switched to instant scroll (smooth was being silently dropped by the body's overflow: hidden auto layout) and pointed at the AI intro message instead.
Python's re.sub interprets \n in the replacement string as a literal newline. My first prompt edit produced literal newlines inside a JS string literal — syntax error, live worker briefly broken. Fix: caught it via post-deploy node --check. Switched to plain string replace. New rule: every relay deploy gets a syntax check first.
The full-list browse with multi-select was reachable only after tapping "I'd like to go out." Fix: added a 4th start chip "✨ Show me everything — I'll narrow down" so Mom can jump straight to all 20 venues from the home screen.
She opens a text from Jason. One sentence, one link. Taps it. The picker walks her through three or four questions — gentle, no rush. She picks favorites, hits "Compare," chooses one, and unwraps a result card with the venue, the photo, a phone number she can tap, and a $150 gift certificate she can use whenever feels right. One more tap sends Jason exactly what he needs to buy the gift cert. The whole thing should take her under five minutes.
project_cf_security_hardening.md has the punch list.