ryan-portfolio

← projects.json

finapse

2026

ReactNode.jsExpressPostgreSQLPrismaPlaid APITypeScript
── case study ────────────────────────────────────────────────────────────

What it is

Finapse is a personal finance dashboard: connect your bank accounts through Plaid and it syncs transactions, categorizes them, and tracks spending over time. Built solo; frontend, backend, schema, and auth all designed from scratch rather than bolted onto a starter kit.

The problem

Making sense of your own financial data usually means fighting a poorly designed banking app or maintaining a spreadsheet by hand: pulling transactions across accounts, re-categorizing the same recurring charge every month, and re-checking spending trends yourself because nothing else will. Finapse automates that whole pipeline. Categorization in particular layers a resolution order (user override, merchant rule, recurring heuristic, then Plaid's own category) so a correction to one charge backfills across that merchant's past and future transactions instead of being a one-off fix.

Approach

The backend is Node.js/Express/PostgreSQL with Prisma across 7 models and roughly 40 API endpoints. The frontend is React 19 + Vite + TanStack Query + Tailwind, with Recharts for the dashboard's spending charts and react-plaid-link for the bank-connection flow. Both are deployed via GitHub Actions and Render. A few of the harder pieces:

  • Auth & authorization — JWT sessions plus Google OAuth, with object-level authorization checks on every resource to prevent IDOR rather than relying on route-level checks alone.
  • Token encryption — Plaid access tokens are encrypted with AES-256-GCM implemented from scratch, packing IV, auth tag, and ciphertext into a single stored field.
  • Webhook verification — Plaid webhooks arrive as ES256 JWTs; verification enforces a 5-minute freshness window and a timing-safe signature comparison to block replay and payload tampering.
  • Rate limiting — a two-tier layer (10 failed attempts/15 min on auth, 300 requests/15 min globally, keyed on real client IP via TRUST_PROXY) isolated behind one module so it can swap to Redis later without touching call sites.
  • Transaction sync — cursor-based pagination pulls up to 24 months of history on first sync and delta-only updates after, tested against 3 live financial institutions.
  • Client-side category resolution — the same override/recurring/detail/primary precedence the backend uses is mirrored in the frontend, so the transaction list and dashboard can show the right category immediately instead of round-tripping to the API to ask what a transaction resolves to.
  • Responsive layout as its own pass — the app shell, dashboard, and transaction/budget views were built desktop-first, then reworked for mobile in a dedicated effort rather than patched with breakpoints as an afterthought.

Process

Solo project, but run like a small team would run it rather than as a pile of commits to main:

  • Linear for issue tracking — features and bugs get an issue first (e.g. FIN-111), and branch names are derived from it (FIN-<number>-short-description).
  • A pull request for every change, even solo — kept small, referenced its Linear issue, and only merged once the backend and frontend both ran clean, typechecked, and passed lint.
  • CI on every PR via GitHub Actions — install, generate the Prisma client, lint, typecheck, and run the backend's Vitest suite before anything can land on main.
  • A Postman collection covering all ~40 endpoints, kept in the repo and updated alongside API changes rather than left to drift.

Just under 50 PRs in following that loop so far.

What I'd do differently

Still messing around on this currently, but its not my main focus right now.

ryan@portfolio ~/content $