How we built a tamper-evident accounting ledger for retail SMBs using SHA-256 hash chaining
At Momentum (a product by ltiora) (https://ltiora.com/), we build retail and wholesale ERP software. One of the more interesting engineering problems we tackled was making our accounting ledger tamper-evident, meaning retroactive modification of historical financial records is cryptographically detectable. Most accounting software for SMBs treats historical records as editable data. An administrator with sufficient access can modify a past journal entry, and while most platforms log user actions, those logs are themselves editable. An employee with enough access can modify a transaction and remove the log entry that recorded it. For retail businesses with multiple staff touching finances, this creates silent audit risk that most operators don't consider until they're in a dispute or undergoing due diligence. We applied the same integrity principle used in blockchain systems to each accounting entry. When a journal entry is written: We compute SHA-256(entry_data) The resulting hash is stored in the previous_hash field of the next journal entry This creates a chain: modifying Entry #N changes its hash, which no longer matches what Entry #N+1 says it should be interface LedgerEntry { id: string; sequence_number: number; posted_at: string; debit_account_id: string; credit_account_id: string; amount_cents: number; description: string; previous_entry_hash: string | null; // null for genesis entry entry_hash: string; // SHA-256 of all fields above } function computeEntryHash(entry: Omit): string { const canonical = JSON.stringify({ id: entry.id, sequence_number: entry.sequence_number, posted_at: entry.posted_at, debit_account_id: entry.debit_account_id, credit_account_id: entry.credit_account_id, amount_cents: entry.amount_cents, description: entry.description, previous_entry_hash: entry.previous_entry_hash, }); return createHash('sha256').update(canonical).digest('hex'); } We expose a chain verification endpoint that traverses all entries in sequence order and confirms that each entry's previous_entry_hash matches the hash of the preceding entry. async function verifyLedgerIntegrity( tenantId: string ): Promise { const entries = await db.ledgerEntries .where({ tenant_id: tenantId }) .orderBy('sequence_number', 'asc') .all(); let previousHash: string | null = null; for (const entry of entries) { if (entry.previous_entry_hash !== previousHash) { return { valid: false, broken_at_sequence: entry.sequence_number }; } const expectedHash = computeEntryHash(entry); if (entry.entry_hash !== expectedHash) { return { valid: false, broken_at_sequence: entry.sequence_number }; } previousHash = entry.entry_hash; } return { valid: true }; } Appends only: The hash chain only works for append only ledgers. This maps well to accounting (journal entries are never modified; corrections are made by posting reversing entries), but it means the business logic layer must enforce append only strictly. Replication: In a multi-region setup, the genesis entry's hash must be agreed upon before any writes. We use a leader election approach for ledger writes to ensure sequence numbers and hashes are assigned deterministically. Performance: Verification is O(n) in the number of entries. For high-volume retail (millions of transactions/year), we run verification asynchronously on a schedule rather than on every write. We deliberately didn't implement blockchain style distributed consensus — that's overkill for a single tenant accounting system. The threat model here is internal fraud and accidental modification, not a distributed Byzantine failure. A simple hash chain is sufficient. If this is interesting to you, we published a user facing explanation of the concept here: https://ltiora.com/blog/tamper-evident-accounting-software
