OpenFinance is a local-first finance and subscription tracker with optional cloud sync/backup, Google Sheets integration, and dashboard analytics.
- Production: https://chameleon-finance-c4y.pages.dev
- Stack: React 19, Vite 8, Tailwind CSS v4, Zustand, Recharts, Cloudflare Pages Functions, D1, R2
| Doc | What it covers |
|---|---|
| docs/DEVELOPMENT.md | Local setup, scripts, project layout, conventions |
| docs/TESTING.md | Unit (Vitest) + E2E (Playwright), how to run and write tests |
| docs/DEPLOYMENT.md | Cloudflare Pages build/deploy, bindings, env vars |
| docs/API.md | Pages Functions API routes, auth, request/response shapes |
| docs/DESIGN_SYSTEM.md | L Health CI: tokens, theming, components |
| CONTRIBUTING.md | Workflow, commit format, PR checklist |
| CHANGELOG.md | Notable changes |
| SECURITY.md | Reporting vulnerabilities |
| CODE_OF_CONDUCT.md | Community expectations |
| CLAUDE.md | Orientation for AI coding agents |
The codebase is split into 4 main runtime layers:
- UI Layer (
src/features,src/shared/ui) - State + Domain Logic Layer (
src/store,src/shared/lib, feature hooks) - Sync/Integration Layer (Google Sheets + Cloud backup helpers)
- Server Layer (
functions/api) on Cloudflare Pages Functions
Data flow is mostly:
UI -> Zustand stores -> localStorage (persist) -> optional external sync/backup (Sheets/D1/R2)
src/
app/
main.jsx # App bootstrap + Cloudflare analytics beacon injection
App.jsx # Top-level app shell, tab routing, autosync/autobackup loops
styles/index.css # Tailwind v4 tokens, global styles, route shells
test/setup.js # Vitest/browser test setup
store/ # Zustand stores (subscriptions, finance, currency, theme)
features/ # Product features (finance, subscriptions, sync, trends, budget, settings)
shared/
lib/ # Pure/non-UI domain logic and helpers
hooks/ # Shared hooks (theme)
ui/ # Reusable UI primitives
functions/
api/ # Cloudflare Pages Function API routes
tests/e2e/ # Playwright end-to-end tests
infra/d1/schema.sql # D1 schema for push subscription/notification data
tools/scripts/ # Utility scripts (VAPID keys, statement PDF conversion)
docs/design/ # Source design exports and handoff assets
- Mounts React root with
StrictMode. - Injects Cloudflare Analytics script when
VITE_CLOUDFLARE_ANALYTICS_TOKENexists.
Responsibilities:
- App-level composition for 2 primary surfaces:
- Finance Tracker
- Subscriptions
- Lazy-loads heavy sections/views for better initial load.
- Coordinates background jobs:
- Auto Sheets sync every 5 minutes + on online/focus/visibility.
- Auto cloud backup (debounced + interval + focus/visibility triggers).
- Owns modal states (
AddSubscriptionModal,SettingsModal) and tab state.
Key design choice:
Appis intentionally orchestration-heavy; feature logic is delegated to hooks (useSheetsSync,useFinanceSheetsSync) and stores.
All state stores use zustand with persistence middleware and custom migration-safe storage handlers.
- State:
subs,step,currentView,income
- Actions:
addSub,editSub,removeSub,setSubs,setStep,setView,setIncome
- Persistence key:
vexly_flow_data
- Compatibility:
- Supports legacy raw-array format and newer
{ subs, income }format.
- Supports legacy raw-array format and newer
- State:
records,filters
- Actions:
addRecord,editRecord,removeRecord,setRecords,clearAll,setFilter
- Persistence key:
chameleon_finance_data
- Compatibility:
- Handles legacy array-only stored format.
- State:
selectedCurrency,currencies
- Actions:
setCurrency,initRates
- Runtime behavior:
- Loads cached exchange rates.
- Refreshes rates daily from
https://open.er-api.com/v6/latest/USD. - Auto-detects first-visit currency via
ipapi.co.
- Persistence key:
vexly_currency(stored as plain string for backward compatibility).
- State:
theme(nullmeans follow system)
- Actions:
setTheme,toggleTheme,getEffectiveTheme
- Persistence key:
subgrid_theme
- Computes effective theme from user preference + system preference.
- Applies
data-theme="dark"to<html>for Tailwind custom dark variant. - Exposes:
theme,isDark,toggleTheme,getChartColors
- Tailwind v4 config + custom dark variant.
- Central CSS variables for text, border, chart colors, and shadow tokens.
- Includes custom rules for treemap, category UI, renewal badges, and global transitions.
- Create/edit subscription records.
- Auto category suggestion (
suggestCategory) from subscription name. - Icon resolution via logo proxy (
/api/logo/:domain). - Analytics events:
subscription_addedsubscription_edited
- List rendering and remove flow.
- Card-level domain display:
- monthly normalization
- category badge
- renewal timing badge
- logo fallback logic
- Two-step UI:
- Step 1: record management
- Step 2: dashboards
- Owns finance modal open/edit state.
- CSV export
- Google Sheet template shortcut
- Statement upload/import (
.csv/.tsv/.txt) with dedupe - Finance pull sync from Sheets
- Safe clear-all with 2-step confirm
- Full finance record editor:
- date, description, type, income/expense values
- payment metadata
- note + done state
- icon domain/custom icon
- color selection
- Uses shared computations:
- type breakdown
- monthly trend
- Supports bar/line/pie/area/treemap/sankey rendering.
- Persists monthly budget (
subgrid_budget) in localStorage. - Computes usage %, threshold status, and status message.
- Display + editor for budget limits with currency support.
- Tracks monthly snapshots in
subgrid_history. - Calculates:
- month-over-month change
- year-over-year change
- trend direction (linear slope approximation)
- Exports trend history CSV.
- UI cards for MoM/YoY + trend direction summary.
- Can force snapshot recording when history is sparse.
- Renewal date computation by cycle (weekly/monthly/yearly).
- Badge severity classes based on days to renewal.
- Upcoming-renewal extraction logic.
- Record filtering
- Summary aggregation
- CSV export/import row parsing
- Breakdown by type
- Monthly trend generation
- Parses statement text from CSV-like formats.
- Delimiter detection + resilient amount/date parsing.
- Field alias mapping and type inference.
- Deduplication using fingerprint:
- date + normalized description + income + expense
- Spreadsheet URL parsing and connection validation.
- Public CSV pull (no API key) for shared sheets.
- Reads tabs:
SubscriptionsBudgetTrends- finance tab (
gidor fallback tab)
- Advanced finance import behavior:
- header alias normalization
- robust date/number/boolean parsing
- monthly tab probing (
January 2026, etc.) - merged/sorted cross-tab output
- Currency conversion + formatting helpers.
- Base currency catalog and locale mapping.
- Category catalog + keyword-based suggestion.
- Quick subscription presets.
- Custom 3-column Sankey layout:
- Income -> Categories -> Subscriptions
- Produces explicit node/link geometry + bezier link paths.
- Squarified treemap algorithm implementation.
- Computes rectangle tiling from weighted values.
- Token validation + storage (
subgrid_server_token) - Cloudflare Access auth status probing (
/api/auth/me) - Backup/restore API client with endpoint fallback:
- primary:
/api/db/backup - fallback:
/api/r2/backup
- primary:
- Builds payload with subscriptions, finance records, budget, trends.
- Pull-only from Google Sheets (read-oriented sync model).
- Merge strategy:
- last-write-wins by
lastModifiedfor matching IDs - keeps unique records from both sides
- last-write-wins by
- Conflict detection:
- only flags near-simultaneous edits (within 60s) with differing payloads.
- Uses Sheets credentials from shared config.
- Pulls finance records from:
- explicit
gidif URL contains it - otherwise
Sheet1(FINANCE_SHEET_TAB)
- explicit
- Tracks offline changes and retries.
- Currently geared to read-only cloud sync semantics (local queue bookkeeping + retry hooks).
Primary localStorage keys:
vexly_flow_data-> subscriptions + income + UI view statechameleon_finance_data-> finance recordssubgrid_theme-> explicit light/dark preferencevexly_currency-> selected display currencyvexly_exchangeRates,vexly_ratesLastUpdate-> cached FX data_sheets_config-> connected spreadsheet metadata_sync_state,_finance_sync_state-> sync timestampssubgrid_budget-> budget objectsubgrid_history-> monthly trend snapshotssubgrid_server_token-> optional legacy backup token
All APIs are in functions/api.
- Accepts either:
- explicit header token
X-User-Token(64 hex chars), or - Cloudflare Access identity headers
- explicit header token
- Cloudflare identity is normalized and hashed (SHA-256) into stable user token.
- Returns whether Cloudflare Access identity is present.
- Supplies login/logout URLs (
/cdn-cgi/access/login,/cdn-cgi/access/logout).
- Uses
USER_DB(preferred), fallback toDB/ABDULL_DB. - Upserts JSON payload by user token in
user_backups. - Used as primary cloud backup backend.
- Enforces auth and R2 binding.
- Injects
data.userPrefix = users/<token>. - Adds CORS headers globally.
- Writes/reads latest backup and timestamped backup history in user namespace.
- Lists historical backups for user.
- Save/list export files in
users/<token>/exports/.
- Download/delete a named export.
- Upload/list subscription attachments (with MIME/type limits).
- Download/delete one attachment.
- Domain-validated proxy to Logo.dev using
LOGO_DEV_API_TOKEN.
- Writes analytics datapoints to Cloudflare Analytics Engine binding
ANALYTICS.
/api/health,/api/kv,/api/transactions,/api/r2(older bindings and utility operations).
Common fields used across UI/store/sync:
id,name,price,currency,cyclecategory,colorurl,startDatenotificationsEnabled,reminderDayslastModified
id,date,description,typeincome,expenses,minimumExpensesinterestRate,balance,dueDatepaymentMethod,howPaid,donenote,iconDomain,customIcon,colorlastModified
git clone https://github.com/KunanonJ/OpenFinance.git
cd OpenFinance
npm install
npm run devOpen http://localhost:5173.
npm test
npm run test:e2enpm run build
npm run previewnpm run build
npx wrangler pages deploy dist --project-name=chameleon-finance --commit-dirty=true- Aliases:
@,@features,@shared,@store
- Manual chunk split:
vendor-chartsforrechartsvendorfor othernode_modules
- JSDOM test environment
- setup file:
src/app/test/setup.js - test include:
src/**/*.{test,spec}.{js,jsx}
wrangler.jsonc(current Pages-oriented config):ANALYTICS,R2_BUCKET,USER_DB
wrangler.toml(compatibility mirror for local/CLI workflows):ANALYTICS,R2_BUCKET,USER_DB
The code resolves modern binding names first and keeps legacy ABDULL_* fallbacks for older
utility endpoints.
- iOS/Android are wrapped with Capacitor (
appId: com.chameleon.app). The nativeios/andandroid/projects are not committed — runcap:syncto generate them fromdist/. - Relevant scripts in
package.json:cap:synccap:ioscap:android
- Create feature files under
src/features/<feature>. - Keep data logic in
src/shared/libor feature hooks. - Keep store mutations centralized in
src/store/*. - Add unit tests next to new logic (
*.test.js). - If feature touches external data, add/extend API route in
functions/api.
- Build selector/aggregation in shared lib (
financeUtils.jsor feature hook). - Create view component in:
src/features/visualizations(subscription charts), orsrc/features/finance(finance charts).
- Register view in
ViewToggleand in parent dashboard switch logic.
- Create file under
functions/api/.... - Reuse auth helpers from
functions/api/_lib/auth.js. - Return consistent JSON and CORS headers.
- Add binding checks (
env.<binding>) and explicit 501/500 fallback messages.
Expected bindings/env vars used by current code:
LOGO_DEV_API_TOKENR2_BUCKET- D1 backup binding:
USER_DB(preferred), orDB, orABDULL_DB ANALYTICS(for/api/event)
Optional legacy endpoints may also use:
ABDULL_KVABDULL_BUCKET
If you are onboarding, read these first in order:
src/app/App.jsxsrc/store/subscriptionStore.jssrc/store/financeStore.jssrc/features/sync/sheetsApi.jssrc/shared/lib/serverStorage.jsfunctions/api/_lib/auth.jsfunctions/api/db/backup.js
This sequence gives you the fastest path to understanding app flow, data lifecycle, and server integration boundaries.