Inspiration

The formatting came from FPS games, mostly COD lobbies, where you drop into a game someone else has already set up and watch the roster fill in front of you. We also borrowed from Strava, where your profile is something you build up over time and other people can see what you've actually done.

Under that was a much more boring problem all five of us recognised. Wanting to play something is easy. Finding nine other people who are free on Thursday at 6 is not. It usually dies in a group chat, and meanwhile the court down the road sits empty.

That's the block we picked. Sport is one of the oldest ways people meet each other, and it's quietly falling apart at the local level. Not because the demand isn't there, and not because the courts aren't there. The two just can't find each other. The people, the spaces and the free time are all sitting in the same suburb, disconnected.

The other half of the theme hit closer to home. Technology is very good at making you feel connected while you sit on your own. We didn't want to build another feed. We wanted the app to get you off the app and into a park with actual people, so the measure of MatchUp working is that you close it and go play.

What it does

MatchUp helps you find a local pickup game and actually turn up to it.

You browse matches near you and filter by location and radius, sport, skill level, date and time. Distances are real, so a match shows up as "0.3 km away" rather than a vague suburb name. The filters most people never touch are folded behind a "More filters" toggle so the page isn't a wall of dropdowns.

Creating a game takes about a minute. Title, sport, location, date, time, how many players, what skill level. You get added to your own roster automatically and land straight on Browse to see it sitting there live.

The part we care about most is that you can see who's going before you commit. Every match card shows the host's face and name, the faces of the players already signed up, and a count like 4/10 with a fill bar. Turning up to a game full of strangers is the scary bit. Seeing three faces and a name makes it much less scary. You're joining people, not a calendar entry.

There's live chat in the bottom right of every page with three rooms: a public one, one for each match you've joined, and direct messages. Messages arrive in real time. You can search for people by name or email, send friend requests, and see a badge on the nav when someone has added you.

One rule we set deliberately: you can only DM someone if you're accepted friends, or you're both signed up to the same match. Bringing people together only works if it's safe to show up. A new player shouldn't be able to be cold-messaged by a stranger just for joining a game.

Blocks, and not leaving people behind

We took the theme fairly literally in two places.

The first is the underused resources bit. The blocks in our version are the ones already sitting around your suburb: the empty court, the eight people who'd play if someone organised it, the person who just moved here and doesn't know anybody yet. None of that is missing. It's just not connected. MatchUp is the connective tissue, and it doesn't require anyone to buy or build anything new.

The second is accessibility, which we treated as a real requirement rather than a nice-to-have, because "no one gets left behind" is easy to write in a pitch and harder to actually ship.

Every match carries a skill level, including "All Levels", and browsing defaults to showing everything rather than filtering beginners out. Roster caps are visible up front so nobody travels to a game that's already full. And the app itself had to be usable by everyone: we measured colour contrast instead of eyeballing it, we made every control reachable and visible by keyboard, we labelled the icon-only buttons for screen readers, we put live regions on the result counts so filtering announces itself, and we made the animations switch off if your operating system says you want less motion.

That last list is unglamorous and no judge will see most of it. It's also the difference between an app for everyone and an app for people with good eyesight and a mouse.

How we built it

React and TypeScript on Vite, with Supabase behind it for Postgres, auth, realtime and file storage. Google Maps Places handles location autocomplete on both the create and browse pages, and we store latitude and longitude so the radius filter can do a real distance calculation rather than string matching.

Chat is one messages table with three scopes (public, match, direct) and a CHECK constraint that makes an invalid combination impossible to write in the first place. Access is enforced with row level security rather than in the interface. Public messages are readable by any signed in user. Match messages are readable only if you're in that match's participant list. Direct messages are readable only by the two people in them, and you can only send one if a can_message() function agrees you're friends or share a match.

The point of doing it that way is that the rules hold even if someone bypasses our frontend entirely and talks to the API. Supabase Realtime respects the same policies, so nothing streams to you that you weren't allowed to read.

On the design side we built a small system before building screens: one display typeface, hard borders, offset shadows, and a colour per sport that flows through cards, filters and icons through CSS variables. It meant five people could build different pages and still end up with one product.

We worked on separate branches and merged through pull requests, just under 40 of them by the end.

Challenges we ran into

Landing on an idea took real time. We wanted something that wasn't trivial but was still finishable in 24 hours, and we talked it through properly before anyone wrote code.

The git workflow was the hardest technical thing, honestly. Five people in one repo with several of us in the same files. We hit some real merge conflicts, the worst being two of us rewriting the same block of the match browser at once. One side had swapped fuzzy location matching for actual kilometre distances, the other had rebuilt the card layout. Resolving that properly meant keeping both, not picking a winner. We also learned that merges break things quietly: at one point a catch-all route got replaced and the 404 page silently stopped rendering, and an AuthProvider ended up mounted twice without anything visibly failing.

Accessibility caught us out once we actually measured it. Our first orange buttons were white text on #FF5A00, which is 3.1:1 and fails WCAG AA. Black on orange got us to 6.7:1 and honestly looks better. Then we had to check every sport colour against both white and its own tint, fix a duplicate h1, and add the focus states and skip link we'd assumed we already had.

We also learned to fail gracefully. When one person's Google Maps key wasn't set, the location field rendered as nothing at all, and the form then demanded a location you had no way of entering. Now it falls back to a plain text input instead of a dead end.

Accomplishments that we're proud of

Chat privacy is enforced in the database, not hidden in the interface. A direct message is readable by exactly two people and that holds at the API level.

The accessibility work is genuine. Measured contrast, keyboard focus, screen reader labels, live regions, reduced motion. We can point at each one.

It looks like one product rather than five people's pages stitched together, because the design system came first.

Everything meaningful is real. Real auth, real realtime, real uploads, real geography. Nothing is mocked.

And we shipped a five person feature set in a day without standing on each other, which we were not confident about at the start.

What we learned

Row level security is a much better place for rules than component logic. Writing the policy first made the frontend simpler, because the interface stopped being the thing standing between a user and other people's data.

Contrast has to be measured. "It looks fine" and "it passes" are different claims, and the accessible version of our palette turned out to be the better looking one anyway.

Merging is where bugs come from. Two clean branches can merge with no conflict markers at all and still break something, so it's worth checking the result rather than trusting a green tick.

And a missing environment variable should never be able to make a form unusable. Degrade, don't disappear.

What's next for Match Up!

Profile badges and endorsements, which is the Strava half of our inspiration. Games played, the sports you actually turn up to, and vouches from people you've played with. That also feeds the trust problem: a newcomer can see that a host is someone twenty people have played with before.

Live notifications. Messages are already realtime, friend requests aren't yet, so that badge only updates when you navigate.

Recurring games, for the same five-a-side every Tuesday.

Row level security across the rest of the schema. Chat is locked down, matches and profiles still need the same treatment before this goes anywhere public.

And mobile properly. It's responsive, but an app about getting outside belongs on a phone.

Share this project:

Updates