How it was built
One author, one day, a whole ecosystem
Choo Choo is an iPhone app, an Apple Watch app with complications, a Mac app with a menu bar item and desktop widgets, a server that streams live train data over gRPC, and an MCP server for Claude and ChatGPT. Almost all of it was written with Claude Code, and the production system runs on Google Cloud Platform, with Cloudflare for DNS.
I'm Joao Martins, CTO of Defaqto. The whole app ecosystem was built in one day, Saturday 7 November 2026. The live data feeds came the next day, because my access to the Trains Data API needed approval first. You can read more about me on LinkedIn.
The timeline
- Thursday 5 November: the day I missed the train. I had just come up the stairs to platform 10 at Stratford when I saw my Southend Victoria train arriving on the other side of the lines. Running back down, across the crowded tunnel and up again would never have made it, and the doors would probably have closed in my face. So while I waited an hour for the next train, I brainstormed with Claude in chat how to solve it. That's where the first scaffold came from: a server with gRPC streaming and MCP, and a SwiftUI iPhone client. The project was called station departures, then Pouca Terra, then Choo Choo.
- Saturday 7 November: the whole ecosystem in one day. The server with gRPC streams and MCP, saved trips, the Apple Watch app and complications, the Mac app, Google Cloud hosting, and sign-in with Google and Apple.
- Sunday 8 November: the live data feeds, once my access to the Trains Data API was approved. The rest of the day went to my Home Assistant setup.
Not even Rome was built in a day
The app was the easy part of the last few days. To submit it to the App Store, Apple needs a privacy policy and terms of use at a public web address, so I built this website, with the policy, the terms and a support page, on top of everything else.
Then there was the paperwork. My Apple Developer subscription had lapsed, and it had to be renewed before I could upload anything. My last app submission was more than five years ago. Their systems were slow then, and they still are. I ended up paying twice, because the subscription status in the store wasn't updated and let me pay again. They have no option to get a refund.
The ecosystem
| Part | What it is | Built with |
|---|---|---|
| Server | Live departures from one API, served over gRPC, Connect, MCP and OAuth | TypeScript on Node 22, Connect, MCP SDK |
| iPhone app | Trips, departures with platforms as they're announced, widgets | Swift 6, SwiftUI, WidgetKit, grpc-swift 2 |
| Apple Watch | Trips, departures and a complication on the watch face | SwiftUI, WidgetKit timelines, WatchConnectivity |
| Mac app | Window, menu bar item, Settings, desktop widgets, network-aware location | SwiftUI, MenuBarExtra, App Intents, SystemConfiguration |
| Claude and ChatGPT | An MCP server with an MCP App board that shows the live departures in the chat | MCP streamable HTTP, MCP Apps, a small HTML app |
| This website | choochoo.stream, the privacy policy and terms, and this page | Node 22, plain HTML and CSS, no framework |
| Infrastructure | Cloud Run, Firestore, Secret Manager, logging and analytics | Terraform, Google Cloud, Cloud Build |
| Release pipeline | Tests, container builds, signed iOS and macOS builds to TestFlight | GitHub Actions on a self-hosted Mac, App Store Connect API |
The architecture
The data stream, end to end
- One poller per query. The server's board hub starts one poller for each station and destination pair that anyone is watching, and every subscriber shares it. The poller stops when the last subscriber leaves.
- Only changes are pushed. Each board gets a fingerprint that ignores its timestamp. A subscriber receives a new board only when the content actually changes, so a quiet station sends nothing.
- Polling that tightens near the train. Boards are polled every 30 seconds, or every 10 seconds when a train leaves within 15 minutes without a platform. Errors back off exponentially.
- A request budget. The upstream API caps requests per minute, hour and day. The server counts its own requests and serves a cached board rather than overspend. Each Cloud Run service has its own share of the budget.
- gRPC server streams to the iPhone and Mac.
WatchDeparturesis a server-streaming RPC over HTTP/2. The app keeps one open per departures screen and reconnects with backoff. Cloud Run ends streams after an hour, and the app reconnects. - Connect JSON to the watch. watchOS apps mostly use URLSession, so the watch makes ordinary HTTP/1.1 calls to the same service with the Connect protocol and fetches departures itself. The phone sends it the trips and a refresh token over WatchConnectivity.
- Timelines for the complication. WidgetKit gets one entry for now and one as each train leaves, so the watch face changes on time without the app running. Cancelled trains are skipped, and boards older than three hours are not shown.
Apple platforms
- iPhone and Mac share code. The Mac app compiles the iPhone app's models and services, so the trip logic, station search and sync are written once. Macs get their own target, so the menu bar, Settings window and widgets don't need
#ifin every view. - Location stays on the device. Stations are downloaded once and searched on the phone. Only station codes ever reach the server.
- The Mac knows where it is without Location Services. It identifies the router by its hardware address through SystemConfiguration, and a network can be tied to a station in Settings.
- Trips sync across devices. Saved trips live on the server, keyed to the Sign in with Apple account. Each save carries the version it was based on, and a conflict is resolved with a three-way merge by trip ID.
The MCP server and the connector
The MCP server exposes four tools (departures, finding a train's platform, station search, nearest stations). get_departures also returns an MCP App: a small, self-contained page that shows the next trains, refreshes itself every 30 seconds, and follows the light or dark theme of the chat. The server is also its own OAuth 2.1 authorisation server, with PKCE, dynamic client registration and Google or Apple sign-in, so Claude and ChatGPT can connect like any other remote MCP server.
Today you can add it by hand as a custom connector at https://mcp.choochoo.stream/mcp. Publishing it in the Claude and ChatGPT connector directories is the next step.
Google Cloud and Cloudflare
- Cloud Run runs two services from one container image in europe-west1: one for gRPC over HTTP/2 end to end, and one for HTTP/1.1, MCP and sign-in. Each scales to zero and runs at most one instance, so the in-memory pollers are shared.
- Firestore stores saved trips in the EU. Secret Manager holds the tokens and API keys. Artifact Registry and Cloud Build build the images.
- Terraform describes all of it, with state in a Cloud Storage bucket. A budget alert is set at £10 a month.
- Analytics are structured log lines, stored in a Log Analytics bucket, linked to BigQuery and charted in Looker Studio. Users are identified by a keyed hash, not by name or email.
- Cloudflare runs the DNS for choochoo.stream. The records are DNS-only (not proxied), because Cloud Run issues its own certificates and long gRPC streams need a direct connection.
How Claude Code was used
Claude Code wrote most of the code. Joao directed each change, reviewed the diffs, ran the apps in the simulators and on real devices, and decided what to build next. Every feature lived on its own git branch, often in its own worktree, so several changes could be in progress at once. A CLAUDE.md file in the repository is the handoff note: what each part does, what has been verified and what hasn't, and the decisions and why. Claude picks it up at the start of each session, so context carries from one day to the next.
The languages (the ones I don't write by hand)
Across the repository there are about 220 tracked files in these languages and formats:
| Language or format | Where it is used |
|---|---|
| Swift 6 | iPhone, Apple Watch, Mac apps and widgets |
| TypeScript | The server, the MCP server, OAuth, analytics and the tests |
| JavaScript (ES modules) | Branding and store-screenshot build scripts, the website |
| Protocol Buffers | The API contract, shared by the server and every app |
| HCL (Terraform) | Google Cloud infrastructure |
| YAML | GitHub Actions workflows, project configuration |
| Bash | Deploy, release and TestFlight scripts |
| SQL | Analytics views in BigQuery |
| HTML, CSS | The website, the MCP App board and the sign-in pages |
| Dockerfile | Container images for Cloud Run |
| JSON, XML property lists | Asset catalogues, privacy manifests, app settings |
| SVG and PNG | The icon, brand artwork and screenshots, all generated by a script |
| Markdown | Documentation, the App Store text, the privacy policy |
Built with love, one train at a time. Back to the home page.