Personal finance without spreadsheets. Zero-based budgeting, credit-card cycles, debts, savings goals and reminders — multi-currency, bilingual, installable as a PWA.
- What it does
- Screenshots
- Tech stack
- Architecture
- Run it locally
- Tests
- Deployment
- Performance
- Security & privacy
- Contributing
- License
| Budget | Three progressive levels — Tracking (just log), the 50/30/20 rule, and zero-based (assign every unit of currency until "To Assign" hits 0). Auto-suggests envelopes from your last 3-month average and copies the previous month in one click. |
| Transactions | Fast entry with auto-categorization that learns from your history, plus recurring transactions that create themselves. Filter by search, type, category, date range and card used (including "no card" for cash); bulk actions and undo. |
| Credit cards | Automatic statement/payment cycles, rule-based cashback, partial payments, statement history, and a card catalog with predefined cashback. |
| Debts | Balances, interest, payment history linked to real transactions, avalanche strategy, and an estimated months-to-payoff. |
| Savings & goals | Goals with logged contributions (each one creates its own linked transaction), projected completion date, optional horizon (short/medium/long) and history with undo. |
| Dashboard | Bento grid built around a liquid-wealth time series you can scrub and pin, with spending donut, budget pace bar and reminders. Month selector to review the past. |
| My Finances | One reconciliation view over cards, savings and debts with a net-worth summary on top. |
| Calendar | Monthly view with past movements and upcoming due dates: debt installments, card payments, goals and recurring items. |
| Settings | Budget level, currency, language (es/en), CSV/Excel import & export, PDF statement import, and category management. |
Multi-currency and bilingual. Each user picks a currency during onboarding and the whole app formats amounts with
Intl; the UI ships in Spanish and English.
All screenshots are generated from the real production build in demo mode
(sample data seeded in memory, no backend involved) — run npm run screenshots
to regenerate them from the current commit.
| Budget · zero-based envelopes | Transactions |
|---|---|
![]() |
![]() |
| My Finances · cards, savings, debts | Calendar · upcoming due dates |
|---|---|
![]() |
![]() |
| Layer | Technology |
|---|---|
| Frontend | React 19 + Vite 8 (Rolldown), installable PWA (manifest.webmanifest + iOS meta tags) |
| Routing | React Router v7 with route-level code splitting (React.lazy per screen) |
| State | Zustand 5 (data cached in sessionStorage; the Supabase session lives in localStorage so it survives browser restarts) |
| Backend / Data | Supabase (PostgreSQL + Auth + RLS; OAuth uses the PKCE flow) |
| Styling | Tailwind CSS v4 (@theme tokens, dark "Stitch" periwinkle theme) |
| Charts | Recharts (lazy — only downloaded by screens that draw charts) |
| Icons | Material Symbols (UI) + JoyPixels v10 PNGs from jsDelivr (category emojis; the unicode→codepoint mapping is local, see src/stitch/emojiCodepoint.js — no runtime emoji library) |
| Animation | Framer Motion |
| Serverless | Vercel functions (/api/parse-pdf imports statements; /api/feedback receives feedback) |
| Tests | Vitest (unit) + Playwright (E2E) |
| CI | GitHub Actions — lint, unit tests, production build and E2E on every push and PR |
fintrack/
├── .github/
│ ├── workflows/ci.yml # CI: lint · unit tests · build · E2E
│ ├── ISSUE_TEMPLATE/ # Bug report and feature request forms
│ └── dependabot.yml # Weekly npm updates (grouped) + monthly Actions
├── api/ # Vercel serverless functions (parse-pdf, feedback)
├── docs/
│ ├── SECURITY.md # Security measures and design decisions
│ ├── decisions/ # Design docs and TDD evidence for the trickier features
│ └── screenshots/ # README images (regenerate: npm run screenshots)
├── public/
│ ├── manifest.webmanifest # PWA manifest (name, icons, standalone display)
│ ├── apple-touch-icon.png # iOS home-screen icon (180×180)
│ └── icons/ # PWA icons (192/512, maskable)
├── scripts/
│ └── screenshots.mjs # Drives the real app in demo mode to capture the README images
├── src/
│ ├── contexts/ # AuthContext (Supabase session) · I18nContext (es/en)
│ ├── data/ # Category templates, card catalog, auto-categorization memory
│ ├── i18n/ # Translations + runtime helpers usable outside React
│ ├── lib/ # Supabase client
│ ├── stitch/ # The whole UI: shell, screens/, components, stitch.css
│ ├── stores/ # Global Zustand state (one store per domain)
│ └── utils/ # Financial calculations, formatting, card cycles, recurrence
├── supabase/
│ ├── schema.sql # Full schema (source of truth, idempotent)
│ ├── MIGRATIONS.md # Migration order for existing databases
│ └── *.sql # One-off migrations + validation scripts
└── tests/ # Playwright E2E (run against the demo mode, no credentials)
Conventions worth knowing
- The UI lives entirely in
src/stitch/. Every screen with sub-components follows a thin shell +screens/<page>/folder pattern, with the business logic extracted into pure selectors (selectors.js,payoff.js,projection.js…) that are unit-tested without React. - One Zustand store per domain (
useTransactionStore,useDebtStore, …), each one responsible for its own Supabase table and its ownsessionStoragecache. - Design decisions and TDD evidence for the harder features are written down in
docs/decisions/.
categories · transactions · budgets · savings · savings_contributions · debts ·
debt_payments · credit_cards · recurring_transactions · plans (legacy; merged into savings)
profiles(user preferences, e.g. budget level) is created by its own migration,supabase/add_profiles_table.sql, not byschema.sql.
git clone https://github.com/Gian-DS1/fintrack.git
cd fintrack
npm install
npm run dev # http://localhost:5173The app boots without any configuration: on localhost the landing page shows a
"Ver demo" button that seeds sample data in memory and drops you into the full app —
no Supabase project, no account, nothing written anywhere. That is the fastest way to
look around.
The
xlsxdependency is installed from the official SheetJS CDN (a maintained build, free of the vulnerabilities in the npm-published package).npm installdownloads it automatically — it only needs access tocdn.sheetjs.com.
To sign in and persist data you need your own Supabase project:
-
Create one at supabase.com.
-
Open the SQL Editor and run the full contents of
supabase/schema.sql. This creates the tables, enables Row Level Security and sets up policies and permissions. It is required: without RLS the anon key would let anyone read other users' data. -
Under Authentication → Providers, enable Email (and optionally Google, with the redirect pointing at your domain /
http://localhost:5173). -
Copy the environment file and fill it with the values from Supabase → Project Settings → API:
cp .env.example .env
VITE_SUPABASE_URL=https://YOUR-PROJECT.supabase.co VITE_SUPABASE_ANON_KEY=your_anon_key
Migrations: a fresh database created from
schema.sqlis already complete. For an existing database that predates the redesign, run the migrations in order — seesupabase/MIGRATIONS.md.The
/apiserverless functions accept optional variables without theVITE_prefix (they live only on the server):WEB3FORMS_ACCESS_KEYfor the feedback form andSTATEMENT_SKIP_PATTERNSfor the PDF importer. The web app works without them. See.env.example.
npm run dev # development server
npm run build # production build (dist/)
npm run preview # preview the build
npm run lint # ESLint
npm run test # unit tests (Vitest)
npm run test:e2e # end-to-end tests (Playwright)
npm run screenshots # regenerate the README images from the current build264 unit tests cover the financial logic and the pure UI selectors — zero-based budgeting, accumulating sinking funds, savings capacity, card cycles and cashback, recurrence, goal projection, debt payoff, currency formatting and the calendar:
npm run testEnd-to-end flows run against the real production build with Playwright. They use the demo mode instead of a real login, so they are deterministic and need no credentials, no network and no access to production:
npm run test:e2eBoth suites, plus lint and the production build, run on every push and pull request
(.github/workflows/ci.yml).
- Import the repo into Vercel.
- Add
VITE_SUPABASE_URLandVITE_SUPABASE_ANON_KEY(Production + Preview). vercel.jsonalready configures the SPA rewrite (excluding/api), security headers/CSP, and the cache policy (immutable hashed assets; PWA manifest/icons for 24 h; HTML never cached).
Open the production URL in Safari → Share → Add to Home Screen. The app opens
full-screen with its own dark icon and a status bar matching the app background.
Sign-in (email or Google) persists across launches: the Supabase session lives in
localStorage with automatic token refresh. Google OAuth uses the PKCE flow, which
survives the iOS standalone-app browser handoff.
- Route-level code splitting: each screen is a
React.lazychunk, so the initial bundle only carries the shell (~62 kB of app code vs ~1.5 MB before the split). Heavy vendors (recharts,framer-motion,@supabase, React) ship as separate long-cached chunks, andxlsx/papaparseonly download when you import or export files. - Fonts: Inter/Manrope/Material Symbols start downloading from
index.htmlin parallel with the JS bundle. - Preconnects: to Google Fonts, jsDelivr (emoji PNGs) and the Supabase project (injected at runtime from the env), so the first data fetch skips DNS+TLS latency.
- Data isolation via RLS. Every query filters by
user_id, and the database enforces it with Row Level Security (auth.uid() = user_id). Runningsupabase/schema.sqlsets this up. - Secrets.
.envis in.gitignoreand never committed. Theanon keyis public by design (safe thanks to RLS). - Local cache. For speed, financial data is cached in
sessionStorage(cleared when the browser closes); only the Supabase session token lives inlocalStorage. Signing out clears every cache. - Demo mode is localhost-only. It seeds data without authentication, so it is gated behind a hostname check and never reaches the public deployment.
- Feedback. The Feedback page sends messages to the developer's inbox through the external Web3Forms service; it stores nothing in your database.
Full write-up in docs/SECURITY.md.
Issues and pull requests are welcome. CONTRIBUTING.md covers
how to get the app running (no backend needed — the demo mode is enough), what the
code expects from a change, and the conventions that matter: pure selectors for
anything that computes money, one Zustand store per domain, both es and en
strings for every new label, and an RLS policy for every new table.
Participation is covered by the Code of Conduct. Found a vulnerability? Don't open a public issue — use a private advisory.
MIT © Giancarlos Estévez






