Case Study
BookEasy
Self-hosted bookkeeping & bank reconciliation for a two-storefront eCommerce business.
BookEasy is a full double-entry accounting and bank-reconciliation web app I built to run the books for a real eCommerce business that sells one shared inventory across two storefronts. Its guiding idea: hide the accounting machinery behind plain-language screens. The owner records a “sell” or a “buy” — and the app generates the balanced journal entries, tracks FIFO inventory cost, reconciles incoming bank deposits against sales, and produces financial statements — without anyone ever typing a debit or a credit.
Note: all screenshots below use sample / demo data (a throwaway database), not real customer records.
Tech stack
- Language / framework
- TypeScript · Next.js 16 (App Router, Server Components)
- Database
- PostgreSQL 17 (Docker) · Prisma 7
- Money math
- decimal.js — never IEEE floats for currency
- Validation
- Zod at every import / API boundary
- UI
- Tailwind CSS 4 · shadcn/ui · dependency-free SVG charts
- AI / search
- pgvector embeddings · Tesseract OCR + Claude vision for invoices
- Testing
- Vitest — pure domain logic (money, posting, FIFO, reconciliation)
Dashboard — this month at a glance
The home screen answers “how am I doing, and what needs attention?” in one view: month-to-date revenue and profit, inventory value at cost, what’s owed to consignment suppliers, and how many bank deposits still need matching — each card links straight to the relevant workflow. Recent sales span both storefronts with per-sale profit.

Business-friendly by design: Sell & Buy
The daily actions are framed as Sell and Buy, not “journal entries.” The double-entry engine still runs underneath — every sale and purchase posts a balanced entry automatically — but it’s invisible to the user.


Two storefronts, one inventory, per-store pricing
The business sells the same physical stock through two storefronts; only the selling price differs per store. Inventory, cost, and purchasing are shared — pricing is not.


Bank import & reconciliation
Statement lines come in via CSV; the app matches incoming Zelle deposits to the sale that produced them.


Consignment settlements
Because consignment stock is “pay when sold,” the app tracks what’s owed to each supplier as their goods sell. Settling records the payment (Dr Accounts Payable / Cr Cash) and clears the balance — voided sales are correctly excluded from the amount owed.

Financial reports
Everything above rolls up into real financial statements.



Engineering highlights
▹ Money is never a JavaScript number.
All amounts flow through a single Decimal-based money module; DB columns are Decimal(18,2). This kills an entire class of rounding bugs.
▹ One double-entry invariant, in one place.
Every posted entry has ≥ 2 lines and SUM(debit) === SUM(credit); the check lives in one posting function that nothing bypasses. All lines write in one Prisma transaction — no half-entries.
▹ Posted entries are immutable.
Corrections are reversing entries, never edits or deletes, preserving a clean audit trail. Sales void by restoring the exact FIFO stock lots and posting the reversal.
▹ FIFO inventory as cost layers.
Stock is tracked per (product, supplier) as dated lots; sales consume the oldest lots first for true COGS rather than a weighted average.
▹ Business terms up front, ledger underneath.
Purchases and sales auto-generate balanced entries — the accounting engine is kept but hidden.
▹ Validation at the edge.
Every CSV row and API input passes through a Zod schema before touching the database.
▹ Local-first AI.
Semantic product matching uses local pgvector embeddings; invoices are read with local Tesseract OCR, falling back to Claude vision for hard photos and mixed-language invoices.
Architecture notes
- ▹Next.js App Router with Server Components for data-heavy pages; a thin client layer only where interactivity is needed (import mapping, reconciliation, forms).
- ▹Prisma 7 with a generated client; schema-first, migration-driven.
- ▹Pure, unit-tested domain modules (money, accounting, inventory, sales, reconcile, reports) sit under the routes, so business rules are testable in isolation from the UI.
- ▹A dedicated test database is the sandbox for any write-verification; scripts guard on the database name so live data is never touched.
What I'd build next
- ▹Bring outflows (fees, refunds, expenses) fully into the ledger so the bank balance ties out to the penny.
- ▹Report drill-down (click a total → see the underlying entries) and a cash-flow statement.
- ▹Excel / PDF export.
- ▹An event pipeline (Kafka) and rules engine for higher-volume reconciliation.