ledgerbridge

LedgerBridge: two-way invoice sync that survives the real world

Keeps an internal invoicing system and QuickBooks Online in lockstep through duplicate, out-of-order and dropped webhooks: every write idempotent, every conflict held for a human, every event replayable.

Solo: design, build, infra
TypeScriptFastifyQuickBooks APIDrizzlePostgresZodVitest

TL;DR

  • Two-way invoice sync between an internal system and QuickBooks Online, built to survive duplicate, out-of-order and dropped webhooks.
  • Every write is idempotent: a create that times out after it actually landed is adopted, never duplicated.
  • 87 backend + 15 web tests, all deterministic against a real Postgres schema, no DB mocks.

Problem

A webhook is a ping, not the truth.

Real integrations never deliver clean events. Webhooks arrive duplicated, out of order, with half a payload, or never at all. Trust the payload and you double-charge, drop an edit, or wedge two ledgers out of sync. LedgerBridge treats every event as a hint to go refetch the real state, then applies it exactly once.

The bridge: an invoice leaves the internal system, lands in the durable outbox, and is applied to QuickBooks exactly once

Architecture

internal system ─webhook─▶ ingest (verify · dedupe · enqueue)
                                │
                          durable outbox ─▶ leased worker: refetch → map → resolve → apply
                                │                                              │
   reconciler sweeps drift ◀────┘        idempotent write ─▶ QuickBooks Online ┘

Two ideas carry the whole design: refetch, don't trust the payload, and idempotent by external id. A Postgres outbox drained by a single leased worker (FOR UPDATE SKIP LOCKED) means duplicate or out-of-order events can never double-apply.

Field-level diff between the internal record and its QuickBooks counterpart, with the drift and the audit trail beside it

Key decisions

Refetch on every event, not the webhook body

Chose to refetch current state from the source before applying, instead of acting on the payload. Trade-off: an extra API round-trip per event, but out-of-order and partial webhooks stop being a problem in one move.

Flag-and-hold conflicts, not last-writer-wins

When both sides edit the same field, chose to hold both directions and queue the conflict for a human, instead of silently picking a winner. Trade-off: someone has to resolve it, but blind last-writer-wins on unsynchronized clocks quietly loses real money.

The conflict resolver: both sides of a same-field divergence, held until an operator picks the winner

A durable outbox over a hosted queue

Chose a Postgres outbox table plus a leased worker over SQS or Inngest. Trade-off: I own the retry and backoff logic, but the entire pipeline is one queryable, replayable table, and the write-up names the hosted queue as the production upgrade.

Idempotent by external id

Before creating in QuickBooks, chose to check by document number first. Trade-off: an extra read, but a create that timed out after it actually landed gets adopted instead of duplicated. That is the one failure that would corrupt a ledger silently.

The events log: every webhook idempotent, retried with backoff, and dead-lettered on exhaustion with one-click replay

A webhook is a ping, not the truth. Once every event just meant "go refetch and converge," idempotency and conflict handling stopped being special cases and became the default.

the idea the whole engine rests on

Harder than expected

Loop prevention. An outbound write to QuickBooks fires a webhook straight back; without echo detection (a state hash plus the QuickBooks SyncToken) the two ledgers ping-pong forever. Proving the echo is dropped in both directions, internal to QBO and back, took the most careful test design in the project.

Results

  • 87 + 15 deterministic tests, backend + web, real Postgres, no DB mocks
  • 10 reproducible end-to-end flows, each pinned to a test
  • 9 findings from an independent security review, all fixed

Live + source

The live dashboard runs on seeded data, no login: watch events flow, open a conflict, replay a dead-letter. The demo panel drives a real QuickBooks sandbox.