Crewmate

matching · recommendation · hackathons

github ↗︎ try it here ↗︎

tami and i started building crewmate because finding hackathon teammates still felt surprisingly manual. most people were posting in discord servers, group chats, or linkedin and hoping they’d find someone before applications closed. we wanted to build something that made finding teammates a little less random.

it’s a react and typescript app on top of postgres and supabase, using row-level security to keep hackathon data scoped correctly.

the first version was pretty simple. students could create a profile, browse other participants, and send teammate invitations. after we started using it and talking to other students, the project kept growing. hackathon-specific profiles, saved teammates, email notifications, and eventually a matching service all came from solving problems we ran into along the way.

v1 profile browsing v2 invitations v3 hackathon workspaces v4 ai-assisted matching v5 recommendation scores

one thing we changed early was how profiles worked. instead of having one profile for everything, every hackathon has its own space. someone looking for teammates at hacktx probably isn’t looking for the same people they’d want to work with at treehacks or calhacks, so profiles, invitations, and teams stay tied to each event.

hackathons profiles teams invitations skills one hackathon row, everything else scoped beneath it

scoping data by hackathon in the schema wasn’t enough on its own — we still had to make sure a request couldn’t reach across events or read another student’s profile just because it knew the row id. instead of enforcing that in application code, every table’s row level security policy checks it directly against the caller’s session: a profile can only be edited by its owner, an invitation only acted on by its sender or recipient, and so on. there are close to two dozen of these policies across the schema now, one for close to every table and action combination that touches user data.

request anon key + session jwt RLS policy auth.uid() = owner or a party to the row checked on every table allowed row returned denied empty result

the matching system became the most interesting part of the project. recommendations start with a fast heuristic so results appear immediately, then a separate ai-assisted matching service looks at things like experience, interests, project ideas, and goals to produce a more complete compatibility score. if that service isn’t available, the app falls back to the heuristic so matching never stops working.

profile request skills + interests heuristic match instant, always available available refined compatibility score unavailable heuristic result stands

another problem was user-submitted hackathons. we didn’t want ten copies of the same event because people pasted different urls or slightly different names. the app first checks the normalized url against every hackathon it already knows about — if that matches, it’s done and nothing else has to happen. if the url is new, ai decides whether it’s the same event listed a different way or an actual new hackathon, and a database-level check on the normalized name acts as a final backstop against duplicates slipping through.

submitted hackathon url/name known url match? no → ai reviews it duplicate reuse existing event new create hackathon row

since these submissions come in as raw urls, we also had to make sure the server never blindly fetches whatever gets pasted in. before fetching a submitted url, we resolve its hostname and reject anything that points at a private ip range, localhost, or cloud metadata addresses like 169.254.169.254 — and every redirect gets re-checked against those same rules instead of letting the fetch client follow it blindly.

submitted url resolve hostname check resolved ip private / localhost / cloud metadata range? blocked request rejected public ip safe to fetch

invitations ended up being more complicated than we expected. students can save profiles, send teammate invitations, accept or decline requests, leave hackathons, and keep everything separate for each event. a lot of the work went into making sure all of those different states stayed consistent as the project grew.

invite sent pending accepted joins the team declined can still leave later

we’re still actively building crewmate. every new hackathon gives us another excuse to improve the matching service, rethink the recommendation logic, and make finding teammates a little easier.