Skip to content

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.


These are table stakes for any modern itinerary app (TripIt, Wanderlog, Roadtrippers, Google Trips legacy). Cross-checked against the spec — gaps flagged.

#Must-haveAlready in spec?Where it lands
1Trip CRUD with cover + datesYes (F1.1)M1
2Multi-destination + per-stop datesYes (F1.2)M2
3Day-by-day timelineYes (F1.3)M2
4Typed activities (transport/lodge/…)Yes (F1.4)M3
5Drag reorder (intra-day, cross-day)Yes (F1.5)M3
6Map view (per-day + per-trip)Yes (F1.7)M5
7Cost rollup + multi-currencyYes (F1.6)M4
8Per-traveller splitYes (F1.6.d)M4
9Photos + URL attachmentsYes (F1.8)M6
10Offline-first read + queued writesYes (F1.10)M7 (foundation laid in M1)
11Sync state visible (synced/queued)Yes (F1.10.d)M7
12Share read-only linkYes (F1.12)M8
13Privacy levelsYes (F1.11)M8
14Live mode + mark doneYes (F2.1–F2.2)M9
15Schedule drift / “running late”Yes (F2.5)M9
16End-of-trip recapYes (F2.6)M10
17Templates / duplicateYes (F1.9)M10
Gaps to add
18Search across trips & activitiesNot in specM10 — basic full-text on title/notes
19Timezone per destinationNot in specM2 — store IANA tz on trip_destinations; render times in dest tz
20Booking-confirmation paste/parseSpec says links only; no parseDefer — owned by 0002-ai-ingest
21Calendar export (.ics)Not in specM10 (small) — server endpoint emits ICS for trip
22Push reminders before activityNot in specM11 (post-GA) — local notifications first, server push later
23Empty-state + onboarding to first trip < 60sImplicit from PRODUCT NFRM1
24Audit fields (created_by, updated_by)ImplicitM1 — add to migrations
25Soft-delete & 30-day undo jobYes (F1.1.d) but no jobM1 schema; M11 cron worker

Items 18, 19, 21, 24 are folded into the milestone plan below. 20, 22 deferred with a note.


Each milestone is a shippable slice: backend migration + endpoint + Flutter screen + tests. “DoD” = definition of done; gates moving to the next milestone.

Goal: a signed-in user can create / list / open / edit / delete a trip end-to-end.

Backend

  • infra/supabase/migrations/0001_trips.sqltrips, 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; dio or http client 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.sqltrip_destinations(name, geo, arrival_date, departure_date, tz, order_index). trip_days already exists; verify regen on date change preserves activities (clipped → “needs re-scheduling” tray = activities with day_id = null and unscheduled = 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).


Goal: full activity model with kinds, drag-to-reorder within and across days.

Backend

  • 0003_activities.sqlactivities per 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/reorder accepting [{ 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.

Goal: per-activity costs roll up to day → trip; multi-currency normalised.

Backend

  • 0004_currency.sqltrip_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_summary for 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.


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

  • MapView widget 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.


Goal: photos and external URLs attached to activities.

Backend

  • POST /v1/uploads/sign returning Supabase Storage signed URL (bucket trip-attachments, path trip_id/activity_id/...).
  • RLS on storage bucket: only owner can read/write.
  • activity_links endpoints.

Frontend

  • Multi-image picker (cap 8 per activity per F1.8.a).
  • URL chip with label.
  • File attachment behind attachments_files_v0 flag (F1.8.c).

DoD: photos visible offline once cached.


Goal: opened trips fully offline; queued ops replay; conflict policy LWW per field.

Backend

  • 0005_sync.sqlupdated_at triggers 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 by client_ts vs updated_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.

Goal: private/unlisted/public visibility + read-only share URL.

Backend

  • 0006_share_links.sqltrip_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.


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.sqltrip_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= — Postgres tsvector over 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-deletes trips where deleted_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%.


ConcernDecision / location
AuthNSupabase JWT; backend JwtAuthGuard; mobile interceptor (already in repo from feature 0002).
AuthZPostgres RLS as primary; backend service-role used only for sync replay & admin paths.
Validationclass-validator DTOs server side; Freezed/Zod-equivalent client side.
Error modelRFC 7807 problem+json from API; mapped to Failure sealed class on client.
Logging/tracePino on backend; traceId propagated from client header.
TelemetrySentry mobile + backend; basic structured events for “trip_created”, “activity_created”, “live_mode_on”.
Feature flagsSimple env-driven flag service for attachments_files_v0, voice_memo_v0, recap_share_image_v0.
MigrationsNumbered SQL in infra/supabase/migrations/, applied via supabase db push in CI.
TestingBackend: Jest unit + supertest e2e against ephemeral Postgres. Mobile: widget + integration with patrol/integration_test.
CI gatesLint, typecheck, unit, e2e on every PR; coverage ≥ 80% on lib/features/trips and apps/backend/src/trips.

  • 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.

M1 CRUD ─► M2 Dests+Days+TZ ─► M3 Activities+Reorder ─► M4 Cost ─► M5 Map
└► M6 Attachments
M3 ─► M7 Sync (parallel after M3) ─► M8 Share ─► M9 Live ─► M10 Recap+Templates+Search+ICS ─► M11 Beta

M5, 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.


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.


  1. Land ADR-0007 (state mgmt + local DB).
  2. Open feature/trip-planner-m1 branch.
  3. Write 0001_trips.sql migration + RLS policies.
  4. Scaffold apps/backend/src/trips/ module.
  5. Scaffold apps/mobile/lib/features/trips/.
  6. CI gates updated to require migration apply + e2e.