Back to Projects
Live

Fondly Held

Group cards and tribute pages: 10 occasions from one Board model, zero-signup contributing

~/fondlyheld

$ deploy --project fondlyheld

HTTP/2 200 OK

SAAS

Key Technical Highlights

12 tables and 5 additive migrations, 10 occasion types x 3 themes seeded as rows: one Board model with tone as data (celebratory / warm / solemn), so a new occasion is an insert, not a deploy, and memorial boards cannot receive confetti by construction.

Zero-signup contributing with layered abuse control: Postgres-backed rate limits (5 posts/IP/board/hour, 20/IP/day, 40 uploads/hour), honeypot plus timing check, hashed IPs. No Redis and no captcha, to keep the one-link-to-post loop frictionless and portable off Vercel.

Uploads go browser to Cloudflare R2 via presigned URLs (25MB image / 100MB video caps, MIME checked server-side), and a daily cron sweeps orphaned objects with a guard that aborts if over half the bucket looks orphaned.

Diagnosed a production outage caused by a pg_dump run through Neon's connection pooler leaking an empty search_path to shared connections; fixed it, then codified direct-URL-only rule for dumps and migrations.

Found and fixed a stored XSS (unescaped board title inside JSON-LD script) and a silent Open Graph bug that suppressed link previews on nearly every board.

The Case Study

The Problem

Group cards for birthdays, farewells and weddings are a familiar category. Memorials are different: the stakes are higher, because a misjudged animation on a tribute page is a product failure, not a cosmetic one. The constraint I set: anyone can contribute from a WhatsApp link on a phone with no account, and the page must feel right for a birthday and for a funeral.

The Numbers

  • 12 tables (8 application, 4 auth) and 5 additive migrations
  • 10 occasion types x 3 themes, seeded as data
  • 3 motion profiles: celebratory, warm, solemn
  • About 10.5k lines of TypeScript across 35 commits (14 to 24 Sep 2026), one author
  • Rate limits: 5 posts per IP per board per hour, 20 per IP per day, 40 upload signatures per hour
  • Upload caps: 25MB image, 100MB video
  • Occasion landing pages statically generated for all 10 occasions; board pages ISR at 60s

Decisions and Trade-offs

One Board model, occasion as data. I rejected per-occasion branches. Each occasion row carries a motion profile, copy, prompts and themes, and reactions are profile-specific (memorial gets quiet ones only). Trade-off: occasion-specific fields have nowhere to live except a JSONB column, which I accepted to keep adding an occasion a row insert.

Postgres rate limiting instead of Redis. It counts rows in the existing database, so there is no extra service and it behaves identically on Vercel or a VPS. The cost is that the check is not atomic and a burst can overshoot by a few; for abuse throttling that is acceptable.

Presigned R2 uploads instead of proxying files through the app. Keeps large media off serverless functions and avoids egress fees on long-lived, media-heavy pages. The cost is orphaned objects when a user uploads and abandons, which is why the cleanup cron exists.

Unoptimized images instead of Vercel Image Optimization. The free-tier quota ran out and every optimized image returned 402 in production. I turned optimization off and pre-sized assets to 720px WebP (about 1.4MB for 19 curated photos) rather than depend on a paid quota.

Soft email verification instead of blocking sign-in. Hard-blocking would have locked out existing accounts; verification gates only the feature that depends on it, the invited-boards list.

How It Was Verified

There is no automated test suite yet. Verification was manual and end to end: the anonymous create, post and moderate flow exercised against production, memorial boards checked specifically for no confetti, 375px overflow checks, and a written tester guide (TESTING.md) for external testers. Lint, type-check and production build were clean at the last recorded status. The orphan sweep was dry-run first (5 objects, 4 referenced, 1 orphan).

Honest Limits and What I'd Change

No automated tests, and the highest-risk rules (solemn never celebrates, private boards stay private) deserve them first. No Lighthouse pass is recorded yet, so I make no performance claims. Privacy and Terms have not had legal review. Guest-board claiming is cookie-based and per-browser.

My Role

Sole engineer: product rules, schema, auth, upload pipeline, abuse controls, SEO structure (JSON-LD, sitemap, per-board OG images, llms.txt), deployment and the operational runbook. All 35 commits are mine.