# Platform architecture (ADRs)

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.

- **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).

