A full-stack personal finance management application for tracking investments, mutual funds, loans, SIPs, and expenses. Features statement import from broker and CAMS/CAS PDFs, a tamper-evident audit ledger, optional client-side data encryption via a personal vault, encrypted backup/restore, and an offline-first local vault mode that keeps all data in an AES-256-GCM encrypted file on your device — no account or server required.
The application is deployed on Railway with the following services:
| Service | Platform | URL |
|---|---|---|
| Frontend | Railway (nginx:alpine container) | https://finora-financetracker.up.railway.app |
| Backend API | Railway (Java 21 container) | https://finora-api.up.railway.app |
| Database | Supabase (PostgreSQL 17) | Managed, not public |
The frontend nginx container proxies all /api/* requests to the backend using the BACKEND_URL environment variable, so the browser never makes cross-origin requests directly.
- Docker Desktop installed and running
docker compose up --build| Service | URL |
|---|---|
| Frontend | http://localhost |
| Backend API | http://localhost:8082/api |
| API docs (Swagger) | http://localhost:8082/swagger-ui.html |
| Health check | http://localhost:8082/actuator/health |
- PostgreSQL (host port 5433 mapped to container 5432) — database
finance_tracker, persisted in a Docker volume - Spring Boot API (port 8082) — REST API with JWT authentication, Flyway migrations, and scheduled background tasks
- React Frontend (port 80) — served via nginx, proxies
/api/*to the backend
docker compose downTo also delete all data:
docker compose down -vgraph TB
Browser["Browser"]
subgraph Frontend["Frontend — nginx:alpine"]
SPA["React SPA (TypeScript + Tailwind CSS)"]
Proxy["nginx reverse proxy\n/api/* → BACKEND_URL"]
end
subgraph Backend["Backend — Spring Boot 3.4.5 / Java 21"]
Auth["Auth Controller\n/api/auth"]
API["REST Controllers\n/api/investments /api/expenses\n/api/loans /api/sips /api/statements\n/api/backup /api/ledger /api/users"]
Services["Service Layer\n(investments, SIPs, expenses,\nloans, statements, ledger, backup)"]
Parsers["Statement Parsers\n(CAS PDF, CAMS PDF,\nBroker Excel / CSV)"]
Prices["Price Provider\n(strategy pattern)"]
Schedulers["Schedulers\nSIP monthly · Loan daily"]
Security["Security\nJWT · Login rate limiter\nField encryption · Vault"]
end
subgraph DB["Database — PostgreSQL (Supabase)"]
Tables["users · expenses · investments\nloans · sips · ledger_events"]
end
subgraph External["External APIs"]
YF["Yahoo Finance\n(stock prices — primary)"]
AV["Alpha Vantage\n(stock prices — fallback)"]
AMFI["AMFI NAV feed\n(mutual fund NAV)"]
end
Browser -->|HTTPS| SPA
SPA --> Proxy
Proxy -->|JWT| Auth
Proxy -->|JWT| API
Auth --> Services
API --> Services
Services --> Parsers
Services --> Prices
Services --> DB
Schedulers --> Services
Prices -->|primary| YF
Prices -->|fallback| AV
Services --> AMFI
classDef fe fill:#e1f5fe,color:#000
classDef be fill:#f3e5f5,color:#000
classDef db fill:#e8f5e8,color:#000
classDef ext fill:#fce4ec,color:#000
class SPA,Proxy fe
class Auth,API,Services,Parsers,Prices,Schedulers,Security be
class Tables db
class YF,AV,AMFI ext
React 18 with TypeScript, built with Vite and styled with Tailwind CSS. Served as a static SPA from nginx. In cloud mode, all API calls go through the nginx proxy at /api/. In local vault mode, the same UI operates entirely in the browser with no network requests.
The frontend uses a DataProvider abstraction layer so every page works identically in both modes. Each domain has a React hook (e.g. useExpenseApi()) that returns either the real REST client or an in-memory local implementation — pages never know which mode is active.
flowchart TB
subgraph UI["UI Layer — Pages & Components"]
Dashboard[Dashboard]
Investments[Investments]
SIPs[SIPs]
Loans[Loans]
Expenses[Expenses]
end
subgraph Hooks["DataProvider Hooks"]
useExpenseApi["useExpenseApi()"]
useInvestmentApi["useInvestmentApi()"]
useSipApi["useSipApi()"]
useLoanApi["useLoanApi()"]
useSummaryApi["useSummaryApi()"]
end
subgraph Cloud["Cloud Mode Path"]
AxiosClient["Axios HTTP Client"]
NginxProxy["nginx /api/* proxy"]
SpringBoot["Spring Boot API"]
PostgreSQL[(PostgreSQL)]
end
subgraph Local["Local Vault Mode Path"]
VaultCtx["LocalVaultContext\n(React state)"]
IDB[("IndexedDB\n(draft persistence)")]
FSA["File System Access API\nor download fallback"]
EncFile["🔒 .enc file\nAES-256-GCM + PBKDF2"]
end
Dashboard & Investments & SIPs & Loans & Expenses --> Hooks
useExpenseApi & useInvestmentApi & useSipApi & useLoanApi & useSummaryApi -->|"isLocalMode = false"| AxiosClient
useExpenseApi & useInvestmentApi & useSipApi & useLoanApi & useSummaryApi -->|"isLocalMode = true"| VaultCtx
AxiosClient --> NginxProxy --> SpringBoot --> PostgreSQL
VaultCtx -->|auto-save draft| IDB
VaultCtx -->|save / open| FSA
FSA <-->|read / write| EncFile
classDef ui fill:#e1f5fe,color:#000
classDef hook fill:#fff3e0,color:#000
classDef cloud fill:#f3e5f5,color:#000
classDef local fill:#e8f5e9,color:#000
classDef file fill:#fce4ec,color:#000
class Dashboard,Investments,SIPs,Loans,Expenses ui
class useExpenseApi,useInvestmentApi,useSipApi,useLoanApi,useSummaryApi hook
class AxiosClient,NginxProxy,SpringBoot,PostgreSQL cloud
class VaultCtx,IDB,FSA local
class EncFile file
Spring Boot 3.4.5 on Java 21. Organized into:
- Controllers — REST endpoints under
/api/*, all protected by JWT except/api/auth/registerand/api/auth/login - Services — business logic for each domain (investments, SIPs, expenses, loans, users, statements, ledger, backup)
- Statement parsers — CAS PDF parser, CAMS PDF parser, and a broker Excel/CSV parser that auto-detects column layout
- Price providers — strategy pattern with Yahoo Finance as primary and Alpha Vantage as fallback
- Schedulers — SIP monthly processing (1st of each month, 9 AM) and loan balance updates (daily, midnight)
- Ledger service — append-only, hash-chained audit log of every create/update/delete written to
ledger_events - Backup service — AES-256-GCM encrypted full-data export and import with ledger chain integrity verification
- Field encryption — optional AES-256-GCM column-level encryption for sensitive fields (name, description, email), controlled by
FIELD_ENCRYPTION_KEY - Vault — optional user-held passphrase that adds a second encryption layer on top of field encryption
PostgreSQL managed by Flyway migrations:
V1__init.sql— all tables withIF NOT EXISTS(idempotent)
Tables: users, expenses, investments, loans, sips, ledger_events
Portfolio management
- Stocks, ETFs, bonds, and mutual funds in one place
- Import holdings directly from broker Excel/CSV exports (Zerodha, Groww, Upstox, HDFC, ICICI, Angel, 5paisa, Kotak, Sharekhan, and others that follow a standard column layout)
- Import from CAMS and CAS (Consolidated Account Statement) PDFs
- Two-step preview and confirm import flow — select which holdings to import, skip manual entries
Automated price updates
- Stock and ETF prices fetched from Yahoo Finance on demand; Alpha Vantage used as fallback when configured
- Mutual fund NAV pulled from the official AMFI daily feed (amfiindia.com) and cached in-memory
SIP tracking
- Monthly SIP installment processing runs automatically on the 1st of each month
- Links SIP records to their corresponding investment holding when imported from a statement
Expenses
- Categorized expense entries with payment method tracking
- Monthly spending trend and category breakdown charts
Loans
- Simple and compound interest support with configurable compounding frequency
- Daily loan balance recalculation
- EMI tracking and remaining tenure display
Audit ledger
- Every create, update, and delete on investments, expenses, loans, and SIPs is written to an append-only
ledger_eventstable - Events are SHA-256 hash-chained (each event includes the hash of the previous event) — any tampering breaks the chain
- Integrity can be verified at any time from the API
Vault (optional)
- Users can enable a personal vault with a passphrase (minimum 8 characters)
- The passphrase derives an additional AES-256-GCM key that wraps field-level encryption
- Without the passphrase, encrypted data cannot be read even with database access
- Passphrase is never stored — loss of passphrase means permanent loss of access to encrypted records
Backup and restore
- Full data export encrypted with AES-256-GCM using a password chosen at export time
- Backup file includes the ledger event chain; integrity is verified before import
- Restoring a backup replaces all existing data for the user
Excel reports
- Per-section Excel exports for investments, expenses, loans, and SIPs
Local vault mode (offline)
- No account or server connection required — works entirely in the browser
- All data stored in a single AES-256-GCM encrypted
.encfile on your device - Encryption uses PBKDF2 key derivation (310,000 iterations) from a user-chosen passphrase
- Draft changes auto-persist to IndexedDB so nothing is lost if the tab closes
- Save vault uses the File System Access API (Chrome/Edge) for in-place file writes, with a download fallback for Firefox/Safari
- Cloud backups can be opened as local vaults and vice versa
- Server-only features (statement import, price refresh, NAV refresh) are automatically hidden in local mode
- Landing page at
/welcomelets users choose between Cloud Mode and Local Vault
Global search
- Keyboard-accessible search across all investments, expenses, loans, and SIPs
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/auth/register |
Register a new account (email + username + password) |
| POST | /api/auth/login |
Login with email and password, returns JWT |
| Method | Endpoint | Description |
|---|---|---|
| GET/POST | /api/investments |
List all / create investment |
| PUT/DELETE | /api/investments/{id} |
Update / delete investment |
| POST | /api/investments/refresh-prices |
Trigger on-demand price refresh |
| GET/POST | /api/expenses |
List all / create expense |
| PUT/DELETE | /api/expenses/{id} |
Update / delete expense |
| GET/POST | /api/loans |
List all / create loan |
| PUT/DELETE | /api/loans/{id} |
Update / delete loan |
| GET/POST | /api/sips |
List all / create SIP |
| PUT/DELETE | /api/sips/{id} |
Update / delete SIP |
| GET | /api/finance-summary |
Aggregated dashboard summary |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/statements/preview |
Upload file, get parsed holdings preview |
| POST | /api/statements/confirm |
Confirm selected holdings for import |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/users/me |
Get current user profile |
| PATCH | /api/users/me |
Update username |
| GET | /api/users/vault/status |
Check whether vault is enabled |
| POST | /api/users/vault/enable |
Enable vault with a passphrase |
| POST | /api/users/vault/disable |
Disable vault |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/backup/export |
Download encrypted backup file |
| POST | /api/backup/import |
Upload and restore from backup |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/ledger/verify |
Verify hash chain integrity for current user |
| GET | /api/ledger/entity/{type}/{id} |
Get audit timeline for a specific record |
All endpoints except /api/auth/register and /api/auth/login require a JWT in the Authorization header:
Authorization: Bearer <token>
Backend
- Java 21
- Spring Boot 3.4.5
- Spring Security (JWT, login rate limiter — 5 attempts per minute per IP)
- Spring Data JPA + Hibernate 6
- Flyway (database migrations)
- PostgreSQL
- Apache PDFBox 3 (CAS and CAMS PDF parsing)
- Apache POI 5 (broker Excel parsing)
- Maven
Frontend
- React 18
- TypeScript
- Vite
- Tailwind CSS
- Recharts (charts)
- SheetJS/xlsx (Excel report generation)
- React Router
Infrastructure
- Railway (container hosting for API and frontend)
- Supabase (managed PostgreSQL)
- nginx:alpine (frontend serving and API reverse proxy)
- Docker / Docker Compose (local development)
External APIs
- Yahoo Finance — stock and ETF prices (no key required)
- Alpha Vantage — stock price fallback (requires
ALPHAVANTAGE_API_KEY) - AMFI (amfiindia.com) — mutual fund NAV daily feed
- Java 21 or later
- Node.js LTS
- Maven 3.6 or later
- PostgreSQL (or use Docker Compose which sets this up automatically)
cd Finora-API
# Create .env with required variables
cat > .env <<EOF
JWT_SECRET=your_jwt_secret_minimum_32_characters
DB_HOST=localhost
DB_PORT=5432
DB_NAME=finora
DB_USERNAME=postgres
DB_PASSWORD=postgres
CORS_ALLOWED_ORIGINS=http://localhost,http://localhost:5173
# Optional
ALPHAVANTAGE_API_KEY=your_key
FIELD_ENCRYPTION_KEY=your_encryption_key
EOF
./mvnw spring-boot:run
# API starts on port 8080 (or PORT env var)cd Finora-UI
npm install
npm run dev
# Dev server starts on port 5173
# Vite proxies /api/* to http://localhost:8080| Variable | Required | Description |
|---|---|---|
JWT_SECRET |
Yes | Secret key for signing JWT tokens (minimum 32 characters) |
DATABASE_URL |
Railway | Full JDBC URL (overrides individual DB_ vars) |
DB_HOST |
Local | PostgreSQL host |
DB_PORT |
Local | PostgreSQL port |
DB_NAME |
Local | Database name |
DB_USERNAME |
Local | Database user |
DB_PASSWORD |
Local | Database password |
CORS_ALLOWED_ORIGINS |
Yes | Comma-separated list of allowed frontend origins |
ALPHAVANTAGE_API_KEY |
No | Alpha Vantage key for stock price fallback |
FIELD_ENCRYPTION_KEY |
No | Enables AES-256-GCM column-level encryption when set |
PORT |
Auto | Injected by Railway; defaults to 8080 |
| Variable | Description |
|---|---|
BACKEND_URL |
Full URL of the backend service (e.g. https://finora-api.up.railway.app) |
PORT |
Injected by Railway; nginx listens on this port |
| Schedule | Task |
|---|---|
| 1st of every month, 9:00 AM | Process monthly SIP installments — deducts monthly amount from each active SIP, updates unit count using current NAV |
| Every day, midnight | Recalculate loan balances and remaining tenure |
Stock price updates are triggered on demand (via the refresh-prices endpoint or from the investments page) rather than on a fixed schedule.
- JWT tokens are signed with HMAC-SHA256 using
JWT_SECRET - Login is rate-limited to 5 attempts per minute per IP address
- Field-level encryption uses AES-256-GCM with PBKDF2 key derivation (310,000 iterations)
- The vault adds a second AES-256-GCM encryption layer keyed from the user's passphrase — the passphrase is never stored anywhere
- Local vault mode uses client-side AES-256-GCM (WebCrypto API) with PBKDF2 (310,000 iterations, SHA-256) — encryption and decryption happen entirely in the browser; the passphrase never leaves the device
- The ledger is protected by an append-only trigger in PostgreSQL; events cannot be updated or deleted at the database level
- Sensitive fields (investment names, expense descriptions, user email) are encrypted at rest when
FIELD_ENCRYPTION_KEYis configured
| Table | Purpose |
|---|---|
users |
Accounts, roles, vault configuration |
investments |
Stock, ETF, bond, and mutual fund holdings |
expenses |
Categorized expense records |
loans |
Loan records with EMI and interest type |
sips |
SIP configurations and unit tracking |
ledger_events |
Append-only hash-chained audit log |
MIT