RC2026

Frontend Engineering

·
JAN–AUG 2026

Vestra Assets

Next.js review workspace for LLM-based OCR of financial statements.

Stack

Frontend EngineeringNext.js + FlaskOCR Review UIPerformanceFirebase Auth

The Product

Screens & Illustrations
Illustration of the Vestra Assets split-pane correction workspace, with synthetic data. A status card reads Needs review, confidence score 94.5 of 100, transaction accuracy 96%. On the left, an issue list groups metadata issues (2), flagged transactions (4) and missing rows (1), with a navigator showing 3 of 7. In the centre, page 2 of a fake Sample Bank statement highlights the selected 14 Mar row. On the right, a bank-statement transaction editor shows Date, Description, Debit, Credit, Balance and Category fields with Save all and Save buttons.
Illustration: the split-pane correction workspace, with issue navigator, source page and typed editor.
Illustration of the Vestra Assets review flow: Upload, Process, Triage, Correct and Export, each with a one-line description and drawn in the accent colour as Rohith's review UI. A branch from Upload handles password-protected PDFs (Unlock, processed copy, Resume) with the note that the password is never stored. Below, a muted box shows the backend pipeline built by a teammate (classify, extract per page, LLM check, score rollup) and its review rule: needs review if confidence is under 98 of 100, transaction accuracy is under 98%, or credits minus debits don't match the change in balance. A legend says who built what.
Illustration: upload to export, the password branch, and the teammate's pipeline shown muted.
Illustration of a before-and-after results panel for virtualizing the transaction table: first render 2,017 ms to 39 ms, DOM nodes 265,030 to 596, JS heap 328 MB to 12.5 MB, and long tasks during a full scroll 1,387 ms to 0 ms, with bars drawn to scale within each metric. Lab benchmark on a synthetic 10,000-row statement, median of 5 runs on one machine, including memoized category lookups.
Measured in a lab benchmark: the virtualized transaction table on a synthetic 10,000-row statement.

Synopsis

Vestra Assets turns uploaded financial statements into structured, reviewable transaction data. The extraction pipeline, built by a teammate on Flask and Firebase Cloud Functions, classifies each document with an OpenAI model, extracts every page as a separate chunk with Reducto, checks the extraction against the page image with an LLM, and rolls up a confidence score, flagging documents for human review. Between January and August 2026, I owned the frontend: a Turborepo with two Next.js apps, an OCR review app and an admin app, covering upload, processing status, split-pane correction against the source PDF and export, plus Firebase sign-in with an admin-role gate. On the backend: the server half of password-protected PDF handling, a categories module, and validation for CPF statements (Singapore's state savings scheme).

Architecture

System Layers

OCR review app (Next.js 16)

App Router app served under /app/ocr: upload, the document list, document detail and the split-pane correction workspace. TanStack Query holds server state and polls files still in progress, TanStack Virtual renders the transaction table, and react-resizable-panels splits the correction view.

Iglobe

Monorepo (Turborepo + pnpm)

Two Next.js apps over shared packages: @repo/ui for shadcn/ui components and CSS-variable design tokens, @repo/shared for the API client, query hooks, types, Firebase config and a Zustand auth store. The admin app sits at the root and rewrites /app/ocr/* to the OCR app, so both apps share one domain.

IIlayers

API (Flask on Cloud Functions)

Flask blueprints registered through one registry and served from Firebase Cloud Functions; the client sends the signed-in user's Firebase ID token as a bearer token. Contributions here: the categories module, CPF result validation, and the unlock route and Firestore claims for password-protected PDFs.

IIIserver

Extraction pipeline

Built by a teammate; context for the review UI. A Storage trigger and Firestore triggers move each file through preprocessing, classification with an OpenAI model, per-page extraction with Reducto, an LLM check against the page image and a rollup. The rollup flags a document for review when confidence falls below 98, transaction accuracy below 98%, or the balances don't reconcile.

IVcpu

Data (Firestore + Cloud Storage)

Firestore holds file status, QA scores and categories; Cloud Storage holds uploads, page chunks, extraction results and the unlocked copy of a protected PDF. Browsers upload and view PDFs directly through expiring signed URLs.

Vdatabase

Core Engineering

Field Notes
01.

Split-pane correction workspace

Resizable panes put the correction panel beside the source PDF. Metadata issues, flagged transactions and rows the pipeline found missing share one issue list with a navigator. Selecting an issue loads the issue's source page into the PDF pane, and an expired signed URL can be refreshed in place without losing the page. Missing rows open a form pre-filled from the detected row, and fixes save per issue or all at once.

02.

Virtualized table, paginated lists

The transaction table mounted every row and resolved each category label by scanning the category list. Moved the table to TanStack Virtual (fixed row height, an overscan of 10 rows, a scroll to the edited row when editing starts off screen) and replaced the scans with memoized lookup maps. A later lab benchmark of that change against the previous version, on a synthetic 10,000-row statement: first render fell from 2,017 ms to 39 ms, the DOM from 265,030 to 596 nodes, JS heap from 328 MB to 12.5 MB, and long tasks during a full scroll from 1,387 ms to none. The gain is virtualization and memoization together. Document lists got server-side pagination over the backend's cursors, plus infinite scroll.

03.

Password-protected PDFs, end to end

Before this work, encrypted PDFs stopped at a requires-action status with no way forward, including PDFs carrying only permission restrictions. Rebuilt the flow around pikepdf: preprocessing rewrites permission-only encryption as a plain copy without prompting, so only a real password stops a file. The unlock route decrypts into a separate processed copy, never stores the password and resumes processing; a wrong password gets a 422 and the file stays blocked. Unlock and delete each claim the file in a Firestore transaction first, so a delete can't race a decrypted upload. The UI takes passwords during upload or later from the document list, one shared password or one per file, with pytest and Vitest covering the failure paths.

04.

Upload to export

Files upload straight to Cloud Storage through signed URLs with per-file progress, then wait for an explicit process step. Polling asks only about files still in progress, every 3 seconds, and merges the answers into the list. A finished document can be re-extracted, corrected, deleted singly or in bulk, and exported as a CSV or as a ZIP holding a metadata CSV plus one transaction CSV per category.

05.

Schema-driven editors per statement type

Transaction fields for bank, credit-card, mortgage and CPF statements are defined once in the shared package, and the correction panel renders the editor from those definitions, down to CPF rows carrying a separate amount per CPF account. Other types, including payslips and income-tax statements, fall back to a generic view.

06.

Firebase sign-in and an admin-role gate

A shared AuthProvider and Zustand store connect Firebase Authentication to both apps. AuthGuard sends signed-out visitors to the login page with the original path attached; AdminGuard also sends non-admins from the admin app to the OCR app. API calls carry the signed-in user's Firebase ID token as a bearer token. The admin app adds invitations, user and role management, and category management. The guards are client-side; server-side role checks belong to the backend.

07.

Monorepo and design tokens

Set up the Turborepo with pnpm, two Next.js 16 apps, shared packages and an app switcher between the apps. Design tokens are CSS variables in one tokens file, documented as the single source of truth and used by the shared shadcn/ui components. The OCR app runs under a /app/ocr base path and the admin app at the root rewrites /app/ocr/* to the OCR app, putting the two apps on one domain. Each app has a Vercel config with a turbo-ignore check, skipping a build when nothing the app depends on has changed.

08.

Backend: categories and CPF validation

Built categories end to end: a Flask blueprint with create, read, update and delete for categories and sub-categories in Firestore, registered through the backend's blueprint registry; a merger step mapping every extracted transaction's sub-category to a main category; the category pickers in the transaction table; and the admin screen for managing categories. Also extended the validation run on saved corrections to accept CPF transactions, with amounts split across three CPF accounts instead of a single amount field.

Execution Flow

Dispatch
Execution Trace5 steps
01

Upload

Files go straight to Cloud Storage through signed upload URLs with per-file progress, then sit in an uploaded state until the process step is triggered.

02

Process

The backend pipeline preprocesses each file, classifies the document, extracts every page as a separate chunk, checks the extraction against the page image and rolls up a confidence score. A PDF needing a password stops at preprocessing until unlocked; the UI polls only files still in progress.

03

Triage

Documents with a confidence score under 98 (of 100), transaction accuracy under 98%, or credits minus debits not matching the change in balance come back flagged for review. The status card shows the score, the accuracy and the flag, and the document list filters on the flag.

04

Correct

An issue opens from the issue list or from a row in the transaction table, with the source page loaded in the PDF pane. Fixes go into the typed editor and missing rows into a pre-filled form; saving sends the corrected document back, where the backend validates the edit and recalculates the score.

05

Export

A finished document downloads as a CSV, or as a ZIP with a metadata CSV and one transaction CSV per category.

Complete✓ OK
Reflections
Outcomes
  • “Measure at the time, not afterwards. The table was virtualized in February 2026 and only benchmarked in October, against the previous version on a synthetic 10,000-row statement. The gains were large but are lab numbers from one machine, with virtualization and memoized lookups measured together.
  • “Virtualizing an editable table is more than a rendering change. The overscan of 10 rows was set for edit safety as well as smooth scrolling, and the table scrolls to a row when editing starts off screen. A fixed row height keeps the math simple but costs flexibility for wrapped descriptions.
  • “Claim before mutating. Unlock and delete act on the same stored PDF, so each first claims the file in a Firestore transaction. A delete can't remove a file mid-unlock, a failed unlock restores the password-required state without overwriting a newer status, and tests cover both orderings.
  • “Older records need the new path too. Files recorded as failed with a PROTECTED error code still had to reach the unlock action, so the UI folds those records and requires-action files into one status, with Vitest tests on the mapping and the document-list filter.
  • “Client-side guards are navigation, not security. AdminGuard only redirects non-admins; the real check is role enforcement in the API, and admin-only routes need server-side tests to match.
Next Steps
  • Benchmark the review workspace on large real statements; the current numbers come from synthetic data.
  • Add typed correction schemas for payslips and income-tax statements, two of the six types the backend extracts and the review UI shows only in a generic view.
  • Add server-side tests for admin-only routes to back the client-side admin guard.

One Last Thing

The core challenge: OCR on financial statements can't be fully automatic. Flagged values have to be found, checked against the source page and corrected quickly, even on statements with long transaction lists. Bank statements also often arrive password-protected, and without an unlock path such files dead-end the pipeline.

✦Built with
intention

More from the Archive

View All