Skip to content
Ritesh FirodiyaGet in touch

Work / DwarSeva Property / Wiki / Concepts

architecture-overview

Conceptcanonicalverified 2026-07-22

CONCEPT.ARCHITECTURE-OVERVIEW

Overview — Subsystems, data flow, boundaries

Subsystems

┌──────────────┐     ┌──────────────┐
│  apps/web    │     │ apps/mobile  │
│  Next.js 16  │     │  Expo 57     │
│  Vercel      │     │  EAS Updates │
└──────┬───────┘     └──────┬───────┘
       │                    │
       │  OpenAPI client (generated from packages/schema)
       │                    │
       ▼                    ▼
       ┌────────────────────────┐
       │      apps/api          │
       │  Hono on Bun           │
       │  Cloudflare Workers    │◄────── Razorpay webhooks
       │                        │◄────── WhatsApp webhooks
       │                        │◄────── Cron Triggers (30d takedown)
       └──┬────────┬────────┬───┘
          │        │        │
          │        │        └──► Cloudflare Queues (photo resize,
          │        │              reverse-image-search, notifications)
          │        │
          │        └──► Cloudflare R2 (photos, tax receipts, NoC)
          │
          ▼
       ┌────────────────────────┐
       │   Neon Postgres        │
       │   + PostGIS            │
       │   + Drizzle migrations │
       └────────────────────────┘

External services:
  - MSG91          — OTP SMS (India-cheap)
  - AiSensy        — WhatsApp Business
  - MapLibre GL    — map rendering (tiles: Protomaps/MapTiler)
  - Sentry         — errors + traces
  - PostHog        — product analytics
  - Better Stack   — logs + uptime

Who owns what

Subsystem Owns Does NOT own
apps/web Public landing SSR/SEO; role-gated routes for buyer/seller/admin; server actions for cookie-authed reads; render layer. Money mutations, wallet math, visibility mask, cron logic.
apps/mobile Buyer + seller UX; local optimistic UI hints (reconciled from server); native camera/photo picker; push registration. Any authoritative state; never trusts a locally-cached balance.
apps/api ALL writes; wallet ledger; visibility-mask projection on reads; polygon queries; SLA calculations; webhook receivers; queue producers; cron handlers. Rendering. Talks JSON only.
Postgres Source of truth; PostGIS geo queries; append-only ledger; RLS as defense-in-depth. Business logic. Row-shape only.
R2 Bytes: original + resized photos, tax receipts, NoC PDFs. Metadata (that lives in Postgres).
Queues Async work: photo resize, reverse-image-search flag, notification fan-out. Anything sync-critical.
Cron Triggers Only two things: (a) 30-day auto-takedown sweep; (b) refund-SLA sweep. Anything else — everything else is user-triggered or webhook-triggered.

Data flow — read path

Buyer requests GET /listings/:id
  → apps/web (server component) OR apps/mobile
  → apps/api handler
      → Drizzle query: fetch listing + version + photos + polygon
      → lib/mask.project(listing, buyerUnlocks)   ← strips hidden fields
      → return projected shape
  → client renders (no un-masking possible)

Data flow — write path (wallet-touching example: unlock)

Buyer taps "Unlock owner contact · ₹5"
  → apps/mobile → POST /unlocks { listing_id }
  → apps/api handler:
      begin transaction
        SELECT wallet FOR UPDATE where user_id=buyer
        IF balance < 500 (paise): rollback → return 402 "recharge required"
        INSERT INTO wallet_ledger (user_id, delta=-500, reason='unlock', ref_id=listing_id, balance_after=...)
        UPDATE wallet SET balance = balance - 500
        INSERT INTO unlocks (buyer_id, listing_id, price_paid=500)
        INSERT INTO audit_events (actor=buyer, action='unlock', target=listing_id)
      commit
      → enqueue notify(seller, "buyer X unlocked your listing")
      → return { contact: { name, phone, whatsapp } }
  → mobile shows unlocked detail

Key invariants (each enforced somewhere concrete)

Invariant Where enforced
sum(wallet_ledger.delta where user_id=X) == wallet.balance where user_id=X Postgres transaction + weekly consistency-check cron; alerts on drift.
listing.state transitions only via lib/state.transition(listing, event) apps/api — direct UPDATE to state is forbidden; caught by a Drizzle wrapper + DB trigger.
photo.grade = 'stolen_or_stock' blocks its listing from reaching live State-transition validator in lib/state.transition.
Only reviewer/admin roles mutate visibility_mask, reviewer_title, polygon_id RLS policy on the row + role check in the handler (defense-in-depth).
Every admin/reviewer write produces an audit_events row Handler-level middleware; missing audit rows are a CI check on integration tests.
Copy strings and prices come from packages/copy only ESLint rule + CATALOG-drift CI check.

Shared code (packages/)

  • packages/schema — Drizzle table definitions + Zod validators + generated OpenAPI TS client. Consumed by apps/web, apps/mobile, apps/api.
  • packages/copy — CATALOG-generated copy + amount tokens. Regenerated in CI; drift fails the build.
  • packages/ui — cross-platform primitives where feasible; per-platform siblings where not.
  • packages/config — env parsing (Zod), feature flags.

What we deliberately do NOT do

  • No client-side wallet math. Optimistic UI hints are cosmetic only; the source of truth is the server response.
  • No cross-service transactions. Money moves inside one Postgres transaction inside apps/api. Nothing distributed.
  • No caching of listing detail on client. Cheap enough to re-fetch; cache-invalidation bugs would leak un-masked data.
  • No admin API surface exposed to mobile. Admin lives on apps/web only.
  • No third-party auth SDK on client. Web uses server actions + HttpOnly cookies; mobile stores a bearer token from Supabase Auth (or equivalent) in SecureStore, and every API call carries it as Authorization: Bearer.

Every project of mine is written down like this.

Read the résumé