ubc meet
a campus app that fills the gap between your classes with people, not a feed.
react 19 · vite · leaflet · fastapi · supabase · auth0 · gemini · cloudflare tunnel
built and demoed at hackcamp 2026, parked since.
A mobile web app built by four of us in twenty four hours at HackCamp 2026. It answers one question for a student standing outside a lecture hall. Who can I hang out with in my next gap between classes, and where.
You add your timetable and a few tags for who you are, international, transfer, exchange, commuter. Before each gap, the app proposes one small group of students who are free at the same time and share something with you, plus a public campus building to meet in. Join or skip. When you actually show up, you scan a groupmate's phone to check in.

why we built it
Everyone on the team had the same gap in their week. An hour and a half between classes, too short to go home, too long to just sit there, spent alone on a phone in a building full of people doing exactly the same thing. UBC has forty thousand students and the loneliest place on campus is the middle of the afternoon.
The apps that claim to solve this are social networks wearing a campus hoodie. They give you a feed, follower counts, posts, and they measure themselves by how long you stay in them. That is the opposite of what we wanted. The only number this app should care about is hangouts that actually happened, which is why the check-in exists at all. If nobody meets, the app failed, no matter how good the engagement graph looks.
So there is no feed, no followers and no posts. Three tabs, right now, map and me, plus a full-screen projector page we built for the stage demo.
the privacy model
This is the part I would defend hardest, and it drove more design decisions than anything else. An app whose whole purpose is helping strangers meet in person is exactly the kind of app that gets abused, and "we'll add privacy later" is how you end up shipping a tool for following people around campus.
So the rule we settled on early is that nothing public carries a name. Open groups before you join, the map, the projector, all of it is anonymous. A group's real meeting building is only serialized to someone who has actually joined that group, and only once the group is past forming. Map counts below three get dropped on the server, so no single person can ever be picked out of a dot. There is no live location anywhere in the codebase. The browser takes one position fix, uses it to centre the Leaflet map, and no coordinate ever reaches the backend.
Two frames from our own Figma file were deliberately not built, because when I went to implement them they broke these rules. One showed initials on open-group cards before joining. The other showed an exact building and room before joining. Both looked good and both leaked.
The seam that keeps it honest is a single function, spot_is_visible_to(), which every
one of the three group routes calls to decide what the caller is allowed to see. One
decision in one place means the three routes cannot quietly drift apart as the hackathon
clock runs down, which, at hour nineteen, they absolutely would have.

how it works
A React 19 front end built mobile-first at 390 pixels talks to a FastAPI backend through
exactly one file, src/lib/api.js, so there is a single place where the contract lives.
FastAPI reads and writes Supabase, which holds students, free blocks, groups, members
and buildings. Matching takes your free blocks and your tags and returns either one
group suggestion for the gap or a waiting state.
Check-in has two paths because phones are unreliable in a basement. A rotating QR payload signed with a 64-bit signature, and a separate six-digit code built on its own HMAC, so a leaked screenshot of one does not grant the other. Timetable screenshots get parsed by Gemini into strictly validated events, and neither the image nor the parsed timetable is ever stored.
The whole thing was served off my laptop by three launchd agents, uvicorn, a vite preview of the built front end, and a Cloudflare tunnel publishing the domain, so phone cameras had the secure context they need and judges could load it on their own devices. Nothing in that chain reloads on its own, which bit us once. Both processes sat six commits stale with no visible symptom, because every endpoint still answered perfectly, just with yesterday's code.
My lane on the team was integration and release, the design pass across every screen, and the product and pitch work. The group lifecycle and the privacy internals were owned by a teammate.
what actually shipped
Honest version, because a hackathon demo and a working product are different things.
Working and verified. Guest sessions, onboarding, the campus map, the projector page, the design pass across all seven screens, and the accessibility work, which was an entered award track rather than a nice-to-have. Fifteen contrast pairs resolved out of the stylesheet with the worst sitting at 4.89 to 1, and no interactive element under 44 pixels anywhere in the three main tabs. The backend suite finished green at 116 passed and 7 skipped, the front end builds clean, and Inter is self-hosted rather than pulled from Google Fonts so the stage demo did not depend on venue wifi.
Not verified, and I will not claim otherwise. The six-digit check-in has never been exchanged between two real phones. It passes its unit tests, and a green test run is not evidence that two humans standing in a hallway can check each other in.
Broken or unfinished. The biggest piece of debt is a split identity. The onboarding profile lives in process memory while everything matchable lives in Supabase, which means a restart loses guest profiles and a second worker would not share them. That split has already produced one live bug, where confirming a parsed schedule changes which groups you match into but not the gap label printed above them. Two lifecycle functions have no callers, so invitations never expire. And we never actually decided the product name, so the domain and the Figma frames say UBC Meet while the manifest and the welcome screen say Hangout.
what i learned
The thing I keep coming back to is how much of a twenty four hour build is release rather than code. Three of my hardest hours went to tunnels, launchd agents, stale processes and secure contexts, none of which appear in a single commit diff, and all of which were the difference between a demo and a laptop screen turned toward a judge.
The other lesson is that a privacy rule only survives contact with a deadline if it lives in one function. We wrote ours down at hour two and then, at hour nineteen, building a frame that violated it, the code simply would not let me do it the easy way. That is the only reason it is still true in the repo today.

