Build Report · Personal Project

Mother's Day Picker

A self-built conversational gift picker for Mom (79). Hand-curated venues, accessible UX, AI-personalized result, full phone-deploy infrastructure built alongside.

Built May 8–9, 2026 · Asheville, NC · Live at mothers-day-picker-may2026.jason-8ce.workers.dev
20
Venues
29
Conversation nodes
8
Grouped categories
2
Cloudflare workers
10
Bugs caught + fixed

What it does

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.

Open the live picker →

Architecture

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.

mothers-day-picker-may2026

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

mdp-api-proxy-may2026

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

Phone-deploy infrastructure

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.

Issues caught and fixed

Ten bugs surfaced during build and QA. Documenting them here as a record and so the patterns aren't relearned next time.

1. Duplicate "come to me" chip on the at-home branch

Two near-identical labels back to back. Fix: removed the duplicate, kept the one that didn't echo "I'd love" already used nearby.

2. "Today" framing throughout the copy

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."

3. iMessage pre-fill was backwards

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"

4. Deploy relay pointed at the wrong worker

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.

5. Auth conflation in the relay

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).

6. Phone deploys didn't exist

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.

7. AI hallucinated overnight stays

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.

8. Browse view auto-scrolled past everything to the bottom

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.

9. JS broke when the prompt was edited via re.sub

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.

10. "Show me everything" was buried

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.

Accessibility decisions (for a 79-year-old user)

What the receipt looks like for Mom

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.

What's still pending