// teardown
The bartr warehouse
Two stores. One writer each.
Operational store
ClickHouse warehouse
Serving
Operations
saving a home promotes it into Convex
// overview
A real-time app database should run the product, not carry its history or a state's worth of public records. bartr keeps Convex as the operational store and gives ClickHouse everything else: a one-way mirror of the app, a 5.9M-parcel public catalog, and the listings mart that browse reads. Nothing is dual-written, readers never choose their own credentials, and personal data sits in databases no reader is granted.
// how it works
01 · operate
Convex runs the product
Convex is the operational store: accounts, homes, saves, messages, and every live query the product renders. It is built for small transactional writes and reactive reads, and that is the only job it is given. Product writes land here and only here. The warehouse receives copies after the fact, never a second write in the same request, so there is no pair of stores to drift apart and no question about which copy is true.
02 · mirror
A one-way mirror, generated from the schema
A sync loop follows Convex's change stream with one cursor per deployment and copies 164 tables into ClickHouse, where the newest revision of each document wins. Columns are generated from the Convex schema, and a drift test fails any schema change that skipped codegen. The raw document rides alongside, compressed, so a column typed wrong is re-derived instead of re-synced. Tables holding identity, private correspondence, or financial detail land in separate personal-data databases.
03 · catalog
The catalog belongs to the warehouse
The parcel catalog never passes through the app. Every parcel NC OneMap publishes statewide, about 5.9 million, loads into ClickHouse county by county and picks up its ZIP boundary on insert. A licensed property-data API then enriches the launch market against a monthly request budget that Convex owns and hands out in blocks. Raw responses are stored compressed, so reprocessing never means paying for the same data twice.
04 · derive
A listings mart built for one read
The browse grid reads a single table. A derive step joins public parcels with the enrichment, mined attributes, and photos into a listings mart keyed for serving, with the displayed price defined once in SQL so the filter, the sort, and the card cannot disagree. The mart joins nothing from the Convex mirror, so no user state can reach a cached page. It is a pure derivation of the catalog, which makes a rebuild seconds of compute and zero vendor spend.
05 · access
The route picks the credential
ClickHouse has no public endpoint. Convex actions, public route handlers, the admin console, and AI tools each call their own relay route, and the relay chooses the database user from the URL path, never from anything in the request. Every reader is read-only at the server, with its own row, memory, and time limits and its own connection pool. No reader is granted the personal-data databases, and a missing reader credential fails the request rather than falling back to the writer.
06 · serve
Cached at the edge, live where it matters
Browse and detail are unauthenticated route handlers, cached at the CDN for minutes and served stale for up to a day, so repeat views skip the warehouse. Empty results are never cached. What only Convex knows (saved, on bartr, promoted) comes from a live query for just the rows on screen. The first time someone saves a home, that parcel is promoted into Convex in one transaction keyed on a canonical address, and every later engagement finds the same row.
07 · watch
Observed from outside, not self-reported
Health checks run from Convex, outside the services they watch. Every five minutes a probe requests browse the way a visitor would and counts an empty 200 as down. Hourly, the common filter combinations are warmed so the edge has something to serve if the warehouse is unreachable. Every 15 minutes the mirror is checked, alerting on a failing sync or a silent heartbeat. Daily, a native BACKUP writes the catalog to R2, and a restore drill proves it comes back with matching row counts.
// decisions
The parts that were a call, not a default — and what the alternative would have cost.
Two stores: Convex operates, ClickHouse remembers
One database for the product, its history, and the catalog
A reactive store is tuned for small consistent writes and live queries. Asking it to scan millions of parcels or aggregate its own history makes every request pay for work the product never shows. Splitting by workload lets each engine do the job it is good at, and keeps the seam explicit: a copy in one direction, a promotion in the other.
A one-way mirror with generated columns
Dual writes from application code
Dual writes drift the first time one side succeeds and the other fails, and nothing notices until the numbers disagree. A mirror follows what Convex actually committed, so ClickHouse converges on the truth rather than on what the app attempted. Generating columns from the schema means a new field arrives without hand-written DDL, and the raw document is kept for the day a generated type is wrong.
Reader credentials chosen by the relay route
Letting callers pick a role, or sharing one read-only user
If the caller names the credential, a compromised caller names the widest one. Binding the user to the URL path puts that choice on the server, and separate readers let the public browse credential rotate alone without taking internal analytics down with it. Safety comes from server-side grants and read-only profiles, not SQL parsing, because a keyword filter is one UNION away from bypassed.
Personal data in databases no reader is granted
One mirror database guarded by app-layer checks
An app-layer check protects nothing once a client can send its own SQL, and a grant that does not exist cannot be misused. Identity, correspondence, and financial tables mirror into their own databases with no reader grants. Live credentials such as bank access tokens go further: they are stripped from the typed columns and the raw document, because backups and relay access sit outside any grant.
Browse from the warehouse, promote on first engagement
Importing every parcel into the app database
Browsing does not need a Convex row; engaging does. Keeping browse a cached, stateless read means millions of parcels never occupy operational storage, and a home enters Convex only when a person does something that needs a row. Promotion is idempotent and keyed on the same canonical address the rest of the app dedupes on, so two paths to one home cannot create two homes.
// guarantees
Personal data walled off
Identity, correspondence, and financial tables mirror into databases no reader is granted. Live credentials are stripped before a row is ever written.
One writer, one direction
Product writes land only in Convex. The warehouse receives copies through a one-way mirror, and every read runs on a read-only credential the server picks.
Fails loud, not blank
An outside probe counts an empty 200 as down, empty results are never cached, and the daily catalog backup is proven by a restore drill.