Back to Work

Talliofi

I built Talliofi as a local-first personal finance app where your data lives on your device first and sync is optional. The goal was to make budgeting and planning useful right away, without forcing account setup or default cloud storage.

Product Outcome: It let people start budgeting immediately instead of asking them to hand over data before they had even tried the product.

The Problem

Talliofi was built around a strict product stance: financial planning should work fully offline, with user data staying local by default. The challenge was delivering complex budgeting workflows with production UX while preserving privacy and correctness guarantees.

Most personal finance apps ask users to trust them with sensitive financial data. That is a real barrier for privacy-conscious users. The technical challenge was building a SaaS-quality experience (charts, budgets, tax estimation) entirely in the browser, with no backend, while keeping money calculations correct down to the penny.

Scope & Ownership

Scope: Built a production-grade local-first finance system: domain engine, normalized data model, repository layer, sync strategy, and reporting UX.

Ownership: Owned architecture, implementation, QA strategy, and roadmap prioritization as founder-engineer.

Why Angular For This Problem

This project was intentionally built in React to validate a framework-agnostic domain architecture. The same layering approach and quality gates directly informs my Angular system design on production teams.

Alternatives Considered

  • Backend-first architecture: rejected to preserve privacy-first positioning and reduce trust friction.
  • Storing aggregate metrics directly: rejected to avoid consistency drift under frequent writes.

Roadmap Outcomes

  • Shipping local-first first reduced delivery risk and validated core product value before sync complexity.
  • Front-loading domain correctness (money types, invariants, validation boundaries) reduced compounding defects as features scaled.

Constraints & Requirements

  • Handle large financial datasets smoothly with no backend dependency
  • Run well on mid-tier devices with no server round-trips
  • Money must be computed as integer cents. Floating-point rounding errors are unacceptable in a finance app
  • Zero third-party analytics, telemetry, or external API calls for user data
  • The architecture must support optional cloud sync later without rewriting the UI or domain logic
  • Domain logic must be framework-free and testable without a browser

Architecture & Technical Approach

I implemented strict boundaries: pure domain logic, validated persistence, repository routing, then UI consumption through query hooks. This made financial calculations testable without the framework and kept storage concerns swappable without rewriting feature code.

  • Local-first architecture: IndexedDB/Dexie as source of truth with optional sync layers.
  • Data strategy: normalized tables and repository abstraction for local, direct cloud, and encrypted cloud tiers.
  • Performance and UX: route-level code splitting, responsive reporting UI, and stable mutation patterns.
  • Privacy and security: no default telemetry, optional encrypted sync paths, and explicit data ownership controls.
  • Testing: 760+ tests across domain, integration, and end-to-end layers.

Key Engineering Decisions

IndexedDB over localStorage for persistent offline storage

Rationale: localStorage has a 5MB limit and its synchronous API blocks the main thread. A finance app with budgets, transactions, and tax records needs structured data, indexed queries, and transactions. Dexie gave me a clean TypeScript abstraction over IndexedDB without hiding the power of the underlying API.

Outcome: 100% offline-capable finance management that handles complex financial modeling without performance degradation.

Branded integer Cents types for money arithmetic

Rationale: Early in development, I found rounding discrepancies where budget totals were off by a few cents. The root cause was floating-point arithmetic. I switched all money values to integer cents and introduced branded TypeScript types so the compiler prevents you from accidentally adding dollars to cents.

Outcome: An entire class of rounding bugs disappeared. I wrote about this in detail: https://dev.to/emmanueln07/how-a-branded-cents-type-eliminated-an-entire-class-of-bugs-across-97-files-2o6o

Route-based code splitting for bundle optimization

Rationale: A finance dashboard with charting, forms, and analytics can get heavy fast. Splitting by route means users only download code for the feature they are actually using.

Outcome: Reduced initial payload meaningfully and preserved responsive dashboard interactions as features expanded.

Repository abstraction for three storage tiers

Rationale: I deferred cloud sync to protect the MVP scope, but the architecture still needed to support it. A shared repository interface lets local-only, Supabase direct, and encrypted Supabase implementations be swapped without touching feature code.

Outcome: Talliofi ships fully local-first today and can add sync tiers later with minimal integration risk. I wrote about this architecture: https://dev.to/emmanueln07/your-financial-data-should-live-on-your-device-here-is-the-architecture-that-makes-that-possible-1764

Compute summaries on demand, never store aggregates

Rationale: Storing pre-computed totals seems efficient, but in a finance app every write path has to update them perfectly or the numbers drift. Deriving summaries from source records on demand eliminates an entire category of consistency bugs.

Outcome: Dashboard metrics are always perfectly consistent with raw data. The calculation engine is pure, stateless, and easy to validate.

Tradeoffs

  • Deferred cloud sync to keep MVP scope focused and protect delivery timeline.
  • Accepted more up-front architecture work to keep domain logic framework-agnostic and testable.

Team Decision Points

  • Prioritized local ownership and trust over faster cloud-first feature expansion.
  • Aligned on a repo-strategy approach so storage-mode changes would not leak into feature modules.

Team Scale Impact

  • Defined repository contracts that let teams introduce future sync tiers without reworking feature modules.
  • Separated domain, persistence, and UI concerns to improve onboarding and enable parallel feature development.

Results & Metrics

Offline capability

Before: N/A (new build)

After: 100% offline

Impact: Every feature works from the first load with zero connectivity

Lighthouse Performance

After: Consistently strong

Impact: Route-based code splitting and lazy loading

Bundle size

Before: Baseline (no splitting)

After: Measurable reduction

Impact: Route-based code splitting

Test suite

After: 760+ tests

Impact: Domain unit tests caught 3 rounding edge cases, integration tests validated IndexedDB consistency, E2E confirmed critical user flows

Money correctness

Before: Floating-point arithmetic

After: Branded integer cents

Impact: Rounding errors eliminated at compile time

Decision Evidence Matrix

Decision: IndexedDB as source of truth

Risk: Cloud-first dependency increases onboarding trust friction and reduces offline reliability.

Change: Adopted local-first persistence with Dexie/IndexedDB and optional sync modes.

Result: 100% offline-capable default mode with strong automated quality coverage.

Source: Source

Decision: Repository strategy for storage tiers

Risk: Storage-mode changes can require expensive feature rewrites and increase roadmap risk.

Change: Implemented repo abstraction for local, direct-cloud, and encrypted-cloud variants.

Result: Storage complexity isolated to data layer; feature modules remain largely mode-agnostic.

Source: Source

Decision: Single source data model (Dexie + Query)

Risk: Multiple persisted state stores can drift and create stale or inconsistent UI states.

Change: Standardized on Dexie persistence with TanStack Query caching/mutation lifecycle.

Result: Reduced data-consistency risk and improved mutation reliability under growth.

Source: Source

Risk Management & Rollback Strategy

  • Enforced integer-cents arithmetic to eliminate monetary rounding risks.
  • Isolated storage behind repositories to reduce migration risk for future sync tiers.
  • Kept domain logic framework-free for high-confidence unit testing.

Incident Handling

Incident: An early build exposed rounding discrepancies in monthly budget totals due to inconsistent arithmetic paths.

Response: Halted feature rollout, audited all money operations, and migrated calculations to strict integer-cents semantics.

Rollback: Kept summary widgets on source-of-truth recalculation paths while disabling unsafe cached aggregate writes.

Prevention: Added domain invariants and regression tests around money arithmetic, blocking merges that violate cents-only constraints.

Operational Readiness

  • 760+ tests spanning pure domain, IndexedDB integration, and E2E workflows.
  • Offline-first behavior validated across core planning and reporting flows.
  • CI pipeline gating lint, typecheck, tests, and build before merge.

Architecture Diagram

Talliofi architecture diagram showing React UI, domain engine, repository layer, and local IndexedDB storage.
System-level view of runtime layers and data flow.

Lessons Learned

  • The floating-point rounding bugs taught me to validate financial assumptions with real test data early. I almost shipped a version where budget totals were off by pennies, and it would have been embarrassing.
  • Building the domain layer as pure TypeScript with no framework dependency was the best architectural decision. It made the 760+ tests fast and reliable because nothing depends on React rendering.
  • IndexedDB is powerful but its API is awkward. Dexie smooths out the rough edges, but I still had to debug transaction failures and schema migrations carefully.
  • I designed the storage abstraction before writing any UI code, which felt slow at first. But when I later added the repository pattern for future cloud sync, everything clicked into place without touching feature logic.
  • Privacy is not just a feature toggle. Designing for zero telemetry from the start affects everything from error handling (no Sentry) to analytics (no Mixpanel) to data modeling (no server schemas).

What I'd Do Differently

  • Invest in migration tooling earlier to simplify future schema evolution.
  • Introduce usability testing cycles sooner to tune novice-friendly onboarding.

Stack

React 19 TypeScript Vite 7 Tailwind CSS v4 shadcn/ui Dexie (IndexedDB) TanStack Query Zustand Recharts React Hook Form Zod nuqs Vitest Playwright