West Virginia real estate listing platform. Admin panel, public listing pages, photo uploads, AI marketing content, Resend lead notifications, and optional Google Drive/Gmail integrations.
Live site: malickland.net
| Layer | Tech |
|---|---|
| Runtime | Node.js 20 LTS |
| Framework | Express 5 |
| Database | SQLite via better-sqlite3 |
| Auth | Session-based (admin panel) + API key (REST) |
| Images | Local disk + Google Drive backup |
| Resend lead notifications; legacy Gmail helper remains available | |
| AI | Vercel AI Gateway, Anthropic, or OpenAI (listing content generation) |
| Deploy | Hostinger VPS — Docker + Traefik (manual deploy) |
flowchart LR
visitor[Visitor] --> publicPages[Public listing pages]
admin[Admin] --> adminPanel[Admin panel]
publicPages --> api[Express API]
adminPanel --> api
api --> db[(SQLite database)]
api --> ai[Optional AI content generator]
api --> drive[Optional Google Drive backup]
api --> email[Resend lead notifications]
wv-property-intelligence/
├── api/ ← Backend (Node/Express)
│ ├── server.js ← Entry point, middleware, static routes
│ ├── db.js ← SQLite connection, schema, migrations, seeding
│ ├── helpers.js ← Shared utilities (esc, slugify, etc.)
│ ├── google.js ← Gmail + Drive helper integration (no SDK)
│ ├── db-migrate-ai.js ← One-time migration script (ai_content columns)
│ ├── middleware/
│ │ ├── auth.js ← requireAuth, requireCsrf, requireApiKey
│ │ └── rate-limits.js ← Per-route rate limiters
│ ├── services/
│ │ └── email.js ← Resend transactional email transport
│ ├── ai-generator.js ← Multi-provider AI listing content engine
│ ├── routes/
│ │ ├── admin.js ← /admin/* (session-protected)
│ │ ├── api.js ← /api/* (public + API-key endpoints)
│ │ └── public.js ← /robots.txt, /sitemap.xml, /listing/:id
│ └── views/
│ └── admin.js ← Server-rendered admin HTML shell
├── app/ ← Frontend (vanilla JS, no build step)
│ ├── index.html ← Public listing search page
│ ├── listings.html ← Static listings index
│ ├── 37-advent.html ← Advent landing page, served at /37-advent
│ ├── listing.html ← Single property detail page
│ ├── admin.html ← Admin login page
│ ├── app.js ← Listing grid, filters, modal, contact form
│ └── listing.js ← Property detail + inquiry form
├── database/
│ ├── wv_property.db ← SQLite database (runtime, gitignored)
│ └── schema.sql ← Reference schema (SQLite, documentation only)
├── listings/ ← Per-property file storage (gitignored)
│ └── _template/ ← Folder structure reference
├── api/Dockerfile ← Multi-stage Docker build
└── compose.yml ← Local Docker Compose
cd api
cp .env.example .env # fill in values
npm install
npm run dev # nodemon server.jsServer: http://localhost:3001
Admin: http://localhost:3001/admin (password from ADMIN_PASSWORD env)
For local review, start with SESSION_SECRET and ADMIN_PASSWORD, then add
optional Google, Gmail, Resend, and AI (OpenAI, Anthropic, or Vercel AI Gateway) credentials
only for the integrations you are actively testing. The local SQLite database and local
file storage are enough to exercise the core listing/admin flow.
docker compose up --buildAll env vars live in api/.env. See api/.env.example for the full annotated list.
Required to run:
SESSION_SECRET— any long random stringADMIN_PASSWORD— admin panel password
Optional integrations:
- Lead notifications:
RESEND_API_KEY,NOTIFICATION_EMAILorLEADS_NOTIFICATION_EMAIL, optionalFROM_EMAIL - Legacy Gmail helper / OAuth:
GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GOOGLE_REFRESH_TOKEN,GOOGLE_GMAIL_USER - Drive photo backup:
GOOGLE_DRIVE_FOLDER_ID - AI content generation:
AI_GATEWAY_API_KEY,ANTHROPIC_API_KEY, orOPENAI_API_KEY(optional provider-specific model overrides) - REST API auth:
API_KEY
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/health |
Public | Health check |
| GET | /api/counties |
Public | All 55 WV counties |
| GET | /api/properties |
Public | Active listings (paginated, filterable) |
| GET | /api/properties/:id |
Public | Single property detail |
| POST | /api/properties |
API Key | Create property |
| PUT | /api/properties/:id |
API Key | Update property |
| DELETE | /api/properties/:id |
API Key | Delete property |
| GET | /api/analytics |
Public | Aggregate stats |
| POST | /api/contacts |
Public | Submit lead inquiry |
| GET | /api/contacts |
API Key | List all leads |
| POST | /api/properties/generate-description |
Public (rate-limited) | Quick AI description |
GET /api/properties query params: q, county, type, minPrice, maxPrice, page, limit
All require session auth (/admin/login).
| Path | Description |
|---|---|
GET /admin |
Listings dashboard |
GET /admin/new |
New listing form |
POST /admin/new |
Create listing |
GET /admin/edit/:id |
Edit listing form |
POST /admin/edit/:id |
Save edits |
GET /admin/photos/:slug |
Photo manager |
POST /admin/upload/:slug |
Upload photo |
GET /admin/report/:id |
Comps + due diligence |
GET /admin/ai/:id |
AI content viewer |
POST /admin/ai/:id |
Generate/regenerate AI content |
GET /admin/integrations |
Gmail + Drive status |
With an AI provider key set, every listing gets a full marketing package. Provider precedence is AI_GATEWAY_API_KEY, then ANTHROPIC_API_KEY, then OPENAI_API_KEY.
Each provider supports optional model overrides; direct OpenAI defaults to gpt-4o with automatic fallback to gpt-4o-mini when account access is limited:
MLS description, investor pitch, Facebook ad, Instagram caption, video script, email blast, SMS, landing page copy.
Cost: ~$0.01–0.03 per listing. Access via Admin → AI button on any listing.
Production runs as a Docker container on Phil's Hostinger VPS (srv1716268 / 31.97.58.203), behind Traefik with Let's Encrypt TLS. Deploys are manual — merging to main does not update production. To deploy a reviewed, Phil-approved commit:
ssh root@31.97.58.203
git -C /docker/wv-property-intelligence/src fetch origin
git -C /docker/wv-property-intelligence/src checkout <sha>
cd /docker/wv-property-intelligence && docker compose build && docker compose up -dAfter deploy, prove the live commit from the local repo:
EXPECTED_SHA=<full-git-sha> bash scripts/verify-vps-prod.shPersistent data lives on the Docker named volume wv-property-intelligence_wv-data, mounted at /data. Set both DATABASE_PATH=/data/wv_property.db and LISTINGS_ROOT=/data/listings in /docker/wv-property-intelligence/.env so admin photo uploads survive rebuilds and docker compose up -d --force-recreate. Secrets are set in that .env file, not in the repo.
The old Railway service (
alert-laughter/wv-property-intelligence) was deleted on 2026-06-18. Production runs solely on the Hostinger VPS, and Railway deployments/GitHub deployment records are not production proof.railway.jsonandrailway.tomlwere removed from the repo. Seedocs/CANONICAL_MAP.mdfor the full production map.
Deep forest green #1B4332 · Warm gold #D4AF37 · System UI / Segoe UI