Inspiration
Cities are legally required to consult the public before they rebuild a street or approve a tower. In practice, that consultation is a slide deck in a community centre on a Tuesday evening.
Think about who that format filters out. Older people. Disabled people. Shift workers. Parents of young children. The residents most affected by bad street design are exactly the ones least able to be in that room at 7pm — so the feedback that reaches city hall is thin, late, and skewed toward whoever happened to be free.
The tools meant to catch everyone else don't work either. A 311 call reports a defect where you're standing, right now — it can't carry an opinion about a street you're afraid to walk down. A comment form strips out the one thing a planner actually needs: the exact spot. "The crossing at Bloor feels unsafe" leaves a planner guessing which crossing, facing which way, and why.
So we asked a different question: what if you could stand in the city, at the exact spot, and leave a note there — from your laptop, at 2am, without asking anyone's permission?
What it does
CityPin is a living 3D map of downtown Toronto that you walk in first person, in your browser. No install, no account.
- Walk — first-person movement through a real 3D reconstruction of downtown.
- Aim — a crosshair sits fixed at screen centre. Whatever is under it is what you're reporting.
- Pin — one key press. Pick a category, type a short note, done.
- Everyone sees it — the pin syncs to every other person in the world immediately, and survives a server restart.
- Upvote — walk past someone else's pin and agree with it. One complaint becomes a signal.
- Hand off — a planner panel lists every pin, filters by category, ranks by votes, teleports you to any of them, and exports GeoJSON or CSV.
Seven categories: accessibility barrier, safety, flooding, no shade, transit, green space, other.
That last step is what makes this a tool rather than a demo. Pins convert to real latitude/longitude on export, so the file opens directly in the GIS a city already runs. Adoption costs a file import, not a procurement cycle.
Two users, one on each end of the same pin. The resident who can't get to a consultation evening opens a link and is standing in the street they want to talk about. The planner who receives it — or an accessibility advisory committee, which every Ontario municipality is required to have under the AODA — gets a filtered, ranked map instead of comment forms with no location attached.
SDG alignment
Our primary target is SDG 11.3 — enhance inclusive and sustainable urbanization and capacity for participatory, integrated and sustainable human settlement planning. Indicator 11.3.2 measures whether a city has a direct participation structure for civil society in urban planning. CityPin is exactly that: a participation channel for the people the current channel excludes.
Two secondary targets, depending on what residents actually pin:
- 11.2 — accessible transport for all, notably older persons and persons with disabilities. Pins mark barriers on the route to transit: missing curb cuts, blocked sidewalks, nowhere to sit.
- 11.7 — safe, inclusive, accessible public space. Pins mark public space that fails in practice: dark underpasses, unshaded waits, unusable parks.
The flooding and no shade categories are also, in practice, climate-adaptation data — urban heat islands and flash flooding, mapped by the people standing in them.
How we built it
A browser client in TypeScript on three.js, bundled with Vite. A relay server in Node using ws. No framework, no database, four runtime dependencies total.
The city. 44,313 OpenStreetMap building footprints with heights, plus roads, parks, water and rail — projected into local metric coordinates around a fixed origin at the CN Tower and served as a single JSON file. Geometry is merged into one buffer per 400m tile, so the whole of downtown draws in a few dozen draw calls instead of tens of thousands. An optional Google Photorealistic 3D Tiles layer swaps the built geometry for real imagery, and falls back to the OpenStreetMap city automatically if the key fails.
The multiplayer. A compact binary protocol over a single WebSocket, with positions and angles quantized down to keep messages tiny. We added three message types for pins — place, upvote, and a full sync for anyone joining — and a small server-side store that snapshots to disk so pins survive restarts.
Abuse handling. Public write access on an open URL is a liability. The relay enforces per-connection rate limiting, a connection cap per IP, and an origin allow-list; pins add a per-player cooldown, a global cap, and note sanitising.
What we inherited, and what we built
We want to be straightforward about this, because a judge deserves to know and the git history shows it anyway.
CityPin was built in a five-hour window, and it stands on a 3D engine one of us wrote before the hackathon — a personal project that already had the OpenStreetMap city pipeline, the photorealistic tile layer, the multiplayer relay, collision, the minimap and the avatar system.
The repository is structured so anyone can see where one ends and the other begins. The first commit imports that engine under its own message, labelled as prior work. Every commit after it is the hackathon build. The diff is the claim — nobody has to take our word for it.
What we built today: the crosshair and pin placement, pin markers and world-space labels, the three new network messages and the server-side pin store, vote dedup and cooldowns, disk persistence, the entire planner panel — list, filters, vote ranking, teleport-to-pin, GeoJSON and CSV export — and a fresh deployment.
The honest framing: we had a 3D Toronto sitting on a laptop, and in five hours we turned it into a civic participation tool. The engine is why this was possible in an afternoon. The tool is what was built today.
We also parallelised hard — four workstreams with one owner per file and the shared interfaces frozen before anyone started, because three people editing the same file at once produces a merge disaster nobody had time for.
Challenges we ran into
Pins don't store a height, and that's deliberate. This was the subtle one. "Ground" means two completely different things in our two world modes: a flat plane in the OpenStreetMap city, versus real terrain elevation under the photorealistic tiles — which also returns nothing at all until those tiles finish streaming in. Storing a height would make every pin wrong in the other mode. So pins store position only and work out their height at render time. It took us a while to realise the bug wasn't in the maths, it was in the schema.
Notes are capped at 48 characters, and that started as a constraint. Our relay enforces a strict payload limit on every inbound message as a defence against hostile clients, and we didn't want to weaken it just to ship a feature — so the note had to fit what was left. It turned out to be one of our better decisions: 48 characters forces a resident to say one specific thing instead of writing an essay, which is exactly what makes the export readable by a planner.
Cutting a game down to a civic tool. The inherited engine had vehicles, planes, health and a kill leaderboard woven through the main render loop. Stripping all of it at import — so the very first commit was already a civic tool rather than a game with the fun removed — was the highest-risk step in the plan. One careless cut and the build breaks before there's anything to show.
What we learned
The schema is the design decision. Deciding a pin has no height was a bigger call than anything we did visually. Data model choices propagate outward into what the product can and can't be, and they're much harder to change later than any piece of UI.
"Why 3D?" needed a real answer. Our first instinct was that the 3D city was the cool part. It isn't — it's the precision part. A missing curb cut, an unlit underpass, a bare patch where a shade tree should be: none of these have a street address. A crosshair in 3D space captures coordinates and a sightline that a text form structurally cannot. Once we understood that, the GeoJSON export stopped being a nice-to-have and became the point of the whole thing.
Being explicit about inherited work is freeing. Drawing a hard line in the git history let us stop hedging and just talk about what we actually built.
Limitations
Stated plainly, because a judge will find them anyway:
- Building heights outside the core are estimates — OpenStreetMap defaults, not survey data. Tower heights are real. The city is accurate downtown and approximate in the residential neighbourhoods.
- There's no curb, sidewalk-width or bench data in the model. This is exactly why CityPin collects what residents observe rather than inferring accessibility from map tags. It's a reporting tool, not an automated audit, and we won't present it as one.
- Open write access needs moderation. Rate limits stop flooding; they don't stop someone writing something abusive in a note. A real deployment needs a report-and-hide path. Not built.
- Pins are unauthenticated, so one person can vote from several browsers. Good enough to demonstrate signal; not good enough to allocate a budget from.
What's next for CityPin
A report-and-hide moderation path, because open write access without one isn't deployable.
A second city from the same pipeline, to prove portability. The cost structure is the argument here: the city data comes from OpenStreetMap, the engine is open source, and hosting is a static site plus one small relay. Any city with OSM building data — which is most of them — can be stood up by re-running the fetch script against a new bounding box. Nothing in CityPin is Toronto-specific except the coordinates.
A conversation with one accessibility advisory committee about whether the export lands in the form they actually need. We can guess at the schema, or we can ask the people who'd have to open the file. We'd rather ask.
Log in or sign up for Devpost to join the conversation.