An end-to-end ML platform that predicts Indian residential property prices in real time, built on a Hexagonal/DDD-lite FastAPI backend, a Random Forest pipeline, and a React 19 frontend β deployed across two live environments with full Dockerization.
- Overview
- The ML Pipeline
- Architecture
- Features
- Tech Stack
- Design System
- Project Structure
- Setup
- Environment Variables
- API Reference
- Model
- Deployment
- The Team
- Screenshots
- Known Constraints / Decisions
Maskani estimates the market value of a residential property in India from 12 inputs (area, BHK, floor, location, society, furnishing, and more), returning a price instantly β no account required. Log in and every prediction is saved to a personal history, and feeds a live, public analytics dashboard tracking demand and pricing trends across the country.
- π Guest-first: anyone can request a prediction with zero friction β auth is opt-in, only needed to persist history.
- π Public, real analytics: total predictions, average price, top locations, a price-vs-area regression chart, and a live demand heat map β all computed from real data, no placeholder numbers.
- π Minimal, honest auth: email + password, a single 7-day JWT, no refresh-token complexity that this app doesn't need.
- ποΈ Built to last: Hexagonal/DDD-lite backend architecture means swapping the ML model or the database only ever touches one layer.
The model behind every prediction was built from scratch by the team, end to end:
- Raw data β an Indian residential real-estate dataset sourced from Kaggle.
- Cleaning & preparation β messy real-world listing data (inconsistent area/floor formats, missing values, noisy categorical fields) cleaned and normalized into the 12 model-ready features (
Area_Numeric,BHK,Floor_Numeric,location_grouped,Society_grouped, and more). - Training β a Random Forest Regressor, wrapped in a scikit-learn
Pipelineso preprocessing (encoding, scaling) ships baked into a single.pklartifact β no separate preprocessing code to keep in sync in production. - Export & integration β the trained pipeline (
house_price.pkl, ~36 MB) is loaded once at API startup and served through a cleanPricePredictorport, so the backend never depends on scikit-learn internals directly.
βββββββββββββββββββββββ HTTP (JSON) βββββββββββββββββββββββββββββββββ
β React + Vite SPA β ββββββββββββββββββββΆ β FastAPI β
β (frontend/src) β ββββββββββββββββββββ β (backend/app) β
βββββββββββββββββββββββ β β
β presentation/ routes+schemas β
β application/ use cases β
β domain/ entities/ports β
β infrastructure/ β
β ml/ β house_price.pkl β
β db/ β NeonDB (Postgres) β
β auth/ β JWT + bcrypt β
βββββββββββββββββ¬βββββββββββββββββ
β
ββββββββββΌβββββββββ
β NeonDB (Neon) β
β users β
β predictions β
βββββββββββββββββββ
The backend follows Hexagonal (Ports & Adapters) / DDD-lite: domain/ and application/ know nothing about FastAPI, SQLAlchemy, or scikit-learn β only infrastructure/ does. The PricePredictor and PredictionRepository ports mean the ML model or the database can be swapped without touching a single line of business logic.
Prediction
- 12-field property valuation form with real-time validation against live backend data (locations, societies)
- Guest predictions with zero login friction; auto-linked to your account when logged in
- Sticky result panel with a live-computed price/sqft breakdown
Analytics Dashboard (public β no login required)
- Real-time metric cards: total predictions, average price, most in-demand location, model type
- Location demand & price bar chart (gradient-filled, recharts)
- Price vs. Area scatter chart with a real least-squares regression trend line, computed client-side from live prediction data
- Animated demand heat map (Leaflet + leaflet.heat) glowing over India, intensity driven by real prediction counts per city
History
- Full prediction history with search + date-range filtering
- Delete any past prediction (ownership-scoped on the backend)
Auth & Team
- Email/password auth, single 7-day JWT,
localStoragepersistence - A real "Team" section on the landing page, wired to GitHub/LinkedIn
Design polish
- Custom MD3-inspired design system (see Design System)
- Animated SVG hexagonal-architecture diagram with pulsing data-flow visualization
- Logo splash screen, shimmer skeleton loaders, active nav-link highlighting
- Full SEO head: Open Graph, Twitter cards, meta description, favicon
| Layer | Tech |
|---|---|
| Frontend | React 19, TypeScript, Vite, React Router, Tailwind CSS v4, shadcn/ui (Radix primitives), Axios, Recharts, React-Leaflet, react-icons |
| Backend | FastAPI, Pydantic v2, SQLAlchemy 2.0 (async), asyncpg |
| ML | scikit-learn 1.9.0 (Random Forest pipeline), pandas, joblib |
| Auth | PyJWT (HS256, 7-day access token), bcrypt |
| Database | NeonDB (serverless Postgres) |
| Infra | Docker, Nginx, Let's Encrypt, Vercel |
| Tests | pytest, httpx, in-memory fake repositories |
A custom Material Design 3βinspired token system (see frontend/src/index.css):
- Typography: Hanken Grotesk for UI text, JetBrains Mono for data points and labels
- Palette: deep forest green primary, vibrant mint accent, full MD3 surface/container scale
- Effects: glassmorphic cards (
backdrop-blur), ambient background glows, shimmer skeletons, animated SVG diagrams - Every
shadcn/uicomponent is re-themed to this palette (not the library defaults) via CSS custom properties in one place
backend/
app/
domain/ entities, value objects, ports (interfaces)
application/ use cases (PredictPrice, RegisterUser, LoginUser, DeletePrediction, GetScatterData, ...)
infrastructure/ ml/ (sklearn + placeholder predictor), db/ (NeonDB), auth/ (JWT + bcrypt)
presentation/ FastAPI routes, Pydantic schemas, DI
core/ config.py (.env), main.py (app factory)
models/house_price.pkl
data/{locations,societies}.json
tests/
frontend/
src/
features/
auth/ Login/Register pages, useAuth, api client
prediction/ prediction form (12 fields), sticky result card
history/ searchable/filterable prediction history + delete
dashboard/ metric cards, bar + scatter charts, demand heat map
landing/ hero, mini-simulator, feature bento, team section
shared/
api/httpClient.ts axios instance + JWT interceptor
components/ui/ shared UI primitives (Field, Skeleton, Spinner, AppSelect, ...)
utils/ small pure helpers (location formatting, etc.)
components/ui/ shadcn/ui generated components (Select, ...)
routes/AppRouter.tsx route table, protected routes for history
Requires Python 3.13 (Python 3.14 and some Windows Python builds are missing prebuilt wheels for a few pinned dependencies β see .wolf/buglog.json if you hit build errors).
cd backend
py -3.13 -m venv .venv
.venv\Scripts\activate # Windows; use `source .venv/bin/activate` on macOS/Linux
pip install -r requirements.txt
cp .env.example .env
# fill in DATABASE_URL (NeonDB, asyncpg driver) and JWT_SECRET
uvicorn app.core.main:app --reload
# http://localhost:8000/docsTables are created automatically on startup (Base.metadata.create_all) β no manual migration step needed for this project's scope.
cd frontend
npm install
cp .env.example .env # defaults to http://localhost:8000, adjust if the backend runs elsewhere
npm run dev
# http://localhost:5173cd backend
pytestTests use in-memory fake repositories (tests/conftest.py), so they run without a real database connection.
| Variable | Description |
|---|---|
DATABASE_URL |
NeonDB connection string, asyncpg driver: postgresql+asyncpg://user:pass@host/db?ssl=require |
JWT_SECRET |
Random secret used to sign access tokens |
JWT_ALGORITHM |
HS256 |
JWT_EXPIRE_DAYS |
Access token lifetime in days (7) |
MODEL_PATH |
Path to the trained pipeline (models/house_price.pkl) |
LOCATIONS_PATH / SOCIETIES_PATH |
Paths to the dropdown JSON files |
USE_PLACEHOLDER_MODEL |
false once the real .pkl is in place; true falls back to a rough estimate so the API still runs without the model file |
CORS_ORIGINS |
JSON array of allowed origins, e.g. ["http://localhost:5173"] |
| Variable | Description |
|---|---|
VITE_API_BASE_URL |
Base URL of the FastAPI backend, e.g. http://localhost:8000 |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /predict |
optional | Predict a price. If a valid Authorization: Bearer token is sent, the prediction is linked to that user; otherwise it's saved with user_id = null. |
| GET | /health |
no | Liveness check |
| POST | /auth/register |
no | { email, password } β creates a user |
| POST | /auth/login |
no | { email, password } β { access_token, token_type } |
| GET | /auth/me |
required | Current user's profile |
| GET | /predictions/me |
required | Current user's prediction history |
| DELETE | /predictions/{id} |
required | Delete one of your own predictions |
| GET | /analytics/summary |
no | Aggregate stats: total predictions, average price, top locations |
| GET | /analytics/locations |
no | Per-location prediction counts + average price β powers the demand heat map |
| GET | /analytics/scatter |
no | {area_sqft, predicted_price} pairs for every prediction β powers the scatter chart |
| GET | /data/locations |
no | List of valid location values for the prediction form |
| GET | /data/societies |
no | List of valid society values for the prediction form |
{
"area_sqft": 1200,
"bhk": 2,
"floor": 3,
"bathroom": 2,
"balcony": 1,
"car_parking": 1,
"location": "gurgaon",
"society": "DLF Skycourt",
"furnishing": "Semi-Furnished",
"transaction": "New Property",
"ownership": "Freehold",
"facing": "East"
}location and society must be values returned by /data/locations and /data/societies. furnishing β {Furnished, Semi-Furnished, Unfurnished}, transaction β {New Property, Resale}, ownership β {Freehold, Leasehold, Co-operative Society, Power Of Attorney}, facing β the 8 compass directions.
- Algorithm: Random Forest, wrapped in a scikit-learn
Pipelinethat includes preprocessing (house_price.pkl, ~36 MB). - Trained on an Indian residential real-estate dataset (Kaggle) β cleaned and feature-engineered by the team (see The ML Pipeline).
- Required scikit-learn version: 1.9.0 exactly β other versions have been observed to raise
AttributeErrorinsideColumnTransformerwhen unpickling, not just a version-mismatch warning.
Two live environments, both from the same codebase:
| Environment | Frontend | Backend | Database |
|---|---|---|---|
| Production | Vercel | Self-managed VPS β Docker + Nginx + Let's Encrypt (ml.tigerbase.cloud) |
NeonDB |
| Local dev | Vite dev server (localhost:5173) |
Uvicorn (localhost:8000) |
NeonDB (or local Postgres via docker-compose.production.yml) |
render.yaml and backend/railway.json are also in the repo as ready-to-go PaaS alternatives (Render/Railway) β kept for reference, not currently used since the VPS is free indefinitely.
Files: backend/Dockerfile, docker-compose.yml (repo root), deploy/nginx/ml.tigerbase.cloud.conf (reference β copy it onto the server).
- DNS: in Hostinger's DNS panel, point an
Arecord forml.tigerbase.cloudat the VPS's public IP. Wait for it to resolve (ping ml.tigerbase.cloud) before requesting SSL. - SSH into the VPS and install Docker + Nginx + Certbot:
curl -fsSL https://get.docker.com | sh apt update && apt install -y nginx certbot python3-certbot-nginx
- Clone the repo and configure secrets:
git clone https://github.com/SolomDev00/Maskani.git cd Maskani/backend cp .env.example .env nano .env # fill in DATABASE_URL, JWT_SECRET, etc. β same values as local
- Build and run the container (from the repo root, where
docker-compose.ymllives):cd .. docker compose up -d --build curl http://127.0.0.1:8000/health # sanity check before wiring up Nginx
- Nginx: copy
deploy/nginx/ml.tigerbase.cloud.confinto place and enable it:cp deploy/nginx/ml.tigerbase.cloud.conf /etc/nginx/sites-available/ml.tigerbase.cloud ln -s /etc/nginx/sites-available/ml.tigerbase.cloud /etc/nginx/sites-enabled/ nginx -t && systemctl reload nginx - SSL (Certbot rewrites the Nginx config to add the HTTPS server block + redirect automatically):
certbot --nginx -d ml.tigerbase.cloud
- Firewall β only 22/80/443 need to be open; the container itself is bound to
127.0.0.1:8000(seedocker-compose.yml) so it's not reachable from outside even without a firewall rule, but lock it down anyway:ufw allow 22 && ufw allow 80 && ufw allow 443 && ufw enable
- Verify:
curl https://ml.tigerbase.cloud/healthshould return{"status":"ok"}.
To redeploy after a code change: git pull && docker compose up -d --build.
- Import the repo in Vercel, set Root Directory to
frontend. - Add an environment variable
VITE_API_BASE_URL=https://ml.tigerbase.cloud. - Deploy (Vercel auto-detects the Vite build).
- Back on the VPS, edit
backend/.envand updateCORS_ORIGINSto include the finalhttps://<project>.vercel.appdomain, thendocker compose restart backendso the browser isn't blocked by CORS.
docker-compose.yml (backend only) is what actually runs in production. docker-compose.production.yml containerizes the entire stack instead (frontend, backend, and Postgres), so the whole app can be built and run with one command β e.g. to demo it, hand it to someone without a Vercel/Neon account, or run it on different infrastructure later:
docker compose -f docker-compose.production.yml up -d --buildThis starts three containers, all bound to 127.0.0.1 only (separate container names and ports from docker-compose.yml, so it's safe to run alongside the real deployment without conflicts):
| Service | URL | Notes |
|---|---|---|
db |
localhost:5432 |
postgres:16 container, data in a named volume |
backend |
http://localhost:8001 |
Same image as production, pointed at the local db container instead of NeonDB |
frontend |
http://localhost:8080 |
Production build of the Vite app, served by nginx |
Tear it down with docker compose -f docker-compose.production.yml down (add -v to also wipe the Postgres volume).
![]() Eslam Wael Sr. Software Engineer & Team Lead GitHub Β· LinkedIn |
![]() Asmaa Ayman ML Engineer β Model Training GitHub Β· LinkedIn |
![]() Ahmed Gamal ML Engineer β Data Cleaning & Presentation GitHub Β· LinkedIn |
ITI AI Level 1 Track
- No refresh tokens β a single 7-day access token.
- Guest predictions are allowed; login is only required to persist history.
- The analytics dashboard is public by design β it's meant to entice guests, not gate them out.
- NeonDB is used for both
usersandpredictions(not just logging β it backs the analytics dashboard too). - Hexagonal/DDD-lite layout β any future change (new model, different DB) should stay confined to
infrastructure/.



