← back to LedgerLamp

Stack

What's actually running.

The technical substrate behind LedgerLamp — the libraries, the storage, the guardrails, and the deliberate choices about where determinism ends and the model begins.

The stack

PHP 8.4 · MySQL · PDO
Deterministic rule + insight engine
Claude (Haiku) · temp 0 · prompt caching
Propose/verify auditor + number-grounding
Tesseract.js · on-device OCR
Append-only audit log · per-call observability
CSRF · rate-limit · bcrypt
PHPUnit · 180 tests · extraction eval
GitHub Actions CI · MySQL 8 service container
Lint + secret scan gates · no skipped tests
No framework · No Composer

Security

CSRF protection on all state-changing requests. Login rate limiting with configurable attempt windows. Session management with configurable lifetime. All inputs validated at the boundary. Designed for single-household self-hosting — this is not a multi-tenant SaaS, and the architecture reflects that.

Privacy model

Pseudonymous accounts — no real name required. Raw email bodies are nulled when an intake item is approved or rejected. Data retention is configurable per household: raw documents from none to 12 months, structured data from 3 to 12 months. Every create, update, and delete writes to an append-only audit log. One exception is worth stating plainly rather than leaving for a reader to find: the extraction audit trail keeps the short verbatim snippet the model cited for each proposed field, because a propose-and-verify pipeline you cannot re-check afterwards is just a claim about itself. Those rows outlive the nulled document, are not household-scoped, and are not covered by the retention setting — a real gap, on the board as an open ticket rather than papered over here.

Tested & measured

180 automated tests on PHP 8.4 (PHPUnit, no Composer), run in CI against a real MySQL service container on every push: the utilization math and every deterministic insight rule, the extraction auditor and the narrative grounding check (stubbed model, no network), plus DB-backed integration tests for the save path, login, rate-limit lockout, and a full dashboard render against a dedicated test database. Each test is mutation-checked — reverting the fix has to turn it red, or it is not a test. CI provisions the database rather than letting the DB-backed half skip itself, and a skipped test fails the build, so a green tick cannot mean “half the suite never ran.” A PHP lint sweep and a credential scan gate the same pipeline. The AI is held to an eval, not to vibes.

Data model

Household-scoped workspace model — one user can belong to multiple households, each with its own set of accounts and bills. Credit accounts track balance, utilization, APR, promo APR with expiry, autopay status and type, statement close date, and payment due date. Bills track recurrence, category, and autopay — and, for a fixed-term obligation like a buy-now-pay-later plan or a car loan, the principal, the number of payments in the term, how many have been made, and what the money bought, so the plan counts down to a final payment and then closes itself instead of scheduling one that is not owed. Account snapshots preserve a rolling history for trend data.

Deterministic insight engine

Rules fire against live account data and produce insight events with severity (info / warning / urgent) and confidence (deterministic / extracted / inferred / needs review). Examples: statement closing within 5 days on a card carrying a bureau balance, autopay status unknown on a card with a balance, promo APR expiring within 45 days. No black-box scoring — every insight has a traceable rule behind it.

Reminders, made idempotent

An insight answers “what is true right now” and is recomputed on every page load. A reminder is different: it is durable, it is dismissible, and it carries a date, so re-running the planner must not quietly produce a second copy of yesterday's notice. The planner is pure — rows in, specs out, no database and no clock of its own — and every spec carries a dedupe key built from the thing and the date it is about, which the storage layer enforces with a unique index. Paying a bill advances its due date, which mints a new key for the next cycle; dismissing one is permanent, because a dismiss button that un-dismisses itself is a lie. Reminders surface in the app: there is no mail transport on the host, so the product does not promise mail it cannot send.

AI, kept on a leash

AI shows up in exactly two places, and never decides anything about your money. Intake: Claude proposes each field with the verbatim source snippet it came from; a deterministic auditor checks that value against the source and labels it extracted, inferred, or — if it can't be found — needs-review, which is dropped rather than guessed. Trends: a plain-English monthly summary where the model only narrates figures the deterministic engine already computed, and a number-grounding check rejects any dollar or percent it didn't supply. Built like production AI: Haiku, temperature 0, prompt caching, a prompt version stamped on every call, with token count and latency logged per call — and an eval harness that scores extraction precision/recall against a labeled set (currently 1.0, hallucination traps included).

Email intake pipeline

Three intake paths are live: paste an email; snap a photo of a bill — on-device OCR (Tesseract.js, vendored locally) reads the image in your browser so the photo never leaves the device; or bulk-paste a pile of statements (or the accounts list you copy out of a banking app) and import every card and bill in one pass, each record verified independently so a slip in one can't taint the rest. EML upload, IMAP polling, and webhook intake are on the roadmap. Raw content lands in an intake queue and is presented for structured review: a deterministic pattern-matcher proposes candidate values — balance, due date, minimum payment, statement balance — each carried with a confidence marker. You approve or correct before anything is written to the account record, and raw bodies are discarded after approval by default. The AI upgrade is now live and keeps the same discipline: an LLM proposes each field with the exact source snippet it came from, a deterministic auditor verifies every value against that source — present verbatim is confirmed, reformatted is flagged for review, ungrounded is dropped rather than guessed — and only grounded fields surface for your approval. The model proposes; deterministic code disposes.