Nestly
AI home search that turns a plain-English prompt into live Canadian listings.
- Role
- Designer & developer
In short
- 01
LLM parses a natural-language prompt into filters run against live Realtor.ca listings
- 02
Firecrawl scraping with a ~30 min cache; empty results returned rather than sample data
- 03
Leaflet map with clustered pins and Nominatim geocoding; no pin without confirmed coords
Built with TypeScript · Node.js · pnpm · React · Vite · Express · PostgreSQL · Drizzle ORM · Zod · Orval · OpenAI · Firecrawl · Leaflet · Clerk · Replit
Problem
Traditional real-estate search makes people translate their wants into filter forms. I wanted a search where someone can describe their ideal home conversationally, such as "4-bed house in Scarborough under $1M", and get back real listings that match.
Two constraints shaped the build: the market is Canada (Realtor.ca as the primary source), and the product must show only real, live listings, never generated or sample houses.
Approach
The search flow is prompt → AI extracts filters → live listings are retrieved, scored, and summarized. The LLM (OpenAI through Replit AI Integrations) handles prompt parsing and search summaries, and the API also exposes conversational AI endpoints; it never produces listings. Listings come from Firecrawl scraping Realtor.ca, with Zolo.ca as fallback.
Search is map-first: a full-bleed dark Leaflet map with listing pins, a map/list toggle, live counters, an AI search bar, and a property drawer. Detail pages show a photo carousel with lightbox, source fields (year built, MLS#, days on market), a derived price per square foot, and a "View on {source}" link; fields the source did not provide read "Not available". Browsing is public; signing in through Replit-managed Clerk unlocks favorites, saved searches, recently-viewed history, and a notification bell.
Architecture
The repo is a pnpm workspace on Node.js 24 and TypeScript 5.9: a React + Vite frontend (artifacts/home-search), an Express 5 API (artifacts/api-server), and PostgreSQL with Drizzle ORM. lib/api-spec/openapi.yaml is the API contract; Orval generates frontend hooks and Zod schemas from it.
User prompt
│
▼
React + Vite frontend ──► Express 5 API
│
├─► propertySearch.ts parsePromptToFilters (LLM)
│
├─► firecrawlSearch.ts Firecrawl → Realtor.ca (Zolo.ca fallback)
│ ~30 min cache by URL · in-memory property store
│
├─► propertySearch.ts scoreProperties · generateSearchSummary
│
└─► geocode.ts Nominatim → lat/lng → geocode_cache (ok | failed)
misses queued; client polls /properties/geocode-status
│
▼
Leaflet map (supercluster pins) · list view · property drawer
Geocoding never blocks listing retrieval: geocode.ts persists terminal ok/failed results to a geocode_cache table and queues misses; the frontend polls geocode-status about every 2.5 s with a hard cap and draws pins only for listings marked ok. The detail route runs a cached per-listing Firecrawl scrape and merges only observed fields, so missing data stays null.
Identity comes from the Clerk session (cookie auth) in a requireAuth middleware, never from the request body or query; a users table is provisioned just-in-time, per-user routes check ownership by user id, and CORS uses a same-origin allowlist.
Technical decisions
- No fake data, ever → a hard product requirement; when scraping returns nothing the API returns an empty set with an honest summary.
- Backend calls Firecrawl's API directly with
FIRECRAWL_API_KEY→ the managed sandbox callback double-encodes JSON bodies and is unavailable in the Node backend. - Aggressive caching (~30 min for scrapes, ~6 h for featured listings) → conserves Firecrawl credits and cuts latency.
- Favorites and visited history persist the client's property snapshot → survives restarts of the in-memory property store.
- Same-origin CORS allowlist, never reflect-all → auth is cookie-based.
- Price per square foot is derived from price/sqft, never extracted from the listing → part of the same data-honesty rule as the "Not available" handling for missing fields.
Challenges
- Direct server fetches of Realtor.ca and Zillow return 403, so retrieval goes through Firecrawl, which needs
location: { country: "CA" },proxy: "auto", and a city-to-province map for Realtor.ca URLs. - A cold search hits Firecrawl and can take 20–60 s; repeat searches are fast because results are cached.
- Nominatim cannot resolve unit-prefixed addresses such as "1007 - 120 VARNA DRIVE" to street level; those listings show "Location pending" or "Map pin unavailable" badges rather than a guessed pin.
- The property detail store is in-memory and lost on restart, so production would need database persistence for it.
Results
The app covers natural-language search returning live Realtor.ca listings; the map-first search page with clustered pins, toggle, counters, and drawer; a home page with a 3D intro and live "Featured Residences"; property detail pages; per-user favorites, saved searches, recently-viewed history, and a notification bell; and Favorites and Market pages. It is built, and the code is on GitHub.
Kavin AI — an AI version of Kavin
Ask about Nestly.
Kavin AI, the AI version of me, answers from the same approved write-up you just read.