Project / 03

TrueCost - local-first personal finance

Expo/React Native finance app that keeps core budgeting data on-device. It combines SQLite/Drizzle storage, receipt-backed expenses, subscription tracking, loan scenario modeling, backup/restore, and optional AI insights through a backend proxy.

role
Solo build
timeline
2025 / 2026
status
complete
stack
Expo, React Native, SQLite, Drizzle, Expo Router, Jest

The problem

Most personal finance apps make users choose between convenience and control. They either expect cloud accounts and connected bank feeds, or they become simple spreadsheet replacements that do not help users reason about recurring costs, receipts, debt, and future obligations together.

TrueCost is built around a different constraint: the core app should keep working locally. It lets a user set up a budget profile, track expenses with receipt images, manage subscriptions, model loan scenarios, compare repayment options, and see monthly cost pressure without depending on an external server for the main workflow.

My role

As a solo build, I owned the mobile app structure, local data layer, financial calculators, receipt persistence, backup/restore flow, and the optional AI insight path. The work was mostly product engineering: deciding which finance concepts needed first-class screens, then keeping the calculations and stored data consistent across the dashboard, calendar, subscriptions, and scenario comparison views.

The repo is organized as an Expo Router app with SQLite at the center. Screens read from the same local tables, shared utility functions handle finance math, and core data remains on-device unless the optional AI insights route is configured.

System design

ON DEVICEOPTIONAL AIDashboardlive queriesExpensescalendarLoan scenarioscomparisonSQLite coreDrizzle schemaSubscriptionsbillingBackuprestoreReceipt imagesFileSystemAI proxyJSON insightsoptional

TrueCost is built around a local SQLite core that every screen reads from and writes to. The whole financial workflow stays on the device, inside the boundary; only the optional AI insights path sends a summarized scenario off-device, drawn as the dashed call.

The local database has five main tables: users, budget profiles, expenses, subscriptions, and loan scenarios. Initialization enables SQLite foreign keys and WAL mode, then applies small incremental migrations for budget-profile fields. The app uses Drizzle on top of expo-sqlite so screens can query structured data instead of passing large state objects through navigation.

The dashboard uses live queries to combine recent expenses, active subscriptions, and loan scenarios into a monthly cost picture. Yearly subscriptions are normalized into monthly cost, while loans are calculated through a shared amortization utility so the dashboard, scenario detail page, and comparison screen do not drift apart.

Key technical decisions

  • Local-first SQLite instead of a backend dependency. Budget data, expenses, receipts, subscriptions, and loan scenarios live on the device. That matches the privacy goal and keeps the app useful without network access.
  • Shared calculators for financial behavior. Loan payments, total interest, time-cost, and credit-card payoff logic live in utilities with Jest coverage. That makes the app easier to trust because screens are not each inventing their own version of the math.
  • Persist receipt images before saving expenses. Picker URIs can be temporary, so the add-expense flow copies receipt images into the app document directory and stores the durable URI with the expense record.
  • Backup/restore at the database-file level. The backup utility checkpoints SQLite WAL state, shares a copied database file, and restores by replacing the database and sidecar files. It is simple, understandable, and useful before a cloud sync system exists.
  • AI behind a proxy boundary. The app does not need an API key embedded in the client bundle. The optional insights route sends a compact scenario summary to a model endpoint, asks for strict JSON, and falls back to deterministic messages when the API is not configured or returns malformed output.

Results

TrueCost reached a working v1 mobile-app shape: onboarding, budget settings, dashboard overview, expense entry, receipt image attachment, calendar browsing, subscription management, loan scenario creation, scenario detail, side-by-side loan comparison, database backup/restore, and optional AI insights.

The repo has concrete technical depth behind the UI. It includes a five-table local schema, live SQLite-backed dashboard reads, monthly normalization for yearly subscriptions, three loan payment frequencies, fixed and variable-rate scenario fields, receipt image persistence, and tests for the time-cost and credit-card payoff calculators. The current Jest suite passes with 10 tests across the financial calculator utilities.

What I would do differently

I would move the manual table-alter migration logic into a more formal migration path. The current setup is fine for a small local app, but a versioned migration system would make future schema changes easier to review and recover from.

I would add integration tests around the SQLite flows that matter most: create expense with receipt, verify it appears on the calendar, add a subscription, add two loan scenarios, compare them, then backup and restore the database. Those flows are where product trust matters more than component-level coverage.

I would also make the AI boundary more explicit in the product UI. Core finance data already stays local, but users should be able to tell when a summarized scenario leaves the device for optional analysis and when the app is showing deterministic fallback guidance instead.