Multicit

Travel planner chatbot with curated data layers, LangGraph states, and flight search API.

πŸ”’ Source code is private


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:

  1. A data release. globe publishes an immutable release that flight loads as a PostGIS geo schema and trip reads as a globe-bundle/1 MessagePack bundle. Consumers pin it by hash and verify it on load.
  2. A library. plan runs trip in-process: a NAPI addon on Node, a WASM module in its Cloudflare Worker.
  3. One HTTP envelope. flight’s client asks the trip-edge Worker for a route preview over trip-edge/2.

Design: system architecture Β· vision and roadmap Β· trip routing Β· flight architecture Β· plan architecture.

LayerResponsibility
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 bans HashMap and 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, not Unroutable, 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-check that 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.

FigureRecord
Dataset9,056 airports, 32,870 cities and 8,720 metros worldwiderelease .6
MVP region2,450 airports and 17,550 cities in a 2.89 MB bundle (budget 3 MB)release .6 Β· .10.1
Road layer109,196 of 112,284 planned directed pairs routed with Valhalla over OSMrelease .10.1
Host parity14 requests answered byte for byte alike by NAPI and workerdT3.11 parity
Graph queryNonstop query at 0.52 ms best warm against a < 100 ms gateF1 review
Planner gates615 unit, 418 Node and 234 workerd tests; 15 of 15 judged routes Valid on both runtimesP2 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.

MilestoneCapabilityStatus
G1 - Dataset buildCountries, airports, cities, metros and regions from locked sources, built deterministically and verifiedβœ… Done
G2 - Explorer and map kitThe map kit, the explorer, the eurasia MVP region and judged city groupingsβœ… Done
G3 - Ground minutesSourced ground durations, then a bus and road tranche with an OSM road layer routed by Valhallaβœ… Done (geo-2026.10.1)
G4–G5Points of interest; a monthly release cadencePlanned
T1 - DomainThe trip document, evidence, night allocation, validation and the mutation catalogueβœ… Done
T2 - RoutesCorridor search, ranking and alternatives, closed against five golden tripsβœ… Done
T3 - Edge and lockstrip-wasm, the trip-edge Worker, host parity, road-layer adoption, stop locks and allocation modesβœ… Done (edge not yet deployed)
T4–T6Enumeration and further generators, an offline TripPack, UniFFI bindingsPlanned
F1 - Consume geoThe pinned geo schema in PostGIS, graph queries and a one-command bumpβœ… Done
F2 - CrawlBudget-admitted collection, captures, fare facts and readings, a dev airport picker, SearchAPIIn progress: F2.1–F2.6 built; infrastructure and a seven-day run remain
F3–F4 - Search and MVPCache-only multi-city search and scoring; the free-tier MVP, local firstPlanned
F5–F7Baselines and cadence, production launch, route previews from trip-edgePlanned
P1 - Route on a mapEndpoints 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 branchesRoute edits as typed mutations, stop and night locks, saved revisions, forks and exact replayβœ… Done
P3 - ConversationLangGraph chat that proposes edits, durable runs and cost tracking; then plan’s first deployNext
P4–P6Grounding, days and points of interest, bring-your-own-key and TripPack exportPlanned
SiteThe multicit.com placeholder; product pages arrive with plan’s first deployPlaceholder

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)