Live for Luther College · usecampusfound.vercel.app
A live campus lost-and-found board where anyone can post a found item in seconds — built so the public write path cannot spam, leak, or rot.
Next.js 16 (App Router) · React 19 · TypeScript · Supabase (Postgres + Storage) · PostHog · Playwright · Tailwind · Vercel
What it is
CampusFound lets anyone on campus post an item they found — a photo, a building, a location type — and lets everyone else browse and filter active listings. There are no user accounts on the public side: posting is deliberately frictionless. That openness is also the whole engineering problem.
The problem
An anonymous, public write path invites spam, junk uploads, and abuse, and a found-item board fills with stale listings if nothing removes them. On top of that, the whole thing runs on Supabase, whose anon key ships to the browser — so the default posture leaks the database unless you close it. The build is mostly about drawing one clean trust boundary and keeping the board fresh without a human watching it.
What I built
moderation_events audit trail behind an 8-hour HMAC-signed session. removed, logging each as a moderation event.Architecture
Browser
Next.js App Router · React 19
Next.js route handlers
the only thing holding the service-role key
Supabase Postgres
RLS forced · anon grants revoked
Supabase Storage
listing images
The browser never talks to Supabase directly. Every read and write goes through a Next.js route handler that alone carries the service-role key, so there is exactly one place to reason about access.
Two decisions and their tradeoffs
I enabled and forced Row-Level Security on every table with no policies, and revoked the default grants from the anon and authenticated roles. The public anon key can therefore read and write nothing directly; all access flows through server routes using the service-role key. The cost: no convenient client-side queries, and every data path is code I have to write. The gain: a single, auditable trust boundary instead of policy sprawl.
Admin auth is a versioned, 8-hour HMAC-SHA256 cookie signed with WebCrypto — no session store to run or migrate for a small moderator group, and secret checks use constant-time comparison. The tradeoff is honest: I can't revoke a single session before it expires without rotating the secret. At this scale, a short TTL is the right lever; a session table is the upgrade path if the moderator set grows.
Status