Nova Projects
07 / 09Live

Travel Agenda

Puts my commutes in my calendar automatically, so I know when to leave and nobody books meetings into them.
StatusLive
Year2026
StackCloudflare Workers, D1, TypeScript, Vite, Google Calendar and Maps APIs

The Problem

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.

My Solution

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.

Features

  • Automatically creates “Leave for X” calendar events with real-time transit durations before each in-person event
  • Creates “Return home from X” events after events end (it currently checks for other events within a timeframe, if there are others, it creates the “Return home from X” event only after the last event)
  • Travel address overrides: set a temporary home for a date range so transit is calculated from the de-facto home base
    • Create an all-day Travel: <address> event in Google Calendar to set a travel address without touching the dashboard
  • Scans:
    • Checks each morning (7am) and again at 5pm for the current and next 2 days
    • Re-checks transit time 90 min and 3 h before each event to catch last-minute disruptions
    • Weekly 14-day deep scan (Sundays)
    • Pause periods: suspend auto-generation for date ranges (e.g. holidays, conferences)
  • Minor refinements:
    • Fallback to driving estimate (inflated 33%) when no transit route is found, clearly labelled
    • Skips Zoom links, Google Meet URLs, “online”, etc.
    • Can watch several calendars and write travel events to a single output calendar
    • Push notifications (Web Push / VAPID) when departure times shift by more than 5 minutes, or on weekly sync completion

Technical Writeup

I will let Claude describe:

Stack

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.

Data model

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 needed
  • travel_periods: manually defined date ranges with a substitute origin address and optional custom start/end times
  • pause_periods: date ranges where processing is suppressed
  • geocode_cache: caches geocoded lat/lng per address string for 30 days
  • push_subscriptions: Web Push endpoint + VAPID keys per user device

Processing pipeline

Three cron triggers drive everything:

CronWhenWhat
0 7 * * *7am daily3-day scan (14-day on Sundays)
0 17 * * *5pm daily3-day scan
*/5 * * * *Every 5 minPre-event refresh (1.5h and 3h windows)

On each daily run, the worker:

  1. Fetches all future events across all configured calendars
  2. Filters out: all-day events, past events, cancelled events, events without a location, virtual locations (Zoom/Meet/Teams/etc.)
  3. For each remaining event, resolves the effective origin address by checking manual travel periods first, then Travel: all-day calendar events, then falling back to the stored home address
  4. Checks event_mappings: if the event’s start time, location, and origin are all unchanged and it was previously processed successfully, it’s skipped
  5. Geocodes origin and destination (cached in D1 for 30 days)
  6. Queries the Google Directions API for a transit route using arrival_time; falls back to driving if no transit route exists
  7. Creates or updates a “Leave for X” calendar event on the configured output calendar, with duration and buffer baked in
  8. Optionally creates a “Return home from X” event after the original event ends, if no other physical-location event starts within the configurable window
  9. Runs deletion detection: any event_mapping whose original event is no longer in the scan window gets its travel event deleted from Google Calendar

Fan-out architecture

Cloudflare 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.

Travel address resolution

The worker supports two ways to set a travel address:

  1. Dashboard travel periods: stored in D1 with start/end dates and optional custom HH:MM times for partial travel days
  2. Calendar-based travel periods: any all-day event in a scanned calendar with a title matching 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:MM

Both 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.

Transit fallback

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.

Push notifications

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:

  • A departure time shifts by more than 5 minutes between runs
  • The weekly Sunday sync completes (summary of created/updated/error counts)
  • Processing or geocoding errors occur

Caching strategy

Two layers of caching reduce API calls:

  • Geocode cache (D1): address string → lat/lng, 30-day TTL
  • Transit cache (D1): a cache key encoding mode + time-type + coordinates rounded to ~110 m + day-of-week + hour bucket → duration in seconds. Day-of-week and hour bucket capture transit schedule variation (weekday peak vs. weekend, etc.)

Footnotes

  1. Which includes managing and monitoring Claudes, testing things, doing things it cannot do like setting up Google Console, etc. ↩