In late 2024 I got a contract to build a full-stack yacht rental marketplace for a Dubai-based client. The requirement was simple on paper: iOS app, Android app, web platform, captain management, real-time availability, payments, KYC verification — and Arabic RTL support throughout.
Budget: $18,500. Timeline: 3.5 months.
Here's what I built, the technical decisions I made, and what happened after launch.
The business problem
Yacht rental in Dubai is a high-trust, high-friction business. Customers want to book online but can't verify if a yacht is actually available. Double-bookings destroy reputation. Captains need a separate workflow. And if you're operating in the UAE, your platform must feel native in Arabic — not just translated, but properly right-to-left.
The client had been running everything through WhatsApp and spreadsheets. They were losing bookings to competitors with actual platforms.
Architecture overview
I chose a monorepo approach with Turborepo:
- Backend: Python 3.12 + FastAPI + PostgreSQL 15 + Redis + Celery
- Web: React 18 (TypeScript)
- Mobile: React Native (iOS + Android from one codebase)
- Infrastructure: AWS ECS Fargate + RDS Multi-AZ + ElastiCache + Terraform
- Payments: Stripe Connect with escrow logic
- KYC: Sumsub (supports UAE Emirates ID and passport)
- Maps: Mapbox
- Notifications: Firebase FCM
- Captain management: Telegram bot (aiogram 3)
- AI: GPT-4o-mini Route Advisor + ML price recommendation
The double-booking problem
This was the hardest technical challenge. Yachts can be booked from web, iOS, and Android simultaneously. A naive implementation creates race conditions.
The solution: Redis distributed lock per yacht + time slot. When a user starts checkout, a 10-minute lock is acquired. If payment fails or times out, the lock releases. No database-level conflicts possible.
async def acquire_booking_lock(yacht_id: int, slot: str, ttl: int = 600) -> bool:
key = f"booking_lock:{yacht_id}:{slot}"
return await redis.set(key, "1", nx=True, ex=ttl)
Result: 0 double-bookings across 312 trips in production.
Arabic RTL
Most developers treat RTL as an afterthought — flip direction: rtl and call it done. That breaks layouts in subtle ways.
What actually works:
- Use CSS logical properties (
margin-inline-startinstead ofmargin-left) - Tailwind has RTL variant support — use it from day one, not as a patch
- Test on real Arabic content, not placeholder text — Arabic words are longer than their English equivalents and break grids
Stripe Connect escrow
The client wanted funds held until trip completion — not paid directly to yacht owners on booking. This is Stripe Connect's "destination charges" model with manual transfer timing.
The flow: customer pays → funds held in platform account → trip confirmed by captain → automatic transfer to owner's Stripe account within 24h.
Results after 6 weeks in production
- $58,000 GMV (214,000 AED) across 312 trips
- 47 verified yachts, 38 verified captains onboarded
- 0 double-bookings
- 99.7% uptime, 94ms average API response
- 4.6⭐ App Store, 4.5⭐ Google Play
The client broke even on the development cost in week 4.
What I'd do differently
Terraform from day one. I added infrastructure-as-code midway through. Starting with it would have saved a full day of manual AWS configuration.
Separate captain app earlier. Captains ended up needing enough distinct functionality that a dedicated React Native app (not just the Telegram bot) would have been cleaner.
Load test the KYC webhook. Sumsub sends verification results as webhooks. Under load, the queue backed up. Added a dedicated Celery worker for KYC processing in week 2.
Stack summary
| Layer | Technology |
|---|---|
| Backend | Python 3.12, FastAPI, Celery |
| Database | PostgreSQL 15, Redis |
| Mobile | React Native (Turborepo) |
| Web | React 18, TypeScript |
| Infrastructure | AWS ECS Fargate, RDS, ElastiCache, Terraform |
| Payments | Stripe Connect |
| KYC | Sumsub |
| AI | GPT-4o-mini, custom ML pricing model |
If you're building something similar or have questions about any part of this stack — happy to discuss in the comments.
Top comments (0)