Case study Personal project
Passports is my trip planner and travel journal in one app. I build the itinerary day by day, record what we actually did along the way, and share either one with a link.
My trips used to live in too many places. Plans were spread across Google Docs, Trello, spreadsheets, browser tabs, emails, and text threads. Afterward, the record of what we did was a pile of photos and screenshots and maybe a few Instagram posts. For a few trips I hand-built pages on a static site with the itinerary plus notes on each place's history, language, and logistics. That showed me what a good trip page could look like, but every page was manually edited and it couldn't help with the planning itself.
I wanted one place to capture a trip from first idea to final memory, with the messy planning version kept separate from the version I'd show anyone else. It also had to accept half-finished information. Flights aren't booked yet, a dinner is an idea until it's a reservation, and the second city might get cut.
- Model trips the way they get planned. A trip has 1–4 bases, each with its own timezone, and each base holds its days. Meals, activities, transport, and lodging attach to a base and a day independently, so a flight home can live at the trip level and still appear on the last day. Times are stored as plain local times. The base timezone only answers "which day is today?" while traveling, so there's no UTC math anywhere.
- Let plans stay incomplete. You set a trip length first and add a start date once flights are booked. Until then, days read Day 1, Day 2, and real dates fill in later. New ideas land in an unassigned pool and move down to a base, a day, and a meal slot as plans firm up. Items move through idea, option, shortlisted, confirmed, and reserved, and any item can be an anchor (a fixed-time dinner reservation) or flex (lunch somewhere near the museum).
- Build one Guide and filter it by viewer. Plan view is my private workspace. Guide view is the reading experience, with an Itinerary tab and a Journal tab that unlocks once the trip starts. Owners, trip members, and the public all get the same Guide code; the viewer's role decides which items come through. A public link needs no login and drops ideas, shortlisted items, costs, reactions, and internal notes automatically. The database enforces the same rules as the app, and sharing the journal is a separate opt-in.
- Add a stage before planning. Destinations are trips with no dates required, arranged on a Wishlist / Planning / Archive board and promoted into full planning when it's time. A map shows every base as a clustered pin labeled Someday, Planned, Traveling Now, or Visited.
- Open it to Claude. The Passports MCP connector (its own case study) lets Claude.ai read my trips and propose changes through the same auth and row-level security as the browser app.
I plan and journal every trip in Passports now. It replaced pasting itinerary screenshots into a notes app. Because real auth and row-level security were in from the first week, adding the MCP connector later didn't need a superuser shortcut.
It's also grown well past a planner. It now covers the whole trip lifecycle: a wishlist and map before a trip is real, overview pages on each place's history, language, culture, logistics, and food, a notes page for saved research, and a journal with photos afterward. The first commit landed April 18, 2026. The shareable itinerary was live nine days later. Since then it's taken 70+ merged pull requests and about 20,000 lines of framework-free JavaScript.
Deep dive: what building Passports taught me
The spec is the thing I actually write. Every major feature starts as a spec in the repo, addressed to "a fresh Claude Code session with no memory of the design conversation." Writing for that reader changes the document. Decisions need their reasoning attached, rejected options get recorded so they don't come back, and gotchas found while building one phase get written into the doc before the next one starts. More than 140 commits carry a Claude co-author credit. My role looks like PM and tech lead combined: scope it, make the calls, review the PR, test it on the Netlify preview. Codacy and Sourcery review every PR too.
The data model has to handle the ambiguity. Bases can't span time zones, but trips can, so the timezone lives on the base. An item's base and day are separate fields, so on a travel day a train can belong to one city and show up on the first day in the next. On transition days, I sort items by hand rather than reconciling two time zones. That was a deliberate trade: a travel planner needs to know what day it is, not what minute it is in UTC.
Status tried to do too many jobs, twice. "Done" started as one more item status, next to confirmed and reserved. Marking something done wiped out the fact that it had been reserved, so completion became its own flag. Later I found every screen recomputing a trip's lifecycle from its dates and never reading the stored status. That worked until the Destinations board needed to trust status to place a card in a column. The fix made the stored status the source of truth, with a quiet correction pass on load. The unused "Upcoming" status got cut and replaced by a "Starting soon" badge for trips within 14 days.
Some calls changed after real use. Public itineraries first showed only confirmed and reserved items. Now they show options too, while the public journal still shows confirmed and reserved only. Checking off an item in the journal now confirms it, since doing something is confirmation that it happened. Cross-column drag on the Destinations board was out of scope at first. It shipped anyway, as a gesture that opens the existing promote or demote step instead of skipping it. The original spec described three modes (planning, active, diary). The app ended up with two views.
Constraints I picked on purpose. No framework, no build step, and no npm dependencies in the app code. Supabase's free tier, kept alive with a scheduled Netlify function. Soft deletes everywhere, so nothing is ever truly lost. And the map never guesses a location from free text: you search, pick a result, and only then does it get coordinates.
The end of one trip feeds the next. "Move to Next Trip" takes the things we didn't get to and carries them into a new wishlist entry. The place overviews that started as hand-built static pages are now structured content I can write myself or have Claude draft through the MCP connector, using the same data and rules either way.