TravelCanvas · 2026 – present
A person is not a user
Building an itinerary platform advisor-first, where most of the hard problems turned out to be naming problems.
Public- 2
- audiences, one product
- 0
- owner columns
- 8
- kinds of trip item
The situation
A trip arrives as a dozen unrelated confirmation emails — flights, two hotels, a car, a restaurant, a tour. There is no single view of what happens when, no reliable way to share it with the people coming along, and no way for an advisor and their client to work on it together.
I co-founded it in February 2026 on a trip through Southeast Asia, which is roughly the most on-brand possible origin for an itinerary tool.
The constraints
- Two audiences with opposed needs in one product: advisors are building and want control, travellers are reading and want clarity. Advisors are the paying side, so it is built advisor-first — without the traveller experience being an afterthought.
- Most people on a trip will never create an account. The advisor types in their client's name long before anyone signs up, and the trip has to be correct in the meantime.
- Confirmation emails and booking PDFs come from systems the product will never control, in every layout their senders have invented, and the dates and names inside them have to come through exactly.
- A founding team of two, so anything that adds ongoing maintenance has to earn it against the hours two people actually have.
The call
Separate people from users. `people` is the identity record, `users` is the login, and a member of a trip points at a person with no user column at all. The whole permission model rests on this, because a client is on the trip well before they are on the platform.
Give an itinerary no owner column. Who may do what derives entirely from the members table. There is a `created_by_id`, and it deliberately grants nothing — permissions and quota never consult it. Ownership fields tend to become a second, quieter permission system that disagrees with the real one.
Keep a canonical glossary and treat colliding vocabulary as a defect. "Advisor" is the product word; the code says `manager` everywhere — and "manager" means two unrelated things, a user role and a per-trip member role. Those collisions caused real mistakes, so one document says which word wins where.
Split the authenticated surface into separate traveller, advisor, and admin applications over one app with role-based routing — with the API client, stores, and query layer in shared packages, so the split is in the UI rather than duplicated underneath.
Read forwarded confirmation emails and uploaded PDFs with Claude rather than writing a parser per provider: a set of templates is permanently behind the long tail of layouts a model can simply read. Extraction only ever produces a draft, and items reach the trip when a person approves them in a review inbox — the email path included. A model can misread a date, and a forwarded document is written by a stranger, so its contents are data to review, never instructions to act on.
Take the traveller app to the App Store as a Capacitor shell rather than a second, native codebase. Two founders cannot maintain a Swift app beside three web applications, and the shell ships the same Vue app — a fix lands on the web and on the phone from the same commit. Sign-in is the exception and goes native, because the web's redirect flow leaves the session in the system browser rather than in the app.
How it fits together
What happened
An advisor builds a trip privately and publishes it, which is the single action that makes it visible and sends invitations. When they want a decision rather than to inform, they group options into a proposal and the client approves or declines.
Because membership rather than ownership carries permissions, the awkward cases stopped being special: a client who never signs in, an advisor handing a trip to a colleague, a traveller on someone else's itinerary. None of them needed their own branch.
The same application travellers use on the web is live on the App Store, and releasing it is one command — from attaching the TestFlight build to tagging the commit that shipped.
What I'd do differently
The glossary should have been the first document, not a response to bugs. I wrote it after the same word meaning two things had already cost real time — and the version that exists now opens with the collisions rather than the definitions, because that is the part that was actually load-bearing.