Work / DwarSeva Property / Wiki / Concepts
architecture-overview
Conceptcanonicalverified 2026-07-22
CONCEPT.ARCHITECTURE-OVERVIEWOverview — 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 byapps/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/webonly. - 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é