Traveloop.
Multi-city itineraries, budgets, and packing — shipped on a hackathon clock.
#tl;dr
Traveloop is a full-stack travel-planning workspace — multi-city itineraries, day-grouped activities, per-stop budgets with charts, packing checklists, journals, and a public community feed. Built end-to-end during the Odoo Hackathon with three teammates.
- What: plan a trip end-to-end — stops, dates, budget, packing, notes — and share it
- Hard part: shipping a real multi-domain app in a weekend with four people in flight
- Why it matters: proves the team can carve a Next.js app cleanly and ship under pressure
#the problem
Existing trip planners pick one lane. Itinerary apps don't track money. Budget apps don't know your trip has stops. Notes apps don't care about any of it. By the time you've stitched together three tools, you've stopped planning and started doing data entry.
Traveloop puts the lanes in one workspace, with a single trip object that owns its stops, activities, budget, packing, and journal. Same source of truth for the dashboard pie chart and the day-by-day itinerary view.
#architecture
Server Components first, Server Actions for mutations. No separate /api/v1/* REST surface to maintain — every mutation is a typed 'use server' function with Zod validation and a safeAction() error envelope. RBAC enforced at the layout level so non-admins literally 404 on /admin.
#key decisions
Server Actionsvs.separate REST API
Every mutation lives in src/server/actions/* as a typed 'use server' function.
No REST surface to keep in sync with the client, no parallel auth checks. Saved
us probably a day of plumbing in a weekend build.
Zod schemas as single source of truthvs.duplicate client + server validation
Schemas in src/lib/validations/* are reused by React Hook Form on the client
AND by safeAction() on the server. One file defines the shape; the form and
the mutation can't drift apart.
safeAction() error envelopevs.throwing exceptions across boundaries
Server actions return { ok: true, data } | { ok: false, error }. The UI is
if (!res.ok) toast.error(res.error) everywhere — one pattern, no try/catch
forests in components.
RBAC at layout, not middleware
(app)/layout.tsx runs auth() once for the protected group. (app)/admin/page.tsx
calls notFound() for non-admins so the route's existence is hidden. No
middleware to debug, no edge-runtime gotchas.
#walkthrough




#the stack
- frontend: Next.js 16, React 19, TS, Tailwind 4, shadcn/ui, Radix, lucide
- forms + charts: React Hook Form + Zod, Recharts, sonner toasts
- api: Server Actions + Route Handlers (image streaming + Auth.js callbacks)
- auth: Auth.js v5 credentials provider, JWT sessions, Prisma adapter
- data: Prisma 7 + PostgreSQL 16 — 12 tables,
User > Trip > Stop > Activity - infra: Vercel + NeonDB serverless Postgres
#what's next
- AI-assisted itinerary generation roadmap
- map view with Leaflet + OpenStreetMap
- currency conversion in the expense ledger
- offline-capable PWA shell
- real-time collaboration on trips (Liveblocks or CRDT)
- export trip as PDF