Soleone/fina

Local-first personal finance tracker

★ 0Forks 0TypeScriptGitHub ↗Compare

README

Fina

Local-first personal finance tracker for understanding monthly budget, recurring commitments, and cash flow.

Quickstart

This repository has an initial Electron/Vite/React scaffold.

pnpm install
pnpm dev
pnpm build

Current direction:

  • App: local desktop app
  • Shell: Electron + Vite
  • Frontend: React + TypeScript
  • UI: shadcn/ui + Base UI + Tailwind
  • Data: SQLite with JSONL export
  • Primary MVP workflow: monthly budget overview
  • App icon: build/icon.png

Next implementation step:

  1. Add the Drizzle SQLite schema and migrations.
  2. Build typed persistence/services for accounts, categories, transactions, and dashboard totals.
  3. Build the first vertical slice: accounts, categories, manual transactions, and dashboard totals.

Product scope

Fina should help answer one core MVP question:

How much money is available this month after income, spending, and recurring commitments?

The first prototype focuses on monthly budget clarity, not complete accounting.

MVP user workflow

A user can:

  1. Add income, expenses, and transfers manually.
  2. Import transactions from a bank or credit card CSV statement.
  3. Review imported transactions before accepting them.
  4. Categorize transactions into budget categories.
  5. Mark recurring income and expenses.
  6. See a current-month overview of:
    • total income
    • total expenses
    • net cash flow
    • recurring commitments
    • remaining available budget
    • spending by category
    • recent transactions

MVP features

Transactions

Core record for income, expenses, and transfers.

Fields:

  • date
  • amount
  • type: income, expense, or transfer
  • description
  • account
  • category
  • optional notes
  • source: manual or imported

Accounts

Small set of accounts used to group transactions and make imports understandable:

  • checking account
  • savings account
  • credit card
  • cash

Full reconciliation can come later.

Categories

Editable budget categories with sensible defaults, for example:

  • salary
  • rent
  • groceries
  • utilities
  • subscriptions
  • restaurants
  • transportation
  • other

Recurring items

Recurring income or expenses help the monthly overview account for predictable commitments before transactions appear in imports.

Examples:

  • salary
  • rent
  • utilities
  • phone bill
  • insurance
  • subscriptions

CSV imports

Imported transactions go through review before saving.

The review step should allow the user to:

  • inspect parsed rows
  • choose or confirm the target account
  • map CSV columns if needed
  • accept, skip, or edit transactions
  • avoid obvious duplicates

JSONL export

Data should stay portable. The app should support JSONL export for durable records:

  • accounts
  • categories
  • transactions
  • recurring items
  • import batches

A likely record shape:

{"recordType":"transaction","schemaVersion":1,"data":{}}

Non-goals for the MVP

Important later, but not part of the first prototype:

  • tax calculation
  • tax filing exports
  • investment performance tracking
  • multi-user support
  • cloud sync
  • automatic bank API integrations
  • advanced forecasting
  • full double-entry accounting
  • mobile app support

Technical direction

Optimize for long-term maintainability and high-quality React/UI architecture while keeping the first prototype small.

Recommended stack

  • Desktop shell: Electron + Vite
  • Language: TypeScript
  • Frontend: React
  • Styling/UI: Tailwind + shadcn/ui + Base UI
  • Routing: TanStack Router
  • Database: SQLite
  • Database layer: Drizzle ORM
  • Database state: TanStack Query
  • Local UI state: React state first, Zustand only if cross-route UI state becomes painful
  • Forms: React Hook Form
  • Validation: Zod
  • Tables: TanStack Table
  • Charts: Recharts, used lightly
  • CSV parsing: Papa Parse
  • Tests: Vitest + React Testing Library, Playwright later

Why this stack

  • Electron gives mature desktop capabilities, filesystem access, and SQLite integration.
  • Vite keeps local React development fast.
  • Drizzle keeps SQLite schema and queries typed without hiding SQL too much.
  • TanStack Query separates database-backed state from UI state.
  • React Hook Form and Zod fit shadcn form patterns and provide reusable boundary validation.
  • TanStack Router and Table are headless, typed, and compatible with app-owned UI.

Architecture sketch

Core domain concepts

  • Account
  • Transaction
  • Category
  • RecurringItem
  • ImportBatch
  • ImportedTransactionCandidate

Accepted import candidates become real transactions.

Data flow

Manual transaction flow:

  1. User opens transaction form.
  2. User enters transaction details.
  3. UI validates required fields and amount/date shape.
  4. App saves transaction to SQLite.
  5. Dashboard recalculates monthly aggregates.

Import flow:

  1. User selects a CSV file.
  2. App parses the file locally.
  3. User maps columns if the format is unknown.
  4. App creates import candidates.
  5. User reviews candidates.
  6. Accepted candidates are saved as transactions.
  7. Import batch metadata is stored for traceability.

Dashboard flow:

  1. UI requests transactions and recurring items for the selected month.
  2. App computes monthly totals.
  3. UI renders summary cards, category breakdown, and recent transactions.

Validation boundaries

Validate where user or imported input becomes application data:

  • transaction date must be valid
  • amount must be numeric and non-zero
  • transaction type must be known
  • account must exist
  • category should exist for income and expenses
  • imported rows must be parsed into a predictable candidate shape before review

Suggested code organization

src/
  app/              # React app setup, router, providers
  components/       # shared UI and app-specific components
  features/
    dashboard/
    transactions/
    imports/
    recurring/
    settings/
  domain/           # pure finance logic and types
  persistence/      # Drizzle schema, migrations, repository functions
  services/         # typed app operations used by UI
  validation/       # shared Zod schemas
  export/           # JSONL export/import helpers
  electron/         # main/preload process code

Keep domain logic framework-independent. React should render and coordinate workflows, not own finance calculations.

Initial routes

  • / dashboard
  • /transactions
  • /imports
  • /recurring
  • /settings

Data model draft

Use SQLite as the source of truth. Store money as integer minor units, for example cents, to avoid floating point errors. Store dates as ISO strings unless a later implementation reason pushes us toward integer timestamps.

Tables

accounts

Represents a place where money moves in or out.

Column Type Notes
id text primary key UUID/CUID generated by the app
name text not null User-visible account name
type text not null checking, savings, credit_card, cash, other
currency text not null Default USD or user-selected later
is_archived integer not null Boolean 0/1
created_at text not null ISO timestamp
updated_at text not null ISO timestamp

categories

Represents budget/reporting categories.

Column Type Notes
id text primary key UUID/CUID generated by the app
name text not null User-visible category name
kind text not null income, expense, or transfer
color text Optional UI color token
is_system integer not null Marks seeded defaults
is_archived integer not null Boolean 0/1
created_at text not null ISO timestamp
updated_at text not null ISO timestamp

Recommended unique index: categories(name, kind).

transactions

Represents accepted financial activity.

Column Type Notes
id text primary key UUID/CUID generated by the app
date text not null ISO date, e.g. 2026-05-10
amount_minor integer not null Positive integer amount in minor units
type text not null income, expense, or transfer
description text not null User/import description
account_id text not null References accounts.id
category_id text References categories.id
transfer_account_id text Optional destination/source account for transfers
notes text User notes
source text not null manual or imported
import_batch_id text References import_batches.id when imported
external_id text Stable imported ID if available
created_at text not null ISO timestamp
updated_at text not null ISO timestamp

Use positive amount_minor plus type instead of signed amounts. This keeps UI and reports explicit. Transfer-specific behavior can be refined later.

Suggested indexes:

  • transactions(date)
  • transactions(account_id, date)
  • transactions(category_id, date)
  • transactions(import_batch_id)

recurring_items

Represents expected future income or expenses.

Column Type Notes
id text primary key UUID/CUID generated by the app
name text not null User-visible name
type text not null income or expense for MVP
amount_minor integer not null Positive integer amount in minor units
account_id text References accounts.id
category_id text References categories.id
frequency text not null weekly, biweekly, monthly, yearly
start_date text not null ISO date
end_date text Optional ISO date
next_due_date text Optional cached ISO date
is_active integer not null Boolean 0/1
created_at text not null ISO timestamp
updated_at text not null ISO timestamp

import_batches

Represents one imported file and its processing status.

Column Type Notes
id text primary key UUID/CUID generated by the app
account_id text Target account if selected
file_name text not null Original file name
file_hash text Helps detect re-imports
status text not null reviewing, accepted, partial, discarded
raw_row_count integer not null Number of parsed rows
accepted_count integer not null Number accepted as transactions
skipped_count integer not null Number skipped
created_at text not null ISO timestamp
updated_at text not null ISO timestamp

import_candidates

Persist import review state so a partially reviewed import can be resumed.

Column Type Notes
id text primary key UUID/CUID generated by the app
import_batch_id text not null References import_batches.id
raw_row text not null JSON string of original parsed CSV row
date text Parsed ISO date
amount_minor integer Parsed amount
type text Parsed or inferred transaction type
description text Parsed description
category_id text User-selected category
status text not null pending, accepted, skipped, duplicate, invalid
duplicate_transaction_id text References likely duplicate transaction
created_transaction_id text References accepted transaction
created_at text not null ISO timestamp
updated_at text not null ISO timestamp

Seed data

Create default categories during initial database setup:

  • income: Salary, Interest, Other Income
  • expense: Rent, Groceries, Utilities, Subscriptions, Restaurants, Transportation, Insurance, Healthcare, Other Expense
  • transfer: Transfer

Create no default accounts until the user adds one. The first-run flow should prompt for an account before manual transaction entry or import.

JSONL export records

Each line should be one durable record with explicit type and schema version:

{"recordType":"account","schemaVersion":1,"data":{"id":"..."}}
{"recordType":"category","schemaVersion":1,"data":{"id":"..."}}
{"recordType":"transaction","schemaVersion":1,"data":{"id":"..."}}
{"recordType":"recurringItem","schemaVersion":1,"data":{"id":"..."}}
{"recordType":"importBatch","schemaVersion":1,"data":{"id":"..."}}

Export accepted records only by default. Import candidates are review state, not essential durable finance data, and can be excluded unless a debug/full export mode is added later.

Prototype constraints

Prioritize end-to-end usefulness over completeness:

  • manual transactions before polished imports
  • CSV import before bank API integration
  • monthly dashboard before advanced reports
  • local SQLite before sync
  • simple categories before rules/automation
  • explicit user review before smart categorization

Open decisions

  • Whether SQLite access should be synchronous or async from the renderer perspective.
  • Whether Electron main process owns all persistence calls, or whether a local service layer is shared differently.
  • Whether import candidates are persisted before review or kept in memory until accepted.
  • How much category automation belongs in the MVP.
  • Whether to include charts in the first prototype or start with text/table summaries only.

Contributors

Soleone

Issues