Fondly Held
Group cards and tribute pages: 10 occasions from one Board model, zero-signup contributing
$ deploy --project fondlyheld
HTTP/2 200 OK
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.