Case Study

A Self-Hosted Finance PWA, Designed and Shipped Solo

No consumer finance tool fit how I think about money. So I built the one that does.

Design Engineering

Information Architecture

AI-Assisted Build

Full-Stack PWA

Self-Hosted Infrastructure

60+ → 8

tables grouped into domains

60+ → 8

tables grouped into domains

8wks

evenings and weekends

8wks

evenings and weekends

250+

API endpoints

250+

API endpoints

39

database tables, in production

39

database tables, in production

My Role

Designer, full-stack engineer, daily user

Timeline

8 weeks of nights and weekends

Team

Solo, with Claude as engineering partner

Tools

Claude Code, Figma, React, Express, SQLite, Docker

Scope

Self-hosted PWA, mobile and desktop

01 - The Problem

12 spreadsheets pretending to be an app

I've been tracking my finances in Apple Numbers for years, across 12 sheet tabs and 60+ sub tables: income, expenses, itemized purchases, assets, liabilities, investments, budget, loans, credit cards, crypto staking, and an archive of everything that didn't fit anywhere else. Cross sheet formulas held it together. It worked, in the sense that nothing else did, but it wasn't an app. No mobile, no quick capture, no shared logic between views, and every new account meant another column in another sheet. 

I tried the consumer options like Mint before it shut down, YNAB, Monarch, and Copilot, but none of them handled how I actually move money, which is income distributed across multiple accounts by purpose, not all of it landing in one checking account first. Self hosting wasn't on the table either, which mattered because I didn't want my full financial picture sitting on someone else's server. So I built it.

01 - The Problem

12 spreadsheets pretending to be an app

I've been tracking my finances in Apple Numbers for years, across 12 sheet tabs and 60+ sub tables: income, expenses, itemized purchases, assets, liabilities, investments, budget, loans, credit cards, crypto staking, and an archive of everything that didn't fit anywhere else. Cross sheet formulas held it together. It worked, in the sense that nothing else did, but it wasn't an app. No mobile, no quick capture, no shared logic between views, and every new account meant another column in another sheet. 

I tried the consumer options like Mint before it shut down, YNAB, Monarch, and Copilot, but none of them handled how I actually move money, which is income distributed across multiple accounts by purpose, not all of it landing in one checking account first. Self hosting wasn't on the table either, which mattered because I didn't want my full financial picture sitting on someone else's server. So I built it.

02 - Information Architecture

From 12 sheets to one clean navigation

The first job was figuring out which of those tables were actually distinct domains, which were duplicates with different framing, and which were just artifacts of how I'd organized things over the years. I audited every table and grouped them into eight core data domains, then worked out the hierarchies and primary actions for each. Accounts became the parent route. Banking, investments, credit cards, loans, and crypto all live inside it as filter views, so the sidebar stays focused and I still have access to everything underneath.

BEFORE

Sheets in Apple Numbers

Cross-sheet formulas, no mobile editing, no audit trail. Worked fine for years, until it didn't.

60+

Tables across 12 sheets

Income, Expenses, Itemized, Assets, Investments, Budget,

Goals, Loans, Income Analysis, Credit Cards, Crypto, Archive

PROCESS

Audit and consolidate

Grouped sheets by function, not by name. Found 8 domains and natural hierarchies.

01

Audit all 87 sheets

Grouped by function, not by name. Mapped what each sheet actually did.

02

Identify 8 data domains

Income, Expenses, Accounts, Loans, Credit Cards, Investments, Budget, Subscriptions.

03

Find natural hierarchies

Banking, Investments, Credit Cards, and Loans are all account types. They live under one parent.

04

Define primary actions

Per domain, named the one most common task. That drove what got bottom nav real estate.

AFTER

Navigation model

5 bottom nav items. 10 total routes. Accounts is the umbrella for 4 financial types. More holds secondary destinations.

Dashboard

Net worth, bills, budget, alerts

Income

Record and distribute

Expenses

Monthly grid, pay and unpay, categories

Recurring

Recurring CC charges

Accounts

All account types, balances, transactions

├─

Banking

Checking, savings

├─

Investments

Brokerage, retirement

├─

Credit Cards

Accounts, statements, rewards

└─

Loans

Amortization, payoff calculators

└─

Crypto

Crypto accounts, staking tracking

Tools

Secondary destinations

History

Audit trail with undo

├─

Budget

Monthly goal, category breakdown, scenarios

├─

Paycheck Calculator

Calculates the distribution of a paycheck

Settings

Theme, user, data

8

bottom nav items

14

routes total

8

data domains

02 - Information Architecture

From 12 sheets to one clean navigation

The first job was figuring out which of those tables were actually distinct domains, which were duplicates with different framing, and which were just artifacts of how I'd organized things over the years. I audited every table and grouped them into eight core data domains, then worked out the hierarchies and primary actions for each. Accounts became the parent route. Banking, investments, credit cards, loans, and crypto all live inside it as filter views, so the sidebar stays focused and I still have access to everything underneath.

BEFORE

Sheets in Apple Numbers

Cross-sheet formulas, no mobile editing, no audit trail. Worked fine for years, until it didn't.

60+

Tables across 12 sheets

Income, Expenses, Itemized, Assets, Investments, Budget,

Goals, Loans, Income Analysis, Credit Cards, Crypto, Archive

PROCESS

Audit and consolidate

Grouped sheets by function, not by name. Found 8 domains and natural hierarchies.

01

Audit all 87 sheets

Grouped by function, not by name. Mapped what each sheet actually did.

02

Identify 8 data domains

Income, Expenses, Accounts, Loans, Credit Cards, Investments, Budget, Subscriptions.

03

Find natural hierarchies

Banking, Investments, Credit Cards, and Loans are all account types. They live under one parent.

04

Define primary actions

Per domain, named the one most common task. That drove what got bottom nav real estate.

AFTER

Navigation model

5 bottom nav items. 10 total routes. Accounts is the umbrella for 4 financial types. More holds secondary destinations.

Dashboard

Net worth, bills, budget, alerts

Income

Record and distribute

Expenses

Monthly grid, pay and unpay, categories

Recurring

Recurring CC charges

Accounts

All account types, balances, transactions

├─

Banking

Checking, savings

├─

Investments

Brokerage, retirement

├─

Credit Cards

Accounts, statements, rewards

└─

Loans

Amortization, payoff calculators

└─

Crypto

Crypto accounts, staking tracking

Tools

Secondary destinations

History

Audit trail with undo

├─

Budget

Monthly goal, category breakdown, scenarios

├─

Paycheck Calculator

Calculates the distribution of a paycheck

Settings

Theme, user, data

8

bottom nav items

14

routes total

8

data domains

03 - The Design System

Old money, not Robinhood

The first decision was what this app shouldn't look like. Robinhood and most fintech apps lean on bright gradients, dark glassmorphism, and screens that feel like a casino dashboard. I wanted the opposite, something closer to an old bank ledger or a private wealth client portal. Deep forest green as the primary accent, coral for expenses, liabilities, and overdue states, because red gets fatiguing on a finance app you open every day. Red still exists in the system, but only for errors and alerts, never for money that's just negative.


Ledger House

Design System · Specimen

Net Worth

$47,329.18

+ $1,284.50 this month

Forest

#3D6B4F

Primary accent

Coral

#ED9474

Expenses, liabilities

Gold

#DEB46A

Income, autopay badges

Red

#EC6060

Errors, alerts only

Salary deposit

· Income

+ $2,400.00

Whole Foods Market

· Groceries

− $87.42


I'm using three typefaces, and each one has a specific job. Inter handles everything functional like body, labels, buttons, and most numbers. Playfair Display is reserved for one place only, the Dashboard's hero net worth number, which is the single editorial moment in the whole app. EB Garamond holds the "Allister's Ledger" wordmark in the sidebar at 24 pixels, a slightly lighter and more delicate serif than Playfair so it reads as identity rather than data. Numbers use tabular nums everywhere so columns line up at a glance, with the same hierarchy across every screen: Playfair on the Dashboard hero, Inter Medium tabular for tables and lists, Inter Regular smaller for metadata. The eye always knows where to land first. No glassmorphism, no animated gradients, no celebration confetti when you log a transaction. Dark and light modes both shipped from day one, and both meet WCAG AA contrast at minimum, AAA where I could get there without breaking the visual system.

03 - The Design System

Old money, not Robinhood

The first decision was what this app shouldn't look like. Robinhood and most fintech apps lean on bright gradients, dark glassmorphism, and screens that feel like a casino dashboard. I wanted the opposite, something closer to an old bank ledger or a private wealth client portal. Deep forest green as the primary accent, coral for expenses, liabilities, and overdue states, because red gets fatiguing on a finance app you open every day. Red still exists in the system, but only for errors and alerts, never for money that's just negative.


Ledger House

Design System · Specimen

Net Worth

$47,329.18

+ $1,284.50 this month

Forest

#3D6B4F

Primary accent

Coral

#ED9474

Expenses, liabilities

Gold

#DEB46A

Income, autopay badges

Red

#EC6060

Errors, alerts only

Salary deposit

· Income

+ $2,400.00

Whole Foods Market

· Groceries

− $87.42


I'm using three typefaces, and each one has a specific job. Inter handles everything functional like body, labels, buttons, and most numbers. Playfair Display is reserved for one place only, the Dashboard's hero net worth number, which is the single editorial moment in the whole app. EB Garamond holds the "Allister's Ledger" wordmark in the sidebar at 24 pixels, a slightly lighter and more delicate serif than Playfair so it reads as identity rather than data. Numbers use tabular nums everywhere so columns line up at a glance, with the same hierarchy across every screen: Playfair on the Dashboard hero, Inter Medium tabular for tables and lists, Inter Regular smaller for metadata. The eye always knows where to land first. No glassmorphism, no animated gradients, no celebration confetti when you log a transaction. Dark and light modes both shipped from day one, and both meet WCAG AA contrast at minimum, AAA where I could get there without breaking the visual system.

04 - A Pattern

Splitting a paycheck without losing a cent

04 - A Pattern

Splitting a paycheck without losing a cent

05 · What got built

Beyond the dashboard

Most of the design work lived past the dashboard. Stepped imports, automated snapshots, a privacy shield for shoulder-surfing, an AI you can ask about your money. The features you don't see until you're using the app every day.


Ask the data Plain-English questions against my actual transactions, accounts, and budgets. The Anthropic key lives in Settings, so the data stays on my server.

Auto snapshots Database backup every ten writes. The last fifteen are kept on disk, every one restorable from Settings in two clicks.

Stepped imports CSV migration from Apple Numbers, broken into five sequential steps. Each step unlocks the next, so dependencies can't get out of order.

Privacy Shield Blur every dollar amount in the app with one keyboard shortcut. For when someone's looking over your shoulder, or for screenshots like this one.

Three palettes Forest, Midnight, and Charcoal. Light and dark variants for each, so the app feels right at any hour.

Keyboard shortcuts Single-key navigation across the whole app. Built for daily use, not first-time tours.

05 · What got built

Beyond the dashboard

Most of the design work lived past the dashboard. Stepped imports, automated snapshots, a privacy shield for shoulder-surfing, an AI you can ask about your money. The features you don't see until you're using the app every day.


Ask the data Plain-English questions against my actual transactions, accounts, and budgets. The Anthropic key lives in Settings, so the data stays on my server.

Auto snapshots Database backup every ten writes. The last fifteen are kept on disk, every one restorable from Settings in two clicks.

Stepped imports CSV migration from Apple Numbers, broken into five sequential steps. Each step unlocks the next, so dependencies can't get out of order.

Privacy Shield Blur every dollar amount in the app with one keyboard shortcut. For when someone's looking over your shoulder, or for screenshots like this one.

Three palettes Forest, Midnight, and Charcoal. Light and dark variants for each, so the app feels right at any hour.

Keyboard shortcuts Single-key navigation across the whole app. Built for daily use, not first-time tours.

06 - The Build

Directing AI to ship production software

The whole thing was built solo, with Claude as the implementation partner. The setup is three roles: I'm the product designer and decision maker, Claude Chat is the thinking partner for architecture and tradeoffs, and Claude Code is the engineer.

I ran two instances of Claude in parallel. A chat session for planning, where I'd talk through what I wanted to build and walk away with a clear prompt, which I'd then hand off to Claude Code in the editor. After Code shipped something, I'd cross check the result back in the chat session, and more often than not it caught issues that would've slipped past me if I'd only been working with Code.

Claude Code wanted to hardcode everything. Every color a literal hex, every spacing a magic number, every radius re-declared from scratch. The fix was writing 'use the design system, no hardcoded values' as a rule in claude.md and auditing every component diff against it. Without that constraint, the first build would have shipped two hundred unique colors and zero tokens.

I asked Claude Code to audit each implementation rigorously, then tested everything myself in real use. Bugs I ran into went into a running notepad, then through Claude Chat to figure out the right fix before going back to Code. A claude.md file at the project root stays current with progress, decisions, and implemented features, so both Claude sessions and I are working from one source of truth.


Single source of truth: architecture decisions, conventions, recent changes, and open questions, shared across every Claude session."

06 - The Build

Directing AI to ship production software

The whole thing was built solo, with Claude as the implementation partner. The setup is three roles: I'm the product designer and decision maker, Claude Chat is the thinking partner for architecture and tradeoffs, and Claude Code is the engineer.

I ran two instances of Claude in parallel. A chat session for planning, where I'd talk through what I wanted to build and walk away with a clear prompt, which I'd then hand off to Claude Code in the editor. After Code shipped something, I'd cross check the result back in the chat session, and more often than not it caught issues that would've slipped past me if I'd only been working with Code.

Claude Code wanted to hardcode everything. Every color a literal hex, every spacing a magic number, every radius re-declared from scratch. The fix was writing 'use the design system, no hardcoded values' as a rule in claude.md and auditing every component diff against it. Without that constraint, the first build would have shipped two hundred unique colors and zero tokens.

I asked Claude Code to audit each implementation rigorously, then tested everything myself in real use. Bugs I ran into went into a running notepad, then through Claude Chat to figure out the right fix before going back to Code. A claude.md file at the project root stays current with progress, decisions, and implemented features, so both Claude sessions and I are working from one source of truth.


Single source of truth: architecture decisions, conventions, recent changes, and open questions, shared across every Claude session."

07 - Outcomes

What this shipped, and what it proves

Allister Ledger is in production as a self hosted PWA on my Synology NAS via Docker, with React 19 on the front end, Express on the API, and SQLite for storage. By the numbers: 12 spreadsheets and 60+ tables turned into 14 routes and 39 database tables, with 250+ API endpoints. The whole thing took eight weeks of evenings and weekends.

14

Routes

39

Tables

250+

Endpoints

8

Weeks

1

Person

What this case study actually proves is the part that's harder to show on a portfolio. I can take a vague personal problem, work out the information architecture, design the system, build the system, ship the system, and live with the consequences. The design decisions had to survive contact with implementation and contact with daily use. The ones that didn't, I changed.

07 - Outcomes

What this shipped, and what it proves

Allister Ledger is in production as a self hosted PWA on my Synology NAS via Docker, with React 19 on the front end, Express on the API, and SQLite for storage. By the numbers: 12 spreadsheets and 60+ tables turned into 14 routes and 39 database tables, with 250+ API endpoints. The whole thing took eight weeks of evenings and weekends.

14

Routes

39

Tables

250+

Endpoints

8

Weeks

1

Person

What this case study actually proves is the part that's harder to show on a portfolio. I can take a vague personal problem, work out the information architecture, design the system, build the system, ship the system, and live with the consequences. The design decisions had to survive contact with implementation and contact with daily use. The ones that didn't, I changed.

Thanks for reading!

© 2026 by Albert Carmona

© 2026 by Albert Carmona

© 2026 by Albert Carmona