Case Study

BookEasy

Self-hosted bookkeeping & bank reconciliation for a two-storefront eCommerce business.

RoleSolo developerScopeProduct · data model · backend · UIStatusSelf-hosted, single-tenant, live data

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.

BookEasy dashboard showing this month's revenue and profit, inventory value, consignment payables, unreconciled deposits, and a recent-sales table

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.

Sales form with store selector, customer, auto-selected source supplier, and a live recent-sales list across both stores
Recording a sale auto-selects which supplier's stock fills the order (so it can never oversell), consumes inventory FIFO to compute COGS, and posts the revenue/COGS entry.
Purchases form and recent-purchases list showing cash vs consignment supplier types
Purchases support two supplier models: cash (Dr Inventory / Cr Cash) and consignment (intake posts nothing; the liability appears on sale).

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.

Products & Inventory catalog with SKU, on-hand quantity, and average cost that build up automatically
On-hand quantity and average cost are never entered by hand — they accrue automatically from purchases and sales.
Store prices matrix: each product priced independently for the two storefronts
A price matrix sets each product's price per store; those prices auto-fill the sale form (still editable at the point of sale).

Bank import & reconciliation

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

Bank workspace, Transactions tab: imported statement lines with unmatched / matched / ignored status badges
Bank workspace, Reconcile tab: unmatched Zelle deposits with suggested matching sales and one-click confirm
Confirming a match learns the sender's name → customer alias, so future statements from the same person auto-match — no AI required. Re-importing the same CSV is idempotent.

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.

Consignment settlements: total outstanding, per-supplier balance, sold-this-month, stock held, and a one-click Settle action

Financial reports

Everything above rolls up into real financial statements.

Profit analysis: best month, best seller, most profitable product, a two-month profit bar chart, and profit by product and store
Profit analysis breaks results down by product, by month, and by store — the two-store split shows which storefront actually earns more.
Income statement (P&L): revenue, shipping income, COGS, and expenses netting to net income
Balance sheet with a green Balanced banner confirming Assets = Liabilities + Equity
The 'Balanced' banner is live proof that the double-entry ledger is internally consistent (Assets = Liabilities + Equity), with a synthetic 'current earnings' line so the identity holds intra-period.

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.