Get in touch
All systems

// teardown

The bartr warehouse

Two stores. One writer each.

in production·2026
ConvexClickHouse 26.3 LTSTypeScriptHonoNext.js 16Cloudflare R2

Operational store

Convex
↓

ClickHouse warehouse

One-way mirror
Parcel catalog
Listings mart
↓

Serving

Read relay
Browse
↓

Operations

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.

chose

Two stores: Convex operates, ClickHouse remembers

not

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.

chose

A one-way mirror with generated columns

not

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.

chose

Reader credentials chosen by the relay route

not

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.

chose

Personal data in databases no reader is granted

not

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.

chose

Browse from the warehouse, promote on first engagement

not

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.

// by the numbers

164
Convex tables mirrored
5.9M
public NC parcels in the catalog
0
reader grants on personal data

What it did

bartr case study

// let's talk

Let's build something.

Have a product that needs both sharp strategy and someone who can ship it? I'd love to hear about it.

Get in touch

Command Palette

Search for a command to run...