A full-stack web application for property listings, viewings, messaging, and payments—built for the Kenyan market. Zeni provides verified listings, map-first search, real-time chat, viewing fees, and a separate Pay portal for secure transactions.
- Description
- Key Features
- Recent Improvements
- Technologies Used
- Installation
- Basic Usage
- Hybrid (Capacitor)
- Features Overview
- Configuration
- Troubleshooting
- Contributing
- License
- Code Structure
Zeni is a MERN-stack (MongoDB, Express, React, Node.js) real estate platform that connects buyers, renters, agents, and admins in Kenya. The app includes:
- Public: Marketing landing page, public map, listing details, login/register, password reset
- User portal: Explore listings, save searches, request viewings, message agents, profile & KYC, refund requests
- Agent portal: Dashboard, listings management, leads, viewings, analytics, verification, boost
- Admin portal: User/listings moderation, verification queue (agents, listings, KYC, business), reports, audit, network access
- Pay portal: Secure payment console (viewing fees, payouts) with separate JWT audience
- Landing page — Zeni branding, scroll-spy nav, orange accent styling, newsletter signup, FAQ, contact
- Map-first search — Public and in-app maps (Leaflet), draw zones, filter by price/type
- Fuzzy search — Typo-tolerant location suggestions (fuse.js); "Kilimnai" → Kilimani; commute-hub suggestions ("Near CBD", "Near Westlands")
- Verified listings — Admin-approved listings and agents; 3-state verification badge (Verified / Unverified / Pending)
- Image gallery + lightbox — Multi-image scroll, thumbnail strip, full-screen lightbox with keyboard navigation (←/→/Esc)
- WhatsApp CTA — Deep-link "Chat on WhatsApp" on listing detail + sticky mobile bar
- Neighbourhood trust block — Safety / water reliability / power / parking / service charge notes for 11 Kenya areas (Kilimani, Westlands, Karen, Lavington, Runda, Parklands, Upper Hill, CBD, Kasarani, Ruaka, Ngong Road)
- Report listing — Modal with category picker (Scam, Bait pricing, Duplicate, etc.) wired to backend API
- Real-time messaging — Socket.IO chat with agents; Zeni Support and listing context in threads
- Viewing requests — Request viewings; optional viewing fee (e.g. 500 KES) held until completion
- Recently viewed — Last 6 viewed listings persisted to localStorage; horizontal strip above search results
- Compare listings — Select 2–3 listings → side-by-side comparison table (price, beds, baths, area, location, verified)
- Save search — Save current filters/query; accessible from Saved section
- Neighbourhood SEO pages —
/rent/kilimani,/buy/westlands, etc. with descriptions, live listings, FAQs, and JSON-LD - Pay portal — Stripe and bank transfer; viewing fees, refund requests (Zeni Shield), invoices
- Agent verification — EARB, ID, business docs; admin moderation queue
- KYC (users) — Profile → Verify identity; admin approves/rejects
- PWA — Web app manifest and service worker; "Add to Home Screen"; offline cached saved listings
- SEO — Open Graph, Twitter Card meta, JSON-LD RealEstateListing structured data per listing page
- LQIP images — Blur-up placeholders via Cloudinary 32px thumbnails (eliminates CLS flash)
- Analytics (optional) — Event tracking endpoint for search, viewing, payment, refund events
| Improvement | Detail |
|---|---|
| Kenyan fallback photos | Curated Nairobi/Westlands/Karen exteriors/interiors are used as placeholders while listings load or when agents haven’t uploaded media yet. Agent-uploaded photos (after admin approval) automatically override the fallbacks. |
| Improvement | Detail |
|---|---|
| Fuzzy matching | fuse.js on location suggestions — typo-tolerant (threshold 0.45) |
| Commute hubs | "Near CBD", "Near Westlands", "Near Upper Hill", etc. in suggestion dropdown |
| KES presets | 6 price bands matching Kenya rental reality: <30K / 30–50K / 50–80K / 80–120K / 120–200K / 200K+ |
| Save search | Bookmark icon in toolbar → named saves stored via API |
| Rich filter callback | onFilter(filters) passes actual values to consumers |
| Improvement | Detail |
|---|---|
| Image gallery | Thumbnail strip, animated slide transitions, LQIP blur placeholders |
| Lightbox | Full-screen, ←/→/Escape keyboard nav, dimmed backdrop |
| WhatsApp CTA | Green button → wa.me/{phone}?text=... deep-link (sidebar + sticky mobile bar) |
| Trust strip | 3-state: Verified (green) / Unverified (amber) / Pending + "Updated X days ago" |
| Neighbourhood trust | Safety/water/power/parking/service-charge notes for 11 Kenya areas |
| Report modal | Category picker + optional message → POST /api/listings/:id/report |
| Share button | navigator.share() → WhatsApp fallback on desktop |
| Signup UX | Clear messaging for duplicate accounts (409/USER_EXISTS) and validation hints so users know to log in instead of re-registering. |
| Improvement | Detail |
|---|---|
| Recently viewed | localStorage hook (useRecentlyViewed), shown as horizontal chip strip on Explore |
| Compare | Toggle on each card (max 3), side-by-side table modal |
| Improvement | Detail |
|---|---|
| Twitter Cards | twitter:card, twitter:title, twitter:image — critical for WA link previews |
| JSON-LD | RealEstateListing structured data injected per listing page |
| Neighbourhood pages | /rent/:neighborhood and /buy/:neighborhood with live listings, FAQs, schema |
| Layer | Technologies |
|---|---|
| Frontend | React 18, TypeScript, Vite, React Router 6, Tailwind CSS, GSAP, Lenis, Framer Motion, Leaflet (react-leaflet), Socket.IO client, Stripe.js, Zod, fuse.js |
| Backend | Node.js, Express, TypeScript, MongoDB (Mongoose), JWT, Socket.IO, Stripe, Nodemailer, bcryptjs, Helmet, express-rate-limit |
| Media | Cloudinary (WebP/AVIF transforms, LQIP auto, responsive widths) |
| Dev/Test | Vitest, Playwright, ESLint, Prettier, Husky, lint-staged |
| Deploy | Docker (optional), PM2, Vite build (static + gzip/brotli), Vercel |
- Node.js 18+ (LTS recommended)
- npm 9+ (or yarn/pnpm)
- MongoDB 6+ (local or remote; e.g. MongoDB Atlas)
- Git
-
Clone the repository
git clone <repository-url> cd ZENI
-
Install frontend dependencies
npm install
-
Configure frontend environment
copy .env.example .env # Windows # cp .env.example .env # macOS / Linux
Edit
.envand set at least:VITE_API_BASE_URL(e.g./apifor same-origin)VITE_DEV_API_TARGET(e.g.http://localhost:4000) if using Vite proxy
-
Install and configure the server
cd server npm install copy .env.example .env # Windows cd ..
In
server/.envset:PORT(e.g.4000)MONGO_URI(your MongoDB connection string)JWT_SECRET(strong random string for production)CORS_ORIGIN(e.g.http://localhost:5173,capacitor://localhostfor dev/native shells)
-
Seed demo data (optional)
npm run seed
Default seed password:
SEED_PASSWORDorChangeMe123!(see server seed script).
npm run dev:full- Frontend: http://localhost:5173
- Backend API: http://localhost:4000
Terminal 1 (backend):
npm run dev:serverTerminal 2 (frontend):
npm run devnpm run buildOutput: dist/ (static assets). Serve with any static host; API must be deployed separately (e.g. npm run build:server then PM2 or Docker).
| Command | Description |
|---|---|
npm run lint |
Frontend ESLint |
npm run build |
Frontend production build |
npm run test |
Frontend unit tests (Vitest) |
npm run test:e2e |
E2E tests (Playwright) |
npm run lint:server |
Backend ESLint |
npm run build:server |
Backend TypeScript build |
npm run test:server |
Backend Jest tests |
Capacitor 8 wraps the existing Vite build so the same codebase ships to Android and iOS. Native platform folders are generated locally and gitignored by default.
- Android Studio (2024.3+), Android SDK Platform 34+, Java 17
- Xcode 15+ and CocoaPods (for iOS)
- Backend reachable over HTTPS (or
http://10.0.2.2to hit a host-running API from the Android emulator)
- Set
VITE_MOBILE_API_BASE_URLto a full HTTPS origin. Relative/apiwill not work once the app runs inside a native webview. - (Optional) Set
VITE_MOBILE_SOCKET_URLif Socket.IO uses a different origin. - Keep
CORS_ORIGINcontainingcapacitor://localhostand your API origin (already reflected inserver/.env.example). - For live reload, point the shell at your dev server: export
CAP_SERVER_URL=http://<LAN_IP>:5173before running the mobile commands, and runnpm run dev:mobileso the server binds to your LAN IP.
npm run mobile:sync— build web assets and runnpx cap sync(prepsandroid/+ios/).npx cap add android/npx cap add ios— one-time native scaffolding.npm run mobile:android— build, sync, and open Android Studio.npm run mobile:ios— build, sync, and open Xcode.npm run mobile:doctor— sanity-check Capacitor + native toolchains.- Live reload on device/emulator:
npm run dev:mobile(binds Vite to LAN) thenCAP_SERVER_URL=http://<LAN_IP>:5173 npm run mobile:android(ormobile:ios). ClearCAP_SERVER_URLfor production builds.
- Use HTTPS API endpoints in production to avoid mixed content; Android allows HTTP only for local dev (see
capacitor.config.ts). - After any web change, rerun
npm run mobile:syncbefore generating signed.aab/.ipa.
| Area | Features |
|---|---|
| Public | Zeni landing (/), public map (/map), listing detail (/listing/:id), neighbourhood pages (/rent/:n, /buy/:n), login, register, forgot/reset password |
| User | Home, Explore (fuzzy search, compare, save search, recently viewed), Inventory, Saved, Viewings, Messages, Profile (KYC), Refunds |
| Agent | Dashboard, Listings (CRUD), Boost, Leads, Viewings, Analytics, Verification, Settings |
| Admin | Overview, Verification (agents/listings/KYC/business), Listings, Users, Reports, Audit, Network access, Refund requests, Settings |
| Pay | Login, Dashboard, Payments, Invoices, History, Profile, Receipt (separate JWT aud=pay) |
Detailed flows: docs/PROTOTYPE_FLOWS.md and docs/HOW_IT_WORKS.md.
| VITE_MOBILE_API_BASE_URL | API origin used inside Capacitor (must be full HTTPS) | fallback to VITE_API_BASE_URL |
| VITE_SOCKET_URL | Socket.IO URL (optional) | same as API origin |
| VITE_MOBILE_SOCKET_URL | Socket origin for Capacitor builds (optional) | fallback to VITE_SOCKET_URL |
| VITE_GOOGLE_CLIENT_ID | Google Sign-In Web client ID | - |
| VITE_SENTRY_DSN | Sentry DSN (production) | - |
| VITE_ANALYTICS_ENDPOINT | Analytics POST URL | - |
| VITE_STRIPE_PUBLISHABLE_KEY | Stripe publishable key | - |
| VITE_CONTACT_EMAIL | Contact email (footer/landing) | zeniapp.ke@gmail.com |
| VITE_CONTACT_PHONE | Contact phone (optional) | - |
| VITE_SOCIAL_* | Instagram, LinkedIn, Twitter URLs | - |
See server/.env.example. Key variables: PORT, MONGO_URI, JWT_SECRET, CORS_ORIGIN, VIEWING_FEE_AMOUNT, admin/pay step-up codes, M-Pesa, SMTP, Stripe, and optional network controls (ADMIN_IP_ALLOWLIST, Tailscale, etc.). Production requires non-default secrets and explicit MONGO_URI / CORS_ORIGIN.
| Issue | What to try |
|---|---|
npm install fails (Windows EBUSY/EPERM) |
Close IDEs/terminals/antivirus; delete node_modules and package-lock.json, then npm install again. |
| Frontend build: "Cannot find module" | Run npm install; ensure all dependencies (e.g. @sentry/react, vite-plugin-compression2, fuse.js) are installed. |
| Backend tests fail (e.g. viewing 400) | Viewing requests require the requested date to be at least 24 hours in the future (EAT). Adjust seed or test payloads. |
| Port already in use | Change PORT in server/.env or run Vite on another port: npm run dev -- --port 5174. |
| MongoDB connection refused | Start MongoDB locally or set MONGO_URI to a running instance (e.g. Docker: docker run -d -p 27017:27017 mongo). |
| Pay/refunds not matching user | Pay portal must use the same user id as the main app so refund-eligible transactions are correctly linked. |
More: docs/DEPLOYMENT_CHECKLIST.md, docs/DEPLOY.md, docs/SECURITY.md.
- Fork the repository and create a branch from
main. - Follow existing code style (ESLint + Prettier); run
npm run lintandnpm run format:check. - Test your changes:
npm run testand, if touching flows,npm run test:e2e. - Commit with clear messages; hooks will run lint-staged.
- Open a Pull Request with a short description of the change and any related issue.
This project is private. All rights reserved. See the repository or a LICENSE file in the repo for exact terms.
src/
├── App.tsx # Root app, error boundary, providers, route progress
├── main.tsx # Entry point, React root
├── routes/
│ └── index.tsx # Route definitions (public, /app, /agent, /admin, /pay, /rent/:n, /buy/:n)
├── pages/ # Route-level pages
│ ├── ZeniLanding.tsx # Landing page
│ ├── PublicMap.tsx # Public map
│ ├── PropertyDetails.tsx # Listing detail (gallery, WhatsApp CTA, trust block, report, share)
│ ├── NeighborhoodPage.tsx # SEO neighbourhood pages (/rent/:n, /buy/:n)
│ ├── NotFound.tsx
│ ├── auth/ # Login, Register, ForgotPassword, ResetPassword
│ ├── user/ # Home, Explore, Inventory, Saved, Viewings, Profile, Refunds
│ │ └── Explore.tsx # Fuzzy search, compare, save search, recently viewed strip
│ ├── agent/ # Dashboard, Listings, Leads, Viewings, Analytics, Verification, Settings
│ ├── admin/ # Overview, Verification, Listings, Users, Reports, Audit, Refund, Settings
│ ├── messages/ # Inbox, Thread (chat UI)
│ └── pay/ # Pay portal pages
├── layouts/
│ ├── UserLayout.tsx
│ ├── AgentLayout.tsx
│ └── AdminLayout.tsx
├── components/
│ ├── ui/ # Button, Input, Badge, Skeleton, Sidebar, etc.
│ ├── guards/ # RequireAuth, RequireRole
│ ├── chat/ # MessageBubble, MessageComposer, ConversationItem
│ ├── listings/ # PropertyCard, FilterBar, ViewingRequestForm, drawer/*
│ │ ├── ImageGallery.tsx # Multi-image gallery + lightbox + LQIP (NEW)
│ │ ├── NeighborhoodTrust.tsx # Safety/water/power/parking notes (NEW)
│ │ ├── ReportListingModal.tsx # Report listing UI (NEW)
│ │ └── CompareModal.tsx # Side-by-side listing comparison (NEW)
│ ├── landing/ # Header, Footer, SafetySection, AgentSection, FeaturedListings
│ ├── SearchBar.tsx # Fuzzy search (fuse.js), commute hubs, KES presets
│ ├── PropertyMap.tsx
│ └── ...
├── context/ # AuthProvider, ChatContext, ToastContext, ThemeContext, NotificationContext
├── hooks/
│ ├── useRecentlyViewed.ts # localStorage recently-viewed hook (NEW)
│ ├── useListingSEO.ts # OG + Twitter Card + JSON-LD per listing (UPGRADED)
│ ├── useDebounce.ts
│ └── ...
├── lib/
│ ├── cloudinary.ts # listingLqipUrl(), listingDetailUrl(), listingThumbUrl() (UPGRADED)
│ └── ...
├── pay/ # PayRouter, PayAuthContext, payApi, Pay portal
├── providers/
│ └── AppProviders.tsx
├── constants/
├── types/
└── utils/
server/src/
├── server.ts # HTTP server, Socket.IO, mount routes
├── app.ts # Express app, middleware, CORS
├── config/
│ ├── env.ts # Load and validate env
│ ├── db.ts # MongoDB connection
│ └── sentry.ts # Sentry init
├── routes/ # REST API routes
│ ├── listing.routes.ts # Includes POST /listings/:id/report
│ ├── saved-search.routes.ts
│ ├── saved-listing.routes.ts
│ └── ...
├── controllers/
├── services/
├── models/ # User, Listing, ViewingRequest, Message, Conversation, etc.
├── middlewares/
├── jobs/ # savedSearchAlerts, queue
└── utils/
For frontend architecture and design patterns, see docs/ARCHITECTURE.md.
For deployment, security, and ops details, see docs/DEPLOY.md, docs/SECURITY.md, and ops/README.md.
- Description
- Key Features
- Technologies Used
- Installation
- Basic Usage
- Hybrid (Capacitor)
- Features Overview
- Configuration
- Troubleshooting
- Contributing
- License
- Code Structure
Zeni is a MERN-stack (MongoDB, Express, React, Node.js) real estate platform that connects buyers, renters, agents, and admins in Kenya. The app includes:
- Public: Marketing landing page, public map, listing details, login/register, password reset
- User portal: Explore listings, save searches, request viewings, message agents, profile & KYC, refund requests
- Agent portal: Dashboard, listings management, leads, viewings, analytics, verification, boost
- Admin portal: User/listings moderation, verification queue (agents, listings, KYC, business), reports, audit, network access
- Pay portal: Secure payment console (viewing fees, payouts) with separate JWT audience
- Landing page — Zeni branding, scroll-spy nav, orange accent styling, newsletter signup, FAQ, contact
- Map-first search — Public and in-app maps (Leaflet), draw zones, filter by price/type
- Verified listings — Admin-approved listings and agents; verified badge on cards
- Real-time messaging — Socket.IO chat with agents; Zeni Support and listing context in threads
- Viewing requests — Request viewings; optional viewing fee (e.g. 500 KES) held until completion
- Pay portal — Stripe and bank transfer; viewing fees, refund requests (Zeni Shield), invoices
- Agent verification — EARB, ID, business docs; admin moderation queue
- KYC (users) — Profile → Verify identity; admin approves/rejects
- PWA — Web app manifest and service worker; “Add to Home Screen”; offline cached saved listings
- Analytics (optional) — Event tracking endpoint for search, viewing, payment, refund events
| Layer | Technologies |
|---|---|
| Frontend | React 18, TypeScript, Vite, React Router 6, Tailwind CSS, GSAP, Lenis, Framer Motion, Leaflet (react-leaflet), Socket.IO client, Stripe.js, Zod |
| Backend | Node.js, Express, TypeScript, MongoDB (Mongoose), JWT, Socket.IO, Stripe, Nodemailer, bcryptjs, Helmet, express-rate-limit |
| Dev/Test | Vitest, Playwright, ESLint, Prettier, Husky, lint-staged |
| Deploy | Docker (optional), PM2, Vite build (static + gzip/brotli) |
- Node.js 18+ (LTS recommended)
- npm 9+ (or yarn/pnpm)
- MongoDB 6+ (local or remote; e.g. MongoDB Atlas)
- Git
-
Clone the repository
git clone <repository-url> cd "FINAL PROJECT"
-
Install frontend dependencies
npm install
-
Configure frontend environment
copy .env.example .env # Windows # cp .env.example .env # macOS / Linux
Edit
.envand set at least:VITE_API_BASE_URL(e.g./apifor same-origin)VITE_DEV_API_TARGET(e.g.http://localhost:4000) if using Vite proxy
-
Install and configure the server
cd server npm install copy .env.example .env # Windows cd ..
In
server/.envset:PORT(e.g.4000)MONGO_URI(your MongoDB connection string)JWT_SECRET(strong random string for production)CORS_ORIGIN(e.g.http://localhost:5173,capacitor://localhostfor dev/native shells)
-
Seed demo data (optional)
npm run seed
Default seed password:
SEED_PASSWORDorChangeMe123!(see server seed script).
npm run dev:full- Frontend: http://localhost:5173
- Backend API: http://localhost:4000
Terminal 1 (backend):
npm run dev:serverTerminal 2 (frontend):
npm run devnpm run buildOutput: dist/ (static assets). Serve with any static host; API must be deployed separately (e.g. npm run build:server then PM2 or Docker).
| Command | Description |
|---|---|
npm run lint |
Frontend ESLint |
npm run build |
Frontend production build |
npm run test |
Frontend unit tests (Vitest) |
npm run test:e2e |
E2E tests (Playwright) |
npm run lint:server |
Backend ESLint |
npm run build:server |
Backend TypeScript build |
npm run test:server |
Backend Jest tests |
| Area | Features |
|---|---|
| Public | Zeni landing (/), public map (/map), listing detail (/listing/:id), login, register, forgot/reset password |
| User | Home, Explore, Inventory, Saved, Viewings, Messages, Profile (KYC), Refunds |
| Agent | Dashboard, Listings (CRUD), Boost, Leads, Viewings, Analytics, Verification, Settings |
| Admin | Overview, Verification (agents/listings/KYC/business), Listings, Users, Reports, Audit, Network access, Refund requests, Settings |
| Pay | Login, Dashboard, Payments, Invoices, History, Profile, Receipt (separate JWT aud=pay) |
Detailed flows: docs/PROTOTYPE_FLOWS.md and docs/HOW_IT_WORKS.md.
| Variable | Description | Default |
|---|---|---|
VITE_API_BASE_URL |
API path or full origin | /api |
VITE_DEV_API_TARGET |
Proxy target in dev | http://localhost:4000 |
VITE_MOBILE_API_BASE_URL |
API origin used inside Capacitor (must be full HTTPS) | fallback to VITE_API_BASE_URL |
VITE_SOCKET_URL |
Socket.IO URL (optional) | same as API origin |
VITE_MOBILE_SOCKET_URL |
Socket origin for Capacitor builds (optional) | fallback to VITE_SOCKET_URL |
VITE_GOOGLE_CLIENT_ID |
Google Sign-In Web client ID | - |
VITE_SENTRY_DSN |
Sentry DSN (production) | - |
VITE_ANALYTICS_ENDPOINT |
Analytics POST URL | - |
VITE_STRIPE_PUBLISHABLE_KEY |
Stripe publishable key | - |
VITE_CONTACT_EMAIL |
Contact email (footer/landing) | zeniapp.ke@gmail.com |
VITE_CONTACT_PHONE |
Contact phone (optional) | - |
VITE_SOCIAL_* |
Instagram, LinkedIn, Twitter URLs | - |
See server/.env.example. Key variables: PORT, MONGO_URI, JWT_SECRET, CORS_ORIGIN, VIEWING_FEE_AMOUNT, admin/pay step-up codes, M-Pesa, SMTP, Stripe, and optional network controls (ADMIN_IP_ALLOWLIST, Tailscale, etc.). Production requires non-default secrets and explicit MONGO_URI / CORS_ORIGIN.
| Issue | What to try |
|---|---|
npm install fails (Windows EBUSY/EPERM) |
Close IDEs/terminals/antivirus; delete node_modules and package-lock.json, then npm install again. |
| Frontend build: "Cannot find module" | Run npm install; ensure all dependencies (e.g. @sentry/react, vite-plugin-compression2) are installed. |
| Backend tests fail (e.g. viewing 400) | Viewing requests require the requested date to be at least 24 hours in the future (EAT). Adjust seed or test payloads. |
| Port already in use | Change PORT in server/.env or run Vite on another port: npm run dev -- --port 5174. |
| MongoDB connection refused | Start MongoDB locally or set MONGO_URI to a running instance (e.g. Docker: docker run -d -p 27017:27017 mongo). |
| Pay/refunds not matching user | Pay portal must use the same user id as the main app so refund-eligible transactions are correctly linked. |
More: docs/DEPLOYMENT_CHECKLIST.md, docs/DEPLOY.md, docs/SECURITY.md.
- Fork the repository and create a branch from
main. - Follow existing code style (ESLint + Prettier); run
npm run lintandnpm run format:check. - Test your changes:
npm run testand, if touching flows,npm run test:e2e. - Commit with clear messages; hooks will run lint-staged.
- Open a Pull Request with a short description of the change and any related issue.
This project is private. All rights reserved. See the repository or a LICENSE file in the repo for exact terms.
src/
├── App.tsx # Root app, error boundary, providers, route progress
├── main.tsx # Entry point, React root
├── routes/
│ └── index.tsx # Route definitions (public, /app, /agent, /admin, /pay)
├── pages/ # Route-level pages
│ ├── ZeniLanding.tsx # Landing page (hero, map, projects, insights, FAQ, CTA)
│ ├── PublicMap.tsx # Public map
│ ├── PropertyDetails.tsx # Listing detail
│ ├── NotFound.tsx
│ ├── auth/ # Login, Register, ForgotPassword, ResetPassword
│ ├── user/ # Home, Explore, Inventory, Saved, Viewings, Profile, Refunds
│ ├── user/home/ # Hero, widgets, activity feed
│ ├── agent/ # Dashboard, Listings, Leads, Viewings, Analytics, Verification, Settings
│ ├── admin/ # Overview, Verification, Listings, Users, Reports, Audit, RefundRequests, Settings
│ ├── messages/ # Inbox, Thread (chat UI)
│ └── pay/ # Pay portal pages (Dashboard, Payments, Invoices, etc.)
├── layouts/
│ ├── UserLayout.tsx # App shell for /app/*
│ ├── AgentLayout.tsx # App shell for /agent/*
│ └── AdminLayout.tsx # App shell for /admin/*
├── components/ # Reusable UI and feature components
│ ├── ui/ # Button, Input, Badge, Skeleton, Sidebar, etc.
│ ├── guards/ # RequireAuth, RequireRole
│ ├── chat/ # MessageBubble, MessageComposer, ConversationItem
│ ├── listings/ # PropertyCard, FilterBar, ViewingRequestForm, drawer/*
│ ├── landing/ # Header, Footer, SafetySection, AgentSection, FeaturedListings
│ ├── admin/ # AdminStepUpModal
│ ├── ErrorBoundary.tsx
│ ├── PropertyMap.tsx
│ └── ...
├── context/ # AuthProvider, ChatContext, ToastContext, ThemeContext, NotificationContext
├── hooks/ # useMotion, useKineticRing, useCursor, useDebounce, useChatSocket, etc.
├── lib/ # API client, auth, chat, validation, format, analytics, socket
├── pay/ # PayRouter, PayAuthContext, payApi, Pay portal pages/components
├── providers/
│ └── AppProviders.tsx # Wraps app with all contexts
├── constants/ # listings, messages, verification
├── types/ # landing, chat, API types
└── utils/ # cn, mockData, etc.
server/src/
├── server.ts # HTTP server, Socket.IO, mount routes
├── app.ts # Express app, middleware, CORS
├── config/
│ ├── env.ts # Load and validate env
│ ├── db.ts # MongoDB connection
│ └── sentry.ts # Sentry init
├── routes/ # REST API routes
│ ├── index.ts # Mounts all route modules
│ ├── auth.routes.ts
│ ├── listing.routes.ts
│ ├── user.routes.ts
│ ├── agent.routes.ts
│ ├── admin.routes.ts
│ ├── chat.routes.ts
│ ├── viewing.routes.ts
│ ├── viewing.agent.routes.ts
│ ├── pay.routes.ts
│ ├── pay.portal.routes.ts
│ ├── refundRequest.routes.ts
│ ├── notification.routes.ts
│ ├── health.routes.ts
│ ├── upload.routes.ts
│ ├── recommendation.routes.ts
│ ├── saved-search.routes.ts
│ ├── saved-listing.routes.ts
│ └── ...
├── controllers/ # Request handlers (auth, listing, user, chat, viewing, pay, admin, etc.)
├── services/ # Business logic (auth, listing, viewing, chat, stripe, notification, etc.)
├── models/ # Mongoose models (User, Listing, ViewingRequest, Message, Conversation, etc.)
├── middlewares/ # auth, rbac, rateLimit, errorHandler, adminStepUp, payAuth, ipAllowlist
├── jobs/ # Cron/scheduled (savedSearchAlerts, queue)
└── utils/ # validators, audit, payStateMachine, totp, etc.
For frontend architecture, design patterns, and gradual refactoring, see docs/ARCHITECTURE.md.
For deployment, security, and ops details, see docs/DEPLOY.md, docs/SECURITY.md, and ops/README.md.