trong.
Full-stack2026

VNMOVE

Logistics và du lịch biển đảo — đặt xe, vé tàu, khách sạn và tour trên một backend.

vnmove.ts
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,
};
Năm
2026
Khách hàng
VNMOVE
Vai trò
Lead engineer
Thời gian
Đang chạy

Kiến trúc

01

Backend

  • 29 module: ride, ferry, hotel, tour, wallet, promo, referral, dispute, enforcement
  • Hai tiến trình — vnmove-api chạy cluster cho HTTP và socket, vnmove-worker fork cho jobs
  • Log JSON winston đổ vào Loki/Promtail/Grafana trên VPS
02

Client

  • Zalo Mini App cho khách (React + zmp-ui)
  • Console vận hành (Ant Design Pro) và PWA cho đối tác khách sạn
  • App tài xế Flutter nhận điều phối realtime
03

Thanh toán & thông báo

  • Zalo Checkout SDK trong Mini App, ký chữ ký ở server
  • VNPay và MoMo trên web, COD ở cả hai, chung một máy trạng thái đơn
  • ZNS cho khách, FCM cho tài xế
04

Monorepo

  • Design token và type TypeScript tách thành package riêng
  • Turborepo + pnpm workspace cho bảy app
  • Goong Maps cho định tuyến và geocoding
Điểm nhấn 01
29
module backend trên một API chung
Điểm nhấn 02
4
app client: Mini App, admin, PWA, Flutter
Điểm nhấn 03
3
đường thanh toán, một máy trạng thái đơn

Ghi chú kỹ thuật

Đề bài

VNMOVE bán những chuyến ra đảo phía nam — và một chuyến đi không bao giờ là một sản phẩm. Khách đặt xe ra bến, vé tàu, khách sạn ngoài đảo, có khi thêm tour sáng hôm sau. Đặt bốn thứ đó ở bốn chỗ khác nhau là cách người ta bỏ cuộc giữa chừng. Đề bài là một luồng đặt chỗ cho tất cả, mà tài xế, khách sạn và bàn điều hành đều làm việc trên cùng một nguồn dữ liệu.

Cách làm

Một monorepo với một backend và bốn client: Zalo Mini App cho khách, console Ant Design Pro cho vận hành, PWA cho đối tác khách sạn, và app Flutter cho tài xế. Design token và type TypeScript dùng chung nằm trong package riêng, nên một enum thêm ở backend đi tới các client web dưới dạng lỗi biên dịch, chứ không phải một báo cáo lỗi.

Backend là modular monolith — 29 module: ride, ferry, hotel, tour, wallet, promo, referral, dispute, enforcement và những cái còn lại — chạy thành hai tiến trình. vnmove-api chạy cluster, giữ HTTP và các kết nối Socket.IO; vnmove-worker là một fork duy nhất ôm phần jobs. Điều phối chuyến không thể xếp hàng sau một tác vụ dựng báo cáo, và việc tách tiến trình biến điều đó thành ràng buộc cấu trúc thay vì trông vào kỷ luật.

Chỗ khó

Điều phối realtime. Một yêu cầu chuyến phải tới đúng nhóm tài xế, đúng thứ tự, và ngừng tới ngay khoảnh khắc có người nhận. Đó là một race condition có khách hàng ngồi nhìn, nên việc nhận chuyến được chốt trong Redis chứ không trong request handler, và tầng socket chỉ báo lại một quyết định đã xong rồi.

Thanh toán khác nhau theo từng mặt tiền. Trong Mini App, Zalo Checkout SDK là bắt buộc và mọi đơn phải được ký ở server; trên web, VNPay và MoMo chuyển hướng theo cách thông thường; COD có ở cả hai. Ba đường thanh toán, một máy trạng thái đơn — cả ba đều đổ về cùng một chỗ xác thực webhook và cùng một bước chuyển trạng thái idempotent.

Thông báo cũng tách y như vậy: ZNS cho khách, những người sống trong Zalo, và FCM cho tài xế, những người sống trong app với màn hình luôn sáng.

Nếu làm lại

Tôi sẽ viết package type dùng chung trước client đầu tiên thay vì sau client thứ hai. Một tuần ngồi hoà giải ba cách hiểu hơi khác nhau về trạng thái một booking là một tuần tôi đã được cảnh báo trước.

Hình ảnh

Booking flow in the Mini App
Dispatch console
Driver app