.## Inspiration

Long-distance travel in Vietnam is broken. Ride-hailing past 100 km gets eye-watering. Traditional car rental drowns you in deposits, KYC, and the unspoken rule: return the car to the same branch you picked it up from. So nobody actually drives one-way.

Meanwhile Tasco — one of the largest auto groups in Vietnam — sits on a network of showrooms across HCM, Vung Tau, and Da Lat that already have parking, staff, and a steady trickle of foot traffic. We kept asking: what if those showrooms were the network? What if the car you drove to Da Lat just stayed in Da Lat for the next person?

That question became Driftway — an inter-city car-sharing platform built on top of Tasco's showroom network as the pickup and drop-off stations.

## What it does

Driftway is a pay-per-trip car-sharing network for inter-city travel in Vietnam:

  • Book one-way. Pick a car in HCM, drop it in Da Lat. No round-trip required.
  • Dynamic pricing with rebalancing. A live pricing engine discounts trips that move cars toward showrooms with low supply, and surcharges trips that drain hot cities. The fleet self-balances over time.
  • Showroom hand-off. Walk into any Tasco showroom, do a 6-angle photo inspection on the iPhone app, scan the QR code on the in-car iPad, and you're driving.
  • AI return inspection. When you arrive, take 6 photos again. A computer-vision pipeline compares before/after and flags damage automatically — no human haggling.
  • Hold mode. Going hiking for 4 hours mid-trip? Put the car on hold instead of returning it. We charge an hourly fee with a daily cap.

The app stack is iPhone (SwiftUI) for the rider, an iPad web app (React/Vite) as the in-car dashboard, an admin web (Next.js) for the ops team, and a FastAPI backend that holds all the business logic.

## How we built it

We ran this as a 10-day sprint end-to-end. The architecture is intentionally boring so the interesting parts can be sharp:

  • Backend — FastAPI + SQLAlchemy + PostgreSQL/PostGIS. Single DB, no Redis, no microservices. Routers split by domain (pricing, bookings, pairing, inspect, admin, dev).
  • Pricing engine — A 9-step deterministic algorithm in pure Python: base + class multiplier + distance + seasonal + demand-signal + rebalancing − Pro discount + unlock fee + hold fee. Tests-first: 90 %+ coverage on pricing_engine.py was the gate before we wrote a single line of iPhone code.
  • Pricing math — every quote is integer VND, no floats anywhere near money. The rebalancing discount and overflow surcharge read from the same DestinationSignal and are mutually exclusive (DB constraint, not just code):

    $$
    \text{final} = \max!\big(0,\; \text{base} + \text{unlock} + \text{overflow} - \text{rebal} \cdot (1 - d_{\text{pro}})\big) + \text{hold} $$

  • iPhone app — SwiftUI + @Observable state, Mapbox for maps, AVFoundation for the 6-angle camera, KeychainAccess for the JWT, Lottie + Pow for the celebratory bits. Bundled fallback photos ship in Resources/DemoPhotos/ so a denied camera permission never breaks the demo.

  • iPad web — React + Vite + Mapbox GL JS. The idle screen opens a WebSocket on mount and falls back to REST polling every 2 seconds if the socket fails within 3s — both paths must work end-to-end before we'll call it done.

  • AI services — Qwen-plus via Dashscope for the pricing-explanation generator (800 ms timeout) and the search re-ranker (1.5 s timeout). Every LLM call has a deterministic Vietnamese fallback. CV damage detection is a pre-computed scenario (clean / minor / major) bound to demo booking codes — honest about what's real and what's mocked.

  • Pairing — A 5-minute single-use token. iPhone POSTs to claim, iPad subscribes via WebSocket or polling. The token round-trip is the only step the user actually performs to "unlock" the car.

  • Deployment — Hostinger VPS, Caddy + Docker, three subdomains: driftway.nullshift.sh, ipad.driftway.nullshift.sh, api.driftway.nullshift.sh.

## Challenges we ran into

  • Pricing edge cases. Mutual exclusion between rebalancing discount and overflow surcharge is the kind of bug that's invisible until your CFO sees a negative invoice. We caught it by writing 21 imbalance scenarios as parametrized tests before the engine existed, and by enforcing it at the DB level with chk_rebal_overflow_exclusive.
  • Pairing on flaky networks. We tested over real LTE, not just localhost — and discovered that some carriers were silently dropping the WebSocket. The polling fallback wasn't a "nice to have", it was the difference between a working demo and a frozen QR code.
  • Camera permissions. Real demos happen on borrowed devices. Bundled inspection photos that the app falls back to silently (not "permission denied" toast) saved at least one rehearsal.
  • LLM latency. Qwen pricing explanations occasionally spike past 1 s. Hard timeout + deterministic Vietnamese template fallback meant the user never sees a spinner — they see a slightly less
    poetic explanation.
  • Demo determinism. A live demo that depends on now() is a trap. Every demo booking is regenerated with NOW()-relative timestamps via POST /v1/dev/reset-demo before each rehearsal.

## Accomplishments we're proud of

  • A pricing engine you could actually run a business on. Not "AI sets the price" hand-waving — a fully deterministic, 9-step, 90 %-tested algorithm that explains every VND in plain Vietnamese.
  • Three frontends, one backend, ten days. iOS, iPad web, admin web, all talking to the same FastAPI, all deployed.
  • Honest AI. Three real AI features (pricing explainer, search ranker, demand predictor) and one explicitly mocked one (CV damage). We don't pretend the mock is real.
  • Failure-resistant demo. WebSocket falls back to polling. Camera falls back to bundled photos. LLM falls back to templates. We rehearsed turning each one off.

## What we learned

  • Tests are the spec. Writing the pricing tests first turned a fuzzy product requirement ("rebalancing should encourage one-way trips") into 21 concrete cases. The code wrote itself after that.
  • Boring infra wins. Single Postgres, no Redis, no message queue, no S3 — local disk for photos, Caddy for TLS. Every hour we didn't spend on infra plumbing went into pricing edge cases.
  • Vietnamese-first means Vietnamese-first. Every UI string, every Qwen prompt, every error message. Anglicizing later is harder than starting in Vietnamese.
  • Showroom networks are the moat. The pricing engine is copyable. The brand is copyable. A nationwide network of showrooms with staff and parking is not.

## What's next

  • Real CV. Replace the 3-scenario mock with a fine-tuned damage-detection model (probably a small ViT) trained on actual showroom inspection photos.
  • Prophet-based demand forecasting. The seasonal lookup table is fine for a demo; production wants real time-series forecasting per showroom.
  • Multi-leg trips. HCM → Da Lat → Vung Tau as a single booking with hold periods between legs.
  • EV-aware routing. Match cars to charger availability at the destination showroom.
  • Pilot. One real corridor (HCM ↔ Vung Tau) with 5 cars and 3 showrooms is the smallest experiment that proves utilization. That's the next milestone.

Built With

Share this project:

Updates