Skip to main content
Your database stays the system of record. Brussle holds a copy of the records you judge, and you keep that copy current through the write API. There is no automatic sync from your database yet. This guide is the pattern that keeps the copy correct: send whole documents after each change, make sure no change is missed, and check now and then that both sides agree. For the first load of existing records, see import existing data.

Send the whole document

After your database commits a change, upsert the record’s current version.
  • Upsert whole records. An upsert replaces the whole document, so the result never depends on what was there before, and sending it twice is harmless. A patch merges fields into the stored document, so the result depends on what it lands on, and nothing sent later repairs a field it did not name.
  • Read the record when you send it, not when it changed. If two changes to the same record are sent out of order, the later write wins, so a write built from an old event can put stale data back. Sending the record as it is now avoids that.
  • Write after your own commit, never before it. Otherwise a change your database rolled back can reach Brussle.
  • Record your own version in an attribute, such as source_version (a version number or your updated_at). Brussle’s revision and updated_at say when it received a write; source_version tells you which of your versions it holds, which is what the check compares.
  • Delete with delete. When you delete a record, send its id in {"delete": ["order_line:5521"]}.
  • Batch when you can. One write takes up to 1,000 documents, so a sync that sends changes every few seconds costs far fewer requests than one per change.
Resending a document that has not changed costs only its write bytes. A judgment whose compiled context did not change keeps its answer and is not billed.

Never miss a change

Writing to Brussle right after your commit is simplest, but a crash or a network error between the two loses that change. If every change must arrive, use an outbox:
  1. In the same transaction as the change, insert a row into an outbox table: the record’s id and kind.
  2. A worker reads unsent outbox rows in order, loads each record as it is now, and upserts them in batches (or deletes the ones that no longer exist).
  3. When the write succeeds, it marks those rows sent. On an error it retries: every write is idempotent, so a retry is always safe.
If several rows name the same record, send it once. Keep one worker per namespace, or split records between workers by id, so the same document is never in two writes at once.

Check that both sides agree

Even a careful sync can drift: a bug, a restore from backup, a record changed by hand. Run a check on a schedule, nightly for most teams.
  1. Find the records changed recently in your database, say in the last two days, with their versions.
  2. Read the same ids from Brussle with a query that returns only source_version:
  3. Upsert every record that is missing or has an older source_version.
  4. Find deletions less often, weekly say: page through the namespace’s ids with rank_by: ["id", "asc"], a Glob filter on the id prefix to keep each scan small, and next_cursor, and delete any id your database no longer has.
Queries are billed by the bytes they scan, and one that includes no state scans little.

Get told instead of polling

The sync above keeps Brussle’s copy of your records current. The other direction, finding out when an answer changes, needs no polling either. Instead of running a query every few minutes for the documents whose answers crossed a line, save that query as a subscription. It sends subscription.entered when a document starts matching and subscription.exited when it stops, to a webhook endpoint and the events feed.
To keep a list in your database of the documents that match, such as an escalation queue:
  1. Create the subscription, with both entered and exited, and wait until its status is live, or for its subscription.synced with reason: "created".
  2. Seed the list from the subscription’s query, which runs the same filter over the documents now.
  3. Apply each event as a change to the list: add on entered, remove on exited. Deduplicate on the event’s id, and for each document keep the highest sequence you have applied, ignoring anything older: deliveries can repeat and arrive out of order.
  4. On subscription.synced, read the list again with the query. A backfill or a threshold edit moves documents without an event for each (see bulk changes).
A subscription only sees settled answers, so a write doesn’t make a document leave the list while its new answer is pending and come back when it lands. Subscriptions and their events are not billed, while every poll is a query billed by the bytes it scans. The nightly check can compare your list with the subscription’s query too.

When to use patch or append

patch and append still have their place when you do not hold the whole record, such as an event stream that only knows the new message. Append skips any value already in the array, so a retry adds nothing, however late, as long as the values are still there. The same value is never appended twice, so give each event something unique, such as its id or timestamp. Concurrent patches and appends to one document are all applied, in the order they commit (see system behavior). What they cannot do is correct a field that an out-of-order write left stale; a whole record sent from your system of record, or the scheduled check, does that. When Brussle holds the only copy of a document you change, such as a rulebook you edit in place, read it, change it and write it back with if_revision, so a change someone made in between is refused rather than overwritten.