Skip to content
Jozef Čabala
Back to projects

Full-stack web application

FlatWatch

A self-hosted apartment watcher for Bratislava: it reads listing alerts, removes duplicates, scores every flat against my filters and real walking and transit time, and emails me the good ones.

  • Next.js
  • React
  • TypeScript
  • Node.js
  • PostgreSQL
  • Drizzle
  • MapLibre
  • OSRM
  • OpenTripPlanner
  • Docker
Role
Design & full-stack development
Project type
Personal project
Year
2026

Problem

Looking for a flat in Bratislava means checking the same portals every day, seeing the same apartment listed several times by different agencies, and guessing how long the commute really is — listings rarely say more than "close to the centre".

On top of that, the two largest Slovak portals explicitly forbid automated data extraction in their terms of service, so the obvious solution — a scraper — was off the table from the start.

Solution

FlatWatch uses the access the portals already offer: saved-search email alerts. A background worker reads those alerts, fetches each new listing, cleans it up, detects duplicates, calculates real walking and public-transport time to one anchor point, scores the flat against my filters and emails me when something good shows up.

Everything runs self-hosted — geocoding, routing, public transport and map tiles — so there are no third-party API accounts or usage limits to manage.

How it works

  1. Ingest — the worker reads alert emails over IMAP, parses the listing links and fetches the full detail of every new or changed listing.
  2. Normalize & deduplicate — listings are converted into one data model and compared using six signals: coordinates, rooms, area, price, text similarity (pg_trgm) and perceptual image hashes.
  3. Real commute times — addresses are geocoded with Nominatim, walking routes come from OSRM and public transport from OpenTripPlanner 2, fed by the official Bratislava (DPB) GTFS timetable.
  4. Score & notify — every listing gets a match score from configurable weights; matches trigger an instant email and a daily digest.
  5. Dashboard — a map with listings and routes, filters and the selected listing kept in the URL, a photo gallery, and settings for the anchor point, sources, notifications and scoring weights.

Application screenshots

Filters and matching listings
Route preview on hover
Price history, commute and match reasons

Technical challenges

Getting the data legally

Scraping was ruled out by the portals’ terms of service. Building the ingestion around saved-search email alerts kept the core idea — automatic detection of new listings — while staying within what each site allows. Sources are adapters behind one interface, so adding another portal is a new file, not a rewrite.

The same flat, listed many times

Agencies re-post the same apartment with slightly different titles, prices and photos. Deduplication combines structural signals with PostgreSQL trigram text similarity and perceptual hashes of the photos, so reposts end up in one group instead of flooding the list.

A deadlock found in a live run

The first run against a real mailbox hung forever. Marking a message as seen from inside a streaming multi-message fetch never resolves: the connection waits for the stream to finish, and the stream waits for the stuck command. I confirmed it by bisecting, then fixed it by fetching one message at a time and fully draining its stream before sending any other command. The same run exposed a scheduler bug — a stuck run left a source permanently "due", so ticks kept starting overlapping runs — now the scheduler skips sources that are still in flight.

Technology & architecture

A pnpm monorepo with a Next.js dashboard and a separate long-running Node.js worker, sharing one PostgreSQL database managed with Drizzle. Pure domain logic (normalization, scoring, dedup) lives in its own package with no I/O, which keeps it easy to test.

  • Frontend — Next.js, React, TypeScript, MapLibre with self-hosted vector tiles (light and dark map style)
  • Backend — Node.js worker (IMAP ingestion, scheduling, notifications), Server Actions
  • Data — PostgreSQL, Drizzle ORM, earthdistance/cube geo queries, pg_trgm
  • Routing & geo — OSRM, OpenTripPlanner 2 (DPB GTFS), Nominatim
  • Infrastructure — Docker Compose on a single server, shared-password access

Result

FlatWatch works end to end on real data: real alert emails are processed, new listings are fetched, deduplicated, geocoded and routed against Bratislava’s actual public transport, and notification emails — including the daily digest — arrive on their own. I use it for my own flat search; the next step is adding a second listing source.

Image 1 of 4

FlatWatch dashboard: a selected flat with its walking route to the anchor point on a map of Bratislava, and its details

Need something similar?

Tell me a few sentences about your project and I’ll reply with next steps.

Get in touch