Open the recorded demo · Live map
Operations (2026-10-29): the recorded demo works without the live backend. Production raw recordings are checksum-verified in S3 and old local server copies are reclaimed automatically. See replay and archive runbook.
A live public-transport map for Greater Rio de Janeiro. It starts with the city's buses and BRT, then adds the state-run trains, metro, tram, ferries and intercity buses. Every operator is shown honestly as live, scheduled only or no public data.
It's also a portfolio case study in distributed, data-heavy real-time systems:
- one adapter per provider, all emitting one canonical event;
- an immutable raw log on S3;
- rebuildable projections;
- a static replay that keeps working after the live backend is switched off.
Status: the ITS adapter, the consumer, the rebuild, the API and the dashboard are built. The repo has a Go module, a Next.js app
(apps/web), local Redpanda + TimescaleDB/PostGIS and a test harness that runs against real
infrastructure. The source inventory is verified (2026-10-12): Rio's city buses and BRT are
live; rail, metro, tram, ferries, intercity and Niterói buses have no public machine-readable
data. apps/adapter-its polls the ITS API one UTC minute at a time, stores every response
byte for byte in a local raw log, and publishes positions.v1 and feed-status.v1 to Redpanda
(contracts in packages/contracts; plan and measurements in
docs/tasks/TASK-its-adapter.md). apps/consumer writes
both topics to TimescaleDB (positions, a hypertable kept for 3 days, and feed_status, kept
forever), and apps/rebuild rebuilds the same tables from the raw log (plan and measurements in
docs/tasks/TASK-positions-consumer.md). apps/api serves
the latest vehicle positions from an in-memory snapshot (it queries the database once per new
minute, never per request), and apps/web draws them on a full-bleed Rio map with a letreiro (the amber LED sign of a Rio
bus) that says plainly whether the data is live, stale or failed, a panel to explore lines and
buses (pick a line, click a bus, follow it, colour by speed; the view lives in the URL), and a
table at /tabela as the non-map alternative (plans and measurements in
docs/tasks/TASK-live-map.md and
docs/tasks/TASK-dashboard.md). A time machine (a timeline along the bottom: scrub the last hours, play at
1×/10×/60× with fading trails, and a way back to live) is built and browser-verified too (2026-10-13)
(docs/tasks/TASK-time-machine.md). Selected lines also have static GTFS routes,
destination variants and an ordered stop list (D3,
docs/tasks/TASK-line-routes.md); the reference geometry stays available
in live and replay views. The production raw log now ships to S3 with checksum manifests every ten minutes.
Prerequisites (versions checked 2026-10-12):
-
Docker with Compose (Docker Desktop on the Mac). The tests start containers, so Docker must be running.
-
Go 1.26.
go.modpinstoolchain go1.26.9, so withGOTOOLCHAIN=auto(the default) thegocommand downloads that patch for this module only. Don't upgrade your global Go for it. -
golangci-lint v2.14.0. Install the prebuilt binary with the official script, not
brew install: the Homebrew formula depends ongoand would upgrade your global Go.curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b "$HOME/.local/bin" v2.14.0
~/.local/binhas to be on yourPATH. -
Node 24 (
apps/web/.nvmrc) and pnpm 11 (pinned inapps/web/package.json). -
make(the only task runner).
make up # copies .env.example to .env if needed, starts Redpanda + TimescaleDB, waits until healthy
make test # Go tests, including the integration smoke test (needs Docker)
make lint # gofmt, go vet, golangci-lint, ESLint
make web-dev # Next.js dev server on http://localhost:3000
make adapter-its # polls the ITS API and publishes to Redpanda (needs `make up`; Ctrl-C stops it)
make consumer # topics -> TimescaleDB (needs `make up`; Ctrl-C stops it)
make rebuild FROM=2026-10-12T14:27:00Z TO=2026-10-12T16:24:00Z # raw log -> TimescaleDB
make api # read-only API on 127.0.0.1:8080 (needs `make up` and a filled database)
make tiles # Rio basemap extract -> apps/web/public/tiles/rio.pmtiles (needs `brew install pmtiles`)
make gtfs # static line routes/stops -> apps/web/public/gtfs/ (retains byte-exact ZIP in the raw log)
pnpm --dir apps/web install # first time only, if apps/web/node_modules is missingRun it locally (the screen):
make up, then fill the database:make adapter-itsandmake consumerfor live data, ormake rebuild FROM=… TO=…from a raw log (the screen then says "Não está ao vivo" and the minute it shows).make tilesonce (about 45 MB, git-ignored; the Protomaps daily build is kept for a week), thenmake gtfsfor route geometry and stops. Run the latter beforemake web-buildtoo: generated assets are git-ignored and must be included in the deployed web artifact.make apiin one terminal andmake web-devin another, then open http://localhost:3000. The web app reaches the API through a rewrite (/api/*→127.0.0.1:8080), so there is no CORS.
| Target | What it does |
|---|---|
make up |
docker compose up -d --wait --wait-timeout 120 |
make down |
docker compose down. Keeps the volumes: no target deletes them |
make test |
go test -count=1 ./... (real Redpanda and TimescaleDB via testcontainers), then pnpm --dir apps/web test (Vitest, pure functions) |
make lint |
gofmt check, go vet, golangci-lint run, pnpm --dir apps/web lint |
make fmt |
gofmt -w on the Go packages |
make dev |
local infra, the API and the Next.js dev server in one terminal (Ctrl-C stops both) |
make web-dev / make web-build |
the Next.js dev server / production build |
make adapter-its |
runs apps/adapter-its with .env loaded. Writes the raw log under RAW_DIR (./var/raw, git-ignored: never delete it, it's the source of truth), publishes to Redpanda, and logs one JSON line per published minute. Set ITS_CONTACT in your own .env |
make consumer |
runs apps/consumer with .env loaded: consumer group transit-pulse-consumer on positions.v1 and feed-status.v1, one transaction per batch, offsets committed after it. Needs DATABASE_URL. Logs one JSON line per batch |
make api |
runs apps/api with .env loaded. Read-only; listens on API_ADDR (default 127.0.0.1:8080). GET /v1/vehicles/latest and GET /healthz, both from memory. Starts and answers 503 if the database is down, then recovers by itself |
make tiles |
pmtiles extract of the newest Protomaps daily build for the Rio bbox into apps/web/public/tiles/rio.pmtiles (git-ignored). make tiles TILES_BUILD=<key> picks a build |
make gtfs |
conditional SMTR GTFS fetch, at most one attempt per 24 h, then deterministic static export. Reads RAW_DIR and optional GTFS_CONTACT (falls back to ITS_CONTACT); saves raw bytes and validators locally. Previous exports survive failed fetches |
make gtfs-export |
offline regeneration from the saved raw location in var/gtfs/state.json; no database, broker or network request |
make rebuild FROM=… TO=… |
runs apps/rebuild: rebuilds positions and feed_status from the raw log for those minutes (both inclusive), oldest first. Writes DATABASE_URL, or DB=postgres://… for another database. Running it again changes nothing |
If the consumer falls more than 24 h behind, the topic has already dropped the oldest
records (positions.v1 keeps 24 h). The minutes are still in the raw log: run make rebuild
for the missing range, then start make consumer again. Rebuilding into the live database is
safe.
Routes are a static reference. The selected line's destination selector shows the GTFS variants; the stop list provides the same information without the map. The capture date, declared feed dates and SMTR attribution are in “Sobre este trajeto”. Geometry does not confirm a bus's actual path or the line configuration at a replay date. Exact unmatched line names show “Sem trajeto no GTFS”; missing assets show a retry action and leave vehicles and playback available. The current public feed was unchanged between the 12 and 13 October captures. See the D3 task for measured export sizes and join coverage.
Local ports (all bound to 127.0.0.1): Kafka 29092, Redpanda Admin API 29644, Postgres
5435. They differ from renewable-pulse's so both stacks can run at once. scripts/idle-memory.sh
prints the median idle memory of the two containers.
CLAUDE.md: workflow, decisions, invariants, testsdocs/architecture.md: design skeleton and open questionsdocs/research/README.md: the verified source inventory and samplesdocs/tasks/: task documents
Layout: deployables in apps/ (apps/web, and each Go binary as apps/<name>), shared code
in packages/ (testharness, samples, contracts, event, raw, its, store). One Go
module at the root. Go binaries today: adapter-its, consumer, rebuild, api.
Vercel Git builds use Root Directory apps/web; vercel-build restores checksum-pinned compact replay/GTFS assets before Next.js builds. See deployment details.