Skip to main content

System Architecture

End-to-end picture of how the KA26 platform fits together. Read this once and you'll have the mental model for everything else in the docs.

Why this shape?

For the rationale behind every architectural choice (monolith vs microservices, single region, single DB, no Kubernetes, etc.) see the dedicated Why monolith — architectural decision record page. That document is the canonical "why" — this page is the canonical "what."


High-level diagram (text)

┌──────────────────────────────────────────────────┐
│ END USERS │
│ 📱 Mobile (Android v17 APK, iOS planned later) │
│ 💻 Web browser (consumer, seller, doctor, admin) │
└──────────────────────────────┬───────────────────┘
│ HTTPS

┌──────────────────────────────────────────────────────────┐
│ GCP Load Balancer (34.8.146.193) │
│ (scheduled to be removed; see TODO) │
└──────────────────────────────┬───────────────────────────┘


┌──────────────────────────────────────────────────────────┐
│ Cloud Run service `ka26-marketplace` │
│ Region: us-central1 │
│ Next.js 15 (App Router) — single deploy serves: │
│ • Web app (consumer / seller / doctor / admin / dp) │
│ • API routes (`/api/*`) │
│ • Server-sent events (`/api/events/*`) │
└──┬─────────┬───────────┬─────────┬───────────┬──────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
Cloud SQL Cloud Storage Sentry Expo Push Google
`ka26` DB `ka26-... project API Workspace
(Postgres images` (errors) (FCM/APNs) SMTP
us-central1) (noreply@)


Recipients
(OTP, order
confirmation
emails)

Three independent web hosts

HostWhat it servesWhere it lives
ka26.shopThe application (web + API)GCP Cloud Run + Load Balancer
ka-26.comPublic landing page + APK downloadGitHub Pages (sidgk/ka26-website public repo)
docs.ka-26.comInternal Docusaurus docs portal (THIS site)Cloudflare Workers Static Assets (sidgk/ka26-docs private repo)

Codebase split

sidgk/ka26-marketplace (private — main app)

src/
├── app/ # Next.js App Router
│ ├── (consumer)/ # Consumer routes — shop, eats, reels, requests, profile
│ ├── (auth)/ # Login pages — consumer-auth, consumer-reset-password
│ ├── (public)/ # Public — restaurant pages, downloads, invite tokens
│ ├── seller/ # Seller dashboard — stores, orders, earnings, profile
│ ├── doctor/ # Doctor app (PWA) — prescriptions, broadcasts
│ ├── delivery_partner/ # Delivery rider app
│ ├── admin/ # Admin dashboard
│ └── api/ # All API routes (REST-style)
├── components/ # Shared React components
├── lib/ # Server + shared logic (auth, push, email, etc.)
├── contexts/ # React contexts (Auth, Language, Toast)
└── hooks/ # Custom React hooks (usePushSubscription, useEventStream)

prisma/
├── schema.prisma # Database models (~85 tables)
└── seed.ts # Initial seed data (categories, etc.)

mobile/ # React Native app (separate from Next.js)
├── app/ # Expo Router screens
├── src/lib/ # Mobile-specific helpers
├── assets/ # App icons, splash screens
└── android/ ios/ # Native projects (gitignored, regenerated by `expo prebuild`)

tests/ # Vitest test suites (1957 tests as of 2026-04-18)
├── components/ # Component integration tests (jsdom + RTL)
├── e2e-*-smoke.test.ts # Production smoke tests (run hourly)
└── *.test.ts # Backend file-shape regression tests

docs/ # Markdown docs mirrored to docs.ka-26.com
.github/workflows/ # CI/CD: deploy.yml, health-check.yml, mobile-tests.yml

sidgk/ka26-website (public — landing only)

Simple Next.js static site exporting to GitHub Pages. Hosts the APK download at /downloads/ka26-latest.apk.

sidgk/ka26-docs (private — internal docs)

Docusaurus 3 site. This portal you're reading. Auto-deploys to Cloudflare on push.


Data flow: a typical order

1. CONSUMER opens mobile app
→ mobile/src/lib/api.ts hits GET /api/stores
→ backend reads consumer's location from JWT, runs Postgres query
→ returns stores within proximity

2. CONSUMER taps a product → adds to cart
→ mobile writes to AsyncStorage (`ka26_store_cart_{storeId}`)
→ no backend call yet (offline-friendly)

3. CONSUMER taps "Place Order"
→ mobile POSTs /api/store-orders
→ backend:
a. Verifies consumer JWT via resolveConsumerId(req)
b. Validates fulfillment type, payment method
c. Forces paymentMethod=pay_at_pickup if pickup (single source of truth)
d. Creates StoreOrder + StoreOrderItem rows in Postgres
e. Creates SellerNotification row (in-app bell)
f. Sends order confirmation EMAIL to consumer (Google Workspace SMTP)
g. Calls pushToSeller() → looks up PushToken rows → sends to Expo Push API
h. Creates Contribution row (2% to charity)
i. Creates pendingEarnings if order was reel-attributed

4. SELLER receives push on their phone (via Firebase) AND/OR top toast in browser (via SSE)
→ Tap notification → opens /seller/orders?highlight={orderId}
→ Page scrolls to order, flashes blue ring
→ Seller taps "Accept"
→ mobile/web PATCHes /api/seller/stores/{id}/orders
→ backend:
a. Verifies seller cookie via getAuthenticatedSeller()
b. Validates status transition via getValidStatusTransitions()
c. Updates StoreOrder.status + acceptedAt timestamp
d. If delivery order: triggers assignDeliveryPartner()
e. Creates consumer Notification + calls pushToConsumer()

5. CONSUMER receives push: "Your order has been accepted!"
→ Mobile tap → routes via _layout.tsx switch on notifType="order_update"
→ Lands on /orders with order highlighted

Repeat similar flows for: status updates (accepted → preparing → ready), delivery dispatch, completion, earnings confirmation.


Authentication — 4 separate flows

User typeToken storageCookie/header nameVerify routeSession lifetime
ConsumerlocalStorageka26_consumer_token/api/auth/consumer-login30 days
SellerhttpOnly cookieseller_token/api/auth/login30 days
DoctorlocalStorage + Bearer headerka26_doctor_token/api/doctor/login30 days
Delivery PartnerlocalStorageka26_delivery_token/api/delivery-partner/login30 days

All four use JWT signed with JWT_SECRET env var. Each route handler verifies the token via the appropriate helper:

  • resolveConsumerId(req) in src/lib/consumer-auth.ts
  • getAuthenticatedSeller() in src/lib/auth.ts
  • Manual jwt.verify() for doctor/delivery routes

Cardinal rule (in bugs_fixed.md): NEVER mix auth contexts. Consumer endpoints use the consumer helper, seller endpoints use the seller helper. Cross-using these breaks the App Isolation rule.


Notification system (end-to-end)

The most-iterated subsystem in the codebase. See features/notifications for the full doc — quick recap here:

Backend dispatch (in src/lib/push.ts):

  • 4 helpers: pushToConsumer, pushToSeller, pushToDoctor, pushToConsumers (bulk)
  • Each looks up PushToken rows by user ID and appType="consumer"|"seller"|"doctor"
  • Dispatches in parallel to:
    1. Expo Push for mobile (platform != "web") → relays to FCM/APNs
    2. Web Push (VAPID) for browsers (platform == "web") → signed request to subscription endpoint

Critical rules (auto-loaded via bugs_fixed.md):

  1. Every event MUST do BOTH: a DB row (prisma.{xxx}Notification.create) AND a pushTo*() call. The DB row gives the bell; the push vibrates the phone.
  2. Every push payload MUST include data.notifType: this is what mobile's _layout.tsx switch routes on. 16 documented values exist (see push notif reference).
  3. IDs go in data directly, never parsed from path string. Mobile reads data.orderId, data.storeId, etc.

Tap routing:

  • Mobile: mobile/app/_layout.tsx switch statement on data.notifTyperouter.push(...)
  • Web: public/sw.js notificationclick handler → opens data.path URL

Seller-specific (added 2026-04-18):

  • <SellerPushSubscriber/> mounted in seller layout — auto-subscribes if permission granted, shows blue "Enable" banner otherwise
  • <SellerNotificationToast/> mounted in seller layout — top-of-screen toast for real-time SSE arrivals

Real-time updates — Server-Sent Events (SSE)

For "thing happened, refresh your view" updates without polling:

  • Stream endpoint: /api/events/seller (sellers), /api/events/consumer (consumers)
  • Hook: useEventStream({ url, enabled, onNotification }) in src/hooks/useEventStream.ts
  • Auto-reconnects on disconnect
  • Used by SellerNotificationToast, the bell-panel notification list, real-time order list refresh

This is on top of push notifications — push gets the user's attention; SSE keeps an open page in sync without reload.


Image storage

  • Hybrid: local filesystem for dev, GCS for production
  • Auto-detects: GCS used iff GCS_PROJECT_ID AND GCS_BUCKET_NAME are both set
  • Server-side compression: sharp resizes to 1200x1200 JPEG quality 85 before storage
  • Multiple images per product supported (up to 10), swipeable carousel on detail page
  • See src/lib/gcs.ts for the upload helper

Feature flags

Server-only env vars on Cloud Run, flippable without redeploy:

FlagDefaultPurpose
PAYMENTS_ONLINE_ENABLEDfalseGates UPI/online payments. Off until bank account opens.
REELS_COMMERCE_ENABLEDfalseGates product/store tagging on reels.

Frontend reads via /api/payments/upi-config (returns both flags + AND'd paymentEnabled). See Feature Flags for details.


Mobile app (React Native via Expo)

  • Codebase: mobile/ folder in same repo (separate from Next.js)
  • Calls the SAME /api/* endpoints as web
  • Uses same JWT auth, same database
  • Distribution: APK from https://ka-26.com/downloads/ka26-latest.apk (latest: v17)
  • Build procedure documented in Mobile build & release
  • Crash reporting: Sentry mobile project ka26-mobile (DSN in mobile/app.json)

Decision log — why we chose X over Y

DecisionWhy
Single Next.js app for everythingOne deploy, one domain. App isolation enforced via route groups + middleware.
Prisma 6 (NOT 7)v7 broke config format (requires prisma.config.ts instead of url in schema)
Expo Push (NOT raw Firebase)One SDK for Android + iOS; no google-services.json wrangling on every build
Cloudflare Workers Static Assets (NOT GitHub Pages) for docsGH Pages requires public repos; we wanted docs source private
GoDaddy DNS for ka26.shop (NOT migrating to Cloudflare)Works fine; no reason to migrate
Cloud Run scale-to-zero (NOT always-on instance)Pre-launch traffic = ~0 most of the day; saves money on cold periods
Per-store custom categories (NOT global master list)A flower shop has different categories than a pharmacy
File-shape tests as primary regression guardFast (~1s for 1957 tests), catches the bug class we hit most often
Component integration tests (added 2026-04-18)File-shape misses UX bugs; jsdom + RTL fills the gap
Hourly E2E smoke against productionCatches "the API is silently broken" within 1 hour vs. 6 days (the [object Object] bug)
Field-visit verification for sellers (NOT just docs)KA26 is hyperlocal (Gadag, ~250k pop); admin can physically visit each shop
2% contribution to charity on every transactionBrand differentiator vs. faceless marketplaces