SPMT
A public-transit platform for Târgoviște: journey planning and a live map for riders, plus an admin console so the transit authority can edit stops, routes, and timetables.

What it is
The rider side is what you'd hope for from a transit app: a journey planner that handles transfers, a Leaflet map showing every line and stop, departure boards per station, and a small portal for announcements, fares, and public documents.
The admin console is where most of the engineering went. The entire network is editable data. Lines, stops, tickets, and subscriptions all have full CRUD, route variants get rearranged by drag and drop, geometry is edited directly on the map, and a calendar manager handles GTFS service exceptions. There's also an AI assistant that can propose changes, though a human has to sign off before anything is written.
It came out to about 39k lines of code and took me the better part of a year and a half.
What I didn't know about buses
Before this project I thought a bus line was a list of stops. It's actually a family of things: two directions, several variants per direction, exceptions for holidays and school breaks, and geometry that needs to hug real roads, because a straight line between two pins looks broken on a map. A journey planner has to get all of this right at once, or it confidently tells someone to catch a bus that doesn't exist.
So the bar I set was: correct multi-leg journeys, fast, and nobody ever editing a GTFS file by hand.
How it works
Backend is Go, Gin and GORM over PostgreSQL, organized into handlers, services, and repositories with interfaces between the layers. Frontend is React 19 with TanStack Router and Query, Tailwind, and Leaflet, shipped as a static SPA behind nginx. Nothing exotic there; the interesting parts are in the routing.
Journey planning is RAPTOR, implemented from scratch. Given a date, it resolves which services actually run that day (weekday rules plus GTFS calendar exceptions), compiles a timetable, and runs bounded rounds until it has the Pareto-optimal set of arrivals. I also search a handful of shifted departure times, because one "best" answer is rarely what a rider wants when they're deciding whether to leave now or in twenty minutes. Compiled timetables are cached per date behind singleflight, so a burst of requests doesn't recompute the same day over and over.
Road-following geometry comes from a self-hosted OSRM instance. Each variant stores its stop sequence as JSONB, and the whole network round-trips through GTFS: import a feed, edit it in the admin, and the export ZIP quietly rebuilds itself on a debounce.
Where it got hard
The most annoying problem was divided roads. OSRM kept snapping stops to the opposite carriageway and drawing U-turn loops in the middle of otherwise sane routes. Shrinking the snap radius was the obvious move, and it made things worse, since pins are rarely exactly on the asphalt. What finally worked was giving OSRM directional context: route through the preceding waypoint, and constrain the approach bearing at each stop to ±90°. The U-turns vanished.
For the AI assistant I split everything into reads and writes. Reads execute immediately. Writes pile up into a pending action plan, with a snapshot of the before-state, and sit there until a person approves them. A model that hallucinates a stop deletion just produces a rejected plan, not a broken network.
Keeping stop sequences in JSONB means I gave up foreign keys on that path. In exchange, edits are atomic single-row writes and reads are cheap. Validation in the service layer covers what the database no longer can.
What I'd change
The timetable cache lives in process memory. For one deployment it's reasonable, but if this ever needs a second instance, that cache has to move somewhere shared.