Side project · Case study

Travel Planner

A collaborative trip planner that puts every place, meal, stay, and train ride on one time axis per day. It includes real travel times, AI itinerary generation, and live co-editing.

  • React
  • TypeScript
  • Node.js / Express 5
  • PostgreSQL / Drizzle
  • Zod
  • WebSocket
  • Auth0
  • Google Maps Platform
  • OpenAI
  • Vitest
  • Docker / fly.io
Role
Solo, end to end (product spec, design direction, frontend, backend, infrastructure)
hi-fi screens designed before code
48
Architecture Decision Records
9
Trip routes covered by the access-control test
34
per AI-generated Trip, about 30 s
~$0.02
01

Why I built it

Trip planning scatters places, restaurants, lodging, and transport across notes, map pins, and group chats. I wanted one place to arrange plans on a real time axis, check travel time between stops, save undecided ideas, and plan with my travel companions.

See it works in 1 minute: demo video

02

What it does

Day time axisClick any time to create an Event; drag to move or resize it. Overlapping Events sit side by side. Four Event kinds: Attraction, Meal, Lodging, and Transport.
Travel between EventsComputes how long it takes to move from one place to another with Google Routes and warns when the gap time is too short.
Opening-hours checkFlags Events that may fall outside a place's opening hours on that weekday.
AI-generated Trips"Kanazawa, 5 days, likes hot springs" becomes a complete Trip with real Google places.
CollaborationInvite Editors by email, share a read-only link, and see others' changes live.
Map View, Backlog, Todos, NotesMap markers colored by Day; a holding area for undecided ideas; per-Trip and per-Event Todos; and a floating Markdown notes panel.
EverywhereDesktop and phone layouts, keyboard shortcuts, English and Traditional Chinese, light and dark themes, and full backup and import.
03

Architecture

flowchart TB
  WEB["<b>Browser · apps/web</b><br/>React + Vite SPA"]
  SHARED[["<b>packages/shared</b><br/>Zod schemas + pure domain rules<br/><i>imported by both web and API</i>"]]
  API["<b>fly.io · one Docker image · apps/api</b><br/>Express 5 · REST /api/*<br/>WebSocket /api/live <i>(LiveHub · signal only, no data)</i>"]
  DB[("<b>PostgreSQL</b><br/>trips · days · events · legs · todos · notes<br/><i>caches: places · place_queries · route_estimates</i>")]
  EXT["<b>External services · each behind a small interface</b><br/>Auth0 <i>identity</i> · Google Places <i>PlaceProvider</i> · Google Routes <i>RouteProvider</i><br/>OpenAI <i>ItineraryGenerator</i> · Image storage <i>ImageStore</i><br/><i>Each has a fake: the app runs and is tested without API keys.</i>"]
  WEB <-->|"HTTPS JSON · changed / presence over WebSocket"| API
  SHARED -.->|types| WEB
  SHARED -.->|validation| API
  API --> DB
  API --> EXT

Three structural choices shape the system:

  • One contract, two apps. Request and response shapes are Zod schemas in a shared workspace package. The API validates against them, and the web app derives its types from them, giving the contract a single source of truth (ADR 0005).
  • Own the domain, delegate identity. My backend owns the rules for Leg lifecycle, Todos when an Event is archived, and conflict resolution. Authentication is delegated to Auth0 because it carries high security risk and offers little product differentiation (ADR 0004, 0006).
  • Every external service has a fake. Places, routes, AI, image storage, and email sit behind small interfaces with fake implementations. The app runs and is tested without API keys; an environment variable selects the real provider.
04

Engineering highlights

05

How I worked

flowchart LR
  PRD["<b>PRD</b><br/>vocabulary, rules,<br/>non-goals table"]
  ADR["<b>ADRs</b><br/>9 decisions +<br/>rejected options"]
  BRIEF["<b>Design brief</b>"]
  DESIGN["<b>High-fidelity design</b><br/>48 screens<br/>light/dark · desktop/phone<br/>empty · error · read-only"]
  PLAN["<b>Implementation plan</b><br/>numbered steps with<br/>&quot;Done when&quot;"]
  CODE["<b>Code + tests</b><br/>lint · type-check ·<br/>tests · build<br/>before every commit"]
  PRD --> ADR --> BRIEF --> DESIGN --> PLAN --> CODE
  CODE -. "&quot;Done as / Not done&quot; record" .-> PLAN
  • Specs before code. A PRD defines the vocabulary and rules, including a non-goals table with a reason for each item (no offline editing, no live cursors, no AI edits to an existing Trip), so settled questions stay settled.
  • Decisions are written down. Nine Architecture Decision Records capture lasting choices and the options I rejected.
  • Design first. A high-fidelity design of 48 screens (light and dark, desktop and phone, empty, error, and read-only states) guided implementation.
  • Small, verifiable steps. The implementation plan breaks work into numbered steps, each with a "Done when" checklist and a "Done as / Not done" record. Lint, type-check, tests, and build must all pass before every commit.
  • Working with AI coding agents. Validate each step, especially real-world use cases. Keep planning and design documents always up-to-date so sessions handover to each other without problems. Use worktrees for parallel and accelerated development.