Project Case Study

NavEire

Real-time public transport tracking and journey planning for Ireland

Role

Solo Designer & Developer

Status

Live

Tools

React, Node.js, Express, SQLite, Leaflet, Vite, Tailwind CSS

Tags

Full-StackReal-Time SystemsProduct DesignPWA
NavEire screenshot

Case Study Snapshot

Problem

  • Irish transit data exists, but rider tools are fragmented.
  • There was no strong map-first view for live vehicles and departures.
  • Multi-modal planning across operators was missing.

Constraints

  • Built solo in 10 to 20 hours a week.
  • Low hosting budget, so managed databases were out.
  • GTFS static and real-time feeds used different stop IDs.
  • The planner had to stay fast on heavy multi-stop queries.

Results

  • Live PWA shipped at naveire.ie.
  • Real-time tracking, NDJSON streaming planner, alerts, and reliability grades.
  • Backend uses bounded queues plus 503 backpressure for load safety.
  • GTFS rebuild runs automatically in CI.

The Problem & My Role

Ireland has good public transport open data. The NTA publishes GTFS static and real-time feeds, and the information is there. The problem was that no tool used it well. Existing apps covered only one agency, lacked live vehicle positions, or buried useful information behind interfaces that made quick checks slower, not faster. What was missing was something map-first that combined real-time tracking, accurate departure times, and multi-modal journey planning across every transport type in the country.

I built NavEire to be that tool. I'm the sole designer and developer. Every architectural decision, every data pipeline, every UI choice was mine. The project runs on ten to twenty hours per week alongside other commitments, so every feature had to earn its place.

The design principle throughout was information density with clarity. Irish commuters checking their phone don't need a beautiful interface. They need to know in two seconds whether their bus is on time. Every interface decision flows from that. The map shows vehicle positions without cluttering the viewport. The departure board surfaces imminent departures with live status front and centre. Alerts appear when they matter, not constantly. The favourites system came from watching how people actually use transit apps: the same handful of stops, every day. If the app prefetches those stops on open, users get value before they've touched anything.

The Process

The first major decision was SQLite over a managed database. With hosting costs as a real constraint and access patterns that are mostly read-heavy with periodic bulk rebuilds, SQLite fits well. The database is rebuilt automatically in CI when the NTA publishes new static GTFS data, compressed as a GitHub Release, and downloaded by the server on cold start.

The harder problem was stop ID reconciliation. The NTA's static and real-time feeds use entirely different identifiers for the same physical stops. The solution was a coordinate-matching pipeline that builds a stop_id_map table at import time. It sounds simple. In practice, edge cases consumed more time than any user-facing feature: stops that moved between feed versions, duplicates within matching radius, IDs that exist in one feed but not the other.

The journey planner brought a different kind of complexity. City-to-city queries across multiple modes and transfers can take several seconds. I implemented progressive streaming via NDJSON so users see results as they arrive, and offloaded expensive queries to worker threads with admission control and a bounded queue to prevent the server from falling over under load.

Technical Architecture

The stack is React and Vite on the frontend, Node.js and Express on the backend, SQLite for data storage, and Leaflet for mapping. The frontend deploys to Vercel. The backend API and database run on Railway. The real-time layer pulls from the NTA's GTFS-RT protobuf feeds at regular intervals, merges live trip updates and vehicle positions against the static schedule, and serves the result with appropriate caching and stale-data fallback when the upstream feed goes quiet.

I'm currently building ghost bus prediction: comparing historical GTFS schedule data against actual real-time trip observations to identify routes that consistently fail to run as scheduled. It's a data problem more than a UI problem, and it's the piece of NavEire I find most interesting to work on right now.

Outcome & What's Next

NavEire is live at naveire.ie, and real commuters use it. It's a production-grade PWA with live vehicle positions, a progressive journey planner, service alerts, route reliability grading, and stop favourites. Building it taught me more about production engineering than anything else I've done: caching strategy, backpressure, data pipelines, and what graceful degradation actually means when upstream data goes stale.

The biggest lesson was that the data infrastructure is where the real work lives. The map and the UI are what people see. The GTFS import scripts, stop ID reconciliation, cache TTL decisions, and stale-data handling are what actually matter. I'd invest in observability much earlier if I started again.

The next milestone is ghost bus prediction, then departure board improvements based on user feedback. Commuter need first, feature completeness second.

External Links

← Back to projects