Timeline

June 2026, Started as a side project

Ongoing, Open-ended playground, one foundation at a time

My Role

Main Developer — architecture, engine, and frontend

Sakay

A side project where I'm rebuilding the core of a ride-hailing platform to better understand how these systems work behind the scenes.

Instead of focusing on the rider app, I'm building the engine underneath it. Riders appear, drivers are dispatched, motorcycles route over real roads, and the entire world streams to a live map in the browser.

Sakay simulation streaming to a MapLibre map in the browser

The live world — motorcycles and riders routing over Makati and BGC streets, streamed to the browser in real time.

Why I built it

I wanted to understand how a ride-hailing system works by rebuilding one. Not the rider app itself, but the engine underneath: continuous demand, a fixed pool of drivers, the dispatcher deciding who picks up whom, and a road network where "nearest" means travel time, not straight-line distance.

It started as a learning project and has grown into my personal playground. It's where I experiment with new ideas like pnpm monorepos, Redis pub/sub, pathfinding, real-time streaming, and AI-assisted development.

The project is inspired by Juraj Majerik's ride simulation, but I rebuilt it from scratch. The goal wasn't to recreate the original, but to learn by making my own engineering decisions.

How it works

The simulation is built around two moving parts: riders and drivers.

Ride requests appear continuously, while drivers move between being available, picking someone up, completing a trip, and becoming available again. The system keeps both sides balanced so the simulation can run continuously without growing out of control.

A few key pieces make it work:

  • A real road network. Vehicles travel on actual roads in Makati and BGC using data from OpenStreetMap instead of moving across a simple grid.
  • Traffic-aware pathfinding. Every vehicle routes with A* over that road graph, weighted so "nearest" is by travel time, not straight-line distance
  • The dispatcher. Each matching pass pairs waiting riders with available drivers by lowest pickup ETA, longest-waiting rider first. A cheap straight-line pre-filter avoids running expensive pathfinding on a driver that's kilometers away. It's greedy rather than globally optimal, which was a deliberate v1 choice: simple, fast, and good enough.
  • A deterministic engine. The simulation runs on a fixed timestep and is deterministic from a seed, so the same seed and config produce a bit-identical run every time. Every spawn and tie-break pulls from one random stream in a fixed order. That reproducibility matters for debugging and for any future analysis.
  • Real-time streaming. The engine emits small state deltas a few times per second, and the frontend interpolates smooth motion between them, so vehicles glide along the roads instead of jumping once a second.

The architecture

The project is split into three independent services, each with a single responsibility.

  • The engine is a standalone Node worker that owns the road graph, the pathfinding, the tick loop, demand and supply generation, and matching. It runs as its own process, one per run, so a heavy simulation tick never blocks the API.
  • The API is a NestJS app that owns Postgres and the WebSocket gateway. It receives deltas from the engine over Redis, persists an event log, and fans deltas out to browsers.
  • The frontend is React and MapLibre, rendering the OSM streets and the motorcycle and rider sprites, keeping an in-memory mirror of the world, and interpolating motion between updates.

The services communicate through Redis and shared TypeScript models instead of depending directly on each other. This keeps the codebase easier to maintain and allows each part to evolve independently.

The reason the engine and API are separate processes is the whole point of the design: the simulation is CPU-heavy, and if it ran inside the API it would block everything. Splitting them, with Redis as the message bus between them, keeps the always-on API responsive and lets the two scale independently.

What I'd do next

It's a working v1, but I keep it deliberately open-ended since it's my playground. The point of the project is that I can keep adding one foundation at a time.