Clearline
Portfolio project · B2B fintech frontend

Spend management where correctness is the feature.

Clearline is a corporate spend platform in the style of Ramp and Brex. It's not happy-path CRUD: it models idempotent payments, separation of duties, role-based access, KYB onboarding, and bank-feed reconciliation — the places where real money software breaks — and surfaces those failure modes honestly in the UI.

React 19TypeScript 6Vite 7Nx monorepoTanStack QueryTailwind v4MSW v2Vitest · Playwright
24epics specified, 22 shipped
48user stories, 46 implemented
~2,000unit & component test cases
14Playwright e2e specs
40+Nx libraries, layered
~4 wkscommit history, Jun 30 – Jul 24, 2026
What's in the app

A full spend platform, scoped by role.

Three personas (Employee, Finance Manager, Controller) see different navigation and different permissions, all derived from one permission predicate — never a role string.

Payments

Idempotent vendor payments

One validation gate (balance, daily limit, self-transfer, recipient status), an Idempotency-Key per intent, 30s timeout, and full-jitter retry on 5xx. 3DS step-up for high-value payments; holds, reversals, and unknown-status handling.

Approvals

Multi-level approvals & batches

Separation of duties, limit-based escalation, stale-action reconciliation, and partial-failure batch approve/reject with per-item keys and mid-batch resume.

Cards

Virtual cards, live feed

Issuance with limits and MCC restrictions, a real-time WebSocket transaction feed, freeze controls, and security-gated declines.

Reconciliation

Bank-feed matching

Exact + fuzzy matching with a similarity breakdown, an actionable exceptions queue, exact-sum split matching, and a Fatal-tier guard that withholds a balance that doesn't net.

Analytics

Dashboards & budgets

Spend by category, department, and vendor with anomaly flags and AI-confidence labels; per-section error boundaries with scoped retry; budget gauges with 80%-warning and over-budget states and period rollover.

Platform

Onboarding, settings, audit

KYB onboarding, sign-up and sessions, 2FA, an append-only Controller-only audit log (opening it is itself audited), a full settings surface with re-auth-gated ownership transfer, and role-based getting-started checklists driven by real domain events.

Exploring the demo: everything runs in your browser against a stateful mock backend — no sign-up needed. Use the floating Demo Beacon (bottom-right) on any page for seeded credentials, one-click role switching (watch the nav and permissions re-scope), the inputs that trip each guardrail, and a reset control.
Architecture

Business rules never depend on React, HTTP, or the DOM.

An Nx + pnpm monorepo with one rule of thumb: if it's an invariant it lives in domain; if it touches the network it lives in data-access; if it renders it lives in ui or the app.

UI layer apps/clearline-web — React 19 + Vite. Pages, route guards (RequireAuth, RequirePermission…), permission-driven nav. Renders typed errors; decides nothing.
Data-access libs/data-access/* (23) — TanStack Query hooks over /api/*. Mints idempotency keys, applies timeouts and retry/backoff, surfaces 4xx as typed errors.
Domain libs/domain/* (11) — pure TypeScript: validation gates, state machines, policies. No React, no fetch. Unit-testable alone.
Cross-cutting ui (Radix-backed, Storybook) · design-tokens (generated --cl-* theme) · icons · money (minor-unit math) · contracts (shared API types) · demo-beacon
Backend seam libs/mock-backend — MSW v2. Stateful TypeScript services hold the invariants; HTTP/WebSocket handlers are thin adapters.
Technology

The stack, and why each piece is there.

Concern Choice Why
Framework / build React 19 + Vite 7 Web-only SPA behind auth; no SEO need, so no SSR complexity.
Language TypeScript 6 (strict) Shared contract types across UI, data-access, and mock backend.
Routing react-router 7 Nested layout routes carry the guard chain; version pinned below the 8.x major on purpose.
Server state TanStack Query 5 Cache, retry, and mutation lifecycles instead of hand-rolled effects.
Forms react-hook-form + Zod Schema-validated input with errors tied to fields for accessibility.
Styling Tailwind v4 + Radix Tokens in @theme, light/dark; accessible primitives underneath.
Mock backend MSW v2 Same handlers run in the browser demo and in tests.
Testing Vitest · Testing Library · Playwright Domain unit tests, component tests, and role-based e2e flows.
Design system Storybook 10 + Chromatic Every component ships stories and docs; visual regression in CI.
Tooling / CI Nx 23 · pnpm · GitHub Actions · Husky · commitlint Affected-only builds and tests; conventional commits tied to story IDs.
Hosting Vercel Git-driven previews; CI verifies, Vercel deploys.
Engineering

Technical decisions

The choices that shaped the codebase, and why I made them.

01 Services-first mock backend

Decision
MSW handlers are thin; stateful TypeScript services own balances, idempotency records, and approval state.
Why
Replayed keys, validation 4xx, and partial batch failures behave like a real server — in the hosted demo and in tests — with no backend to run.
Cost
Server logic is re-implemented in the mock; contracts must stay honest.

02 Pure domain libraries

Decision
validatePayment, retry policy, match classifier, and auth rules are framework-free TypeScript.
Why
Invariants are the product. They're testable in milliseconds and can't be accidentally coupled to a component.

03 Idempotency by default

Decision
Every payment intent mints an Idempotency-Key; 5xx retries use full-jitter backoff; batches use per-item keys.
Why
A network retry must never move money twice. Unknown status is shown as unknown, not guessed.

04 Permissions, not roles

Decision
Nav, route guards, and even the getting-started checklist derive from one permission predicate.
Why
A persona is only ever offered what it can do. "Client hides, server decides": the UI is never the enforcement point.

05 Money as minor units

Decision
A single @clearline/money lib owns currency conversion; budget percentages and overages are computed on integers.
Why
Floating point has no place near a ledger. Shared once, not re-derived per feature.

06 Fail loudly, withhold bad data

Decision
A Fatal-tier integrity state withholds a ledger balance that doesn't net; dashboards use per-section error boundaries with scoped retry.
Why
A wrong number shown confidently is worse than a clear "can't show this."

07 Accessibility as a cross-cutting epic

Decision
WCAG 2.1 AA: status never by color alone, ≥3px focus ring with forced-colors fallback, spoken money labels, field-linked errors, focus-trapped dialogs.
Why
Built into the component library and checked in e2e, not retrofitted.

08 Append-only audit trail

Decision
One central audit store that payments, approvals, and card controls emit into; no update or delete path at any layer.
Why
Corrections are appended, never edited — and viewing the log is itself logged.

09 Demo Beacon as a product tool

Decision
A standalone, page-aware helper library documents seed data and scenarios in place; gated by VITE_ENABLE_MOCKS.
Why
A reviewer shouldn't read fixture files to find what to try. A production build never loads the UI chunk.
Building with an LLM

How I run an AI-assisted development cycle.

I set the architecture, make every technical and product decision, and built the development system that three AI tools work inside. Nothing moves from one stage to the next until I have read the actual text and said yes. It's the process a good product team runs — written specs, recorded decisions, tests first, code review, and a check on the real screen — defined in the repo and directed the way a tech lead directs a team.

Why a system at all: an assistant starts every session with no memory of the last one. Left alone, that produces work that contradicts last week's decisions and specs that describe a product you don't have. So the project is run like a team: a registry of user stories and epics (Clearline has 24 epics and 48 stories in specs/), a numbered record for each technical decision, and a fixed cycle every feature goes through.

Three tools, three roles

Each tool has one job and one thing it is never allowed to do. The tool that writes the code is not the one that judges it.

Who Role Does Never
Me Architect, tech lead, product owner Set architecture and constraints, design the workflow and its rules, make every engineering decision, review specs and code, test on the real screen, merge Approve a summary instead of the actual text
Claude Desktop Reviewer & planner Reads reports against the specs and decision records, recommends a ruling, drafts the next prompt, runs the design work Touches the repository
Claude Code Executor Runs the repo's commands to write specs, tests, code, commits and pull requests; uses subagents for review, deviation audits and context loading Continues past an approval gate on its own
Claude Design Designer Holds the design system and draws every screen state on a design canvas Invents a state no story asks for, or adds a token without approval

A typical exchange: Claude Code stops at a gate and reports what it did, found, and needs decided. I paste that report into Claude Desktop, which checks it against the records and returns one recommendation and one complete prompt. I approve, change or reject it, and paste it back. One prompt at a time, because the first result changes what the second should say. Prompts stay short because the procedure lives in the repo's own commands, versioned with the code.

The feature cycle

Every feature follows the same path from idea to merged code. Each step is a command in the repo, so the process is identical in every session.

  1. Brief

    What the app should do, why now, what's out of scope, what done looks like. Open product questions are settled first.

  2. Spec

    /spec-feature turns the brief into an epic and user stories with acceptance criteria, all draft. /validate-registry checks structure.

  3. Approval

    I read the full acceptance criteria and run /approve-epic myself. The build command checks for approval and stops without it.

  4. Design

    Claude Design draws every state the stories imply. /validate-design-export checks it against the design system.

  5. Load context

    /load-spec-context gathers the epic, stories, decisions and designs once, so every session starts from the same picture.

  6. Reconcile decisions

    /reconcile-decisions finds every earlier decision that applies and says whether this work follows, amends or replaces it.

  7. Resolve open choices

    /assess-tradeoffs lays out any real engineering choice with its consequences. I decide; the decision is written down.

  8. Build

    /implement-feature works one story at a time, test first — each test must fail for the right reason before any code exists. /apply-design translates designs into components, listing every state first.

  9. Review functionality

    I test the built functionality in a real browser against the design, state by state.

  10. Code review

    /review-code checks the change against the project's binding rules and quotes any rule it breaks.

  11. Deviation audit

    /review-deviations compares what was built with what was specified across the whole epic.

  12. Pull request

    /create-pull-request writes the description, including manual test steps and any declared deviations.

  13. Merge & close

    I merge. Stories are marked implemented only if every criterion is met.

  14. Explainer

    /explain-implementation writes up how the feature works and what was learned while it's fresh.

Small fixes skip most of this. Anything touching money, permissions or personal data gets the full cycle whatever its size.

The design pipeline

One source, one copy

A design system holds tokens, icons, component guidelines and contrast documentation. The repo holds a read-only copy, updated only by syncing from the source and never edited by hand. Code reads only that copy.

One canvas per epic

Every state gets its own artboard: empty, loading, error, offline, light and dark. Every visual value is a token; new tokens are approved into the system as a separate step.

No open questions

There's no designer to leave questions for, so every product and data question is settled before the brief is written. An open question produces invented scope.

Differences are recorded

If the app and the design differ, the difference goes in a hand-kept log. Nobody edits a design file to make it match. After merge, approved artboards replace the library's.

The rules that keep it trustworthy

Written in the repo's CLAUDE.md as binding constraints. Review commands check work against them and quote the rule that was broken.

  • Approval means the exact text. Silence, enthusiasm or "sounds good" doesn't count, and nothing is approved after work has started.
  • The repo beats memory. No tool states what a file contains without reading it fresh.
  • The spec is the contract. If it's wrong, the assistant stops and the spec is amended with a reason and history entry first.
  • Decisions are written down — a numbered record with options and the reason.
  • Tests come first and must prove something. Mocks stand in for outside services, never for the code under test.
  • Deviations are declared. A partly-met criterion is marked partial with a follow-up story; incidental findings go to an audit file, not a quiet fix.
  • A person checks the functionality before any UI pull request opens.
  • Git stays readable. One branch per epic, one concern per commit, squash merges, no rewriting shared history, never commit on main.
  • Raise decisions, don't make them quietly — new dependencies, schema shape, anything with a real alternative.
  • Report accurately, including on its own work and shortcuts.

When a gate or review catches a defect, the fix isn't only the code — I change the rule and record why, so the same failure costs less next time.

What I learned

The records matter more than the model

Output quality followed the quality of the spec, the decision history and the rules file, not how cleverly I worded a prompt.

Separate doing from judging

The session that wrote the code carries its own assumptions about what it built. A separate session doesn't, so it checks the work against the spec and the rules instead, and catches gaps the builder can't see.

Gates are cheap

Reviewing the acceptance criteria before the build takes minutes. Fixing a misunderstanding after the code is written means rework.

Put the process in the repo

Commands, templates and rules are versioned and reviewed like code, so the process improves with each change and survives every new session.

Keep the specs next to the code

Epics and user stories live in the repo as versioned files, not in a separate tool. The assistant reads the same requirements I do in every session, a change to a requirement goes through review like any code change, and each commit traces back to a story.

Honest status beats clean status

A registry that says "implemented" about partial work is confidently wrong, and every later decision inherits the error.

This works on a team too. Nothing here is specific to solo work. Stories with acceptance criteria, decision records, pull requests with test steps, and design files tied to tokens are what teams already use. On a team, a product manager and a designer would share the product and design decisions, and I'd stay in the technical lead role. Colleagues could review the assistants' output the same way they review each other's work.
Take a look

Inspect it the way you'd inspect a teammate's PR.

Try the flows, then read the code. The domain libraries and their tests are the best place to start.