← All posts

August 9 - treating the listing database as a cache, not the record

The thing that clicked today: if the source can rewrite itself, then your normalised view is a cache, and the only durable record is the bytes you captured at the time.

Homestead is a house-hunting workspace. A browser extension captures a listing from the tab I already have open, and the app turns it into something comparable across properties. The interesting constraint is that listing sites are not an archive. Prices change, photos get swapped, descriptions get rewritten, and when a house sells the page can simply go away.

Built / shipped

Every capture is written as an immutable generation: a JSON document in a private directory, 0600, never edited after the fact. A capture of the same property later does not replace the earlier one, it appends another generation. The SQLite index beside it holds the canonical row per listing, capture lineage, flattened facts and metrics, media, price events, tax events and schools.

The part that matters is the direction of authority. The database can be rebuilt from the generations. It is derived. If the index is corrupted or the schema changes shape, the recovery path is to rebuild it from evidence, not to restore a backup of the index and hope.

Media archival landed the same way. During save, the app downloads eligible photos, floor plans and direct videos, bounded per asset and per capture so one pathological listing cannot fill the disk. Bytes are SHA-256 content-addressed, so the same photo appearing in two listings, or in three generations of the same listing, is stored exactly once. The dossier prefers the archived copy and reports how many assets succeeded and how many failed, rather than silently showing you the live URL and pretending it is yours.

Hosted 3D tours keep their metadata and link plus any downloadable preview, because cloning an interactive app is not something a capture can honestly claim to have done.

Problems & fixes

Zillow's page contains more than one object that looks like the property. An early version of the adapter confidently picked a nested recommendation, so the capture was of a house I had never looked at. The fix was selecting the canonical property object explicitly rather than the first plausible match; the lesson is that "it parsed" and "it parsed the right thing" are different tests.

Redfin's facts arrived as a nested amenity dump that rendered as an unreadable wall. Rather than flatten it further, the view now derives proper grouped fact cards and a real room-and-dimensions table from the numbered room groups, and hides the machine-only reference keys from the presentation layer while keeping them in the raw evidence. The immutable generation is allowed to be ugly. The view is not.

One saved capture needed repair after a transient download failure left assets missing. Because the generation was intact, the repair was a rebuild-and-re-archive command against evidence that already existed, and the original generation was preserved untouched.

Decisions

Capture is user-triggered and active-tab only. No crawler, no background monitor, no logging in on my behalf. It reads the page I am already looking at, which keeps the whole thing on the right side of a line I care about.

Deploys require a clean committed tree, install an immutable runtime, run canaries, and automatically restore the previous runtime if a canary fails. The rollback path is not a document, it is the default behaviour.

Learned

Immutability made everything downstream easier to reason about. Repair, dedup, comparison and rebuild are all straightforward when nothing you depend on can be edited behind you. Most of the design difficulty was resisting the urge to "just update the row".

Still open / next

The comparison endpoint returns the cross-listing fact matrix but nothing consumes it properly yet. That is the next piece: making the comparable view earn the capture pipeline underneath it.