All writing
SwiftHum · 5 min read

Why the application node — not the database — is authoritative on balances

An offline-capable P2P payment system can’t trust the database as the single source of truth for a balance. Here’s where authority actually has to live, and why.

The problem

SwiftHum moves money between two phones that may share only the air between them — a payment token is encoded into ultrasonic sound and played from one device to another, with no internet on the receiver. That constraint breaks the assumption most CRUD apps quietly rely on: that the database is always reachable and is therefore the natural place to decide whether a balance is sufficient.

If the receiver is offline at the moment of transfer, "read the balance, check it, write the new balance" is not a transaction you can run end-to-end against the database at the point of sale. So the question becomes: what component is actually allowed to say a balance is real?

The naive approach, and why it fails

The naive design makes the database row the source of truth and treats every service as a thin proxy over it: to spend, you SELECT the balance, subtract, and UPDATE. Under any concurrency this races — two transfers read the same balance and both succeed, double-spending. The usual patch is a row lock or a "current balance" column guarded by a WHERE clause, which helps online but does nothing for the offline case, where the spend decision has already left the building on a sound wave before the database ever hears about it.

Storing a running balance as a mutable column also throws away history: when two views of the balance disagree — which they will, across an offline hop — you have no ledger to replay and no way to prove which transfers are legitimate versus replayed.

The decision and its trade-offs

SwiftHum inverts the authority. The balance is not a mutable number the database owns; it is a derived fact over an append-only ledger, and the authoritative decision to admit a transfer lives in the payments service, enforced with HMAC-SHA256-signed tokens settled under Serializable isolation.

Each token is signed, so a receiver (or the settling node) can verify authenticity without trusting the transport. Settlement is idempotent: replaying the same signed token — the obvious attack on an offline, broadcast medium — is a no-op, because the ledger keys on the token’s identity, not on wall-clock arrival. Serializable isolation on the settling path means concurrent transfers are ordered as if they ran one at a time, so the "both reads saw the same balance" race cannot resolve into a double-spend.

The trade-off is cost: Serializable isolation is the strictest and most contention-prone level, and an append-only ledger is more storage and more query work than a single mutable column. That’s the right trade for money — correctness under adversarial replay beats throughput on a hot row — but it would be over-engineering for, say, a view counter.

How it was verified

The properties worth checking are exactly the failure modes above: replay a captured token and confirm settlement is a no-op; fire concurrent transfers against the same balance and confirm the ledger admits only what the balance supports; and reconcile the derived balance against the ledger to confirm they never drift.

The general principle travels beyond ultrasonic payments: when a system has to stay correct across an untrusted or intermittent boundary, authority belongs to the component that can enforce it (a signed, idempotent, serializable ledger), not to whichever datastore happens to hold the latest number.