All projects

SPMT

A public-transit platform for Târgoviște. Riders get a route planner and a live map; the people running the network get an admin console where they can edit stops, line geometry, and timetables directly.

  • React
  • Go
  • OpenStreetMap
SPMT — screenshot
SPMT

Overview

SPMT is the software behind a municipal bus network. On the rider side there's a multi-leg journey planner, an interactive Leaflet map with every line and stop, departure boards per station, and a portal for announcements, fares, and public documents.

The admin side treats the whole network as editable data. There's CRUD for lines, stops, tickets, and subscriptions, a drag-and-drop editor for route variants, a map-based geometry editor, a GTFS calendar manager, and an AI assistant that can suggest changes for a human to approve.

The project is around 39k lines, built over roughly 17 months.

The problem

Transit data turned out to be much harder than I expected. A single line has directions, variants, service-day exceptions, and real geometry that has to follow the road instead of drawing straight lines between pins. Route planning has to respect all of that: schedules, transfers, and which days a service actually runs.

I had two goals. Riders should get correct multi-leg journeys quickly, and the people maintaining the network should never have to touch a GTFS file by hand.

Approach & architecture

The backend is Go (Gin + GORM over PostgreSQL), split into handlers, services, and repositories with interfaces injected at each seam. The frontend is React 19 with TanStack Router and Query, Tailwind, and Leaflet, deployed as a static SPA behind nginx.

For journey planning I implemented RAPTOR from scratch, the round-based transit routing algorithm. It figures out which services run on a given date (weekday rules plus GTFS calendar exceptions), compiles a timetable for that date, and runs bounded rounds to find Pareto-optimal arrivals. It also searches a few shifted departure times so riders see alternatives, not just one answer. Compiled timetables get cached per date, with singleflight so concurrent requests don't all recompute the same thing.

Line geometry comes from a self-hosted OSRM instance. Stop sequences live as JSONB on each variant, and everything round-trips to GTFS: you can import a feed, edit it in the admin, and the export ZIP regenerates itself on a debounce.

Challenges & tradeoffs

The most annoying problem was divided roads. OSRM kept snapping stops to the wrong carriageway and drawing U-turn loops in the middle of a route. I first tried constraining the snap radius, but that broke whenever a pin sat slightly off the road. What actually fixed it was routing through a preceding waypoint for directional context and constraining each waypoint's approach bearing to ±90°.

The AI assistant runs in two phases on purpose. Read tools execute immediately, but every write gets bundled into a pending action plan with before-state snapshots, and a human approves or rejects it. It's slower than letting the model act directly, but it means a hallucination can't quietly corrupt the network.

Storing stops as JSONB gives up referential integrity in exchange for atomic single-row edits and fast reads. I made that call knowingly and backstopped it with validation in the service layer.

What I'd change

The route-search cache is process-local. That's fine for a single municipal instance, but scaling horizontally would need a shared cache.