From → to
PHP 8 and MySQL on UVM’s Silk shared hosting, behind CAS → Next.js 16 and TypeScript on Vercel, Neon Postgres with Drizzle, Vercel Blob for photos, and Resend for mail.
projects / catamount-sublets
Sublet marketplace for UVM students

Catamount Sublets is where UVM students find and post sublets — browse by price, semester, and distance from campus on a grid or a map, and share any listing with a public link. It’s the second version. The first, UVM Sublets, was a PHP and MySQL app I launched in March 2025 on UVM’s Silk web server, behind CAS single sign-on. I rebuilt it as Next.js and TypeScript on Vercel and moved everyone over in a live cutover on October 9, 2026.
PHP 8 and MySQL on UVM’s Silk shared hosting, behind CAS → Next.js 16 and TypeScript on Vercel, Neon Postgres with Drizzle, Vercel Blob for photos, and Resend for mail.
CAS sign-in came from an Apache module on Silk, so leaving Silk meant replacing it. Sign-in is now a one-time email code through Better Auth (hashed, 10-minute expiry, three attempts), sent with Resend. Sign-up is restricted to NetID@uvm.edu: dotted First.Last aliases are refused so every student maps to one account, migrated accounts are matched by NetID, and anyone else needs an admin-approved access request. The domain check runs before any email is sent and again when the user row is created, and a resend cooldown plus per-email and per-IP limits in Postgres-backed buckets sit in front of every code.
A tested TypeScript script reads the legacy MySQL — from a dump or over an SSH tunnel — prints every planned insert, update, and delete as a dry run, then applies them and verifies each photo against the original by hash. It honors opt-outs and was rehearsed against a Neon branch before the real run. It moved 44 listings and 199 photos, reusing 196 images already uploaded in rehearsal.
Freeze the old site read-only, take a final export, dry-run against production, snapshot a Neon restore branch, apply, open read-only, swap Silk’s document root for a redirect shim, smoke-test, and turn writes on — 07:45 to 08:03 on a Friday morning. Old share links and preview images redirect to the new site, go.uvm.edu/sublet points there until January 2027, and a 48-hour rollback path was ready.
Users and sessions, listings with their photos, semesters, and an event history, plus access requests and Postgres-backed rate-limit buckets. Six Drizzle migrations.
Uploaded straight from the browser to a private Blob store, then processed with sharp: auto-rotated, capped at 3000px, EXIF and GPS stripped, and written out as a 1600px WebP, a 600px thumbnail, and a JPEG share card.
36 Vitest files cover the schema, services, rate limits, the legacy migration and its dump parser, image processing, and the auth allowlist. GitHub Actions runs lint, typecheck, tests, and the build on every push.
A moderation console: bans, listing status, photo cleanup, semester archives, announcements, a read-only switch, and broadcast email.