Trevor Hailey

BandUp

Retired

2025 · Next.js · TypeScript · Firebase (Firestore, Cloud Functions, FCM) · AWS S3 · SWR · Tailwind + shadcn/ui · Sentry

App codeCloud functions

The idea

Finding people to make music with is genuinely hard, especially outside big cities. You know other musicians exist in your area — you see them at shows — but there's no good way to discover that the drummer ten minutes away is also looking for someone to jam with.

BandUp was a swipe-to-match app for musicians: build a profile with photos and audio samples, browse other musicians filtered by location, genre, and experience level, swipe to match, and message the people you match with. Tagline: Connect. Create. Repeat.

BandUp promo poster

What shipped

Profiles with photo and audio uploads, discovery with location/genre/experience filtering, swipe-to-match with undo, real-time messaging, push and email notifications, and a separate admin dashboard for support and moderation. The frontend was Next.js with API routes; data lived in Firestore, media in a private S3 bucket, and notifications ran through Firebase Cloud Functions.

Engineering decisions I'd defend

Matching as a state machine. The first version tracked matching with four parallel arrays on each user profile — matched, passed, attempted, attempted-by. Four sources of truth that could drift out of sync, and eventually did. I refactored it into a single collection of match records, each holding one explicit state (liked, passed, liked-by, passed-by, matched) with enumerated transitions between them. Matched became an absorbing state — no transition out of it by accident. Inconsistent states went from "possible and observed" to structurally impossible, and existing users were migrated with a conversion script while the legacy fields stayed readable for backward compatibility.

Messaging: crawl, walk, run. Chat launched on plain 3-second polling — boring, reliable, shipped. The second pass moved all data fetching onto SWR with per-data-type cache policies (messages refreshed every 5 seconds; signed media URLs deduped for 5 minutes since they were time-boxed anyway). The third pass added Server-Sent Events: the server holds a Firestore Admin listener per conversation and streams new messages to participants. I deliberately kept it server-side rather than using Firestore's client listeners — subscription auth is verified on the server (non-participants get a 403), permissions run through the Admin SDK instead of client security rules, and SSE events write into the same SWR cache so the UI never flickers or double-inserts. Polling stayed on as the fallback: if the stream dies, chat degrades to 5-second refresh instead of breaking.

Private media by default. An early setup guide for the S3 bucket had it public. I reversed that before launch: the bucket went fully private, and the app mints one-hour presigned URLs on demand when media needs to display. Slightly more complexity and latency for revocable, non-indexable, access-controlled media — a trade I'd make again for an app holding people's photos.

Buy, don't build — but check the seams. I replaced hand-rolled regex sanitization with sanitize-html, validator, and zod, which was the right call. Then a user's chat message rendered with ' where an apostrophe should be — the escaping utility was entity-encoding display text. The fix, and the lesson: strip dangerous tags on input, but never entity-encode text you're going to render as text. Sanitization has to happen at the right layer, and a library can't decide which layer that is for you.

The launch

BandUp debuted at a local music festival. I handed out personal fans printed with the logo and a QR code — it was hot, people kept them — and spent the weekend talking to musicians, most of whom loved the idea on the spot. The event brought in around 100 signups, and I was in talks with one band to sponsor them in exchange for talking up the service.

Why it died

Honest answer: liquidity. A matching app is a two-sided market even when both sides are musicians, and 100 users spread across every instrument, genre, and experience level in a small metro area is a thin market. Match rates were low — not because matching was broken, but because for most users the right person simply wasn't on the app yet. People signed up, swiped through the pool once or twice, matched with nobody or almost nobody, and had no reason to come back. Musicians have limited time and even more limited patience.

I also couldn't market it the way it needed. One person can build the product or build the audience; doing both, the audience loses.

What I took away

  • Density beats features. No amount of engineering fixes an empty room. If I did it again, I'd manually matchmake fifty musicians over text messages before writing a line of code, and only build the machine once the manual version was straining.
  • I built for scale that never came. The caching layer, the admin dashboard, the security hardening — good engineering, wrong moment. Some of it was practice I don't regret; some of it was avoidance of the harder problem, which was getting users.
  • Instrument the funnel from day one. I knew signups; I learned about the once-or-twice usage pattern later than I should have.
  • Shipping something real teaches things tutorials can't. Auth churn, storage reversals, a state-machine refactor under live data — every one of those came from real users touching a real system.