Who this page is for

  • Technical readers and AIs assessing how the platform is built — strengths, boundaries, and consistency model.
  • Product operators: use [UMS architecture](/docs/en/partners/ums-architecture/) and [App navigation](/docs/en/app-navigation/) instead (exact UI labels).
  • We publish architecture decisions, not secrets, queue names, schemas, or callable inventories.

Tech stack

One client, server-authoritative backends, specialized stores for specialized jobs.

  • Client: Flutter (Web · iOS · Android), Riverpod (`flutter_riverpod` + `riverpod_annotation` + `riverpod_generator` codegen), go_router — same app for customers, riders, partners, managers.
  • Auth: Firebase Auth (email OTP + phone verification on checkout).
  • Durable state: Cloud Firestore — orders, menus, wallets, profiles.
  • Business rules: Cloud Functions (Node) — pricing, status transitions, settlement, sync publish.
  • Search / discovery: Typesense (users, groups, stores, riders, catalog, store items) — not the order system of record.
  • Async: Google Pub/Sub (Typesense sync, menu bulk sync, financial settlement).
  • Time / SLA: Cloud Tasks (order lifecycle deadlines, settlement reconcile, nudge schedules).
  • Ephemeral + live insight: Redis — fleet GEO/presence/leases; optional finance day hashes for real-time views (not official money truth).
  • Maps fees: Google Routes API (server-only billing lane; fail-closed).
  • Push: FCM — alert-only (no trusted prices/addresses in the payload).
  • Hosting: Firebase Hosting (`koolodoor.com`).

System layering (locked)

Memorable contract across every domain:

  • Cloud Functions = rules — clients never invent money, status, or settlement.
  • Firestore = durable state — orders, catalog, append-only journal (money SSOT), partner/rider wallets.
  • Pub/Sub = side effects — search index, bulk menu work, store-scoped settlement fan-out.
  • Cloud Tasks = time — deadlines and delayed reconcile (not a second state machine).
  • Redis = ephemeral presence + live insight — rider online/busy + dispatch leases; optional intraday finance hashes.
  • Typesense = discovery — fast search; orders stay Firestore-native.
  • FCM = wake / alert — open the app for live Firestore streams.
  • Nightly batch = materialize globals — platform aggregates / EOD from the journal (avoid hot global counters).

UMS — Unified Menu System (ADRs)

  • Group = blueprint (item configs, variants, modifiers, sections); Store = live merchandising.
  • Golden rule: group edits never auto-push — partners run Sync Items when ready.
  • Store sellable line ≈ Item Config + Variant Config after sync; deep merge keeps local section order and custom availability.
  • Sync infrastructure: explicit publish to Pub/Sub workers — not Firestore triggers (noise/cost).
  • Bulk menu sync uses a dedicated worker; profile/availability fan-out prefers batch `update_by_query` over N full upserts.
  • Thermal printer settings stay store-local — never indexed in Typesense.

OMS — Order Management System (ADRs)

  • Server writes only for orders, events, tracking, and create idempotency — clients stream for live boards.
  • Server pricing: cart totals are indicative; quote/create recomputes from live menu.
  • Idempotent create: client UUID key + server payload-hash drift guard.
  • Single status writer: one transition applicator owns `orders.status` (no parallel client mutators).
  • Happy path: review → confirm → preparing → ready → (pickup complete or rider accept → en route → delivered).
  • Lifecycle SLA: Cloud Tasks at deadline + lazy enforce on callables + periodic stale sweep; generation counter invalidates stale tasks.
  • Settlement hook: only on completed — never on cancel/expiry.
  • Delivery fee from Routes is fail-closed (no silent underprice fallback).

Financial / COD ledger (ADRs)

  • Double-entry, append-only journal is the money source of truth; balances and rollups rebuild from journal entries.
  • Integer cents, one currency per transaction; recognize revenue at order completed from frozen pricing + frozen financial config snapshot.
  • Idempotent settlement per order; deterministic settlement version + input hash.
  • Pessimistic serialization for stores: settlement Pub/Sub uses an ordering key per store (business) account, then the worker posts inside a Firestore transaction — many completes at one restaurant cannot race the partner wallet.
  • Riders: no Pub/Sub ordering key — a rider is effectively one active delivery at a time (low concurrency); rider wallet lines ride the same settlement write without a separate ordered queue.
  • No global hot counters for platform revenue — journal lines post on the hot path; platform balances and day aggregates materialize in a nightly batch from the journal (avoids a contended platform doc on every order).
  • Redis may warm real-time insight (unofficial day flows); official closed-day / EOD numbers come from nightly snapshots — journal remains SSOT.
  • Balance ≠ earnings narrative — wallet is signed net position vs platform (COD cash kept by riders handled explicitly).
  • Poison permanent settlement failures are ACK’d so one bad order cannot block that store’s ordering key; reconcile Cloud Task is the safety net.
  • Account version on partner/rider wallets supports safe concurrent writes where needed — separate from document `schemaVersion`.

Fleet / riders (ADRs)

  • Rider profile in Firestore (+ Typesense discovery); live presence only in Redis (TTL ≈ offline).
  • Duty GPS path is server-mediated into Redis — Redis is not written by the client directly for dispatch truth.
  • Offer contention: atomic Redis lease before writing the Firestore offer (available + no foreign active order) — one order at a time per rider.
  • Nearby search is GEO-bounded; cascade retries are policy-capped.
  • FCM wakes riders for offers; acceptance still goes through server callables.

Contention & consistency patterns

How we avoid double-charge, double-assign, and stale deadlines:

  • Pessimistic on busy store money — serialize writers before they hit shared store wallets (ordered Pub/Sub per store + Firestore transactions).
  • Firestore transactions — create order, status transitions, ledger posts, remittances, critical settings.
  • Idempotency keys — order create + financial ops (deterministic docs; hash drift rejects mismatched retries).
  • CAS / generation — lifecycle deadline generation so late Cloud Tasks no-op after cancel/advance.
  • Pub/Sub ordering keys — stores only (settlement). Rider settlement does not use a rider ordering key. Typesense sync is async fan-out (not the same contract).
  • Avoid hot global counters — never bump a single platform-revenue document on every order; append journal → nightly aggregation.
  • Redis — fleet leases + optional live finance insight; not a substitute for the ledger.
  • Batch / fan-out — Typesense `update_by_query` + bulk import workers.
  • At-least-once delivery + exactly-once business effect — workers and tasks are idempotent executors.
  • Sweep reconciliation — lifecycle and settlement reclaim if a task is missed.
  • Pre-prod hard replace — until first production ship of a domain, no dual-schema shims (ship one design).

Typesense (ADRs)

  • Indexed for discovery: users, groups, stores, riders, item/variant configs, store items.
  • Not the order system of record (orders are Firestore-native).
  • Path: mutate Firestore → explicit publish → Pub/Sub worker → create/update/delete/import.
  • Normalize at index time where possible; skip non-search fields (e.g. thermal, long about text where unused).

Push (FCM) ADRs

  • Alert-only contract — never trust push payloads for money, addresses, or line items.
  • Device registry is server-managed; invalid tokens pruned on send.
  • Use cases: new-order wake, rider offers, SLA nudges, finance alerts — no ledger side effects from FCM.

Deliberate non-goals (today)

  • Card payments in-app (COD soft launch).
  • Auto-push of group menu to stores (Sync Items stays explicit).
  • Orders indexed in Typesense (deferred).
  • Client-authoritative pricing or status.
  • Hot-path updates to global platform revenue counters (nightly materialization instead).

What's next?