← All work
Case 02 · Product of the practice · Mar 2025 — present

A product built solo, from design system to five platforms

A cash-flow calendar that puts every paycheck and bill on the day it lands, with a running balance for every day ahead. Designed, built and run by one engineer, and now in private beta.

Role
Founder · sole engineer
Stack
React 19TypeScriptViteTanStackGoOpenAPIPostgresRedisTauriFly.io
The application · month view
The Fudger month view: a calendar grid with a running balance on every day and colour-coded income and expense entries.
  • 5Platforms from one codebase: web, macOS, Windows, Linux, iOS
  • 3,364Automated tests across API, front end and end-to-end
  • 68API operations, server and client generated from one spec
  • 1Engineer, from design system to deployment
The short version
The brief

Show every paycheck and bill on the day it lands, with a running balance for every day ahead, on the web, the desktop and the iPhone.

What I built

The design system, a React app, native shells for four platforms, a Go API on Postgres and Redis, the release pipelines and the docs. The marketing site is live at fudger.io.

Why it matters to clients

Every pattern Kithara recommends runs here first, on a system I have to operate myself.

The architecture

Three tiers, one generated contract

The system turns on one domain rule: Fudger ignores time of day. The date a user picks is the date stored and returned, byte for byte, with no timezone conversion. Most of the structure below exists to enforce it.

Client tier
fudger-mktg
Static site. The landing page is a playable demo that makes zero network calls.
fudger web
React 19 · TypeScript · Vite. TanStack Router, Query and Table. Served by nginx, which proxies /api same-origin.
native clients
macOS, Windows, Linux and iOS via Tauri, from the same bundle. The Rust shell is 58 lines.
generated client
TypeScript client generated from the same OpenAPI document as the fudger-svc API tier. 100 files, none hand-edited.
↓ /v1 · bearer JWT↑ JSON · calendar dates as bare text
Service tier
fudger-svc
Go · Chi · Ent. OpenAPI-generated handlers over one 105-method datastore interface. Recurring entries are projected per request.
↕ entries · overrides · anchors · sessions
Data tier
Postgres
Fly Managed Postgres. The five user-facing date columns are text, so no timezone can attach to them.
Redis
Sessions, plus shared rate limits, entitlements and cooldowns. Degrades to process-local, never to “allow”.
Not shown: recurring occurrences are never stored. Overrides, such as a one-month rent rise or a skipped payment, are the only rows a series spawns.

The contract between tiers is the OpenAPI document: 49 paths and 68 operations, from which both the Go server and the TypeScript client are generated.

Key decisions

The calls that shaped the product

One line each. Open any card for the engineering detail.

The domain model0 timezone conversions

Built on the calendar day

Dates are stored as calendar text, so a bill due on the 28th shows on the 28th wherever the user is.

Engineering detail

Money in a budget moves by the calendar day. Rent paid on 28 February belongs to 28 February wherever the user happens to be, so the app treats a date as a date: never a timestamp, never shifted by a timezone.

Every user-facing date is stored as plain YYYY-MM-DD text. It has no timezone to drift, sorts chronologically, and reads the same in the database, the API and the UI.

The rule holds across the stack. The API returns each date exactly as it was entered, and the date-sensitive tests run in three timezones on every build.

5
User-facing date columns stored as calendar text
0
Timezone conversions between database and UI
3
Timezones exercised on every CI run
One spec, two languages49 paths · 68 operations

Made the wire contract a build artifact

Server and client are generated from one OpenAPI document, so a renamed field fails the build before any user sees it.

Engineering detail

Both sides are generated from one OpenAPI 3.0 document, so a renamed field breaks the build before a customer sees it.

Generator bugs get wrappers. The generated parser turned a present-but-empty query parameter into a silent false, so both route tables mount through a wrapper that strips it first.

Recurrencestored once

Projected recurring money per request

A recurring bill is stored once and projected on request. Editing “this and all future” starts a new series with its lineage recorded.

Engineering detail

A recurring entry is one row: its first occurrence and the template for the rest. The others are projected per request with deterministic ids. Editing “this and all future” starts a new series with recorded lineage. Payment state belongs to one occurrence, so it never copies from the template.

“The 15th and the last business day”, rolling off weekends like payroll, is a preset, and a bill due on the 31st lands on a real date in shorter months.

Balancesto the cent

Built a forecast that re-anchors to the bank

Tell the app what the bank says, and the running balance restarts from that number.

Engineering detail

The product is named for its signature move: telling the app what the bank actually says. An anchor declares that on this date the balance is this number, and the running total restarts there.

Each day is computed from the most recent anchor, so one bad day cannot carry forward. The ledger also reads as confirmed-only: settled money on the date it landed, at the amount that left.

An older wire model had narrowed anchors to 32-bit floats, storing $391.60 as 391.6000061035156. A repair pass found them by exact float32 representation, and the endpoint now rejects sub-cent balances.

Proving the tests3,364 tests

Made test-driven development something CI can check

Every test a branch adds must fail against the base branch first, so each test proves new behaviour.

Engineering detail

CI checks the evidence that tests came first: every test a branch adds must fail against the base branch. A new test that passes against unchanged code either transcribes the implementation or describes behaviour that already worked. Both had shipped here.

A test that does not compile against the base satisfies the check. A dependency bump, which has no failing test, carries a conspicuous commit-message marker. A deliberately failing commit, named red in its message, is welcome as the specification if the branch is green before the pull request opens.

3,364
Tests across API, SPA and E2E
85,303
Lines of test code
82,451
Lines of production code
Design as a contract4.5:1 enforced

Made the colour system enforceable by tests

Eight colour schemes in light and dark, each tested for 4.5:1 text contrast against the colours actually on screen.

Engineering detail

Eight colour schemes, five layouts and three entry treatments, in light and dark, are too many to review by eye. So legibility is a contract: text must clear 4.5:1 against the surface it is actually drawn on.

Measuring text on translucent tinted chips against the bare cell overstated contrast by up to 0.9, hiding six failing role and mode combinations. A test now checks the whole matrix against composited colours. Income and expense must differ in brightness as well as hue, so they survive greyscale and common colour blindness.

Timezone matrix3 timezones

Ran the date-sensitive suites in three timezones every build

Date-sensitive tests run west of UTC with daylight saving and in the deepest zone without it, on every build.

Engineering detail

A suite that only runs in its author’s timezone proves little about timezone independence, so the date-sensitive suites also run in a zone west of UTC with daylight saving and in the deepest zone without it.

The harness sets the zone at process spawn, which survives date-library caching and matches how a browser sees a fixed zone. It fails if a rename drops a file from the matrix.

One machine → manynever fails open

Took the service from one instance to several, then proved it

Shared state moved to Postgres and Redis, and limits now hold across instances, verified from outside.

Engineering detail

The original design had a hard ceiling: state in process memory and a database on a volume that attaches to one machine. Scaling out took a Postgres cutover and moving rate limits, entitlement caching and per-user cooldowns onto Redis behind a narrow interface.

If Redis stops answering, each control degrades to process-local, and a store error never becomes an allow. The cost is written down: when Redis drops, one caller gets a fresh local allowance.

A validation script pins requests to specific instances through the platform proxy, so “counted on one, enforced on the other” is observed from outside.

Migrations that repairappend-only

Wrote migrations that are allowed to run more than once

Data migrations live in an append-only registry, and the ones that repair data are safe to run on every boot.

Engineering detail

Data migrations are an append-only registry keyed by name, each claiming its own bookkeeping row in a transaction so a racing deploy loses cleanly.

The ORM re-renders the text date columns on every automigrate, so the damage repeats while a name-keyed repair runs once. Restore a snapshot and you get corrupted dates behind a green deploy. Migrations can now be marked convergent and re-run on every bootstrap. Two of the seven are.

Others refuse to guess: the migration that reduced timestamps to calendar days runs only where the original offset survives, and another lists four data shapes it will not repair as unprovable.

Five targets, one codebase58 lines of Rust

Shipped one application to the browser and four platforms

Tauri wraps the same React app for macOS, Windows, Linux and iOS, so no platform becomes a fork.

Engineering detail

Tauri wraps the SPA for all four platforms in a 58-line Rust shell, kept small so no platform becomes a fork. The release pipeline checks that a production bundle baked in the production API before it publishes.

In a workspace three packages deep, the expensive failures are a library built in the wrong flavour and a route with no component bound to it. A mapped type over the generated route ids makes the second a compile error.

Docs that cannot rot179 docs

Put the documentation under the same gate as the code

Every one of 179 docs carries a status line with a verification date and commit, and CI fails the build when one drifts.

Engineering detail

The two repositories hold 179 markdown documents, and the index once rotted to 31 of 59 entries. Each document now opens with a status line: state, verification date and commit. A shell-and-grep check fails the build on a missing header, a disagreeing index row or a dead link.

Specification documents are read from the code and cite file and line for each claim. API and front-end changes cross repositories through a hand-off document stating the decisions made and what not to change.

The product

What it looks like

Captured from running builds. Figures and payee names in the application are fictional. Layout, density and behaviour are what ships.

The Fudger month view: a weekday calendar grid with a running balance on each day, colour-coded income and expense rows, and a summary bar.
The application · month viewEvery day carries its own balance, future days included, and the toolbar shows the next paycheck, bills due and what is left at month end. Weekends are hidden but counted.
The Fudger marketing playground: three answered setup questions beside a generated month calendar with paydays and bills.
Marketing · the playgroundThree questions build a working month in the browser with no signup. The demo sends nothing, so it has nothing to leak.
The Fudger marketing hero: large type reading “Build your month. Right now. No signup.”
Marketing · heroThe visual identity is CSS and SVG. The only raster image is one social card.
The Fudger add-entry dialog: title, category, entry type, amount, date, an auto-debit toggle and a recurring option.
The application · entry editorOne row is a dated event or a series template. Auto-debit entries settle nightly, never overwriting a status a human set.
The Fudger categories screen: a hierarchical table of categories with colour swatches and system badges.
The application · categoriesSeeded system categories are shared, and users add their own. A category’s semantic role tells the toolbar which entries are paychecks.
The Fudger month view on a phone-sized screen, with balances compacted to abbreviated amounts.
The application · mobileThe same grid at phone width, honouring safe-area insets. The native iOS build renders it from the same code.
Live surfaces
  • fudger.io ↗Live marketing site and interactive demo.
  • fudger.io/downloads ↗Desktop and mobile builds.
  • app.fudger.ioThe application, in invite-only private beta.
  • api.fudger.ioThe API, gated behind a written launch runbook.
Systems owned

Sole author, three repositories

Counts taken on 2026-09-07. Generated code is excluded from every line count.

Show all 3 repositories
fudger-svc
REST API · Go, Ent, Postgres
290 commits · sole author
84,673 lines · 1,339 tests

The domain lives here: entries, recurrence projection, categories, declared balances, settlement, invites and admin. Handlers reach the ORM only through one datastore interface.

  • OpenAPI-first: 49 paths and 68 operations, plus 20 entity schemas that generate the ORM.
  • Auth: JWT with issuer, audience and expiry validation, and OAuth built at the identity level so a non-OIDC provider can plug in later. A password change invalidates sessions, even when a reset lands mid-request and would otherwise make a stolen session permanent.
  • Operability: client IPs resolve through trusted proxies, because the router default reads attacker-controlled headers. Rate-limit responses name their machine, and per-stage migration timeouts report which stage stalled.
  • A 194-test integration suite on real Postgres over real HTTP. It refuses non-loopback targets and has no override flag, because that flag gets set to turn red runs green.
fudger
SPA + native clients · React 19, TypeScript, Tauri
327 commits · sole author
83,081 lines · 2,025 tests

A pnpm workspace of three packages (shared domain and API layer, router, application), plus the Rust shell for the native builds.

  • A written calculation boundary: the API owns balances; the front end owns the cascade under an anchor, three-month grid assembly, week resolution and forward-looking aggregates.
  • Routing: search parameters parse as strings, so an invite code is never read as exponential notation.
  • 13 end-to-end specs against a real backend, with a reporter listing the API endpoints each run touched.
fudger-mktg
Marketing site · React 19, Tailwind
Static build, no server
15,709 lines

A playable demo computes a visitor’s month in the browser and forgets it when the tab closes. The page makes no network calls, so the privacy claim can be verified.

  • Ten landing-page prototypes from a direction bake-off stay behind a noindex gallery, so the chosen direction can still be argued with.

These are the patterns the practice brings to client systems, proven first where the consequences land on us.

Start a conversation