Trip Planner — Implementation Plan (v0)
Build plan for 0001-trip-planner.md. Pillars P1 (planner) + P2 (live tracker). Thin vertical slices, each shippable behind a flag. Backend (NestJS + Supabase) and Frontend (Flutter) move in lockstep per milestone.
0. Industry must-haves to fold into v0
Section titled “0. Industry must-haves to fold into v0”These are table stakes for any modern itinerary app (TripIt, Wanderlog, Roadtrippers, Google Trips legacy). Cross-checked against the spec — gaps flagged.
| # | Must-have | Already in spec? | Where it lands |
|---|---|---|---|
| 1 | Trip CRUD with cover + dates | Yes (F1.1) | M1 |
| 2 | Multi-destination + per-stop dates | Yes (F1.2) | M2 |
| 3 | Day-by-day timeline | Yes (F1.3) | M2 |
| 4 | Typed activities (transport/lodge/…) | Yes (F1.4) | M3 |
| 5 | Drag reorder (intra-day, cross-day) | Yes (F1.5) | M3 |
| 6 | Map view (per-day + per-trip) | Yes (F1.7) | M5 |
| 7 | Cost rollup + multi-currency | Yes (F1.6) | M4 |
| 8 | Per-traveller split | Yes (F1.6.d) | M4 |
| 9 | Photos + URL attachments | Yes (F1.8) | M6 |
| 10 | Offline-first read + queued writes | Yes (F1.10) | M7 (foundation laid in M1) |
| 11 | Sync state visible (synced/queued) | Yes (F1.10.d) | M7 |
| 12 | Share read-only link | Yes (F1.12) | M8 |
| 13 | Privacy levels | Yes (F1.11) | M8 |
| 14 | Live mode + mark done | Yes (F2.1–F2.2) | M9 |
| 15 | Schedule drift / “running late” | Yes (F2.5) | M9 |
| 16 | End-of-trip recap | Yes (F2.6) | M10 |
| 17 | Templates / duplicate | Yes (F1.9) | M10 |
| Gaps to add | |||
| 18 | Search across trips & activities | Not in spec | M10 — basic full-text on title/notes |
| 19 | Timezone per destination | Not in spec | M2 — store IANA tz on trip_destinations; render times in dest tz |
| 20 | Booking-confirmation paste/parse | Spec says links only; no parse | Defer — owned by 0002-ai-ingest |
| 21 | Calendar export (.ics) | Not in spec | M10 (small) — server endpoint emits ICS for trip |
| 22 | Push reminders before activity | Not in spec | M11 (post-GA) — local notifications first, server push later |
| 23 | Empty-state + onboarding to first trip < 60s | Implicit from PRODUCT NFR | M1 |
| 24 | Audit fields (created_by, updated_by) | Implicit | M1 — add to migrations |
| 25 | Soft-delete & 30-day undo job | Yes (F1.1.d) but no job | M1 schema; M11 cron worker |
Items 18, 19, 21, 24 are folded into the milestone plan below. 20, 22 deferred with a note.
1. Milestones (thin vertical slices)
Section titled “1. Milestones (thin vertical slices)”Each milestone is a shippable slice: backend migration + endpoint + Flutter screen + tests. “DoD” = definition of done; gates moving to the next milestone.
M1 — Skeleton & Trip CRUD (foundation)
Section titled “M1 — Skeleton & Trip CRUD (foundation)”Goal: a signed-in user can create / list / open / edit / delete a trip end-to-end.
Backend
infra/supabase/migrations/0001_trips.sql—trips,trip_days(auto-gen via trigger on insert/date change), audit columns,deleted_at, RLS policies (owner_id = auth.uid()).apps/backend/src/trips/— module, controller, service, DTOs, Supabase client wrapper, JWT guard.- Endpoints:
POST/GET/GET:id/PATCH/DELETE /v1/trips. - Day auto-gen on create + on date change (preserve activities trigger placeholder).
- Health check covers DB.
Frontend
apps/mobile/lib/features/trips/skeleton:data/(repo),domain/(model),view/(TripList, TripDetail, TripEditor sheet).- Riverpod providers;
dioorhttpclient with Supabase JWT interceptor. - Local store (Drift or Isar — pick in ADR-0007) — read path only stub for now.
- Empty state on TripList linking to “New Trip” (NFR: < 60 s to first trip).
Tests
- Backend: e2e for CRUD with RLS (a second user cannot read).
- Mobile: widget test for TripEditor; repo unit test against fake API.
DoD: AC-1, AC-2 green. Crash-free smoke on Android + iOS sim.
M2 — Multi-destination + day timeline + timezones
Section titled “M2 — Multi-destination + day timeline + timezones”Goal: a trip has destinations, each with a timezone, and a day-by-day timeline renders.
Backend
0002_destinations_days.sql—trip_destinations(name, geo, arrival_date, departure_date, tz, order_index).trip_daysalready exists; verify regen on date change preserves activities (clipped → “needs re-scheduling” tray = activities withday_id = nullandunscheduled = true).- Endpoints for destinations CRUD + reorder.
- Validation: dest dates ⊂ trip dates.
Frontend
- TripDetail: destinations strip + day tabs (with date + dest tz badge).
- Add/edit destination sheet with map-search stub (returns lat/lng, tz lookup via geo→IANA).
- “Needs re-scheduling” tray UI placeholder (empty until M3 lands clipping).
DoD: AC-3 green (date shrink moves activities to tray, none deleted — verified via test fixture even though activities land M3).
M3 — Activities CRUD + drag reorder
Section titled “M3 — Activities CRUD + drag reorder”Goal: full activity model with kinds, drag-to-reorder within and across days.
Backend
0003_activities.sql—activitiesper spec §7,activity_links,activity_attachments(rows only; storage in M6).- Endpoints
POST/PATCH/DELETE /v1/trips/:id/days/:dayId/activities/:actId. - Bulk reorder endpoint:
PATCH /v1/trips/:id/activities/reorderaccepting[{ id, day_id, order_index }]for cheap drag commits.
Frontend
- ActivityEditor bottom sheet — kind picker first (validates AC-4).
- Day list with
ReorderableListView; cross-day drag via target drop. - Optimistic local reorder; rollback on API failure.
Tests
- AC-4, AC-5 green. Reorder endpoint stress test with 100 activities.
M4 — Cost rollup + traveller split
Section titled “M4 — Cost rollup + traveller split”Goal: per-activity costs roll up to day → trip; multi-currency normalised.
Backend
0004_currency.sql—trip_currency_snapshots(trip_id, base, rates_jsonb, fetched_at).- Job/endpoint to fetch rates (provider TBD: open.er-api.com or exchangerate.host) at trip create + manual refresh.
- View
v_trip_cost_summaryfor fast read.
Frontend
- Cost field on ActivityEditor with currency selector.
- Day total chip + Trip summary card (planned vs N/A live yet).
- Traveller count → split display.
DoD: AC-6 green.
M5 — Map view
Section titled “M5 — Map view”Goal: per-day map with ordered pins, full-trip map.
ADR
- ADR-0008 map provider decision (Mapbox vs MapLibre + free tiles vs Google). Cost + offline tile caching are gating.
Frontend
MapViewwidget using chosen SDK; pin-to-activity link; toggle per-day / per-trip.- Geocode entry from ActivityEditor location field.
Backend
- No new endpoints; geo already on activity & destination.
DoD: AC-7 green.
M6 — Attachments (photos + URLs)
Section titled “M6 — Attachments (photos + URLs)”Goal: photos and external URLs attached to activities.
Backend
POST /v1/uploads/signreturning Supabase Storage signed URL (buckettrip-attachments, pathtrip_id/activity_id/...).- RLS on storage bucket: only owner can read/write.
activity_linksendpoints.
Frontend
- Multi-image picker (cap 8 per activity per F1.8.a).
- URL chip with label.
- File attachment behind
attachments_files_v0flag (F1.8.c).
DoD: photos visible offline once cached.
M7 — Offline-first + sync engine
Section titled “M7 — Offline-first + sync engine”Goal: opened trips fully offline; queued ops replay; conflict policy LWW per field.
Backend
0005_sync.sql—updated_attriggers everywhere; soft-delete tombstones in pull.POST /v1/sync/pull { since }— incremental with cursor.POST /v1/sync/push { ops[] }— accept idempotency key per op; conflict resolution = LWW per field byclient_tsvsupdated_at.
Frontend
- Local DB (Drift) mirrors all entities.
- Outbox table; background isolate replays on connectivity event.
- Sync chip in header: synced / queued(N) / conflict.
- Offline read path swaps in for all trip screens.
Tests
- AC-8, AC-9, AC-10 green. Two-device test with airplane-mode replay.
M8 — Privacy + share link
Section titled “M8 — Privacy + share link”Goal: private/unlisted/public visibility + read-only share URL.
Backend
0006_share_links.sql—trip_share_links(slug PK, trip_id, revoked_at).- Endpoints to create/rotate/revoke.
- Public render endpoint
GET /s/:slug→ minimal HTML preview (server-rendered, no auth) + deep-link banner. - Visibility change invalidates cached share within 60 s (use short cache TTL on edge).
Frontend
- Privacy selector on TripEditor.
- “Copy share link” CTA on unlisted/public trip.
DoD: AC-11 green.
M9 — Live mode + mark done + drift
Section titled “M9 — Live mode + mark done + drift”Goal: travel-day UI: today view, tick done with actuals, late indicator + shift CTA.
Backend
- Add
actual_start,actual_end,done,done_at(already in schema sketch — confirm in M3 migration).
Frontend
- LiveDay screen with “next up” hero card.
- Tick → record actuals (default now).
- Drift > 15 min → “running late” + one-tap “shift remaining of today” (recomputes order, batched PATCH).
- Auto-suggest live mode when device date ⊂ trip range (AC-12).
DoD: AC-12, AC-13, AC-14 green.
M10 — Recap + duplicate/templates + search + ICS
Section titled “M10 — Recap + duplicate/templates + search + ICS”Goal: end-of-trip value + restart loop + findability.
Backend
0007_templates.sql—trip_templates(payload jsonb).POST /v1/trips/:id/duplicate,POST /v1/trips/:id/save-as-template,POST /v1/trips/from-template/:tplId.GET /v1/trips/:id/calendar.ics— VEVENT per activity with location, tz, alarms.GET /v1/search?q=— Postgrestsvectorover title/notes; per-user filter via RLS.
Frontend
- Recap screen (cost vs planned, photo strip, completed list) — AC-15.
- Duplicate / Save-as-template menu.
- Search bar in TripList.
- “Add to calendar” share action.
DoD: AC-15 green; recap renders with no internet.
M11 — Hardening, hard-delete worker, beta cut
Section titled “M11 — Hardening, hard-delete worker, beta cut”Goal: ship-ready.
apps/workers/— cron worker hard-deletestripswheredeleted_at < now() - 30d(and cascades).- Crash + perf telemetry (Sentry already wired? if not, add).
- Local notifications: 30 min before activity
time_start(opt-in). - A11y pass: 200% font scale, screen reader labels.
- Beta build → TestFlight + Play Internal.
Gate to GA: AC-1..AC-15 green for two consecutive builds; crash-free > 99.5%.
2. Cross-cutting concerns
Section titled “2. Cross-cutting concerns”| Concern | Decision / location |
|---|---|
| AuthN | Supabase JWT; backend JwtAuthGuard; mobile interceptor (already in repo from feature 0002). |
| AuthZ | Postgres RLS as primary; backend service-role used only for sync replay & admin paths. |
| Validation | class-validator DTOs server side; Freezed/Zod-equivalent client side. |
| Error model | RFC 7807 problem+json from API; mapped to Failure sealed class on client. |
| Logging/trace | Pino on backend; traceId propagated from client header. |
| Telemetry | Sentry mobile + backend; basic structured events for “trip_created”, “activity_created”, “live_mode_on”. |
| Feature flags | Simple env-driven flag service for attachments_files_v0, voice_memo_v0, recap_share_image_v0. |
| Migrations | Numbered SQL in infra/supabase/migrations/, applied via supabase db push in CI. |
| Testing | Backend: Jest unit + supertest e2e against ephemeral Postgres. Mobile: widget + integration with patrol/integration_test. |
| CI gates | Lint, typecheck, unit, e2e on every PR; coverage ≥ 80% on lib/features/trips and apps/backend/src/trips. |
3. Open ADRs needed before / during build
Section titled “3. Open ADRs needed before / during build”- ADR-0007 Mobile state mgmt + local DB (Riverpod + Drift assumed).
- ADR-0008 Map provider (M5 gate).
- ADR-0009 Sync conflict policy beyond v0 LWW (post-beta).
- ADR-0010 Currency rate source + refresh policy.
4. Risks & mitigations (delta from spec §10)
Section titled “4. Risks & mitigations (delta from spec §10)”- Drift conflict storms when multiple devices come online simultaneously. Mitigation: per-op idempotency key + server-side dedupe.
- Map SDK cost scaling with MAU. Mitigation: ADR-0008 must compare per-1k-load pricing; default to MapLibre + free tiles unless feature gap forces otherwise.
- Storage abuse (photos). Mitigation: per-user quota check on signed-URL endpoint; 8/activity hard cap.
- Tz pitfalls for activities crossing midnight in destination tz. Mitigation: store all times as UTC + per-activity
tz(inherits dest tz); render in tz, never in device-local.
5. Sequence summary
Section titled “5. Sequence summary”M1 CRUD ─► M2 Dests+Days+TZ ─► M3 Activities+Reorder ─► M4 Cost ─► M5 Map └► M6 AttachmentsM3 ─► M7 Sync (parallel after M3) ─► M8 Share ─► M9 Live ─► M10 Recap+Templates+Search+ICS ─► M11 BetaM5, M6, M7 can run in parallel after M3 if staffed; M8 depends on M7 (share serves a snapshot via sync read path). M9 depends on M3 + M4.
6. Out of scope (re-confirm)
Section titled “6. Out of scope (re-confirm)”Per spec §3.2 — AI ingest, communities, marketplace, deals, real-time co-edit, web client. Booking-email parse (must-have #20 above) explicitly punted to spec 0002.
7. Next actions
Section titled “7. Next actions”- Land ADR-0007 (state mgmt + local DB).
- Open
feature/trip-planner-m1branch. - Write
0001_trips.sqlmigration + RLS policies. - Scaffold
apps/backend/src/trips/module. - Scaffold
apps/mobile/lib/features/trips/. - CI gates updated to require migration apply + e2e.