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.
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
| Host | What it serves | Where it lives |
|---|---|---|
| ka26.shop | The application (web + API) | GCP Cloud Run + Load Balancer |
| ka-26.com | Public landing page + APK download | GitHub Pages (sidgk/ka26-website public repo) |
| docs.ka-26.com | Internal 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 type | Token storage | Cookie/header name | Verify route | Session lifetime |
|---|---|---|---|---|
| Consumer | localStorage | ka26_consumer_token | /api/auth/consumer-login | 30 days |
| Seller | httpOnly cookie | seller_token | /api/auth/login | 30 days |
| Doctor | localStorage + Bearer header | ka26_doctor_token | /api/doctor/login | 30 days |
| Delivery Partner | localStorage | ka26_delivery_token | /api/delivery-partner/login | 30 days |
All four use JWT signed with JWT_SECRET env var. Each route handler verifies the token via the appropriate helper:
resolveConsumerId(req)insrc/lib/consumer-auth.tsgetAuthenticatedSeller()insrc/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
PushTokenrows by user ID andappType="consumer"|"seller"|"doctor" - Dispatches in parallel to:
- Expo Push for mobile (
platform != "web") → relays to FCM/APNs - Web Push (VAPID) for browsers (
platform == "web") → signed request to subscription endpoint
- Expo Push for mobile (
Critical rules (auto-loaded via bugs_fixed.md):
- Every event MUST do BOTH: a DB row (
prisma.{xxx}Notification.create) AND apushTo*()call. The DB row gives the bell; the push vibrates the phone. - Every push payload MUST include
data.notifType: this is what mobile's_layout.tsxswitch routes on. 16 documented values exist (see push notif reference). - IDs go in
datadirectly, never parsed from path string. Mobile readsdata.orderId,data.storeId, etc.
Tap routing:
- Mobile:
mobile/app/_layout.tsxswitch statement ondata.notifType→router.push(...) - Web:
public/sw.jsnotificationclick handler → opensdata.pathURL
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 })insrc/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_IDANDGCS_BUCKET_NAMEare both set - Server-side compression:
sharpresizes to 1200x1200 JPEG quality 85 before storage - Multiple images per product supported (up to 10), swipeable carousel on detail page
- See
src/lib/gcs.tsfor the upload helper
Feature flags
Server-only env vars on Cloud Run, flippable without redeploy:
| Flag | Default | Purpose |
|---|---|---|
PAYMENTS_ONLINE_ENABLED | false | Gates UPI/online payments. Off until bank account opens. |
REELS_COMMERCE_ENABLED | false | Gates 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 inmobile/app.json)
Decision log — why we chose X over Y
| Decision | Why |
|---|---|
| Single Next.js app for everything | One 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 docs | GH 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 guard | Fast (~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 production | Catches "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 transaction | Brand differentiator vs. faceless marketplaces |
Related docs
- External Services — every external dependency
- Domains & Hosting — DNS + hosting per domain
- Testing & QA — test frameworks and approaches
- Monitoring & Observability — detection + alerting
- Authentication — deep dive on JWT flows
- Notifications — push system end-to-end
- Database schema — every Prisma model
- API reference — all backend endpoints