DealDirect
A production real-estate marketplace that removes the middleman - owners meet buyers directly, with AI-generated agreements and enterprise-grade security.
- Timeline
- 2025 - now
- Role
- Lead full-stack engineer
- Team
- Team
- Status
- live
Technology Stack
✦Overview
DealDirect is a production real-estate marketplace that removes the broker from the transaction entirely. Owners list directly, buyers reach them directly, and the paperwork that usually happens over WhatsApp and printouts is generated, signed and stored inside the platform.
The problem was never a shortage of listings - India's portals have millions. The problem is trust and friction: listing dumps run by broker networks, no verification, and a legal process that falls back to manual document exchange. DealDirect treats the marketplace as a system-of-record, not a bulletin board - every listing, lead, chat and agreement is a first-class, access-controlled entity.
✦Key Features
Direct owner ↔ buyer marketplace - 50,000+ property records with no broker layer - owners publish, buyers contact them directly. Separate buyer, admin and API deployments run independently.
Search that stays fast at scale - MongoDB compound indexes and aggregation pipelines drive filtered search across the full catalog - query latency down ~60% versus the naïve find-and-filter it replaced.
Real-time buyer ↔ owner chat - Socket.io channels let a buyer and owner negotiate live, with a rewards ecosystem layered on to drive engagement.
AI-assisted agreements - A requirements exchange becomes a client-ready agreement, rendered to PDF through a Puppeteer pipeline - roughly 90% less manual legal paperwork.
Hardened API gateway - RBAC + JWT + OAuth 2.0 behind one Express 5 gateway: Helmet CSP, CORS lockdown and rate limiting run before any request reaches a service.
✦Technical Implementation
01Architecture
A layered, service-oriented design: three purpose-built frontends - a Next.js SSR buyer app (40+ pages), a Vite SPA, and a 30+ page admin panel - all sit over a single Express 5 API gateway. Behind the gateway are eight domain services (auth, property, leads, agreements, chat, notifications, rewards, blog), backed by 34 Mongoose models, 21 controllers and 21 route files. Sentry is wired in for production error tracking.
02The gateway middleware chain
Security is enforced at the edge, once, for every route - not sprinkled per-handler. Each request runs the same ordered chain before it can touch a service, so an unauthenticated or abusive request is rejected long before business logic.
// every request clears the same gate before hitting a service
app.use(helmet()); // CSP + secure headers
app.use(cors({ origin: ALLOWED, credentials: true }));
app.use(rateLimit({ windowMs: 60_000, max: 100 }));
app.use(express.json({ limit: "1mb" }));
app.use("/api/agreements", requireAuth, agreementsRouter);
app.use("/api/rewards", requireAuth, rewardsRouter);
// requireAuth verifies the JWT and attaches req.user (RBAC) -
// downstream handlers assume an authenticated, scoped caller.03Data model
Eight domains, each owning its collections. Money paths (agreements, rewards) are isolated so ownership checks and atomic operations can be enforced without leaking across concerns.
| Domain | Collections | Purpose |
|---|---|---|
| Auth | user, session, role | JWT + OAuth 2.0, role-based access |
| Property | property, media, index | 50k+ listings, compound-indexed search |
| Leads | lead, inquiry | Buyer → owner contact + tracking |
| Agreements | agreement, template | AI drafts + Puppeteer PDF, idempotent |
| Chat | thread, message | Socket.io real-time negotiation |
| Rewards | wallet, ledger | Atomic credits, double-spend guarded |
04Search at scale
Filtered search runs as an aggregation pipeline against compound indexes rather than pulling documents into the app and filtering in JS. The index covers the common filter shape (city + type + price), so the hot path is served from the index.
// compound index backs the common filter shape
db.property.createIndex({ city: 1, type: 1, price: 1 });
const results = await Property.aggregate([
{ $match: { city, type, price: { $gte: min, $lte: max } } },
{ $sort: { verified: -1, updatedAt: -1 } },
{ $skip: page * SIZE },
{ $limit: SIZE },
]); // ~60% lower latency vs. find().filter() in app✦Project Structure
dealdirect/
├─ apps/
│ ├─ buyer/ # Next.js SSR storefront (40+ pages)
│ ├─ admin/ # 30+ page ops panel (Vite SPA)
│ └─ web/ # marketing / Vite SPA
├─ api/
│ ├─ gateway.ts # Express 5 — helmet, cors, rateLimit, auth
│ ├─ services/ # auth, property, leads, agreements,
│ │ # chat, notifications, rewards, blog
│ ├─ models/ # 34 Mongoose schemas
│ ├─ controllers/ # 21 controllers
│ └─ routes/ # 21 route files
└─ packages/
└─ shared/ # types, validators, config✦Design Decisions
Service-oriented, not a monolith
Eight bounded domains behind one gateway keep money paths (agreements, rewards) isolated from browsing paths. A bug in search can't touch a wallet. It also let the three frontends evolve independently against a stable API contract.
Security enforced at the gateway
Helmet, CORS, rate limiting and JWT/RBAC run once at the edge as an ordered chain, so every downstream handler can assume an authenticated, scoped caller. Defense-in-depth beats per-route guards you can forget to add.
Self-hosted PDF over a third-party API
Agreements are rendered with an in-house Puppeteer pipeline rather than a paid document API - no per-document cost, no customer legal data leaving our infrastructure, full control over the template.
✦Challenges
Rewards double-spend race
Two concurrent redemptions could both read the same wallet balance and both succeed. Fixed by moving the debit into an atomic, condition-guarded update (decrement only if balance ≥ cost) so the second request fails cleanly instead of overdrawing.
IDOR on booking payments
A booking-payment endpoint trusted the ID in the request without checking ownership - any authenticated user could pay against another's booking. The 150-file pre-launch audit caught it; every money path now re-checks req.user against the resource owner.
Non-idempotent agreements
A retried agreement request could generate duplicate documents. Added an idempotency key so the same logical request resolves to a single agreement, retries included.
Want the full walkthrough - architecture, decisions, war stories?
ask me about it

