Ledgertwo systems that disagree

A working demo · not a product

The CRM says one thing. The books say another.

Every integration job eventually becomes this: two systems holding the same customer, both edited by people who never speak, and a nightly job that has to decide. Ledger is that job, with its reasoning showing — and with the two rules that stop it quietly destroying data.

Break it on purpose with the buttons below. Then run it twice and watch the second run do nothing at all.

Aster CRM

Tallowbook

linked and agreeing a value moved this run both sides changed held: the transform will not run backwards proposed, waiting for a person

The field mapand which way each field is allowed to move
Mapping two systems nobody has mapped beforethe only part a model touches

Field names are a language problem, so this is the one job here handed to a small open-weight model. Two things keep it honest. A plain string-similarity pass runs first, so there is always a deterministic answer to compare against. And every line the model returns is checked against the real field names before you see it — a mapping onto a field that does not exist is not a suggestion, it is noise. The sync engine itself never sees a map a person has not confirmed.

Why it is built this way

A baseline per field, not per record

Comparing two records tells you they differ. It cannot tell you who changed, and a sync that guesses is a coin toss where the last writer wins. Ledger stores the value both sides agreed on, field by field, so drift on each side is known independently — and “both changed” becomes a conflict with a policy instead of an accident.

three-way, not two-way

Some transforms only run one way

A first and last name join into a display name exactly. Splitting one back apart is a guess, and a guess repeated nightly rewrites somebody’s name over a year. So each mapping declares whether it is reversible, and the unsafe direction is reported and held rather than applied. That single rule is most of the difference between a sync you can leave running and one you cannot.

lossy → reported, never guessed

The plan is the product

A dry run is not an estimate of what would happen; it is the same decision list the real run executes. So you can read it, argue with it, show it to a client and store it beside the run. Every line names the field, the direction, the old value, the new one and the rule that chose.

plan → apply · identical code path

The second run does nothing

A run repeats its passes until nothing changes, capped at three — because creating a record on the far side produces a record that then needs its own computed fields, and a single-pass engine leaves that until tomorrow. Run it again and it writes zero. That is the property you actually want from something running at four in the morning.

idempotent · and it says so

What it is made ofand what it will not do

Built with

  • Cloudflare Workers on ledger.malikakhtar.dev/api/*
  • D1: one JSON document per workspace, so a run is atomic without a transaction
  • Dice coefficient on character bigrams for fuzzy matching — no dependencies
  • Workers AI (a small open-weight model) for proposing a map between unfamiliar shapes
  • Hand-written HTML, CSS and JavaScript. No framework, no build step

Limits, on purpose

  • Workspaces are kept for 7 days

Aster CRM and Tallowbook are invented, and so is every person, company and invoice in them. Nothing here connects to a real system: this is the decision engine, which is the part that is hard and the part that is the same whichever two products you name.