1. Client situation
A mid-size tour operator in Krabi province handling 50-80 customers/day, running snorkeling, sunset cruise, and island hopping (4 islands, Hong Islands, Phi Phi). The team is 1 owner + 3 admins + ~15 partner guides and boat captains.
Pain points found during discovery:
- LINE booking overload: 100+ inquiry and booking messages/day on LINE OA, and 3 admins working in rotation couldn’t keep up between 9-11 AM.
- Excel scheduling: Ferry schedules in Google Sheets shared across the team, no locking mechanism — 2-3 double-bookings per week in high season.
- No real-time inventory: Snorkeling and island hopping share boats on some days, but Excel doesn’t model that relationship — admins had to track it from memory.
- Bank transfer + slips: Customers transfer to SCB then forward the slip via LINE. Admins opened the bank app to verify each one, 5-10 minutes per slip.
- Cancellation chaos: Cancellations/reschedules written into notebooks, never synced to Excel. Several complaints resulted.
- Tax/reporting headache: Compiling month-end totals for accounting took 2 full days.
The client had tried 2 off-the-shelf solutions:
- TourCMS: UX is admin-heavy and Western-style; Thai staff couldn’t operate it fluently, and it doesn’t support PromptPay.
- Bokun: ~USD 800/month + per-booking commission — over budget — and still needed workarounds for multi-tour inventory.
Clear targets: cut admin time from 6 hours/day → 1.5 hours, and lock down overbooking to 0.
2. Why generic solutions don’t work
- PromptPay is a dealbreaker: Off-the-shelf SaaS like TourCMS, Rezdy supports only credit cards and Stripe. 80% of Thai customers pay via PromptPay/bank transfer. The system needs to generate dynamic per-booking QR codes and verify slips automatically.
- Multi-tour inventory is layered: Multiple tours share resources (single boat, single guide, seats on the same larger boat) — generic systems that model tours as standalone products can’t represent that. You need a schema that separates “resource” from “tour product”.
- Thai-language UI the admins actually understand: The admin team is 35-50 years old, lives in LINE, and has never used an English CRM/SaaS dashboard. They need a “LINE-like” UI — thread-based, Thai everywhere, no jargon.
3. Our approach
Stack chosen:
- Next.js 14 (App Router) — admin dashboard + customer booking page in one codebase
- tRPC + Prisma + PostgreSQL — type-safe end-to-end, easy schema migrations
- Cloudflare Workers — edge functions for the LINE webhook + slip verification queue
- PromptPay QR library (
promptpay-qr) — dynamic QR generation per booking - Google Cloud Vision API — OCR for Thai bank slips
- LINE Messaging API + Webhook — chatbot and confirmation push
ROI we presented to the owner:
| Option | Year 1 | Year 2 | Year 3 |
|---|---|---|---|
| SaaS (Bokun ~USD 800/mo) | ~USD 9,600 | ~USD 9,600 | ~USD 9,600 |
| Custom (one-time ~USD 9,500 + hosting USD 200/mo) | ~USD 11,900 | ~USD 2,400 | ~USD 2,400 |
Break-even in month 8 — after that, custom is ~75% cheaper per year and becomes the company’s owned asset.
The 4 pillars:
- Real-time inventory engine — WebSocket broadcast on every new booking, every browser session sees seat counts update instantly + DB-level locking prevents race conditions.
- PromptPay automation — Generate QR per booking, OCR slip verification (Google Vision reads account number + amount + timestamp).
- LINE webhook chatbot — Answer FAQs with clear patterns (price, departure time, meeting point); push complex bookings to admin.
- Mobile-first admin dashboard — Admins are on phones 70% of the time, so UI design started at the mobile breakpoint.
Discovery process: Before writing any code we asked for a full shadow day (8 AM-6 PM) sitting next to each admin and noting every pain point. We found admins spent 40% of their time “looking up old bookings” — which led us to add search-first design to the dashboard.
4. Week-by-week
Week 1-2: Discovery & architecture
- 1-day shadow with admin team
- Stakeholder interviews with owner + admin lead
- Database schema design — 15 tables (bookings, tours, resources, schedule_slots, customers, payments, slip_verifications, line_messages, etc.)
- User-journey mapping across 4 personas: walk-in customer, repeat customer, agency, owner
Week 3-5: Backend core
- Next.js + tRPC scaffold + Prisma migrations
- Authentication (admin + agency tier)
- Booking domain logic + inventory locking
- Payment integration: SCB Easy Net API + PromptPay QR generator
- Critical unit tests: race-condition booking, inventory overflow
Week 6-8: Frontend & realtime
- Admin dashboard UI (Thai-first, mobile breakpoint primary)
- Customer-facing booking flow (3-step: choose tour → enter info → pay)
- Real-time inventory via WebSocket (Cloudflare Durable Objects)
- Calendar view + drag-to-reschedule
Week 9-10: LINE & OCR
- LINE webhook setup + intent classification (rule-based + GPT fallback)
- FAQ chatbot for 12 most-frequent questions
- Booking confirmation push message with QR code
- OCR slip verification pipeline + manual review queue
Week 11: Migration & UAT
- Excel import tool (ingested 6 months of legacy bookings, ~2,400 records)
- UAT with 3 admins — found 8 critical bugs (3 high, 5 medium), all fixed in 4 days
- Initial penetration test (SQL injection, auth bypass)
Week 12: Launch
- Soft launch: 50% traffic via feature flag for 3 days
- 4 admin training sessions (1 hour each) entirely in Thai
- Full launch + monitoring dashboard for first 24 hours
- Knowledge transfer doc + runbook
5. Problems encountered
-
OCR accuracy only 87%: SCB/Krungthai standard-format slips read well, but mobile banking screenshots (especially older Kbank app versions) had low confidence. Fixed by adding a manual review queue for slips below 90% confidence — admins 1-click confirm in ~15 seconds/slip (down from 5-10 minutes).
-
Scope creep mid-project: In week 7 the owner asked for an agency tier pricing feature (commission for travel agencies sending customers). Not in original scope. We estimated +1 week, signed a clear contract amendment (+THB 25,000, timeline +5 days). The owner approved before we started writing — important not to do it for free, or you crack the door open for more scope creep.
-
Slow admin adoption: First 2 weeks, 1 admin was still calling support 3-4 times/day. Fixed with 8 Thai-language video tutorials (2-3 minutes each) + 1 month of hand-holding (4-hour response SLA). By week 4, calls dropped to 0-1 per week.
6. Post-launch results + ongoing
Month 1 results:
- Admin time: 6 hours/day → 2 hours/day (-67%)
- Double-booking: 2-3/week → 0
- LINE response time: 4 hours → 8 minutes (chatbot handles 60% of queries, freeing admin to focus on bookings)
- Slip verification: 5-10 minutes/slip → 15 seconds/slip (OCR auto-confirms 87%)
- Month-end reporting: 2 days → 30 minutes (dashboard export)
Lessons learned:
- Thai tour operators want tools that “feel like LINE” — Initially we designed a clean Notion-like UI; admins said “it looks too plain, I can’t tell what’s important”. We pivoted to card-heavy + familiar emoji and adoption improved immediately.
- Shadow-days are the highest-ROI investment — 1 day sitting with admins delivered more insight than 5 requirement-gathering meetings.
- Manual review queues aren’t a failure — Users actually liked them because they felt “in control”. 100% automation isn’t always the target.
Ongoing engagement: We continue on retainer at 8 hours/month covering bug fixes, small features (last quarter we added a voucher system + multilingual booking page EN/CN), security patches, and monitoring. Six months post-launch we’re discussing phase 2 — adding a supplier portal so boat captains can check customers in via mobile.