A solo build, measured by craft rather than traction. See the sunset note on why I moved on.
The problem & the bet
Most people never get to see the two questions that decide financial independence: when can I stop working, and what does a real life change cost me? Moving cities. Taking a sabbatical. Buying versus renting. The tools that answer this well (Boldin, formerly NewRetirement) are powerful, but expensive and dated.
Exityear’s bet: put professional-grade modeling (Monte Carlo, a full US tax engine, multi-scenario “Life Branches,” and geographic-arbitrage cost-of-living) behind a fast, opinionated UI at a fraction of incumbent pricing, and compute all of it on the user’s device so their finances never touch a server.
Architecture at a glance
Three constraints drove every technical decision: correctness (wrong numbers kill a finance product), privacy(people won’t hand their net worth to a startup), and instant feedback (planning is exploratory, so latency kills it).
Browser
├─ React UI (Next.js 15, App Router)
├─ Web Worker ──► calculation engine ← ~8k LOC, pure, no I/O
│ (income · debt · tax · Monte Carlo · drawdown)
└─ PocketBase ──► auth + your saved scenarios only
(the math never leaves the browser)Decisions & trade-offs
The interesting part of a build isn’t the feature list. It’s the calls made under real constraints, and what they cost.
01A pure, testable engine as the defensible core
- Decision
- Build the retirement math as ~25 dependency-free TypeScript modules with a locked regression suite, rather than lean on a BI/charting library or a backend service.
- Trade-off
- I had to build (and own the correctness of) a full US tax model, covering federal brackets, FICA, NIIT, and all 50 states, instead of buying one.
- Outcome
- Any change to tax or drawdown logic had to prove it didn't move known-good outputs, so I could iterate on a 1,300-line projection engine fast without silently shipping wrong retirement numbers.
- If I did it again
- Same call. The regression suite paid for itself the first time a refactor tried to change a number it shouldn't have.
02Run the whole simulation client-side in a Web Worker
- Decision
- Compute every projection in the browser, off the main thread, rather than server-side.
- Trade-off
- No one's income, assets, or debts ever hit my server (privacy) and recalcs felt instant (no round-trip). The cost: I owned data integrity at the worker boundary and gave up server-side precompute for sharing/SEO.
- Outcome
- Added a sanitization layer so non-finite math could never reach the charts. Privacy became a real product promise, not marketing.
- If I did it again
- Keep it. But I'd invest earlier in the serialization contract between worker and UI; that boundary caused the subtlest bugs.
03Migrate the entire backend from Supabase to PocketBase
- Decision
- Replatform auth + data off Supabase to a self-hosted stack to cut recurring cost and own the layer. A high-blast-radius change touching every read and write.
- Trade-off
- Meaningful monthly savings and full control, paid for with a tail of concurrency bugs (PocketBase auto-cancellation) I hadn't priced in.
- Outcome
- Sequenced it staging-first with idempotent migration scripts and kept the old layer live until the new one verified, then absorbed the stabilization tail.
- If I did it again
- I underestimated the new platform's concurrency model. I'd spike the riskiest integration (request cancellation) before committing the whole migration.
04Monte Carlo as the paywall line
- Decision
- Model uncertainty (success-rate, percentile bands) instead of a single deterministic 'you retire at 52,' and make Monte Carlo the free/paid boundary.
- Trade-off
- Running 1,000 simulations in the browser meant trading accuracy against latency, so I tuned simulation count and volatility defaults to keep it responsive.
- Outcome
- The paywall mapped cleanly to value: certainty is the premium feature. Monetization read as product judgment, not a tollgate.
- If I did it again
- I'd A/B the free-tier limits earlier; I set the free/paid line by intuition, not data.
Engineering for trust
For a financial product, credibility is the whole game. The tax modules cite their sources (IRS / Tax Foundation) in-file, and a Vitest suite pins known-good outputs so the numbers can’t quietly drift:
// projections-regression.test.ts: the numbers are the product
expect(result.retirementBalance).toBeCloseTo(expected, 2)
expect(result.annualRetirementIncome).toBe(knownGood)
That same suite came along verbatim when I forked the engine for the simplified live version ↗ and still passed, proving the port didn’t move a single number.
Build once, use twice
The geographic-arbitrage feature ran on a dataset of 100+ cities’ cost-of-living indices. That same dataset generated 130+ programmatic-SEO destination and comparison pages, dynamic Open-Graph social cards, and structured data tuned for LLM-answer visibility. One dataset served both the product and a free organic acquisition channel.
Why I sunset it
Sunsetting Exityear was a deliberate prioritization call, not a flameout. I set out to test whether a solo builder could ship a correct, trustworthy finance product. Technically, the answer was yes.
The harder answer was the market one. Retirement and FIRE planning is a saturated space with entrenched, well-funded incumbents (Boldin and others), and I concluded a solo product had little room to carve out a durable, defensible wedge. Rather than keep pouring time into a crowded market, I made the opportunity-cost call and moved on. I reallocated focus to Kaiwaflow, a Japanese language-learning app, where I saw more room to build something differentiated.
Choosing to stop a project you’ve invested real work in is the same “saying no” muscle a TPM uses to kill a low-ROI workstream and redeploy the effort. Exityear never chased scale, so there was no user base to wind down. Instead, the engine lives on as an open, privacy-first free tool ↗.
An honest note on how it was built
Exityear was built solo with heavy AI assistance. What I owned, and what this page is really about, is the problem framing, the three architectural constraints, the trade-off calls (client-side compute, the backend migration, the paywall line), the sequencing, and the decision to sunset. Those are the parts that don’t come from a code generator.