Skip to main content
RapidDev - Software Development Agency

How to Build a Travel Itinerary App with Lovable

Build a collaborative trip planner in Lovable where multiple travelers edit a shared day-by-day itinerary in real time. Features itinerary items, trip member roles, shared expense tracking with an automatic splitting algorithm, and Supabase Realtime so every collaborator sees updates instantly.

What you'll build

  • Trip creation with destination, date range, cover photo, and member invitation by email
  • Day-by-day timeline with drag-to-reorder itinerary items (activities, hotels, flights, restaurants)
  • Real-time collaborative editing using Supabase Realtime — changes appear instantly for all members
  • Shared expense tracker with categories, payer selection, and automatic per-person split calculation
  • Trip members management with roles (owner, editor, viewer) enforced by RLS
  • Balance sheet showing who owes whom after the trip using a settlement algorithm
  • Export itinerary as a shareable summary page with public read-only access
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate13 min read2.5–3 hoursLovable Pro or higherApril 2026RapidDev Engineering Team
TL;DR

Build a collaborative trip planner in Lovable where multiple travelers edit a shared day-by-day itinerary in real time. Features itinerary items, trip member roles, shared expense tracking with an automatic splitting algorithm, and Supabase Realtime so every collaborator sees updates instantly.

What you're building

A travel itinerary app has three core features: planning, collaboration, and expenses. Planning uses a trips table with date ranges and an itinerary_items table with a day_number and sort_order for drag-and-drop reordering. Each item has a type (activity, hotel, flight, restaurant, transport) that controls its icon.

Collaboration is handled by a trip_members table with roles. RLS policies check trip membership before allowing any read or write operation. Supabase Realtime broadcasts changes to the itinerary_items table so all members see additions, edits, and deletions without refreshing.

Expenses use a trip_expenses table where each expense has a payer (one member) and a split configuration. The simplest split is equally among all members. The split is stored as a JSONB column: { memberId: amountOwed }. After the trip, the app runs a settlement algorithm that calculates the minimum number of transactions needed for everyone to settle up — for example, if Alice owes Bob $30 and Bob owes Charlie $20, the result is Alice pays Charlie $20 and Alice pays Bob $10.

Final result

A collaborative travel planner where multiple people can co-edit an itinerary in real time, track shared expenses, and see a settlement breakdown after the trip.

Tech stack

LovableFull-stack app generation
SupabaseDatabase, Auth, Realtime, Storage
shadcn/uiCards, Dialog, Avatar, Badge, Tabs
react-hook-form + zodTrip and expense forms
date-fnsDate range calculations and day grouping
RechartsExpense breakdown pie chart

Prerequisites

  • Lovable Pro account for real-time and multi-table generation
  • Supabase project with URL and anon key saved to Cloud tab → Secrets
  • Supabase Auth configured (email/password is sufficient for inviting members)
  • Optional: a real trip in mind to populate with test data

Build steps

1

Create the trip schema with member roles and RLS

The schema is the most important step. Member-based RLS is more complex than user_id-based RLS — a user can access a trip if they are in the trip_members table for that trip.

prompt.txt
1Build a collaborative travel itinerary app. Create these Supabase tables:
2
3- trips: id, created_by (FK auth.users), title, destination, description, start_date (date), end_date (date), cover_image_url, share_token (uuid, default gen_random_uuid()), is_published (bool default false), created_at
4- trip_members: id, trip_id (FK trips), user_id (FK auth.users), email (text, for pending invites), role (owner|editor|viewer), joined_at, UNIQUE(trip_id, user_id)
5- itinerary_items: id, trip_id (FK trips), day_number (int, 1-indexed), sort_order (int), item_type (activity|hotel|flight|restaurant|transport|note), title, description, location, start_time (time nullable), end_time (time nullable), booking_reference, created_by (FK auth.users), created_at, updated_at
6- trip_expenses: id, trip_id (FK trips), paid_by (FK auth.users), title, amount (numeric), category (food|transport|accommodation|activity|other), expense_date (date), split_config (jsonb, { userId: amountOwed }), receipt_url, created_at
7
8RLS policies:
9- trips: SELECT/UPDATE/DELETE where id IN (SELECT trip_id FROM trip_members WHERE user_id = auth.uid())
10- trip_members, itinerary_items, trip_expenses: same trip_id IN (...) pattern
11- Only owners can delete trips and manage members
12- Allow public SELECT on trips WHERE is_published = true (for share link)
13
14Enable Realtime on itinerary_items and trip_expenses tables.

Pro tip: Ask Lovable to create a Supabase function invite_to_trip(trip_id, email, role) that either links an existing user by email lookup in auth.users, or creates a pending invite row in trip_members with just the email. When a new user signs up with that email, a trigger automatically updates the trip_members row with their user_id.

Expected result: All four tables are created with member-based RLS. The trips list page shows with a 'Create Trip' button. Creating a trip auto-adds the creator as an owner in trip_members.

2

Build the day-by-day itinerary timeline

Create the itinerary view with a day-by-day layout. Each day is a column or section. Items within a day can be reordered by dragging.

prompt.txt
1Build the itinerary page at src/pages/TripItinerary.tsx:
2
31. Page header: trip title, destination, date range, member Avatars (show up to 5, then '+N more'), 'Invite' Button, 'Share' Button
42. Day tabs: one Tab per day of the trip labeled 'Day 1 — Jun 15', 'Day 2 — Jun 16' etc. (calculate from start_date + day_number - 1)
53. Day content:
6 - Timeline list of itinerary_items for this day ordered by sort_order
7 - Each item is a Card with: type icon (map pin for activity, bed for hotel, plane for flight, fork for restaurant), title, location, time range, booking reference (grayed out), edit/delete Actions menu
8 - Item type Badge with different colors per type
9 - Drag handle on left side. On drag end, update sort_order values for all affected items in a batch Supabase update
104. 'Add Item' Button at bottom of each day: opens a Dialog with fields for item_type Select, title, description, location, start_time, end_time, booking_reference
115. Subscribe to itinerary_items changes via Supabase Realtime. Show a 'Member Name edited this itinerary' toast when another user makes a change

Pro tip: Ask Lovable to add an 'Add Day' button for trips that get extended. This inserts a new day_number (max + 1) and also extends the trips.end_date by one day. Members see the new day appear in real time.

Expected result: The itinerary page shows day tabs. Adding items to a day creates Cards in the timeline. Opening the same trip in two browser windows shows real-time sync when items are added or reordered.

3

Build the expense tracker with split calculation

Add the expense tracking tab. The split algorithm calculates what each member owes the payer based on the split_config JSONB and then runs a settlement algorithm after the trip.

src/utils/expenseSettlement.ts
1// src/utils/expenseSettlement.ts
2export interface Balance {
3 userId: string
4 name: string
5 balance: number
6}
7
8export interface Settlement {
9 from: string
10 fromName: string
11 to: string
12 toName: string
13 amount: number
14}
15
16export function calculateSettlements(balances: Balance[]): Settlement[] {
17 const debtors = balances.filter((b) => b.balance < 0).sort((a, b) => a.balance - b.balance)
18 const creditors = balances.filter((b) => b.balance > 0).sort((a, b) => b.balance - a.balance)
19 const settlements: Settlement[] = []
20
21 let i = 0
22 let j = 0
23 while (i < debtors.length && j < creditors.length) {
24 const debtor = debtors[i]
25 const creditor = creditors[j]
26 const amount = Math.min(Math.abs(debtor.balance), creditor.balance)
27
28 if (amount > 0.01) {
29 settlements.push({
30 from: debtor.userId,
31 fromName: debtor.name,
32 to: creditor.userId,
33 toName: creditor.name,
34 amount: Math.round(amount * 100) / 100,
35 })
36 }
37
38 debtor.balance += amount
39 creditor.balance -= amount
40
41 if (Math.abs(debtor.balance) < 0.01) i++
42 if (creditor.balance < 0.01) j++
43 }
44
45 return settlements
46}

Expected result: The expenses tab shows all trip expenses as Cards. The settlement section shows 'Alice pays Bob $35' style entries that represent the minimum transactions needed to settle all debts.

4

Add member management and invitations

Build the member management panel where trip owners can invite new members by email, change roles, and remove members. Invite new users via email using Supabase Auth's invite feature.

prompt.txt
1Build a member management Sheet (slides in from right) for trip owners:
2
31. Member list: Avatar + display name + email + role Badge (owner=purple, editor=blue, viewer=gray)
42. Role change Select per member (owners only): clicking changes role in trip_members
53. Remove member Button (owners only, cannot remove self if only owner)
64. Invite section at bottom:
7 - Email Input + Role Select + 'Send Invite' Button
8 - On submit: call invite_to_trip(trip_id, email, role) Supabase RPC function
9 - Show pending invites section: members with no user_id yet show as 'Pending — email@example.com' with a 'Revoke' button
105. Trip visibility toggle: 'Private' vs 'Public link' Switch. Setting to public sets is_published = true and shows the share URL: /trips/public/SHARE_TOKEN
116. Create a public route /trips/public/:token that fetches the trip by share_token WHERE is_published = true (no auth required) and shows a read-only itinerary view

Expected result: The member management Sheet opens from the trip header. Inviting a new member by email creates a pending entry. The public share link works without being logged in.

Complete code

src/hooks/useTripRealtime.ts
1import { useEffect } from 'react'
2import { useQueryClient } from '@tanstack/react-query'
3import { supabase } from '@/integrations/supabase/client'
4import { toast } from '@/hooks/use-toast'
5
6interface Options {
7 tripId: string
8 currentUserId: string
9}
10
11export function useTripRealtime({ tripId, currentUserId }: Options) {
12 const queryClient = useQueryClient()
13
14 useEffect(() => {
15 const channel = supabase
16 .channel(`trip-${tripId}`)
17 .on(
18 'postgres_changes',
19 {
20 event: '*',
21 schema: 'public',
22 table: 'itinerary_items',
23 filter: `trip_id=eq.${tripId}`,
24 },
25 (payload) => {
26 queryClient.invalidateQueries({ queryKey: ['itinerary', tripId] })
27 if (payload.new && 'created_by' in payload.new && payload.new.created_by !== currentUserId) {
28 toast({
29 title: 'Itinerary updated',
30 description: 'A collaborator just made a change.',
31 duration: 3000,
32 })
33 }
34 }
35 )
36 .on(
37 'postgres_changes',
38 {
39 event: '*',
40 schema: 'public',
41 table: 'trip_expenses',
42 filter: `trip_id=eq.${tripId}`,
43 },
44 () => {
45 queryClient.invalidateQueries({ queryKey: ['expenses', tripId] })
46 }
47 )
48 .subscribe()
49
50 return () => {
51 supabase.removeChannel(channel)
52 }
53 }, [tripId, currentUserId, queryClient])
54}

Customization ideas

Map view with pinned locations

Integrate Mapbox GL JS (or Google Maps via an Edge Function proxy) to show all itinerary items with coordinates on a map. When users add a location to an itinerary item, use the Mapbox geocoding API to convert the address to coordinates and store them in itinerary_items.coordinates (point type in PostgreSQL).

Flight and hotel search integration

Add an 'Add Flight' dialog that calls the Amadeus or Skyscanner API via an Edge Function to search for flights. Show results in a list and let users click to add the flight details (departure/arrival times, flight number) directly to the itinerary. Store the API key in Edge Function secrets.

Packing list with member assignment

Add a packing_items table linked to a trip. Each item has a name, category, quantity, and assigned_to member. Show a checklist UI where members can check off items as they pack. The Realtime subscription updates the checklist for all members live.

Budget tracker with currency conversion

Add a trip budget (total_budget column on trips) and convert all expenses to a home currency using a daily exchange rate stored in a currencies table updated by a scheduled Edge Function. Show a budget vs actual Recharts BarChart that converts expenses to the trip's home currency.

PDF itinerary export

Create an Edge Function that generates a PDF of the full itinerary using a template. The PDF includes trip header, day-by-day timeline, and expense summary. Use a Deno-compatible PDF library or an HTML-to-PDF service like Puppeteer via an external API. Add a 'Download PDF' button to the trip page.

Common pitfalls

Pitfall: Using user_id = auth.uid() for trip-related table RLS

How to avoid: Use the membership-based RLS pattern: WHERE trip_id IN (SELECT trip_id FROM trip_members WHERE user_id = auth.uid()). Apply this pattern to all trip-related tables. For write operations, also check the user's role (editor or owner) for modifications.

Pitfall: Not handling Realtime subscription cleanup

How to avoid: Always return a cleanup function from useEffect: return () => { supabase.removeChannel(channel) }. The useTripRealtime hook in the complete code example shows the correct pattern.

Pitfall: Storing expense splits as simple equal division rather than JSONB

How to avoid: Store the split as a JSONB object in split_config: { 'userId1': 25.00, 'userId2': 25.00, 'userId3': 50.00 }. The Add Expense dialog should show each member with an editable amount that sums to the total. Default to equal split but allow manual adjustment.

Pitfall: Calculating settlement on every render

How to avoid: Memoize the settlement calculation using useMemo: const settlements = useMemo(() => calculateSettlements(balances), [balances]). The calculation only re-runs when the expense data changes, not on every render.

Best practices

  • Use trip member roles in RLS policies, not just membership. Viewer-role members should not be able to insert or update itinerary items. Check role in the RLS policy or in a before-insert trigger.
  • Subscribe to Supabase Realtime at the trip level (all itinerary items for a trip), not at the global level. Filter subscriptions by trip_id to avoid receiving updates from other trips the user is a member of.
  • Store the settlement algorithm result as a snapshot in the database after the trip ends, rather than recalculating on every view. Create a trip_settlement table and populate it when the trip status changes to 'completed'.
  • Use Supabase Storage for receipt images in trip_expenses. Store the path in receipt_url and use signed URLs for display so storage objects are not publicly accessible. Delete storage objects when expenses are deleted.
  • Add an is_archived status to trips instead of deleting them. Archived trips remain accessible for expense reference and memory browsing but don't appear in the main trips list by default.
  • Validate that itinerary items have day_number values within the trip's date range. A CHECK constraint or trigger prevents adding Day 10 items to a 7-day trip.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a collaborative travel app where multiple users share a trip and track expenses together. I have a trip_expenses table with a split_config JSONB column that stores { userId: amountOwed } for each expense. Help me write a JavaScript function that takes an array of expenses and trip members and returns: 1) each member's net balance (positive = owed money, negative = owes money), 2) the minimum set of transactions to settle all debts. Show the full algorithm with TypeScript types.

Lovable Prompt

Add a 'Day Notes' feature to the travel itinerary. On each day tab, show a TextArea at the top for free-form daily notes (restaurant reservations, reminders, packing notes). Store in a day_notes table: id, trip_id, day_number, content (text), updated_at, updated_by. Auto-save on keystroke debounce. Show who last edited the note and when. Broadcast changes via Supabase Realtime so all members see the note update in real time.

Build Prompt

In Supabase, create a trigger function that fires when is_published changes to true on the trips table. The trigger should: 1) verify the trip has at least one itinerary item, 2) create a row in trip_activity_log with action='published', trip_id, and user_id from auth.uid(). Also create a separate scheduled Edge Function that runs weekly and sets is_published = false for trips whose end_date was more than 90 days ago to clean up old public links.

Frequently asked questions

How does real-time collaboration work when two people edit the same itinerary item simultaneously?

Supabase Realtime broadcasts database changes, not a CRDT (conflict-free replicated data type) system. If two people edit the same item simultaneously, the last write wins. For a travel app used by small groups, this is acceptable. The toast notification ('A collaborator made a change') alerts users to refresh. For more robust conflict resolution, you'd need to implement optimistic locking with a version column.

Can people view the itinerary without creating a Lovable account?

Yes, through the public share link. Setting is_published = true on the trip creates a read-only URL at /trips/public/SHARE_TOKEN. This route fetches the trip without any auth headers using the anon key, and the RLS policy allows SELECT on published trips. Viewers can see the itinerary but cannot edit anything.

How accurate is the expense settlement algorithm?

The greedy settlement algorithm in this guide produces the minimum number of transactions needed to settle all debts. It sorts creditors and debtors by absolute balance and matches the largest debtor to the largest creditor first. For typical trip group sizes (2-10 people), the algorithm is exact. It works best when splits are entered accurately — rounding to 2 decimal places prevents floating-point accumulation errors.

How do I handle members who joined the trip partway through and shouldn't share earlier expenses?

The split_config JSONB approach handles this. When adding an expense, the dialog shows all current members but you can set the amount to 0 for members who weren't present. The settlement algorithm only settles non-zero amounts. Add a 'joined_date' to trip_members and pre-fill the Add Expense dialog with zero amounts for members whose joined_date is after the expense date.

Can I add a map showing all the places we're visiting?

Yes, that's the first customization idea in this guide. Add a coordinates column (point type) to itinerary_items, geocode addresses using Mapbox or Google Maps API via an Edge Function, and display all pins on a map. Ask Lovable to add the map view after completing the base build — it's a separate step that requires an API key in Cloud tab → Secrets.

What is the maximum number of members per trip?

There is no hard limit set in this schema. Supabase can handle hundreds of members per trip from a database perspective. For Realtime, each connected browser tab counts as one Supabase Realtime connection. Supabase Free plan allows 200 simultaneous Realtime connections, which is more than enough for travel groups. The practical limit is what makes sense for a shared trip — most groups are 2-15 people.

Can I import an existing itinerary from Google Trips or TripIt?

Not directly — there's no import feature in the base build. However, you can ask Lovable to add a CSV import dialog that accepts a specific format (date, time, item_type, title, location, description) and bulk-inserts itinerary items. Google Trips no longer exists, and TripIt has no public export API, but you can manually create the CSV from any source.

What happens to the itinerary if the trip owner deletes their account?

Add a CASCADE constraint or trigger that reassigns ownership to another editor-role member when an owner leaves. Without this, the trip becomes ownerless and no one can manage membership. Ask Lovable to add an on_member_delete trigger that promotes the oldest editor-role member to owner if the deleted member was the last owner.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.