This repository contains the main Expense Tracker application, a self-hosted monthly budgeting app built with Rails.
The app helps users:
- plan budgets one month at a time
- reuse recurring transactions such as paychecks, bills, subscriptions, payment plans, and credit cards
- import CSV activity and track real spending against planned entries
- manage manual account balances and net worth snapshots
- back up and restore user data with versioned JSON exports
The hosted marketing site and user documentation live in a separate frontend repository at ../financetrackingapp.
Expense Tracker is designed for people who budget by month and want one place to plan, review, and maintain that budget over time.
The product is intentionally opinionated around a month-based workflow:
- users build or roll forward a month
- recurring transactions provide the starting structure
- manual and imported entries fill in the month as reality changes
- account context helps budgeting and balances stay connected
- overview, help, and backup flows reduce setup friction for self-hosted users
This is not a bank-sync-first app. Manual planning, recurring reuse, account context, privacy, and self-hosting are the core value proposition.
- Overview page with current-month status, recurring progress, account summaries, and quick actions
- Month-by-month budgeting with support for cloning or generating from recurring templates
- Recurring transaction management for pay schedules, subscriptions, monthly bills, payment plans, and credit cards
- Guided entry wizard for fast manual entry creation with optional recurring-template creation
- Multiple month review surfaces including grouped timeline, full list, calendar, breakdown, and money-flow views
- CSV import flows for bringing historical or current month activity into the app
- Manual accounts and balance snapshots for account coverage and net worth tracking
- Backup and restore flows with preview support and versioned JSON export/import
- In-app help and release notes so product guidance stays visible inside the app
- People or households who budget manually by month rather than relying on live bank-sync categorization
- Self-hosters who want a private budgeting tool they can run themselves
- Users who value recurring planning structure, account context, and month roll-forward workflows
When making product or UX decisions, optimize for clarity, trust, and reducing budgeting friction for returning users. Budgeting UX should make money movement explainable: keep history, drilldowns, and plain-language context close to charts, balances, and account totals.
- Ruby
4.0.5via.ruby-version - Rails
8.1.3 - PostgreSQL
- Devise for authentication
- Solid Queue, Solid Cache, and Solid Cable from Rails
- Hotwire with Turbo + Stimulus
- Importmap, not a Node-based frontend bundler
- Tailwind CSS via
tailwindcss-rails - Propshaft for assets
- Chart.js and Apache ECharts loaded from CDN
- RSpec + FactoryBot present alongside Rails' default test framework
app/modelsdomain models such asBudgetMonth,ExpenseEntry,Account, and recurring transaction typesapp/servicesbusiness logic for budgeting, recurring generation, accounts, overview, backups, and platform concernsapp/controllersuser-facing controllers plus admin and auth flowsapp/helpersview-oriented helper methods shared by ERB templatesapp/javascript/controllersStimulus controllers for interactive UI behaviorapp/viewsERB views, Turbo frame content, and modal partialsapp/jobsbackground jobs, including recurring-entry completion flowsconfig/releases.ymlsource of truth for in-app release notes and GitHub release publishingspecRSpec coverage for requests, services, models, DB, and system flowstestRails test framework files that are still used by current CIdocssupporting documentation for the app repo
- The app now leans on domain-oriented services under areas such as
Budgeting,Recurring,Accounts,Overview, andPlatform ExpenseEntryis the main month-instance record; recurring template models describe reusable planning definitions- Recurring transaction types stay in separate models because their rules differ, but they share common concerns and catalog metadata
- Overview behavior is assembled through
app/services/overviewand exposed to the page through a presenter - Backup/restore logic is service-driven and namespaced under platform services rather than buried in controllers
- Account-aware linking logic lives in dedicated services and model concerns
- Account balance work treats snapshots as reconciliation points, paid entries as current activity, planned entries as projections, and source/destination accounts as the canonical money-flow links
- Release notes are backed by release services and
config/releases.yml - Cloudflare Turnstile support exists in the auth flow through
turnstileconcerns, controllers, and verification services - Public Devise entry points are rate limited through
DeviseRateLimitedand Rails controllerrate_limit; production counters use Solid Cache and email-based keys use an app-secret-backed HMAC rather than raw addresses
- Follow Rails conventions first unless the existing codebase has already established a better local pattern
- Write concise, idiomatic Ruby with descriptive method and variable names
- Prefer double-quoted strings unless single quotes improve readability by avoiding escaping
- Replace repeated conditionals or branching logic with small, named methods or POROs when the behavior starts to sprawl
- Keep controllers focused on authorization, request parsing, orchestration, and response formatting
- Favor service objects or presenters for non-trivial business logic, cross-model workflows, or view-specific composition
- Keep comments focused on why, tradeoffs, or non-obvious side effects rather than narrating straightforward code
- Prefer constants over unexplained magic values when the value has domain meaning
- Respect the app's product language, especially around "Recurring Transactions", months, accounts, and backups
These conventions include lessons from "Smooth UI animations on server-rendered HTML" by Adrien Siami: keep Rails-rendered HTML as the source of truth, let Turbo morphing and view transitions handle page motion, and use Stimulus as a small state-toggle layer.
- Default to server-rendered HTML and Hotwire flows before reaching for custom JavaScript
- Use Turbo Frames for scoped updates such as modals, editors, side panels, and in-page workflow surfaces
- Use Turbo Streams when multiple parts of a page need to update together after a mutation
- Keep Stimulus controllers focused on UI behavior, state transitions, and browser APIs; do not move business rules out of Rails just because JavaScript is available
- Preserve stable
data-controller,data-action, anddata-*-targetcontracts during refactors, especially for modal and wizard flows - When extracting ERB partials, keep Turbo frame boundaries and Stimulus target ownership explicit so the frontend contract does not become ambiguous
- Prefer progressive enhancement: pages should still make sense when rendered fresh from the server
- Treat modal forms as Turbo-first flows with clear pending, success, and validation-error states
- Reuse shared submit-state behavior where possible so wizard, editor, and template modals stay consistent
- For UI refactors, look for opportunities to move view setup into presenters/helpers while keeping the rendered HTML easy for Turbo to replace
- Prefer Turbo morphing for same-page refreshes that should preserve scroll, focus, and local input state; use
turbo_refreshes_with method: :morph, scroll: :preserveon page surfaces that redirect back to themselves after small mutations - Preserve the app's existing
<meta name="turbo-view-transition" content="true">setup and treat view transitions as progressive enhancement, not as a dependency for correctness - For animated server-rendered updates, give stable repeated elements unique
view-transition-namevalues based on durable record identity such asdom_id(record), and group related motion with existingta-vt-*transition classes - Keep view-transition names unique within a rendered page; duplicate names can cause confusing motion or browser transition failures
- Prefer browser/Turbo-managed transitions over custom JavaScript animation code, and keep custom CSS short, scoped, and covered by
prefers-reduced-motion - Use Stimulus for small optimistic UI details by toggling one stateful class or ARIA/data attribute, then let Tailwind/CSS express the visual states
- Do not put financial calculations, permission rules, persistence decisions, or server response assumptions into Stimulus controllers; Stimulus should make already-rendered server state feel responsive
- Tailwind is the primary styling approach; prefer utility classes over new custom CSS
- Match the existing visual language of the app instead of introducing a new design system page by page
- Keep desktop and mobile layouts both working when adjusting month views, overview cards, or modal flows
- Prefer extracting repeated card, modal, or summary markup into partials once the repeated structure is clear
- Icons should come from the existing icon approach already in the app; do not introduce a second icon system casually
- For charts and dashboards, keep the server-side data contract explicit and make JS initialization resilient to Turbo navigation
- Use plain-language account movement terms such as "Money leaves / activity account", "Money goes to", "Card being paid down", and "Money leaves account" instead of exposing internal source/destination terminology to everyday users
- Pair charts and dashboard totals with supporting numbers, captions, and nearby entry lists or drilldowns; avoid chart-only summaries for financial data that users may want to audit
- Keep onboarding light and progressive: ask for the smallest useful setup step first, then reveal deeper account, recurring, and month workflows after the user has context
- Make the next useful action obvious on onboarding and dashboard surfaces; avoid leaving new users to infer where to click from a dense financial dashboard
- Add trust cues near sensitive flows such as sign-up, account setup, backup restore, and balance snapshots so users understand where data lives and how calculations are grounded
- Favor weekly or daily check-in affordances over heavy notification systems until there is a clear user need for external reminders
- Account balance screens should make the rollforward understandable by showing the latest snapshot/base, paid activity, planned impact, current balance, projected balance, and month-by-month history
- Prefer visible drilldown links, detail rows, or expandable sections for financial auditability over hidden chart interactions
- Keep tables, balance history, and drilldown views scannable on mobile and desktop; use compact rows, responsive tables, or card-like repeated items when needed
- Consider visual goal or progress patterns for savings targets, debt payoff, and card payoff, but keep motivation secondary to clarity and trust; positive reinforcement should be calm and non-judgmental
- Let lightweight preferences, such as financial rhythm, tune guidance without fragmenting the core workflow or adding separate data models too early
- When money-flow terminology changes, update in-app help, release notes, and recurring-template copy so the same concept is named consistently everywhere
This repository currently uses a mixed testing setup.
Observed test coverage locations:
test/contains Rails default tests and is still used by the main CI test jobspec/systemis run in CI with RSpecspec/models,spec/requests,spec/services, andspec/dbcontain additional RSpec coverage that exists outside the defaultbin/rails testpath
Current GitHub Actions behavior:
- lint runs
bin/rubocop -f github - Ruby security scans run
bin/brakeman --no-pagerandbin/bundler-audit - JS dependency scanning runs
bin/importmap audit - main test job runs
bin/rails db:test:prepare test - system test job builds Tailwind assets and runs
bundle exec rspec spec/system
When changing behavior, prefer running the narrowest relevant test first, then broaden out if the change touches shared flows.
- Prefer fast isolated specs for services, models, presenters, and helpers when the logic can be tested without a browser
- Prefer request specs for controller behavior, Turbo responses, authorization, and multi-model mutations
- For Devise authentication throttles, keep request specs around the custom Devise controllers and clear the
DeviseRateLimitedtest store between examples - Use system specs sparingly for high-value Hotwire flows that truly need browser behavior, especially Stimulus or Turbo interaction states
- For Turbo/Stimulus work, a strong pattern is:
- isolated spec for Ruby-side composition or rendering
- request spec for Turbo response shape when practical
- one focused system spec for the end-to-end browser behavior
- Add characterization coverage before large refactors in legacy or controller-heavy code
- Avoid testing private methods directly; extract a PORO or presenter if the logic deserves direct tests
- Use factories thoughtfully; keep them lean and prefer explicit setup over dense callback chains
- For financial dashboards, account balances, and drilldowns, pair service specs for math and payloads with request specs for navigable detail pages and focused system specs for high-risk UI paths
bin/setupbin/devbin/rails serverbin/rails consolebin/rubocopbin/brakeman --no-pagerbin/bundler-auditbin/importmap auditbin/rails testbundle exec rspecbundle exec rspec spec/system
Prefer Docker for a quick start:
cp .env.example .envdocker compose up --build
The default host port is 4287, mapped to Rails port 3000 inside the container.
Compose services:
dbuses PostgreSQL 16webrunsbin/dev
bin/dev uses Procfile.dev, which currently starts:
bin/rails server -b 0.0.0.0bin/rails tailwindcss:watch
If you are running locally without Docker, make sure the Ruby version matches .ruby-version. This repo is not friendly to the system Ruby that ships with macOS.
- Always read the surrounding files before changing patterns that may be shared across other month, modal, or recurring flows
- Favor editing existing files and extending established patterns before introducing new abstractions
- Keep month workflow language and product terminology consistent with the README and in-app copy
- Treat
config/releases.ymlas user-facing product history; new entries should be newest-first - When changing product behavior or terminology, check whether the frontend repo at
../financetrackingappalso needs updates toindex.html,docs/, or shared messaging - Favor service objects, presenters, or concerns for non-trivial business rules rather than growing controllers, helpers, or Active Record models
- Respect the existing Hotwire/importmap setup; do not introduce a Node bundler unless explicitly requested
- When refactoring view code, preserve the Turbo and Stimulus contract unless the task explicitly includes updating the frontend behavior
- Watch for controller actions that are doing too much orchestration, derivation, or branching; they are usually candidates for services or presenters
- When a view starts accumulating many locals or repeated partial setup, consider a presenter or smaller partial extraction
- Keep the boundary between recurring templates, month entries, and accounts clear; avoid reintroducing type-specific branching where a shared interface already exists
- For Hotwire features, review both the server-rendered HTML contract and the browser interaction contract before calling a refactor complete
- Prefer incremental refactors with regression tests over large rewrites, especially in month-generation, import/export, and dashboard flows
- Pushing changes to
config/releases.ymlonmaintriggers.github/workflows/publish_release.yml - The release workflow publishes a GitHub release and versioned Docker images using the first entry in
config/releases.yml - The public marketing/docs site should stay aligned with app screenshots, feature names, and setup instructions in this repo's
README.md