Instantly check if a Singapore HDB resale flat or private property is fairly priced based on real transaction data from data.gov.sg and URA.
🔗 Live site: worthit.canlah.app
A free tool for Singapore homeowners and buyers — check HDB resale prices, condo transaction history, price trends, and fair-value estimates for any town, district, or project.
Check My Price: search any HDB postal code, enter an asking price, and get a 0–100 Deal Score with a fair-value range from storey-adjusted comparable sales nearby — flat type, size, floor, and remaining lease are inferred automatically from the block's transaction history.
WorthIt uses a split architecture:
- Frontend: Static files (
public/) served via Cloudflare Pages (ornode server/index.jslocally) - API Server: Node.js/Express on Fly.io with SQLite database
public/config.jsauto-detects environment and points API calls to the right backend
npm install
pip install requests pyproj # for data download scriptsnpm run download-hdb # HDB transactions (~230K records)
# npm run download-ura # Optional: URA private property data (requires URA_API_ACCESS_KEY)This downloads HDB resale transaction records from data.gov.sg into server/db/resale.db.
npm startOpen http://localhost:3000 in your browser. Express serves both the frontend and API.
npm install -g flyctl
fly auth loginfly launch # Follow prompts (choose region, set app name)
fly secrets set ONEMAP_TOKEN=your_token_here # optional, for geocodingfly deployThe Fly.io free tier (256MB RAM) is too small to run the Python download scripts. Instead, build the database locally and upload it:
# 1. Build database locally
python scripts/download_data.py
python scripts/download_ura_data.py # optional
# 2. Upload to Fly.io volume
fly ssh sftp put server/db/resale.db /data/resale.db
# 3. Restart the machine to pick up the new DB
fly machine restart <machine-id>curl https://<your-app>.fly.dev/api/status
# Expected: {"status":"ok","total_transactions":370252,"latest_month":"2026-05",...}fly logs # live log stream
fly logs --no-tail # recent logs onlyfly machines list # list all machines and their states
fly machine status <id> # detailed status of a machinefly ssh issue # one-time: create SSH certificate
fly ssh console # open a shell on the machineInside the SSH shell:
ls -la /data/ # check database file exists
ls -la /app/ # check deployed files
curl localhost:8080/api/status # test API from inside the machine
cat /app/server/index.js | head # verify deployed codefly ssh sftp put local_file.txt /data/remote_file.txt # upload
fly ssh sftp get /data/resale.db ./downloaded.db # downloadfly machine restart <machine-id> # restart a specific machine
fly machines restart # restart all machines| Problem | Cause | Fix |
|---|---|---|
503 Database not ready |
No DB file on volume | Run download scripts locally, then fly ssh sftp put |
| OOM kill on SSH | Not enough RAM for Python | Build DB locally and upload via SFTP |
| SSH auth failed | No SSH certificate | Run fly ssh issue then fly ssh establish |
no_database status |
DB missing at /data/resale.db |
Upload via SFTP |
# Re-download latest data locally
python scripts/download_data.py
# Re-upload to Fly.io
fly ssh sftp put server/db/resale.db /data/resale.db
fly machine restart <machine-id>npx wrangler pages deploy public --project-name=worthitFirst run will prompt browser authentication. This uploads public/ and gives you a live URL (e.g., https://worthit.pages.dev).
- Push code to GitHub
- Go to dash.cloudflare.com → Workers & Pages → Create
- Connect your GitHub repo
- Settings:
- Framework preset: None
- Build command: leave empty
- Build output directory:
public
- Deploy
# Re-deploy after changes
npx wrangler pages deploy public --project-name=worthitThe frontend auto-connects to the Fly.io API via config.js.
The frontend is live at worthit.canlah.app. Here's how it was configured:
- Go to dash.cloudflare.com → Add a site
- Enter your root domain (e.g.,
canlah.app) → Continue - Select Free plan → Continue
- Review DNS records → Continue
- Note the two nameservers Cloudflare provides
- At Porkbun (or your registrar) → Domain Management → Authoritative Nameservers
- Replace with Cloudflare's nameservers
- Wait for propagation (minutes to a few hours)
- Cloudflare emails you when active
- Go to Cloudflare Dashboard → Pages → worthit → Custom domains
- Click Set up a custom domain
- Enter
worthit.canlah.app - Cloudflare automatically creates the CNAME record and provisions SSL
- SSL is automatic (Universal SSL) — no extra steps needed
curl -I https://worthit.canlah.app
# Should return HTTP 200 with Cloudflare headers| Endpoint | Description |
|---|---|
GET /api/status |
Database status and record count |
GET /api/towns |
List all HDB towns + district labels |
GET /api/flat-types |
List all flat types |
GET /api/resolve?q= |
Resolve town name or postal code |
GET /api/area-overview?town= |
Main HDB market overview endpoint (accepts lat/lng for 500m radius) |
GET /api/valuation?postal=&price= |
Deal Score + fair value from storey-adjusted nearby comps |
POST /api/geocode |
Batch geocode addresses |
POST /api/feedback |
In-app feedback (rate-limited, separate feedback.db) |
GET /api/private/projects?q= |
Search private property projects |
GET /api/private/project-overview?project= |
Private project details |
GET /api/private/property-types |
List private property types |
GET /api/private/district-summary?districts= |
District aggregate stats |
GET /api/private/district-overview?district= |
Full district overview |
GET /api/nearby-hdb?lat=&lng= |
Nearby HDB transactions + private projects (800m) |
GET /api/seo/metadata?route= |
Bot metadata for edge-function injection |
GET /api/seo/sitemap |
Sitemap URLs (consumed by Cloudflare edge function) |
WorthIt/
├── scripts/
│ ├── download_data.py # HDB data download (data.gov.sg)
│ └── download_ura_data.py # URA private property data
├── server/
│ ├── index.js # Express API server
│ └── db/
│ └── resale.db # SQLite database (generated)
├── public/
│ ├── index.html # Main SPA
│ ├── config.js # API base URL config
│ ├── css/
│ │ └── styles.css
│ └── js/
│ ├── api.js # API client
│ ├── app.js # Main app logic
│ ├── charts.js # Chart.js configs
│ └── map.js # Leaflet map
├── Dockerfile # Fly.io Docker config
├── fly.toml # Fly.io app config
├── package.json
├── requirements.txt
└── README.md
- Data: data.gov.sg HDB Resale Flat Prices + URA private property transactions
- Pipeline: Python 3 + SQLite
- Backend: Node.js + Express + better-sqlite3
- Frontend: Vanilla JS + Tailwind CSS + Chart.js + Leaflet
- Hosting: Fly.io (API) + Cloudflare Pages (frontend)
- HDB Resale Flat Prices via data.gov.sg
- URA Real Estate Transactions via URA
Prices shown are indicative and based on historical transaction data. They should not be considered as professional valuations. Always consult a licensed valuer or property agent for formal advice.