Multicit
Travel planner chatbot with curated data layers, LangGraph states, and flight search API.
Multicit
Multi-city trip composition: fly in, travel overland, fly out. In development; nothing is deployed yet.
Multicit is built for one kind of trip: fly into one airport, cross a built-up region overland, and fly home from another (AβB, B~C, CβD). The scope is Europe, Turkey and Cyprus, the Caucasus, the Levant, South, East and South-East Asia, and Egypt, Morocco and Tunisia. Two independent products serve that trip from one website. flight finds open-jaw fare combinations ranked by how much cheaper than usual they are, with deep links so the traveller checks each price at the source. plan proposes a route between the arrival and departure endpoints (stops, nights, ground legs) on a map, and lets the traveller edit it, keep every revision and branch from any of them; conversational editing arrives at P3. Both stand on two shared foundations: globe, a curated geographic dataset, and trip, a deterministic route engine. Correctness beats coverage, and the model proposes while the engine decides.
This repository is the multicit.com site (run and deploy notes). The product repositories are private; they are linked below as proof of the architecture, and access is available on request.
Architecture
Five repositories, split by charter. They couple through contracts and running services, never through shared code:
- A data release. globe publishes an immutable release that flight loads as a PostGIS
geoschema and trip reads as aglobe-bundle/1MessagePack bundle. Consumers pin it by hash and verify it on load. - A library. plan runs trip in-process: a NAPI addon on Node, a WASM module in its Cloudflare Worker.
- One HTTP envelope. flightβs client asks the
trip-edgeWorker for a route preview overtrip-edge/2.
Design: system architecture Β· vision and roadmap Β· trip routing Β· flight architecture Β· plan architecture.
| Layer | Responsibility |
|---|---|
Reference data (globe) | A Go builder fetches OurAirports, GeoNames, Natural Earth, Wikidata, GTFS and OSM into a content-addressed cache, then builds offline into a release geo-YYYY.MM.N identified by content_hash and artifact_hash. It also ships the Rust globe-geo reader (wasm32-ready), a TypeScript map kit (one declarative LayerSpec drawn by d3 and MapLibre adapters), an ODbL road layer routed with Valhalla, and an explorer for judging each release by eye |
Engine (trip) | A pure Rust engine with no I/O, no clock and no unseeded randomness: corridor lens, k-best layered dynamic-programming search, night allocation, validation and scoring, typed mutations and stop locks. Shipped as a NAPI addon, a WASM module, and trip-edge, a stateless Cloudflare Worker with an edge cache |
Fares (flight) | A Go service. A Postgres attempt ledger admits every provider call against a budget, with SearchAPI as the production source behind a provider port. Raw captures are stored by SHA-256, fare facts go to partitioned Postgres, and versioned reading rules pick cheapest and best. Search reads only that cache. React client |
Planner (plan) | TypeScript use cases behind ports, served by Hono on two runtimes: Node over Postgres, and a Cloudflare Worker over D1. Idempotent commits with a revision compare-and-swap, forks from any revision, exact replay. React and MapLibre client |
Site (web, this repository) | The Astro site on Cloudflare Pages; the only thing that links flight and plan |
ββ SOURCES ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β OurAirports Β· GeoNames Β· Natural Earth Β· Wikidata Β· GTFS Β· OpenFlights Β· OSM β
ββββββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β content-addressed fetch β offline build
βΌ
ββ GLOBE β immutable release geo-YYYY.MM.N ββββββββββββββββββββββββββββββββββββββ
β geo schema Β· globe-bundle/1 + road layer Β· map kit for both front-ends β
β identified by content_hash + artifact_hash; consumers pin and verify β
βββββββ¬βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ¬βββββββββββββββ
β pinned bundle β pinned schema
βΌ βΌ
ββ TRIP β Rust engine βββββββββββββ ββ FLIGHT β Go Β· PostGIS βββββ
β suggest Β· mutate Β· validate β trip-edge/2 β budgeted crawl β
β NAPI Β· WASM Β· trip-edge Worker β ββββββββββββββΊ β β captures β fare facts β
βββββββ¬ββββββββββββββββββββββββββββ route previews β search reads the cache β
β in-process βββββββββββββββ¬βββββββββββββββ
βΌ β
ββ PLAN β TypeScript Β· Hono βββββββ β
β Node + Postgres Β· Worker + D1 β β
β revisions Β· forks Β· replay β β
βββββββ¬ββββββββββββββββββββββββββββ β
βΌ βΌ
ββ MULTICIT.COM β links the two products; no data passes between them βββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Why this shape.
- Contracts, not shared code. Pinned data, a library contract and one HTTP envelope are the only couplings. flight and plan never call each other, and either is complete without the other.
- The data is half the product. globe is curated against sources, not scraped and trusted: claims are sourced and dated, unmeasured durations stay null, 6,797 city-subdivision pairs were judged one by one into reversible groupings, and share-alike data stays out of the clean release.
- Deterministic and provenance-stamped. The same inputs build the same
content_hash. The same request, bundle and policy hashes give a byte-identical route natively and under WASM: the objective is integer-only, and clippy bansHashMapand std float maths. Every plan carries its data and engine identities, and plan replays any command byte for byte. - The model proposes, the engine decides. Words become typed mutations that the engine applies atomically or rejects.
- Store facts, read by rules; report honestly. flight keeps every returned itinerary and reads
it through versioned rules, so a rule can change without re-collecting. trip reports
SearchIncomplete, notUnroutable, when a search was truncated, and nothing invents minutes, dates or clock times. - Boundaries are enforced, not documented. golangci depguard layer rules in flight; a
lint-enforced MVC split and a runtime-neutral core in plan; schema drift gates; and an offline
deploy-checkthat refuses stale, fixture or tampered packages.
Selected figures
Taken from committed release notes and measurement records. Timings are local runs on the recorded host, not production.
| Figure | Record | |
|---|---|---|
| Dataset | 9,056 airports, 32,870 cities and 8,720 metros worldwide | release .6 |
| MVP region | 2,450 airports and 17,550 cities in a 2.89 MB bundle (budget 3 MB) | release .6 Β· .10.1 |
| Road layer | 109,196 of 112,284 planned directed pairs routed with Valhalla over OSM | release .10.1 |
| Host parity | 14 requests answered byte for byte alike by NAPI and workerd | T3.11 parity |
| Graph query | Nonstop query at 0.52 ms best warm against a < 100 ms gate | F1 review |
| Planner gates | 615 unit, 418 Node and 234 workerd tests; 15 of 15 judged routes Valid on both runtimes | P2 steps |
Tech stack
- Data pipeline: Go 1.26 Β· pgx Β· Postgres 17 + PostGIS 3.5 Β· Valhalla + osmium Β· PMTiles / Protomaps Β· MessagePack
- Engine: Rust 1.97 (edition 2024) Β· serde Β· libm Β· NAPI-RS Β· wasm-bindgen Β· proptest Β· criterion
- Fares: Go Β· pgx Β· sqlc Β· goose Β· chromedp Β· SearchAPI
- Planner: TypeScript 6 Β· Hono Β· Postgres Β· Ajv + schema-generated types; LangGraph.js chat planned for P3
- Front-ends: React 19 Β· Vite 8 Β· MapLibre GL + PMTiles Β· d3-geo Β· Astro (this site)
- Edge: Cloudflare Workers + D1 (plan, trip-edge) Β· Cloudflare Pages (site) Β· wrangler Β· Miniflare / workerd
- Quality: Docker Compose behind Make, no host toolchains Β· Vitest Β· Playwright + axe-core Β· golangci-lint (depguard) Β· clippy Β· ESLint boundary rules Β· schema drift gates Β· Wasmtime
Roadmap
Each repository keeps its own ladder (globe Β· trip Β· flight Β· plan), and a milestone closes at a reviewed pause on recorded evidence. plan deploys first, after P3; trip-edge deploys with the flight side.
| Milestone | Capability | Status |
|---|---|---|
| G1 - Dataset build | Countries, airports, cities, metros and regions from locked sources, built deterministically and verified | β Done |
| G2 - Explorer and map kit | The map kit, the explorer, the eurasia MVP region and judged city groupings | β Done |
| G3 - Ground minutes | Sourced ground durations, then a bus and road tranche with an OSM road layer routed by Valhalla | β
Done (geo-2026.10.1) |
| G4βG5 | Points of interest; a monthly release cadence | Planned |
| T1 - Domain | The trip document, evidence, night allocation, validation and the mutation catalogue | β Done |
| T2 - Routes | Corridor search, ranking and alternatives, closed against five golden trips | β Done |
| T3 - Edge and locks | trip-wasm, the trip-edge Worker, host parity, road-layer adoption, stop locks and allocation modes | β Done (edge not yet deployed) |
| T4βT6 | Enumeration and further generators, an offline TripPack, UniFFI bindings | Planned |
| F1 - Consume geo | The pinned geo schema in PostGIS, graph queries and a one-command bump | β Done |
| F2 - Crawl | Budget-admitted collection, captures, fare facts and readings, a dev airport picker, SearchAPI | In progress: F2.1βF2.6 built; infrastructure and a seven-day run remain |
| F3βF4 - Search and MVP | Cache-only multi-city search and scoring; the free-tier MVP, local first | Planned |
| F5βF7 | Baselines and cadence, production launch, route previews from trip-edge | Planned |
| P1 - Route on a map | Endpoints and dates in, a validated route with stops, nights and ground legs on a map, on Node and on the Worker | β Done |
| P2 - Editing and branches | Route edits as typed mutations, stop and night locks, saved revisions, forks and exact replay | β Done |
| P3 - Conversation | LangGraph chat that proposes edits, durable runs and cost tracking; then planβs first deploy | Next |
| P4βP6 | Grounding, days and points of interest, bring-your-own-key and TripPack export | Planned |
| Site | The multicit.com placeholder; product pages arrive with planβs first deploy | Placeholder |
Keywords
- Languages:
GoΒ·RustΒ·TypeScriptΒ·SQL - Architecture:
Multi-RepositoryΒ·Contract-FirstΒ·Hexagonal Ports and AdaptersΒ·Deterministic BuildsΒ·Content-Addressed ReleasesΒ·Provenance StampingΒ·Runtime-Neutral CoreΒ·Edge Computing - Geodata:
Geospatial ETLΒ·PostGISΒ·Curated Reference DataΒ·Evidence LedgerΒ·Licence Partitioning (ODbL)Β·OSM Road Routing (Valhalla)Β·Vector Tiles (PMTiles) - Engine:
Route Search (k-best DP)Β·Typed MutationsΒ·Integer ObjectiveΒ·WebAssemblyΒ·NAPIΒ·Byte-Identical Cross-Host ParityΒ·Golden Trips - Fares:
Budget-Admitted CrawlingΒ·Append-Only CapturesΒ·Store Facts, Read by RulesΒ·Fenced Work Queue - Planner:
Idempotent CommitsΒ·Optimistic ConcurrencyΒ·Branching Revision HistoryΒ·Exact ReplayΒ·Cloudflare Workers + D1 - Quality:
Docker-Only ToolchainsΒ·Lint-Enforced BoundariesΒ·Schema Drift GatesΒ·Property TestingΒ·Playwright E2EΒ·Accessibility Checks (axe)