VNMOVE
Logistics and island travel — rides, ferries, hotels and tours on one backend.
export const project = {
type: "fullstack",
stack: [
"Express",
"MongoDB",
"Redis",
"Socket.IO",
"React",
"zmp-ui",
"Ant Design Pro",
"Flutter",
"Goong Maps",
"Zalo Checkout SDK",
"VNPay / MoMo",
"Zalo ZNS",
"Firebase Cloud Messaging",
"Turborepo + pnpm",
],
shipped: true,
};- Year
- 2026
- Client
- VNMOVE
- Role
- Lead engineer
- Duration
- Ongoing
Architecture
Technical notes
The brief
VNMOVE sells trips to the islands off Vietnam's southern coast — and a trip is never one product. A customer books a car to the pier, a ferry ticket, a hotel on the island, maybe a tour the next morning. Booking those in four different places is how people give up halfway. The brief was one booking flow across all of it, with the driver, the hotel and the operations desk all working from the same data.
The approach
A monorepo with one backend and four clients: a Zalo Mini App for travellers, an Ant Design Pro console for operations, a PWA for hotel partners, and a Flutter app for drivers. Shared design tokens and shared TypeScript types sit in their own packages, so an enum added to the backend reaches the web clients as a compile error rather than a bug report.
The backend is a modular monolith — 29 modules: ride, ferry, hotel, tour, wallet, promo, referral, dispute, enforcement and the rest — deployed as two processes. vnmove-api runs clustered and holds HTTP plus the Socket.IO connections; vnmove-worker is a single fork that owns the jobs. Dispatch cannot afford to queue behind a report generation, and the split makes that structural rather than a matter of discipline.
The harder parts
Realtime dispatch. A ride request has to reach the right drivers, in the right order, and stop reaching them the moment someone accepts. That is a race condition with a customer watching it, so acceptance is settled in Redis rather than in the request handler, and the socket layer only ever reports a decision that has already been made.
Payments differ by surface. Inside the Mini App, Zalo's Checkout SDK is mandatory and every order must be signed server-side; on the web, VNPay and MoMo redirect the usual way; COD exists on both. Three payment paths, one order state machine — every one of them lands on the same webhook verification and the same idempotent transition.
Notifications split the same way: ZNS to customers, who live in Zalo, and FCM to drivers, who live in the app with the screen on.
What I'd do differently
I'd have written the shared types package before the first client instead of after the second. The week spent reconciling three slightly different ideas of what a booking status meant was a week I had already been warned about.