Live for Luther College · usecampusfound.vercel.app

CampusFound

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

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

Server-only data access behind forced RLS

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.

Stateless HMAC admin sessions, no session table

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

Open to new-grad roles

ResumeEmail