I live in Berlin and often have to take different public transport combinations to get where I need to go, which makes it difficult to assess how long my travel duration is, especially so since strikes, repairs, upgrades, or delays due to heat or cold often change the duration. I mostly checked for the exact duration as I was leaving the house, which meant that I then know that I will have to rush as much as possible in order to be still barely on time, or that things will be fine, or even that I will be quite early.
Additionally, when sharing my calendar with my thesis supervisor so he could book a meeting, I had to always manually block out some time before and after each in-person event, so that he would not book a meeting for a time where I’m in public transport.
A website that connects to my GCal, looks for all events that have something in the location field, calculates how long it takes to get there from my home address (or travel address if I’m staying somewhere else), and then creates an event for said duration (and also to get back home).
The bare-bones version took very little time to build with Claude Code, but to get it to support travel addresses and some other features, ironing out some quirks, setting up the infrastructure it needs, etc. took much longer. But most of the time spent on this was letting Claude work in the background. I don’t know how much time I’ve spent on it in total, but maybe active times1 of: 20 mins for the bare-bones version; 3–6 h for the full version.
Travel: <address> event in Google Calendar to set a travel address without touching the dashboardI will let Claude describe:
The backend is a single Cloudflare Worker (TypeScript, deployed with Wrangler) with a Cloudflare D1 SQLite database. The frontend is a minimal vanilla-TypeScript SPA built with Vite, deployed to Cloudflare Pages.
Six tables in D1:
users: Google OAuth tokens, home address, config (buffer, timezone, return-home window, calendar IDs, event colour)event_mappings: tracks which worker-created travel event corresponds to which original Google Calendar event, plus a snapshot of the last-seen start time, location, and origin address used to detect whether reprocessing is neededtravel_periods: manually defined date ranges with a substitute origin address and optional custom start/end timespause_periods: date ranges where processing is suppressedgeocode_cache: caches geocoded lat/lng per address string for 30 dayspush_subscriptions: Web Push endpoint + VAPID keys per user deviceThree cron triggers drive everything:
| Cron | When | What |
|---|---|---|
0 7 * * * | 7am daily | 3-day scan (14-day on Sundays) |
0 17 * * * | 5pm daily | 3-day scan |
*/5 * * * * | Every 5 min | Pre-event refresh (1.5h and 3h windows) |
On each daily run, the worker:
Travel: all-day calendar events, then falling back to the stored home addressevent_mappings: if the event’s start time, location, and origin are all unchanged and it was previously processed successfully, it’s skippedarrival_time; falls back to driving if no transit route existsevent_mapping whose original event is no longer in the scan window gets its travel event deleted from Google CalendarCloudflare Workers have a hard limit of 50 outbound subrequests per invocation. Since each event needs up to ~4 API calls (two geocodes, one directions lookup, one calendar write), the worker uses a self-service-binding fan-out: events are batched in groups of 8, and each batch is dispatched to a fresh SELF invocation via an internal HTTP endpoint. This gives each batch its own 50-subrequest budget.
The worker supports two ways to set a travel address:
Travel: <address> and a non-empty location field is treated as a travel period. Custom start/end times can be embedded in the event description as custom-start: HH:MM / custom-end: HH:MMBoth mechanisms are resolved per-event: the effective address at the event’s start time is used for the outbound travel event, and the effective address at the event’s end time is used for the return home event.
When the effective address changes between runs (e.g. travel just ended), the worker automatically expands processing to the full 14-day window so all upcoming events get recalculated with the new origin.
If the Google Directions API returns ZERO_RESULTS for transit, the worker retries with driving mode. Driving results are inflated by 33% to account for uncertainty, and the calendar event is titled FALLBACK: Travel to X with a warning in the description. Fallback events are always reprocessed on the next run so they get upgraded to a real transit route as soon as one becomes available.
Web Push is implemented without any third-party library. The worker builds VAPID JWTs from scratch using the Web Crypto API (ES256/ECDSA), signs them against the stored PKCS8 private key, and sends them directly to the push service endpoints stored in push_subscriptions. Expired subscriptions (HTTP 410/404 from the push service) are automatically cleaned up.
Notifications fire when:
Two layers of caching reduce API calls:
Which includes managing and monitoring Claudes, testing things, doing things it cannot do like setting up Google Console, etc. ↩