Nestopia is a full-stack rental housing platform that matches renters with landlords based on housing preferences, property features, and per-listing tenant requirements. Compatibility is shown as a percentage on listings, detail pages, and a daily matches feed.
Live: nestopia-rental.vercel.app · API: https://nestopia-production.up.railway.app
cd backend
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
uvicorn app.main:app --reloadAPI: http://127.0.0.1:8000 · Docs: /docs
cd frontend
npm install
npm startApp: http://localhost:3000 — CRA proxies /api to port 8000. No .env needed locally.
Copy frontend/.env.example → frontend/.env only if you need overrides (e.g. Stripe keys).
Browser (React) → /api/* proxy (dev) → FastAPI
↓
PostgreSQL or SQLite
uploads/ (images, profile pics)
Redis + Celery (optional daily matching jobs)
| Layer | Stack |
|---|---|
| Frontend | React 19, React Router 7, Axios + httpOnly cookies, Leaflet/OSM |
| Backend | FastAPI, SQLAlchemy, Alembic, Authlib (Google OAuth), optional Celery |
- Auth — email/password, Google OAuth, roles (renter/landlord), TOTP 2FA, httpOnly session cookie
- Renter preferences — budget, locations (OSM), move-in window, amenities, lifestyle fields → property fit
- Listings — CRUD, photos, geocoding; per-listing tenant requirements → tenant fit
- Compatibility — 72% property fit + 28% tenant fit; breakdown on cards and detail pages
- Matches —
/matchesdaily ranked feed for renters with preferences set - Applications — contact host → rental application workflow with tours and move-in
- Maps — browse map, mini-map, Nominatim search via backend proxy
Tenant requirements are listing-specific (not global landlord prefs).
Nestopia/
├── backend/ # FastAPI — app/, alembic/, Dockerfile, railway.toml
├── frontend/ # React — src/api/endpoints.js = canonical API paths
├── backend/.env.example
└── frontend/.env.example
| Route | Description |
|---|---|
/, /login, /signup |
Home, auth |
/onboarding |
Renter prefs or landlord → create listing |
/listings, /listing/:id |
Browse + detail (match %, tenant requirements) |
/listing/new, /listing/edit/:id |
Landlord listing form |
/matches, /saved, /preferences |
Matches, saved homes, edit prefs |
/profile, /users/:id |
Profile, activity, security; public profiles |
| File | Use |
|---|---|
backend/.env.example |
Copy to backend/.env for local dev; production vars go in Railway |
frontend/.env.example |
Optional local overrides; production: Vercel REACT_APP_API_BASE_URL |
Never commit .env files.
Railway (root directory backend, PostgreSQL required):
DATABASE_URL=${{Postgres.DATABASE_URL}}
SECRET_KEY=<random 32+ chars>
SESSION_SECRET_KEY=<random 32+ chars>
FRONTEND_URL=https://nestopia-rental.vercel.app
CORS_ORIGINS=https://nestopia-rental.vercel.app
API_PUBLIC_URL=https://nestopia-production.up.railway.app
COOKIE_SECURE=true
COOKIE_SAMESITE=none
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
GOOGLE_REDIRECT_URI=https://nestopia-production.up.railway.app/api/auth/google/callback
UPLOADS_DIR=/data/uploadsPersistent uploads (Railway volume — stays on Railway, no extra vendor):
Railway does not put volumes under Settings anymore. You add them from the project canvas (the diagram with your services):
- Open your Railway project (not just the service logs).
- Right-click empty space on the canvas → Add volume
or press ⌘K (Mac) / Ctrl+K (Windows) → search “Volume” → create one. - When prompted, select your Nestopia API service.
- Set Mount path to
/data/uploads. - Redeploy the API service.
After deploy, logs should show: Upload storage: Railway volume mounted at /data/uploads.
CLI alternative (from repo, linked to the API service):
railway link
railway volume add --mount-path /data/uploadsPlan note: Volumes require a Hobby (or Pro) plan. On the Free tier, the Volume option is hidden / unavailable — upgrade the project to Hobby if you do not see it.
If the container runs as non-root, add variable RAILWAY_RUN_UID=0 on the API service.
Re-upload listing photos and profile pics once after the volume is attached (earlier files were on ephemeral disk).
Use backend/Dockerfile + backend/railway.toml. Clear custom start commands. After deploy: alembic upgrade head.
Vercel (root directory frontend):
REACT_APP_API_BASE_URL=https://nestopia-production.up.railway.appInclude https:// — without it, requests incorrectly go to vercel.app/nestopia-production... and return 405.
Google OAuth — JavaScript origins: Vercel URL + http://localhost:3000. Redirect URI on the API: /api/auth/google/callback.
curl https://nestopia-production.up.railway.app/health
curl -sI -X OPTIONS 'https://nestopia-production.up.railway.app/api/listings/' \
-H 'Origin: https://nestopia-rental.vercel.app' \
-H 'Access-Control-Request-Method: GET'
# Expect: access-control-allow-origin: https://nestopia-rental.vercel.appIn the browser (DevTools → Network):
- API calls go to
https://nestopia-production.up.railway.app/...(not undervercel.app/...) POST /api/auth/login→ 200, thenGET /api/users/me→ 200PATCH /api/users/me→ 200 when saving profile- Cookie
nestopia_sessionappears under the Railway domain after login
All routes use prefix /api unless noted. Session cookie: nestopia_session. Interactive docs: /docs on the backend.
Canonical frontend paths live in frontend/src/api/endpoints.js — keep in sync with backend/app/routers/.
| Method | Path | Auth |
|---|---|---|
| GET | /health |
No |
| Method | Path | Description |
|---|---|---|
| POST | /signup |
Create account |
| POST | /login |
Email login (may return 2FA challenge) |
| POST | /verify-2fa |
Complete login with TOTP |
| POST | /logout |
Clear session |
| GET | /session |
Check session |
| POST | /password-reset/request |
Request reset token |
| POST | /password-reset/confirm |
Set new password |
| GET | /google/login |
Start Google OAuth |
| GET | /google/callback |
Google OAuth callback (API host) |
| Method | Path | Description |
|---|---|---|
| GET | /me |
Current user |
| PATCH | /me |
Update profile |
| GET | /profile/{id} |
Public profile (optional auth) |
| POST | /profile/{id}/reviews |
Submit review |
| POST | /upload-profile-doc |
Profile photo upload |
| POST | /change-password |
Change password |
| POST | /link-wallet |
Link wallet address |
| Method | Path | Description |
|---|---|---|
| GET/POST | /renter |
Renter housing preferences |
| GET/POST | /landlord |
Legacy landlord prefs |
| Method | Path | Description |
|---|---|---|
| GET | /, `` |
List listings (optional auth, match scores for renters) |
| POST | / |
Create listing (landlord) |
| GET | /owned |
Landlord's listings |
| GET | /{id} |
Listing detail |
| PUT | /{id} |
Update listing |
| DELETE | /{id} |
Delete listing |
| POST | /upload-image |
Upload listing image |
| GET | /saved/ |
Saved listings |
| POST/DELETE | /saved/{id} |
Save / unsave |
| Method | Path | Description |
|---|---|---|
| GET | /api/matches/daily |
Daily ranked matches (renter) |
| GET | /api/applications/activity |
Activity feed |
| POST | /api/applications/from-contact |
Apply from listing contact |
| POST | /api/applications/{id}/* |
Withdraw, accept, reject, tours, move-in |
| GET | /api/geo/search |
Location search (Nominatim proxy) |
| GET | /api/security/status |
2FA status |
| POST | /api/security/totp/* |
TOTP setup, confirm, disable |
| GET | /api/stats/summary |
Platform stats |
Payments (/api/payments), tokens (/api/tokens), wallet (/api/wallet), blockchain (/api/blockchain).
PostgreSQL in production; SQLite for local dev (tables auto-created on startup).
cd backend && alembic upgrade headcd backend && pytest
cd frontend && npm testredis-server
cd backend
celery -A celery_app.celery worker -l info
celery -A celery_app.celery beat -l infoSet USE_ML_MATCHING=true only if workers are running.
- Rotate secrets in production; use HTTPS cookies (
COOKIE_SECURE=true,COOKIE_SAMESITE=nonefor split deploy) - Do not commit
.env,nestopia_dev.db, oruploads/ - On Railway, attach a volume at
/data/uploadsso uploads survive redeploys (see Production section)
- Semantic matching needs heavy optional ML deps
- Minimal test coverage
- Payment/wallet/blockchain routers exist but are not wired in the UI