Greenbar ClearingGreenbar Clearing — a local-first bank reconciliation app for mid-market finance teams.
Built for accountants who need cryptographic audit integrity without sending raw transaction data to a SaaS. Reconcile bank ↔ ERP, resolve exceptions, sign off with separation-of-duties enforcement, and export audit-ready reports — all on your machine. An optional FastAPI/PostgreSQL server adds centralized user management, server-enforced RBAC, and a tamper-evident audit ledger that scales to ~1M transactions/month.
Highlights
🔒 SHA-256 hash-chained audit log; tamper detection via periodic chain verification 👥 Maker/checker workflow with monotonic state transitions + SoD enforcement 🏢 Multi-entity, multi-account, period-scoped storage 🔌 Plaid integration for bank data (server-side credentials, never in browser) 📊 Indexed matching engine + Web Worker offload for large reconciliations 🗄️ Partitioned audit ledger with cold-tier archival (JSONL.gz + manifest) and read-replica routing 🖥️ Tauri 2.0 desktop wrapper (Windows/macOS/Linux) — runs offline by default 💾 IndexedDB-backed row store with tiered eviction policy
- Formatter wizard — drop
.xlsx/.xls/.csv, auto-map columns, normalise dates (DD/MM vs MM/DD auto-detected) and money values (currency symbols, parens-negatives, trailing CR/DR), surface issues before reconciling. - Matching rule engine — four-rule pipeline (exact, reference, amount-tolerance, fuzzy description) with score-descending greedy assignment within each pass. Every paired row carries the rule that produced it + a confidence score + the per-rule sub-scores.
- Resolution drawer — write-off, adjustment, force-match, reclassify, carry-forward, escalate. Every action is reversible from the resolution log.
- AI resolution assistant — local pattern-learning engine observes your past resolutions, suggests treatments for new exceptions, supports bulk auto-resolve with per-group preview. CSP-safe; nothing leaves the device.
- Outstanding-items carry-forward — items roll into next month's reconciliation; stable content-based IDs survive re-runs.
- Anomaly detection — z-score / IQR outliers, duplicates, sign mismatches, weekend posts, volume spikes.
- Approval workflow — preparer attestation → reviewer sign-off → exportable approval record. Reopening clears attestation so re-submission requires re-attesting.
- Multi-entity / multi-account — one install can manage multiple legal entities and multiple bank/GL accounts per entity. Every silo is physically isolated.
- Audit-ready report — 11-sheet XLSX with cover, summary, all transactions, match provenance, exceptions, resolutions, outstanding items, anomalies, workflow history, engine config snapshot. Tamper-evident Report ID.
- Mapping templates — auto-detect recurring bank/ERP formats so the column mapping step can be skipped.
- Plaid integration — optional, network-required, opt-in on desktop. Pulls bank transactions via Plaid Link.
- Download the installer for your OS from the latest release.
- Run it. The app appears as Greenbar Clearing in your applications list.
Data location after install:
| OS | Path |
|---|---|
| Windows | %APPDATA%\com.greenbarsystems.clearing\greenbar.db |
| macOS | ~/Library/Application Support/com.greenbarsystems.clearing/greenbar.db |
| Linux | ~/.local/share/com.greenbarsystems.clearing/greenbar.db |
A single SQLite file. Back it up by copying that file somewhere safe; restore by copying it back.
You can run the same UI in a browser without installing anything. ES modules need an HTTP server — file:// won't work for module imports.
python -m http.server 8000
# then visit http://localhost:8000/In browser mode the data lives in localStorage. There is no sync to a server unless you wire one up (see Storage backends below). Use this for evaluation or single-machine dev only — localStorage is unencrypted and survives only as long as the browser profile.
The default is local-first. Cloud capabilities are added one at a time, each opt-in, each governed by an explicit data-classification table.
┌────────────────────────────────────────────────┐
│ Greenbar Clearing (desktop) │
│ ┌────────────────────────────────────────────┐ │
│ │ Webview (OS native — WebKit/WebView2) │ │
│ │ All UI, matching engine, AI assistant, │ │
│ │ formatter, audit report, … │ │
│ └─────────────┬──────────────────────────────┘ │
│ │ tauri::invoke (IPC) │
│ ┌─────────────▼──────────────────────────────┐ │
│ │ Rust host (Tauri) │ │
│ │ kv_get / kv_set / kv_remove / kv_list │ │
│ │ → SQLite at app_data_dir/greenbar.db │ │
│ └────────────────────────────────────────────┘ │
└────────────────────────────────────────────────┘
│
│ (opt-in only, per data class)
▼
┌────────────────────────────────────────────────┐
│ Cloud broker (optional — disabled by default) │
│ • Workflow state (plaintext JSON today; │
│ E2EE planned — Batch 3) │
│ • User registry (email, role, hash) │
│ • Entity / account / template catalogues │
│ • Aggregated statistics digests │
│ │
│ NEVER: │
│ • Raw transactions │
│ • AI pattern KB raw events │
│ • Plaid raw transaction data │
└────────────────────────────────────────────────┘
Every storage key carries one of three privacy labels. The source of truth is
src/services/dataClassification.js.
| Class | Behaviour | Examples |
|---|---|---|
LOCAL_ONLY |
Hard-coded local. No UI toggle. Never leaves the device. | Outstanding items, Plaid link cache, AI KB, active scope, session, sync prefs themselves |
SYNC_OPT_IN |
User-toggleable. Default OFF. Plaintext JSON crosses the wire today — review your server-side controls (TLS, at-rest encryption, access logs) before enabling. E2EE wrapping for workflow state is planned (Batch 3). | Workflow state, users, entities, accounts, mapping templates |
AGGREGATE_OPT_IN |
Never raw. Only fixed-shape digests (counts, percentages, rule histograms, balance proof totals) computed by a dedicated reducer ever cross the wire. | Period-close statistics |
Counts (matched / partial / resolved / unresolved). Match-rule histogram (exact / reference / tolerance / fuzzy). Clearance %. Balance-proof totals. Workflow state in plaintext JSON (preparer/reviewer names, attestation timestamps, free-text notes, audit history) — server operator can read all of it until Batch 3 lands the E2EE wrapping. User emails + roles + PBKDF2 password hashes + salts. Entity / account / template names + tax IDs + masked-to-last-4 account numbers.
Raw transactions. Descriptions. References. Vendor names. AI pattern tokens. Plaid transaction data. Per-row diffs.
| Batch | Scope | Status |
|---|---|---|
| 1 | Tauri scaffold + data classification table | Landed |
| 2 | HybridRouter backend + sync-settings UI |
Landed |
| 3 | E2EE reviewer handoff (Option B per architecture review) | Planned |
| 4 | Aggregates reducer + /stats/{period} endpoint contract |
Planned |
| 5 | OAuth broker for ERP integrations (Xero / QuickBooks / NetSuite) | Future |
Batches 2+ stay disabled at runtime until a real user demand surfaces. The local product is the product; the cloud is the convenience.
Prerequisites:
| Platform | What you need |
|---|---|
| All | Rust toolchain (stable, 1.77+) |
| All | Node.js 18+ (just for the Tauri CLI) |
| Windows | "Desktop development with C++" Visual Studio workload + WebView2 (preinstalled on Win11) |
| macOS | Xcode command-line tools (xcode-select --install) |
| Linux | libwebkit2gtk-4.1-dev, libssl-dev, libgtk-3-dev, libayatana-appindicator3-dev, librsvg2-dev (or distro equivalents) |
One-time setup:
npm install # installs @tauri-apps/cli
npm run tauri:icons # only if you have a 1024x1024 source PNG readyDev — runs the desktop app with hot reload of the webview when you edit src/*:
npm run tauri:devRelease build — produces a code-signed installer (.msi on Windows, .dmg on macOS, .deb / .AppImage on Linux) under src-tauri/target/release/bundle/:
npm run tauri:buildNo build step. Serve the static files:
python -m http.server 8000
# http://localhost:8000/ES modules are blocked over file:// in Chromium, so a server is required. Any HTTP server works (npx serve, caddy file-server, etc.).
For the desktop build to work without a network connection, drop SheetJS into vendor/:
curl -L https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js \
-o vendor/xlsx.full.min.jsindex.html probes for the vendored copy first and falls back to cdnjs if it isn't there. .gitignore excludes the binary so it isn't checked in.
The frontend always calls one set of functions —
kvGet / kvSet / kvRemove / kvList. What
they do under the hood depends on what's available at boot:
| Detector | Backend | Where data lives |
|---|---|---|
window.__TAURI__ present |
TauriBackend |
SQLite at app_data_dir/greenbar.db |
<meta name="app-api-base"> set (browser only) |
RemoteHttpBackend |
REST API at the configured base URL |
| Neither | LocalStorageBackend |
Browser localStorage |
The desktop build always picks TauriBackend. The other two are for dev /
evaluation. The Batch 2 HybridRouter will sit above TauriBackend and
selectively forward sync-eligible writes to a remote, leaving raw data
classes pinned to SQLite.
Sign in as an administrator → the header gains a 🔁 Sync button.
The panel exposes:
- Endpoint URL — runtime override for
<meta name="app-api-base">. Save → reconfigure happens immediately (no app restart). - Test connection — one-shot
GET /kv?prefix=__probe__to verify reachability + auth. - Per-class toggles — one switch per sync-eligible store. Defaults all OFF.
- Push / Pull buttons per enabled class for first-enable bootstrap and on-demand refresh.
- Last push / last pull timestamps so it's clear which side is the latest anchor.
- A collapsible list of every
LOCAL_ONLYstore (hard-coded, never syncs — for transparency).
Conflict policy in Batch 2: last pull wins. The server overwrites local on pull; local overwrites server on push. Per-key versioning (vector clocks / E2EE blobs) lands with Batch 3.
Default behaviour: with everything OFF (factory default), the HybridRouter is transparent — every write goes only to the local backend (Tauri SQLite or browser localStorage). The cloud is never touched until an admin opts in.
When <meta name="app-api-base" content="https://api.example.com"> is set,
the browser build expects four endpoints:
| Method | Path | Body | Response |
|---|---|---|---|
| GET | /kv?prefix=<p> |
— | 200 [{key, value}, ...] |
| GET | /kv/{encodedKey} |
— | 200 value | 404 |
| PUT | /kv/{encodedKey} |
JSON | 204 |
| DELETE | /kv/{encodedKey} |
— | 204 | 404 |
Requests carry Authorization: Bearer <session-token> when a user is signed
in. Server is responsible for per-tenant keyspace isolation.
- Local-first by default. Desktop builds keep raw data in SQLite at the OS user-data location; no network unless you opt into a cloud feature.
- HTML escaping (
esc()) on every user-supplied string interpolated into innerHTML, including the anomaly engine output. - CSV / formula injection guard (
safeCell/safeRow) on every XLSX export. - Content-Security-Policy — stricter in the desktop build (no cdnjs, no plaid.com); deploy-time configurable allow-list in the browser build.
- SheetJS pinned with SHA-384 SRI integrity +
crossorigin="anonymous"when loaded from cdnjs; can be fully vendored for offline use. - File-upload guards — 25 MB cap,
.xlsm/.xlsb/.xltmrejection, extension allow-list,FileReader.onerrorhandlers,raw:falseXLSX parsing. - Workflow integrity —
wfReopenclears preparer attestation and checklist so re-submission requires re-attestation. - Cross-tab
storageevent listener — approvals in one tab refresh the others. - Auth — local user registry; PBKDF2 (200k iterations, SHA-256, per-user salt) via SubtleCrypto. Sessions auto-expire after 8 hours of inactivity, extended by user activity (mouse / keys, throttled).
- Role gates — preparer / reviewer / administrator. Same person can't be preparer and reviewer of the same period.
- Plaid — opt-in; the browser never sees the Plaid access token (server-side only).
- Browser-mode
localStorageis unencrypted; don't use it on shared devices. - Auth in the local product is attribution, not real authentication. Anyone with file-system access to
greenbar.db(or, in browser mode, the localStorage profile) can read or edit the user registry. Cloud sync (Batch 2+) introduces real server-side identity. - Modal a11y (focus trap, Esc to close,
aria-modal) is not yet implemented. fParseCSVregex emits spurious empty cells for embedded-comma quoted fields. Fine for typical exports; not RFC-4180 compliant.- SheetJS 0.18.5 is pinned with SRI but predates CVE-2023-30533 (prototype pollution). Bump to 0.20.x when convenient.
Optional. Network required. Opt-in in the desktop build — the Plaid panel is hidden until you explicitly enable it under sync settings (Batch 2).
Plaid credentials never live in the browser. The page loads only Plaid
Link (an iframe from cdn.plaid.com); your backend holds client_id /
secret and brokers every Plaid REST call. Four endpoints are required:
| Method | Path | In | Out | Plaid call |
|---|---|---|---|---|
| POST | /api/plaid/link-token |
{entityId, accountId, scope} |
{link_token} |
/link/token/create |
| POST | /api/plaid/exchange |
{entityId, accountId, scope, publicToken, metadata} |
{item_id} |
/item/public_token/exchange — server stores access_token |
| POST | /api/plaid/sync |
{entityId, accountId, scope, itemId, cursor} |
{added, modified, removed, next_cursor} |
/transactions/sync |
| POST | /api/plaid/unlink |
{entityId, accountId, scope, itemId} |
204 |
/item/remove |
Plaid sends amount with positive = outflow; this app uses the opposite
convention. ingestPlaidDiff() flips the sign — don't double-flip server-side.
index.html Entry page — HTML body + inline on*= handlers (CSP-allowed)
styles/main.css All styles
package.json npm scripts for Tauri (dev/build/icons)
.gitignore Ignores Tauri target/, node_modules/, vendored SheetJS
src-tauri/ Tauri host (Rust)
Cargo.toml tauri 2, rusqlite (bundled), parking_lot
build.rs tauri-build glue
tauri.conf.json window, CSP, bundle config
src/main.rs kv_get / kv_set / kv_remove / kv_list + data_location
Backed by SQLite at app_data_dir/greenbar.db (WAL + idx)
src/
main.js Boot — wires every handler onto window, runs init
state.js Shared mutable state singleton
utils/
dom.js esc() XSS guard, safeCall window helper, getBalances
format.js fmt, parseAmt, toast, fileGuard, MATCH_EPS, …
parsers.js Date/number parsing + column auto-mapping (formatter)
strings.js Tokenisation, Levenshtein, tokenSetRatio (matching)
dates.js Timezone-safe date helpers (matching)
services/
storageBackend.js kv API + 3 backends (Tauri / Remote / Local) + reconfigureSync
dataClassification.js Per-key privacy class (Batch 1)
syncPrefs.js Per-device sync settings (Batch 2 — LOCAL_ONLY)
hybridRouter.js Per-class routing wrapper around local + remote (Batch 2)
storage.js Outstanding-items store; global purge
matching.js 4-rule matching engine + suggestion engine
reconciliation.js Core — rRun, rReconcile, rApplyRes, learning hook
workflow.js Maker/checker approval workflow + role gates
auth.js Local PBKDF2 user registry + sessions
entities.js Entity + account CRUD + scope routing + migration
templates.js Saved column-mapping presets
aiResolutionEngine.js Local pattern learning + bulk auto-resolve
anomalies.js Z-score / IQR / duplicate / sign / weekend detection
reporting.js Standard XLSX exports + template downloader
auditReport.js 11-sheet audit-ready XLSX
plaid.js Plaid Link + sync (opt-in, network required)
components/
header.js App switcher + contextual header actions
toast.js Re-exports toast() (lives in utils/format.js)
balanceBar.js Balance proof box
formatter.js Formatter wizard (drag/drop, mapping, format, export)
importPanel.js Reconciler drop zones + preview renderer
resultsTable.js Results table + tab dispatcher (rShowTab)
exceptionsPanel.js Exceptions tab + resolution log
outstandingPanel.js Outstanding-items tab + per-period actions
resolutionDrawer.js Inline "Resolve" drawer (incl. AI + match suggestions)
modals.js Force-match / suggestion / combine dialogs
anomaliesPanel.js Anomalies tab renderer
reportPanel.js Final report renderer
aiSuggestionsPanel.js AI suggestion panel + bulk auto-resolve modal
entityPicker.js Header entity/account dropdowns
entityManager.js Entity + account CRUD modal
authPanel.js Header chip + sign-in / create-user / user manager
syncSettingsPanel.js Sync settings modal (Batch 2 — admin only)
plaidLinkButton.js Plaid status panel (Import tab)
legacy/ Pre-refactor single-file build (reference only)
vendor/ Optional: vendored SheetJS for offline desktop builds
Internal — see repo settings.