A personal finance budgeting application to track income, expenses, budgets, and assets with a simple, responsive mobile-friendly web UI. Single-user, no authentication.
- Framework: Ruby on Rails 8.x (latest)
- Database: SQLite
- CSS: Tailwind CSS
- JavaScript: Hotwire (Turbo + Stimulus) - Rails default
- Dependencies: Minimal - stick to Rails defaults
- Containerization: Docker with Docker Compose
- Create/edit/delete financial accounts (checking, savings, credit card, cash)
- Track current balance per account
- Currency per account (USD, EUR, GBP, etc.)
- View account transaction history
- Track non-cash assets and liabilities:
- Investments: Stock portfolios, retirement accounts, crypto
- Property: Real estate, vehicles
- Liabilities: Mortgage, loans
- Fields: name, asset_type, value, currency, notes
- Manual value updates with history tracking
- Appreciation/depreciation tracking over time
- Archive feature:
- Archive assets that no longer exist or were replaced
- Archived assets are hidden from main lists but appear in collapsible sections
- Historical valuations preserved for net worth history
- Can still backfill historical valuations for archived assets
- Broker-linked assets cannot be archived (must close positions first)
- Archived assets excluded from current totals but included in historical charts
- Add income/expense transactions
- Fields: amount, date, description, category, account
- Edit/delete transactions
- Import from bank statements: Upload CSV/XLS/XLSX files for AI-powered extraction
- Upload CSV/XLS/XLSX bank statements
- AI-powered extraction using Ollama LLM (configurable model)
- Automatic category matching based on description
- Duplicate detection via hash of date + amount + description
- Preview extracted transactions before import
- Manual fallback when AI extraction fails
- Max file size: 5MB
- Automatic category suggestion for new transactions using a 3-phase hybrid approach:
- Pattern Matching - Instant matching against category patterns (manual and learned) using word-boundary regex
- Embedding Similarity - Semantic matching using vector embeddings when patterns don't match
- LLM Fallback - For ambiguous cases, the LLM picks from the top 3 embedding candidates
- Category Patterns:
- Two sources: "Manual" (user-created) and "Learned" (extracted by LLM from transaction history)
- Managed in Admin → Categories, with inline CRUD for manual patterns
- Learned patterns extracted hourly via background job (
CategoryPatternExtractionJob) - Pattern learning also triggered after import confirmation (scoped to categories used in the import)
- Conflicting learned patterns resolved by highest confidence, then highest match count
- Embeddings:
- Categories and transactions get vector embeddings via Ollama (
mxbai-embed-large) - Embeddings computed asynchronously via background jobs (
CategoryEmbeddingJob,TransactionEmbeddingJob) - Cosine similarity used for matching; configurable confidence threshold (default: 0.75)
- Categories and transactions get vector embeddings via Ollama (
- UI Integration:
- Transaction form shows AI-suggested category with accept/dismiss controls
- Suggestion fetched via Stimulus controller calling
suggest_categoryendpoint
- Reduces LLM calls by ~70-80% compared to pure LLM categorization
- Set recurring budgets per expense category
- Two budget periods:
- Monthly: Track spending per calendar month (e.g., $500/month for groceries)
- Yearly: Track spending per calendar year (e.g., $3000/year for vacation)
- Optional start date to apply budget from a specific month forward
- One budget per category (no duplicates)
- Track spending vs budget with visual progress indicators
- View budget status for any month/year
Split into two views:
- Year Navigation:
- Navigate between calendar years using prev/next arrows
- URL parameter:
?year=2025 - Default to current year
- Shows all 12 months of the selected year (Jan-Dec)
- Month Filter:
- Clickable month tabs/buttons within the displayed year
- URL parameter:
?year=2025&month=3(for March) - "Year" option shows full year aggregate
- Selected month highlighted in the monthly table
- Donut chart reflects selected period (month or full year)
- Summary Card - Compact flow showing: +Income − Expenses = Net (X% saved)
- Single cohesive card with visual progress bar showing expense/savings ratio
- Nested Donut Chart:
- Inner ring: Income broken down by category (cool tones)
- Outer ring: Expenses by category + savings slice if positive net (warm tones)
- Center: Net amount (green if positive, red if negative)
- Monthly Breakdown Table - All 12 months of selected year with income/expenses/net/saving rate
- Current month highlighted, selected month has accent border
- Links to filtered cash flow view when clicking a month
- Budget Status - Two-column layout for monthly and yearly budgets with progress bars
- Budgets follow the navigation context (selected year/month)
- Budgets link to filtered transaction list for category and time range
- Summary Cards:
- Cash (all accounts)
- Total assets
- Total liabilities
- Net worth
- Assets by Group - Expandable sections showing assets grouped by AssetGroup
- Each asset shows current value and liability badge if applicable
- Totals Footer - Assets - Liabilities = Net
- Connect brokerage accounts to auto-sync portfolio positions
- Currently supports Interactive Brokers via Flex Web Service API
- Setup:
- Add broker connection with account ID, name, and API credentials
- For IBKR: Flex token (encrypted) and Flex Query ID required
- Create a Flex Query in IBKR Account Management that returns Open Positions
- Position Mapping:
- Each broker position (symbol) can be mapped to an existing Asset
- Unmapped positions are tracked but don't update any asset
- Multiple positions can map to the same asset (values summed)
- Syncing:
- Automatic daily sync at 4am via Solid Queue scheduled job
- "Apply Broker Values" button copies cached position values to assets
- Position valuations recorded daily for historical tracking
- UI:
- Settings → Brokers for connection management
- "Broker" badge shown on assets with mapped positions
- Position history viewable on individual position pages
- Manage master data:
- Currencies - ISO 4217 codes (e.g., USD, EUR, GBP)
- Account Types - name (e.g., checking, savings, credit, cash)
- Asset Types - name, is_liability flag (e. g., property, mortgage)
- Categories - name, category_type (income/expense), category patterns (manual/learned)
- Seed file populates defaults
- Admin UI to add/edit/delete master data
# Master Data (Admin)
Currency
- code:string (ISO 4217, e. g., "USD", unique)
- default:boolean (one currency marked as default for reporting)
AccountType
- name: string (e.g., "checking", "savings")
AssetType
- name:string (e. g., "property", "investment")
- is_liability:boolean (default: false)
AssetGroup
- name:string (e.g., "Retirement", "Real Estate")
- description:string (optional)
Category
- name:string
- category_type:string (income, expense)
- embedding:binary (vector embedding for semantic matching)
CategoryPattern
- category_id:references
- pattern:string (e.g., "AMAZON", "WHOLE FOODS")
- source:string (manual, learned)
- confidence:decimal (0.0-1.0, for learned patterns)
- match_count:integer (how many transactions matched)
- unique index on [pattern, source, category_id]
# User Data
Account
- name: string
- account_type_id:references
- balance: decimal
- currency:string (ISO 4217 code, e. g., "USD")
Asset
- name:string
- asset_type_id:references
- asset_group_id:references (optional)
- value:decimal
- currency:string (ISO 4217 code, e.g., "USD")
- notes:text
- archived:boolean (default: false)
AssetValuation
- asset_id: references
- value:decimal
- date:date
Transaction
- account_id:references
- category_id:references
- amount:decimal
- transaction_type:string (income, expense)
- date:date
- description:string
- exchange_rate:decimal (rate at transaction date)
- amount_in_default_currency:decimal (for reporting)
- duplicate_hash:string (SHA256 for import duplicate detection)
- embedding:binary (vector embedding for semantic matching)
Budget
- category_id:references
- amount:decimal
- period:string (monthly, yearly)
- start_date:date (optional)
BrokerConnection
- broker_type:integer (enum: ibkr=0)
- account_id:string (unique per broker_type)
- name:string (display name)
- flex_token:string (encrypted, IBKR only)
- flex_query_id:string (IBKR only)
- last_synced_at:datetime
- last_sync_error:text
BrokerPosition
- broker_connection_id:references
- symbol:string (e.g., "AAPL", "VTI")
- description:string (security description)
- asset_id:references (optional, null if unmapped)
- last_quantity:decimal
- last_value:decimal
- currency:string
- last_synced_at:datetime
- unique index on [broker_connection_id, symbol]
PositionValuation
- broker_position_id:references
- date:date
- quantity:decimal
- value:decimal
- currency:string
- unique index on [broker_position_id, date]- Mobile-first responsive design
- Tailwind CSS for styling
- Simple navigation (bottom nav on mobile, sidebar on desktop)
- Minimal, clean aesthetic
- Cash Flow (root path) - 12-month income/expense overview, budget status
- Net Worth - Assets, liabilities, and net worth summary
- Transactions - List with filters (account, category, month/date range), add/edit forms
- Import Transactions - Upload bank statements, preview and import extracted transactions
- Accounts - Account list and management
- Assets - Asset/liability list with value history, grouped by AssetGroup
- Budgets - Budget setup and tracking
- Admin - Master data management
- Currencies (with default currency setting)
- Account Types
- Asset Types
- Asset Groups
- Categories
- Broker connections and position mappings
- Multi-stage build for smaller image
- Based on official Ruby image
- Install Node.js for asset compilation
- Precompile assets in build stage
- Run with production settings
- SQLite database stored in volume
- Single service for the Rails app
- Volume mount for SQLite database persistence
- Volume mount for Rails storage
- Environment variables for Rails configuration
- Expose port 3000
- Includes Ollama service for AI-powered transaction import
- Auto-pulls configured model on first start
- Environment variables:
OLLAMA_HOST- Ollama API endpoint (default: http://ollama:11434)OLLAMA_MODEL- LLM model to use (default: llama3.1:8b)OLLAMA_EMBEDDING_MODEL- Embedding model (default: mxbai-embed-large)OLLAMA_EMBEDDING_CONFIDENCE- Cosine similarity threshold (default: 0.75)OLLAMA_TIMEOUT- Request timeout in seconds (default: 120)
- Use Rails scaffold generators where appropriate
- Turbo Frames for inline editing
- Turbo Streams for real-time updates
- Stimulus for minimal JS interactions
- DB-level balance updates via callbacks or service objects
- Seed file with default currencies, account types, asset types, and categories
- Store currency as ISO 4217 code string (validated against Currency)
- One currency marked as default for all reporting/dashboard views
- All dashboard amounts converted to default currency using exchange rates
- Swiss-style number formatting: apostrophe as thousand separator (e.g., CHF 1'234.56)
- AssetValuation created automatically when Asset value changes
- Asset.value always reflects current value; history in AssetValuation
- ExchangeRateService fetches rates from Frankfurter API (supports historical dates)
- ExchangeRateService returns nil on API failure; records retry via ExchangeRateRetryJob (every 6 hours)
- Transaction exchange rates captured at transaction date for accurate historical reporting
- Transaction import uses Ollama LLM for extraction and categorization
- Import services: OllamaService, CsvParserService, CsvMappingAnalyzerService, DeterministicCsvParserService, DuplicateDetectionService
- Categorization services: CategoryMatchingService (3-phase pipeline), CategoryPatternExtractionJob (learns patterns from history)
- Background jobs: TransactionEmbeddingJob, CategoryEmbeddingJob, CategoryPatternExtractionJob (hourly), CategorizationMaintenanceJob (daily), EmbeddingModelMigrationJob
- Broker integration uses factory pattern (BrokerSyncService) for multi-broker support
- IBKR uses Flex Web Service API (2-step: SendRequest → GetStatement)
- IbkrSyncService handles API calls, XML parsing, and asset value sync
- Daily broker sync at 11:30pm via Solid Queue (BrokerSyncJob) with 14-day valuation backfill
- Position valuations track historical position values separately from asset valuations
See AGENTS.md for development guidelines, code style, and task checklists for AI agents working on this project.