Open standards, a feed-agnostic monorepo, and a per-feed adapter contract. Everything we publish is reproducible from public sources — and the code is open.
The app does most of its work on the public GTFS Schedule and GTFS-RT data interfaces. We add a small set of named extensions for things the public spec is silent on — and those extensions stay in the consumer-specific blob, never in the published zip.
GTFS Schedule· public spec, no extensions in the published zip
GTFS-RT· vehicle positions, trip updates, service alerts — standard protobuf
The publisher monorepo
n3ary/gtfs-publisher is the feed-agnostic orchestrator. Every per-feed decision is delegated to an adapter package — the core has zero per-feed code.
@n3ary/gtfs-speclibrary
One spec, shared everywhere
The single library every other piece in the monorepo depends on. Owns the GTFS Schedule types, the SQL DDL, the ZIP readers, and the "along-the-path" calculations that would otherwise get rewritten in every feed.
All spec validations — required fields, uniqueness, type checks
Standardised public names so the same struct is reused for static and RT
Shared distance / timing helpers used by the static pipeline
Runs the static pipeline: resolve feeds → acquire zips → run the adapter → emit a sqlite blob for the app and republish the raw .gtfs.zip for everyone else.
SQLite blob is the per-consumer artifact — per-feed extensions live here
.gtfs.zip stays spec-clean, content-addressed by sha256
Adapter owns the cleanup — colors, timing, direction, name resolution
The Fastify HTTP proxy that turns the per-feed live mess into one standard GTFS-RT protobuf stream. Anything that speaks the public spec can subscribe — not just the n3ary app.
Multiple non-standard upstream sources → one standard wire format
Per-feed quirks live in adapter rt/ modules, registered at boot
Sits behind the CF edge; raw bytes cached at the edge for replay
The data pipeline is the core, but five small repos keep every other repo in line. Each one ships independently and stays small enough to read in one sitting, and together they are the building blocks for scaling development across the org.
Shared actions
Composite actions and reusable workflows every n3ary repo consumes: PR validation, pnpm + GH Packages auth, version bump, ASCII commit check, standards-drift check. A bug fix or standard change ships to every consumer in one merge.
Single source of truth for the n3ary / neary visual identity: source SVGs, brand manual, asset dist. Also published as a website so consumers can grab logos, social previews, and other media without cloning the repo.
A GitHub App + Cloudflare Worker that owns the calver tagging and release notes for every n3ary repo. On every merged PR it opens a version-bump PR with auto-merge enabled - releases stop being a chore.
The 13 markdown files of org-wide rules: worktrees, commit hygiene, release flow, naming, documentation. Plain prose, written for humans and AI agents. Auto-vendored into every consumer's docs/standards/ by a daily workflow, with a sync header that detects drift on PR.
This site. MIT-licensed, so other orgs can fork it. Two views: the customer home at n3ary.com and the developer page at n3ary.com/developers/, plus a friendly 404. No build step.
Where it all runs. Every piece of the pipeline is reproducible from a public repo.
GitHub ActionsCI for every repo; the publisher runs on a daily cron, plus on-demand.
Cloudflare PagesHosts the website (this site) and the n3ary/app PWA at app.n3ary.com.
Cloudflare R2Stores the .gtfs.zip and .sqlite3.gz artifacts and the feeds.json catalog at gtfs.n3ary.com.
HetznerThe single VM that runs the gtfs-rt Fastify server, behind the CF edge cache.
Adapters
Every per-feed decision lives in an adapter package. The generic publisher has zero per-feed code; the source reconciliation, the per-feed validations, and the extensions that don't fit the public spec all sit inside one operator's adapter. The contract is the same for every adapter; the per-feed logic is whatever that feed needs.
Adapter contract
Three subpaths, all dynamic-imported by the orchestrator. An adapter is just a published npm package that exposes them.
addContractruntime
One contract, many implementations
An adapter is a published npm package. The publisher orchestrator doesn't know which feeds it supports — it loads the package named in feeds/<id>/config.json's source.publisher and calls its subpaths.
Each adapter ships its own speed profiles (peak / off-peak / night) and busy windows in config.json. The static pipeline uses them to propagate schedules to intermediate stops when the upstream feed only publishes terminals — no more "the bus magically teleports between two published times".
Cluj-Napoca · timing
From feeds/cluj-napoca/config.json. Every other configured feed ships its own values.
Speed (peak)
14 km/h
Speed (off-peak)
22 km/h
Speed (night)
28 km/h
Peak windows
07:00-09:30, 16:00-19:00
Night window
22:30-05:30
Dwell
20 s
Adapter list
Cluj-Napoca
CTP Cluj-Napoca
The most complete adapter so far, shown here so the per-feed shape is concrete. To add a new adapter, copy the cluj-napoca package, replace the per-feed logic, and ship a new feeds/<city>/config.json entry in n3ary/gtfs-publisher. The cards below list what this one does.
Three sources, one feed
Transitous (curated structure) + Tranzy.ai (live-updated static) + CTP's own CSV timetables (authoritative departure times) - reconciled into one .gtfs.zip.
Direction fix for GTFS-RT
CTP's live vehicle stream doesn't always carry direction_id reliably. The adapter recovers it from the trip's route_id + shape, so a vehicle's direction in the app matches the schedule view.
Route tags extension
Night, metropolitan, airport express - derived from the route's short name (25N, M26, etc.) and stored in _route_tags. The app uses them as filters.
extension
GTFS networks + route_networks
TE (Transport Elevi, the school transport lines) get their own network surface via the public spec - no extension table needed - so school runs and regular transit share the same map view.
CTP CSV to proper GTFS
CTP publishes timetables as CSVs. The adapter converts them to spec-compliant stop_times, including the GTFS Frequency model for headway-based routes.
OKLab route color fix
Tranzy's published colors don't match the real-world livery. The adapter rewrites them using the OKLab color method so the map matches what you see at the stop.
Pre-configured speed & busy hours
Peak, off-peak, and night speeds plus peak windows - the static pipeline uses them to fill in intermediate stops when the upstream only publishes terminals.
Coming soon: Tranzy GTFS-RT
Tranzy exposes a non-standard live feed. The adapter will convert it to standard protobuf GTFS-RT so the same gtfs-rt server can serve it.
The contract is open. New adapters land here as more operators come online - each one is a separate n3ary/gtfs-adapter-<city> package, not a change to this site.
The code behind these projects is licensed under PolyForm Noncommercial 1.0.0 — free for individuals, hobbyists, education, and research. Any commercial use needs a separate license from us.