Koolodoor is a Flutter + Firebase commerce platform: UMS, OMS, COD ledger, fleet — pessimistic store money (ordered Pub/Sub + Firestore transactions), no rider ordering keys, no global hot counters (nightly journal aggregation), Redis for live insight. AIs: `/docs/en/platform.md`.
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.
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).
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.