01Problem
AI trip planners optimise for looking plausible, not for being followable. The output reads well and falls apart the moment you try to use it: attractions scheduled when they're closed, meals at odd hours, and stops ordered so you criss-cross a city all day.
The underlying issue is that a language model asked for "an itinerary" produces text shaped like an itinerary. Nothing forces the result to satisfy the constraints that make a plan usable — and a plan you have to rebuild yourself is worth less than no plan at all.
Group trips add a second problem. Planning happens across chat threads and screenshots, nobody knows which version is current, and sharing a plan usually means giving everyone the ability to rewrite it.
02Approach
The constraints are enforced in the prompt rather than hoped for. The generator is given hard rules — no teleporting between stops, restaurants at meal times, attractions inside opening hours, a cap on stops per day, geographic clustering, and a total that fits the budget — and asked to return structured JSON that the app validates and stores as real rows rather than free text.
Generation runs through a provider chain rather than a single API. Groq serves requests first, Gemini backs it up, and Anthropic is supported behind a config flag. Both defaults are free tiers, and the order is an environment variable, so an outage or a price change is a restart rather than a deploy.
Permissions are modelled explicitly. A trip has an owner, editors and viewers; the owner controls what a share link grants, and links default to view-only because a link can be forwarded past the people it was sent to.
The project was migrated off Supabase onto Neon Postgres with Drizzle mid-build, which meant rebuilding the data and auth layers against a live schema without losing the working parts.
03Architecture
04Decisions & trade-offs
Decision 1
The original Supabase schema enforced access with row-level security. The trips policy queried trip_members, and the trip_members policy queried trips, so Postgres answered every read with "infinite recursion detected in policy" — including reads by the trip's own owner. The app could not load a single trip. Access control now lives in one function that every route calls. There is exactly one definition of who can view and who can edit, it is readable without knowing Postgres policy semantics, and it cannot re-enter that state. The trade-off is that the database no longer defends itself if a query bypasses the API — acceptable here because every query goes through routes I control.
05What I'd do differently
Pick the data layer before writing features. Migrating off Supabase onto Neon and Drizzle mid-build meant rebuilding auth and data access against a live schema — a decision that would have cost an afternoon on day one cost days later.
Design access control as one explicit function from the start. The row-level-security recursion only surfaced once trips and trip members referenced each other; starting with a single, readable permission check would have avoided the detour entirely.
Check the generated itinerary against the hard constraints in code as well as in the prompt — opening hours, meal times, budget — so a plan that breaks a rule is caught and regenerated instead of reaching the user.


