Skip to case study

Work / Budżetówka

Own product Business analysis Information architecture System design UX UI AI-assisted build

Budżetówka

A household budgeting app for two people, built local-first for macOS. Every złoty gets a job before it is spent - and the couple decides together, in advance, rather than reconciling a spreadsheet after the fact.

Platform

macOS · Swift / SwiftUI

Architecture

Local-first · SQLite (GRDB)

Method

Zero-based budgeting

Scope

v2 MVP + v3 documents & AI

01

Why this product exists

The introduction: who it is for, what was actually broken, and what I set out to prove by building it end to end.

Two people share their money but not their view of it. Each has their own accounts, their own habits and their own bank app - and none of those apps will join two people’s finances into one picture. So couples fall back on a spreadsheet, and the spreadsheet drifts: two versions, two truths, and a monthly argument about where the money went.

The alternative most budgeting tools offer is a shared cloud account that owns your entire financial history. For a household ledger - salaries, debts, savings, who paid for what - that is a large amount of trust to hand over for a convenience.

Budżetówka takes the opposite position. The database lives on your own Mac. The cloud is a copy you can switch off, not a condition of the product working. And the method is zero-based budgeting: every złoty is assigned a job before it is spent, so the conversation happens in advance instead of in hindsight.

I ran the whole thing myself - requirements and business analysis, information architecture, system and data design, UX, UI and the shipped build. That is the reason this case study is structured the way it is: it follows the actual chain of artefacts, because the point is not one screen, it is that the decisions hold together from the BRD down to the schema.

02

Three rules nothing was allowed to break

Set during analysis and enforced through every later decision - schema, interface, and the AI layer alike. They are the reason the product is predictable.

Amounts

Money is integer grosze only - 149,99 zł = 14999. No float, no double, no Decimal anywhere on the path, including values arriving from OCR and from the model. Rounding errors in a shared household budget are not a rendering bug, they are an argument.

Zero-based

The target is To Be Budgeted = 0. Every złoty carries an assigned job before it leaves the account. The interface therefore has one unmistakable success state, and the whole month's screen design answers a single question: what is still unassigned?

AI

Code computes the numbers, the model writes the words. Nothing reaches the database without the user accepting it. The model is allowed to read, propose and describe - never to total, and never to commit.

03

The system, and where trust ends

The context diagram from the product documentation: who uses the app, what feeds it, which external systems it talks to - and the boundary past which data leaves the device. Four layers, and a domain core that knows nothing about the network or the UI.

System context diagram: two budget co-owners, source documents and CSV input feeding Budżetówka.app, which is split into presentation, application, domain core and data layers, with a trust boundary separating Google OAuth, Sheets, Drive, Gemini, exchange-rate and macOS framework integrations
System contextDocumentation · page 01

Who and what touches the budget - and where does data leave the Mac?

Two co-owners and three inputs on the left; the app in the middle, split into four layers; six external systems on the right. The red dashed line is the trust boundary, and every arrow that crosses it is labelled with what goes over. What stays hidden is the inside of each layer: its components and how they depend on each other.

  • 2 co-owners
  • 3 inputs
  • 4 layers
  • 6 external systems
  • 1 trust boundary
Data hidden - shown for presentation purposes only

Two equal sources of writes

The app and the spreadsheet can both change the same row. That forced updated_at + row-hash markers, tombstones and a Last-Write-Wins rule - without them the two copies drift apart silently.

The cloud is optional, not critical

Every feature except synchronisation and AI has to work with no network. That single constraint rules out any screen that waits on a server response.

AI is an add-on, not a dependency

Off by default, three separate consents, aggregates instead of raw data. Turning AI off must not disable anything else.

04

Eight documented paths, clicked through

Every path below is a real use case from the product documentation - the steps, the wording, the screens and the edge cases are the artefact, not a retelling of it. Pick a path, or let it play.

05

The screens, and the decision inside each one

Eleven screens assembled from one shared component library - so a change to a row, a stat or the sidebar propagates instead of being redrawn. Six of them carry the design decisions worth arguing about.

Budgetthe screen the whole method rests on

One number decides whether the month is done

To budget is the largest figure on the screen and the only one with a success state: zero. Everything else - the fixed / variable / non-monthly split, the progress bars, the per-category Available column - exists to answer what is still unassigned.

Overspend is shown, never blocked. A negative Available turns red and stays visible; the method’s job is to make the trade-off obvious, not to refuse the purchase that already happened.

Budget screen: To budget 1 240,00 zł, month income, planned and spent, and a category matrix split into fixed, variable and non-monthly with planned, spent, available and progress columns

Comparisonmine, yours, shared

Separating two people without merging their bank accounts

Every account carries an owner - person A, person B or both - and that single field drives three columns across the whole product. Two people can run one budget while keeping separate banks, which is the actual situation most couples are in.

The perspective switcher narrows the entire app to one person. The data never changes; only the lens does.

Comparison screen: net worth, income, expenses and balance in three columns for person A, person B and shared, with each person's share of monthly expenses and a per-category breakdown

Importthree steps on one screen

Do the boring part, flag the risky part

Load, map, preview - in one view, because splitting them across a wizard hides the only thing that matters: what is about to enter the ledger. Duplicates are detected on date, amount and description similarity and held back with a badge rather than silently dropped.

Both 1 234,56 and 1,234.56 parse, and every value converts to integer grosze before it touches the database.

Import screen: CSV file load with separator and encoding detection, column mapping, and a preview table with duplicate rows marked

Dashboardthe start screen after setup

State first, actions second

Net worth and the month’s flow sit above everything else, then budget execution, goals and upcoming payments. It answers “where are we?” before offering anything to click - which is what someone opening a budget app at the end of a month actually wants.

Dashboard screen: net worth with trend, this month's flow, accounts, budget execution, savings goals and upcoming payments

Recurring paymentsa plan that feeds the budget

Obligations that reconcile themselves

Overdue / next 30 days / later, with the monthly obligation totalled. Annual and quarterly amounts are reduced to a monthly equivalent automatically, and the plan can fill category budgets in one action.

A matching transaction - same account and category, with amount and date inside a defined tolerance - marks the payment settled and rolls the due date forward - but the app never creates the transaction itself, because that is how duplicates start.

Recurring payments screen: overdue, next 30 days and later sections, the monthly obligation total, and actions to reconcile with transactions and fill the budget

Settingswhere the trust boundary is made visible

Telling the user exactly what left the device

The Google scope is stated in plain language, the spreadsheet the app writes to is linkable, sync is switchable, and the local database’s path and size are shown outright. A local-first promise is worth nothing if the product will not show you what it sent.

Account addresses and document links are redacted - shown for presentation purposes only.

Settings screen: Google account with drive.file scope, spreadsheet links, people in the budget, synchronisation, security with PIN and Touch ID, appearance and local data

06

Structure, rhythm and schema

Eight of the nine diagrams from the documentation - the ninth, the context diagram, opened this case study. They are the reason the eleven screens could be designed once and the eight paths written without guessing.

How to read them. Every diagram uses one notation. Box titles, type tags and arrow labels are left readable so each flow can be followed; only the descriptions inside the boxes are blurred.

  • Screen · user action
  • System process
  • Decision · YES / NO
  • Data table · write
  • External system
  • Error · rejection
  • v3 · documents & AI
Information architecture map: ten navigation sections with every view, modal and function inside them
Information architectureDocumentation · page 02

What lives where in the app - decided before any screen was drawn?

One card per navigation section, each answering the same three questions, whose headings stay readable: what the user sees, what they can do, and which windows, data and rules sit behind it. Every section sits inside one shared shell - lock screen, sidebar, toolbar and month bar. Blurred: the answers themselves, the full inventory of views, actions and modals.

  • 4 shell elements
  • 10 sections in v2
  • +2 in v3
  • 3 questions per section
Data hidden - shown for presentation purposes only
Zero-based budgeting cycle flow: income, allocation, spending, rollover and the new month, with formulas and boundary cases
The monthly ZBB cycleDocumentation · page 04

What has to happen every month for “every złoty has a job” to stay true?

One month, top to bottom: balances carry over, income is recorded, the loop checks whether anything is left unassigned, spending updates the categories, and the close opens the next month. The two decisions are the questions the loop keeps asking; the undesired state is the one it exists to prevent. The notes beside it hold the engine’s formulas, exclusions, multi-currency handling and tested edge cases - precisely the part that is blurred.

  • 12 steps
  • 2 decisions
  • 1 undesired state
  • 6 reference notes
Data hidden - shown for presentation purposes only
Entity relationship diagram: sixteen SQLite tables plus two for version three, with keys, relations, synchronisation columns and invariants
Data modelDocumentation · page 08

Where does every number the app shows actually live?

Each card is one table. Its name and role tag stay readable - dictionary, core, budget, automation, import, infrastructure - while the columns, keys and invariants underneath are blurred. The schema grew through versioned migrations, and three conventions hold in every table without exception; they sit below the cards, together with the relation table, outside this crop.

  • 16 tables in v2
  • +2 in v3
  • 14 relations
  • 6 table roles
Data hidden - shown for presentation purposes only

The four process flows behind them - each one drawn before a single screen was designed, with the refusal, error and conflict branches on the same page as the happy path.

Onboarding and access flow: first launch and household setup, the lock-screen gate on every later start, and the decision between a fully local mode and Google sign-in with a shared spreadsheet
Onboarding and accessDocumentation · page 03

How does a couple get from an empty install to a shared, protected budget - and what if they never want the cloud?

The first launch sets up the household: names, accounts, categories and a first allocation. Every later start passes the lock-screen gate. One decision then splits the flow - stay fully local, or continue to Google sign-in and a shared spreadsheet - and skipping the cloud costs no local feature. The notes on the right explain the security and scope choices; their reasoning is blurred.

  • 22 steps
  • 4 decisions
  • 2 end states
  • 5 design notes
Data hidden - shown for presentation purposes only
Transactions and import flow: manual entry, bank-statement import and receipts merging into one validation chain and a single write, with a rejection path and a statement-reconciliation side path
Transactions and importDocumentation · page 05

Is there a shortcut into the ledger? No - and this diagram is the proof.

Three ways in - typed by hand, imported from a bank statement, read from a receipt - merge into one chain: amounts normalised, currency checked, duplicates caught, a category assigned, splits validated, then a single write. The red rejection box is where a write that doesn’t add up is stopped. After the write, the budget recalculates and the change is queued for sync; a side path reconciles an account against its statement. Blurred: how each check decides.

  • 3 entry paths
  • 1 write path
  • 4 decisions
  • 1 rejection path
Data hidden - shown for presentation purposes only
Documents and AI flow: a receipt processed locally on the Mac, one optional network step to an AI model, and a preview-and-approval screen before anything is saved
Documents and AIDocumentation · page 06 · v3

How does a photo of a receipt become a split transaction - without trusting the model blindly?

Almost every step runs locally on the Mac. A single step, tagged “AI model (network)”, calls a model - and it is optional: with AI switched off, no key or no quota, the same path still reaches the approval screen. Nothing is saved until a person previews the result and approves it. A panel below the flow answers what the model does, what it never does, what leaves the device and what it costs; those answers are blurred.

  • 20 steps
  • 5 decisions
  • 1 optional network step
  • 1 approval gate
Data hidden - shown for presentation purposes only
Two-way synchronisation flow: Path A sends app changes to the Google spreadsheet, Path B brings hand-edited rows back, with conflict and exception branches
Two-way synchronisationDocumentation · page 07

What happens when the app and the spreadsheet both change the same row?

The spreadsheet is a backup, a sharing channel and an editor people can type into - so there are two lanes. Path A takes a change made in the app out to the spreadsheet; Path B brings a hand-edited row back into the app, where the critical question sits: did the same row change on both sides? Two exception branches cover no network and the API pushing back. How a conflict is settled, and the conditions the mechanism depends on, stay blurred.

  • 2 lanes
  • 3 decisions
  • 2 exception branches
  • Top project risk · R1
Data hidden - shown for presentation purposes only
Requirements catalogue: every requirement with its ID, name, screen or area and status, followed by the architecture decision record and the risk register
Requirements catalogueDocumentation · page 09

What exactly is in scope - and what counts as done?

The page works as the scope contract: if something isn’t on it, it doesn’t get built without a decision first. Every entry keeps its ID, name, screen or area, and status readable. Only the description and acceptance criterion - the part that says how each requirement must behave - is blurred. Further down the same page, outside this crop, sit the architecture decision record and the risk register.

  • 50 entries
  • 8 functional
  • 4 non-functional
  • 10 ADRs
  • 10 risks
Data hidden - shown for presentation purposes only

07

The process, as it was actually documented

Thirteen pages, nine diagrams, three UI/UX pages - one artefact per decision, each one the input to the next. Then the app itself went to six test groups. This is the part most portfolios skip.

Analysis

Requirements and system foundations

BRD and a system-foundations study established who the product is for, the value it has to create, and the non-negotiables - before a single screen was drawn.

BRDSystem foundations studyInviolable rules
Structure

Context diagram and information architecture

A context diagram fixed the actors, external systems, trust boundaries and data directions. The IA map then laid out ten navigation sections with every view, modal and function inside them.

Context diagramIA map · 10 sections
Behaviour

Five flows, including the edge cases

Onboarding and access; the monthly ZBB cycle with its formulas and boundary cases; transactions and import; the v3 document and AI pipeline; and synchronisation with conflict resolution and offline behaviour.

OnboardingZBB cycleTransactions & importDocuments & AISync
Data

Model and requirements matrix

Sixteen SQLite tables plus two more for v3 - keys, relations, synchronisation columns and invariants. Alongside it, an FR / NFR / REQ / NR matrix tying every requirement to a priority, a screen and an implementation status.

ERD · 16 + 2 tablesRequirements matrix
Interface

Eleven screens, nine modals, eight user paths

Screens built from a shared component library so a change propagates rather than being re-drawn. Then eight use cases assembled from instances of those very screens, with transition labels and edge-case variants under each path.

11 screens · 1440 px9 modals & statesCS-01 … CS-08
Build

Shipped with AI in the pipeline, not in the ledger

The app was built end to end with AI assistance - while the product's own rule held: code computes the numbers, the model writes the words, and nothing is committed without a person accepting it.

Swift / SwiftUIProduction desktop app
Validation

Six test groups, then iterations on what they said

The working app went to six groups of test users. Their feedback was collected and turned into successive iterations of the application. Documentation proves the logic holds together; testing with real people is what shows whether it makes sense to them.

6 test groupsFeedback collectedApp iterations

Code computes the numbers. The model writes the words.

Rule set during analysis - and the reason the AI layer could ship

08

What I did on it

Single-handedly, end to end - which is the point: the same person set the requirements, drew the system, designed the interface and shipped the build.

Business analysis

Requirements gathering, BRD, system-foundations study, and the constraint set that governed everything downstream.

Information architecture

Ten navigation sections mapped down to every view, modal and function, with the reasoning recorded per section.

System & data design

Context diagram, five process flows, a sixteen-table data model with sync columns and invariants.

UX

Eight documented user paths with transition labels and edge-case variants, built from real screen instances rather than sketches.

UI & design system

Eleven screens and nine modals assembled from a shared library, with tokens for type, colour, radius and elevation.

AI-assisted build

A production macOS app shipped with AI in the delivery pipeline, under an explicit rule about what the model is never allowed to do.

09

What it came to

The conclusion: what shipped, what the constraints bought, and what the next version is for.

A production macOS app, running local-first, used by an actual two-person household - not a concept deck. The documentation behind it is thirteen pages deep, and every screen in it traces back to a requirement and forward to a table in the schema.

The constraints turned out to be the product. Integer grosze removed a whole class of rounding disputes. To Be Budgeted = 0 gave the interface one unambiguous success state, which is why the budget screen needed no dashboard of charts. Local-first meant no screen could be designed around a spinner. And the AI rule - code computes the numbers, the model writes the words - is what let an AI layer ship into a financial product at all without asking anyone to trust it with arithmetic.

The next version is about documents: receipts and invoices arriving as PDFs and photos, running through extraction, parsing and categorisation, and stopping at a human acceptance gate before anything is written. It is deliberately the same shape as the rest of the product - the model is allowed to propose, never to commit.

What I would take to the next project: that writing the system down properly is not overhead. Eight user paths were cheap to document because the components, the flows and the schema already agreed with each other. The documentation did not describe the design - it was the design work.