Wanderlist
City search · live place data · trips & sharing
A full-stack travel planner built on live place data — no static content, no manual curation. Search any city and get a curated split of headline attractions and local finds with photography layered in, then save to named trips, estimate a budget, view on a map, and share a read-only link. The Trip Quiz turns five questions about vibe, pace, and landscape into a ranked itinerary shortlist. Auth adds persistence; anonymous search works without an account.
What it does

- Type any city. Wanderlist geocodes it, pulls live place data from Geoapify, layers in Unsplash photography, and returns a curated split of headline attractions and hidden gems — no content team, no manual curation.
- Signed-in users get the full product: save places to named itineraries, see an estimated trip budget, view everything on a map, and share a read-only trip link with anyone.
- Not sure where to start? The Trip Quiz asks five questions — vibe, duration, exploration style, landscape, pace — and returns a ranked list of personalized itinerary matches. Or browse the Discover page for curated trips filterable by type and interest, and copy any of them to your account in one click.
- Anonymous users still get full search and recent history — the auth layer adds persistence, it doesn't gate the core experience.
What I built
- Server-side API pipeline — geocoding, dual place queries (top spots + hidden gems), retry logic, timeouts, and optional Unsplash enrichment all live in a single Route Handler. The client gets clean, normalized results or a clear error — never a partial state.
- Auth with row-level security — Supabase email/password auth with password reset flow. Every private route validates the user's JWT before touching the database; RLS policies enforce ownership at the data layer so the app is secure even if API logic has a gap.
- Trip Quiz & personalization — a 5-question flow that matches users to itineraries by vibe, duration, exploration style, landscape, and pace. Results are ranked by match score with destination photography, duration, and budget — then immediately copyable into the user's trips.
- Discover & curated itineraries — a pre-built library of trips, filterable by type (City, Country, Multi-Country) and interest tag (Culture, Food, Beach, and more). One click clones any itinerary into the user's account, ready to customize — no blank-slate friction.
- Itineraries, saves, and sharing — users create named trips, save places into them, set a budget, and generate shareable links. Shared trips are served read-only using the Supabase service role — no auth required for the recipient, no data leakage.
- Resilient UI states — every async path has a designed state: loading, success, empty, and error. The app never shows a spinner with no resolution, and search results return even if the history write fails — discovery is never blocked by a secondary operation.
- Production hardening — rate limiting on sensitive routes, input sanitization, JWT validation before any trusted Supabase operation. The kind of security setup you'd ship to production at a company, not bolted on after the fact.







Stack
- Next.js 14 (App Router) — client components for interactive search and maps, Route Handlers for the API layer.
- Supabase — auth, Postgres with RLS, and per-request clients scoped to the user's session for private data.
- Geoapify — geocoding, places, and autocomplete. All calls are server-side; the key never touches the client.
- Unsplash API — city hero and per-place photography when configured; the UI degrades gracefully to category-based fallback imagery when it's not.
- Leaflet — dynamically imported map on the trips page (no SSR), with place pins pulled from saved itinerary data.
- Netlify — deployed with a clean environment contract; portable to any Next.js-compatible host.
Data flow
- Search
Client posts a city string → server geocodes it → fires two parallel place queries (categories tuned for "top" vs "gems") → normalizes results → optionally enriches with Unsplash photos → writes to search history → returns JSON. If the history write fails, results still return. The client never sees a partial or inconsistent payload. - Private data
Every authenticated request carries the user's access token. Server-side route handlers callgetUser()before any database write; Supabase RLS policies enforce row ownership as a second layer. No auth = no data, at the DB level. - Shared trips
Sharing generates a unique token on the itinerary. The shared view uses the Supabase service role to serve read-only data by token — no session required, no private data exposed.

Design decisions
- Editorial aesthetic — serif headlines, warm cream palette, and generous photography keep the places front and center. The UI stays calm enough for long browsing sessions without competing with the content.
- Auth as enhancement, not a gate — anonymous users get the full search experience. Sign-in unlocks persistence. This is a deliberate product decision: make the value obvious before asking for commitment.
- Complexity lives server-side — retries, filtering, image enrichment, and normalization all happen in Route Handlers. The client owns state and presentation. Easier to test, easier to change, and the browser payload stays clean.
- Save flows are explicit — a modal prompts users to choose or create a trip before saving a place. No mystery heart button that saves somewhere unknown; every saved spot has a home.
Outcome
- Wanderlist is a complete product, not a prototype — search, auth, trips, maps, sharing, quiz, and security all ship in one codebase and work together end to end. Every surface has a designed state for every async outcome: loading, success, empty, and error.
- The security model is real: JWT validation at the route layer, RLS at the database layer, server-only API keys. Not a bolt-on — it was designed in from the start and holds up to the same scrutiny as a production system.
- The resilience decisions are deliberate — search results survive a failed history write, the app degrades gracefully when third-party APIs don't cooperate, and complexity stays on the server so the client stays predictable. That's production thinking, not happy-path thinking.
- Code lives here: github.com/meganleclair/wanderlist.