A client hired me to build their product from scratch. No team. No legacy code. Just a brief, trust, and the responsibility of making every technical decision on my own.
Waliki is a mobile travel planner. Browse destinations, discover places through a filtered catalog, and build day-by-day itineraries you can share with whoever you’re dragging along on the trip.
I designed the UI, chose the stack, defined the architecture, and shipped it to production: mobile app in React Native (Expo), admin panel in Next.js, Supabase as backend, shared package for types and Zod validations, all wired through Turborepo.

The app works offline. Actually works.
You can edit your entire itinerary in airplane mode. Add places, reorder your day, delete that restaurant you changed your mind about at 2am. The UI doesn’t skip a beat because every change hits the local cache first, optimistically. When you get wifi back, TanStack Query’s onlineManager replays the queued mutations against Supabase. A small banner at the bottom tells you “3 changes pending” so you’re never guessing. Tested extensively on actual flights where I should have been sleeping.
The tricky part was temporary IDs. You create an activity offline, it gets a temp_ UUID. The server assigns the real one on sync. The cache reconciles on settle. Took a few iterations to get right, and a few more to stop dreaming about race conditions.

Catalog caching that doesn’t annoy people
Catalog data (destinations, places, categories) caches for 24 hours. But I didn’t want users seeing yesterday’s content if something changed. So on every app launch and every foreground resume, one RPC call (get_catalog_versions) returns max(updated_at) per table. Compare with local versions, invalidate only the queries for tables that actually changed. Most sessions: zero invalidations. The app feels instant and the data stays fresh.
Localization without the boilerplate
Content in Supabase is { es: "...", en: "..." }. The obvious approach is a getLocalizedText() call on every field in every component. I tried that for about a day before I got tired of typing it. So I wrote a proxy layer that wraps each service. Components just receive strings, already in the right language. Switch locale, cache invalidates, done. Laziness is a virtue when it produces better architecture.
Image pipeline with precomputed variants
When an admin uploads a photo, the dashboard generates sized variants (96px avatar, 800px card, 1200px detail) and stores them in Cloudflare R2. On the app side, AutoImage picks the right file based on a preset prop. No one downloads a 4000px original for a thumbnail. Your data plan can thank me later. Blurhash placeholders fill the gap while loading.
What else is in there
- Subscription system with RevenueCat (monthly/annual plans, trial periods, billing issue handling)
- Collaborative trips with invite links and shared itineraries
- Flight lookup for adding travel legs
- Cross-platform map abstraction (Apple Maps on iOS, Google Maps on Android, arguments about which is better: everywhere)
- Admin dashboard with full CRUD for the content catalog, image processing pipeline included
- Onboarding flow with destination selection and personalized recommendations