A personal finance intelligence platform. Connect your Gmail, and FinnLens automatically extracts credit card transactions, subscriptions, and investments from your emails — with PDF statement parsing.
| Dashboard | Accounts |
|---|---|
![]() |
![]() |
| Calendar View | Gmail Sync |
|---|---|
![]() |
![]() |
| Investment Tracking | Subscription Tracking |
|---|---|
| Layer | Tech |
|---|---|
| Frontend | React 19, Vite, Tailwind CSS v4, shadcn/ui, TanStack Query |
| Backend | Django 6, Django Bolt (Rust-powered API server) |
| Auth | JWT via django-bolt (email/password) |
| Gmail Sync | Google OAuth 2.0 + PKCE (read-only) |
| ML | GLiNER (entity extraction), GLiClass (classification) |
| pdfplumber (text + table extraction) | |
| Database | PostgreSQL 16 (production), SQLite (local dev fallback) |
| Task queue | arq + Redis |
| Package managers | pnpm (frontend), uv (backend) |
| AI coding agents | Claude Code (Opus), OpenCode (GLM-5) |
finn-lens/
├── frontend/ # React app (Vite + Tailwind v4 + shadcn/ui)
├── backend/ # Django + Django Bolt API
│ ├── accounts/ # Auth viewsets (login, me)
│ ├── banking/ # Credit cards, transactions, bills, subscriptions
│ │ ├── email_extractor/ # Standalone email data extraction (publishable)
│ │ └── parsers/ # PDF statement parsers
│ ├── gmail/ # Gmail sync, email parsing, sender rules
│ │ └── parsers/ # Email content parsers (CC alerts, statements, etc.)
│ ├── classifier/ # ML classification pipeline
│ ├── oauth/ # Google OAuth endpoints
│ └── finnlens/ # Django settings
├── docker-compose.yml # Development (hot-reload, volume mounts)
├── docker-compose.prod.yml # Production overlay (optimized builds, Caddy)
└── Makefile # All run commands
Choose your setup:
Docker (recommended) — least local deps, one command to run everything
- Docker + Docker Compose
- Make (usually pre-installed on macOS/Linux)
# 1. Clone and configure backend env
cp backend/.env.example backend/.env
# Edit backend/.env with your Google OAuth credentials (see GCP Setup below)
# 2. Build and start all services
make docker-build
make docker-up
# 3. Run migrations and create a user
make docker-migrate
make docker-createsuperuser
# 4. View logs
make docker-logsOpen http://localhost:5174 and sign in with your superuser credentials.
make docker-downmake docker-shell| Service | Port |
|---|---|
| Frontend (Vite) | http://localhost:5174 |
| Backend API | http://localhost:8000 |
| PostgreSQL | localhost:5432 |
| Redis | localhost:6379 |
Local Development — for contributors working on the codebase
- Python 3.12+
- Node.js 20+
uv—pip install uvpnpm—npm install -g pnpm- Redis —
brew install redis(macOS) orapt install redis(Linux)
cd backend
# Install dependencies
uv sync
# Copy and configure environment
cp .env.example .env # edit with your Google OAuth credentials
# Run migrations and create a user
uv run python manage.py migrate
uv run python manage.py createsuperuser
# Start the API server (port 8000)
uv run python manage.py runbolt --devcd frontend
# Install dependencies
pnpm install
# Start dev server (port 5174)
pnpm devThe arq worker processes the Gmail sync pipeline (fetch → classify → parse → materialize).
# Terminal 1: Start Redis
make redis
# Terminal 2: Start worker (auto-reloads on code changes)
make workermake -j4 upOpen http://localhost:5174 and sign in with your superuser credentials.
Docker Production — optimized builds, Caddy, multi-process
Production mode uses:
- Caddy for the frontend (static build + API reverse proxy)
- Django Bolt with
--processes 4, connection pooling viapsycopg[pool] - PostgreSQL with persistent volumes
- No hot-reload, no dev volume mounts
# 1. Configure backend/.env with production values:
# - DEBUG=False
# - ALLOWED_HOSTS=your-domain.com
# - BOLT_JWT_SECRET=<strong random value, min 32 chars>
# - SECRET_KEY=<strong random value>
# - CORS_ALLOWED_ORIGINS=https://your-domain.com
# - DATABASE_URL=postgresql://finnlens:finnlens@postgres:5432/finnlens (set automatically by compose)
# 2. Optional: override compose settings via environment
# POSTGRES_PASSWORD=your-strong-password
# FRONTEND_PORT=443
# ALLOWED_HOSTS=your-domain.com,127.0.0.1
# CORS_ALLOWED_ORIGINS=https://your-domain.com
# 3. Build and start
make docker-prod
# 4. Run migrations
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec backend \
uv run python manage.py migrate
# 5. Create superuser
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec backend \
uv run python manage.py createsuperusermake docker-prod-down| Service | Port |
|---|---|
| Frontend (Caddy) | 80 (configurable via FRONTEND_PORT) |
| PostgreSQL | Not exposed externally |
| Redis | Not exposed externally |
Note: For automatic HTTPS, update the
Caddyfilewith your domain: replace:80withyour-domain.com.
Demo Mode — zero backend, fully mocked
Demo mode runs the frontend with zero backend dependency — all API calls are intercepted and return realistic mocked data (Indian financial data: INR, Indian banks, merchants).
cd frontend
# Dev server with demo mode
pnpm dev:demo
# Production demo build
pnpm build:demoOpen http://localhost:5174 — you'll see a marketing landing page instead of the login form. Click "Quick Login as Demo User" or enter demo / demo.
The marketing landing page is only enabled for pnpm dev:demo. pnpm build:demo still uses mocked data, but keeps the normal login page.
What works in demo mode:
- All pages display realistic mock data (overview, accounts, transactions, analytics, calendar, subscriptions, investments, budgets, assets, life events, waitlist, notifications, settings)
- Write operations (add account, update profile, manage subscriptions, manage sender rules) modify in-memory state
- Gmail sync simulates a real pipeline — click Sync and watch the 6-step pipeline progress in real time
- Category overrides on transactions persist in-memory
- A subtle "Demo Mode" banner appears at the top of the app
What is mocked:
- No real API calls are made — the backend is not required
- Login is local (
demo:democredentials, no JWT validation) - Gmail OAuth flow returns mock responses
Google Cloud Platform (GCP) OAuth Setup — required for Gmail sync
- Go to Google Cloud Console
- Click the project dropdown → New Project
- Name it (e.g.,
FinnLens) and create
- Navigate to APIs & Services → Library
- Search and enable:
- Gmail API (for email sync)
- Navigate to APIs & Services → OAuth consent screen
- Choose External user type
- Fill in:
- App name:
FinnLens - User support email: your email
- Developer contact email: your email
- App name:
- Under Scopes, add:
emailprofilehttps://www.googleapis.com/auth/gmail.readonly
- Under Audience → Test users, add your email address so only you can access this
- (Optional) Submit for verification to make it available to anyone
- Navigate to APIs & Services → Credentials
- Click Create Credentials → OAuth client ID
- Application type: Web application
- Name:
FinnLens Web - Authorized JavaScript origins:
http://localhost:5174(local dev)http://localhost(Docker prod)- Your production domain (e.g.,
https://finnlens.com)
- Authorized redirect URIs:
http://localhost:5174/oauth/google/callback(local dev)http://localhost/oauth/google/callback(Docker prod)https://your-domain.com/oauth/google/callback(production)
- Click Create
- Copy the Client ID and Client Secret
Add to backend/.env:
GOOGLE_CLIENT_ID=<your-client-id>
GOOGLE_CLIENT_SECRET=<your-client-secret>
GOOGLE_REDIRECT_URI=http://localhost:5174/oauth/google/callback
GMAIL_TOKEN_ENCRYPTION_KEY=<generate with command below>Generate a Fernet encryption key for storing Gmail tokens securely:
uv run python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"Create a Superuser (Django Admin)
make docker-createsuperusercd backend
uv run python manage.py createsuperuserFollow the prompts — email and password are all that's required.
Tip: If you get a database connection error, make sure PostgreSQL is running and
DATABASE_URLis set correctly inbackend/.env.
First-Time Onboarding — after signing in
After signing in, you'll land on the Dashboard. You should see an empty state with no accounts or transactions.
- Navigate to Settings → Gmail (or click the Gmail sync prompt)
- Click Connect Google Account
- Authorize the app when redirected to Google
- You'll be redirected back — your Gmail account is now linked
- Navigate to Accounts or click Sync in the sidebar
- Click Sync Now
- The 6-step pipeline runs automatically:
- Fetch → Classify → Parse → Materialize → Classify Transactions → Detect Subscriptions
- Wait for completion (first sync may take a few minutes depending on email volume)
- Dashboard — overview of spending, upcoming bills, net worth
- Accounts — credit cards and their balances
- Transactions — all extracted transactions with search and filters
- Analytics — spending by category, merchant, and time period
- Subscriptions — auto-detected recurring charges
- Investments — portfolio data from financial emails
- Category overrides — click any transaction to re-categorize it
- Sender rules — manage which email senders are processed for financial data
- Settings — update profile, manage Gmail connection, configure preferences
- Fetch — Gmail API (read-only) pulls emails from financial senders
- Classify — Pattern-matching rules assign emails to types (credit_card, subscription, etc.)
- Parse — Parsers extract structured data from email body + PDF attachments
- Materialize — Extracted data becomes CreditCard, Bill, Transaction records
- Classify — ML pipeline categorizes transactions (food, travel, bills, etc.)
- Bill summary (total due, min due, due date, billing period) extracted from email body
- PDF statement transactions extracted via pdfplumber (text + table parsing)
- Statement data supersedes alert data (better merchant names, correct forex amounts)
- Transactions linked to bills by billing period date window
- Each bill links back to the source Gmail message for quick reference
The banking/email_extractor module extracts structured CC statement data from email HTML with zero Django dependencies. Can be published as a separate package.
from banking.email_extractor import extract_cc_statement
result = extract_cc_statement(subject="...", body_html="...")
print(result.total_due) # 13593.37
print(result.min_due) # 930.0
print(result.due_date) # 2026-03-30
print(result.billing_period_start) # 2026-02-13
print(result.billing_period_end) # 2026-03-12
print(result.card_last4) # 9005
print(result.pdf_password) # suji0501| Command | Description |
|---|---|
make backend |
Start backend (local) |
make frontend |
Start frontend (local) |
make worker |
Start arq worker (local) |
make redis |
Start Redis (local) |
make migrate |
Run Django migrations (local) |
make -j4 up |
Start all local services in parallel |
make docker-build |
Build Docker images |
make docker-up |
Start Docker dev services |
make docker-down |
Stop Docker dev services |
make docker-logs |
Tail Docker logs |
make docker-shell |
Shell into backend container |
make docker-migrate |
Run migrations in Docker |
make docker-createsuperuser |
Create superuser in Docker |
make docker-prod |
Build and start production |
make docker-prod-down |
Stop production |
- Use
backend/.env.exampleas the source of truth for backend environment variables. - Setup-specific environment details are documented in the Getting Started sections above.
- LinkedIn: https://www.linkedin.com/in/sureshdsk/
- X: https://x.com/sureshdsk
- License: MIT
Technical backlog status for core data coverage:
| Features | Status | Supported now (parser-level) | Pending |
|---|---|---|---|
| Bank statement parser | partial |
ICICI | IDFC FIRST, SBI, Federal Bank |
| Credit card statement parser | partial |
ICICI, Axis (+ generic fallback) | HDFC, SBI Card, Kotak, IndusInd, Standard Chartered, RBL, Yes Bank, AMEX, HSBC, Citi |
| Investment email parser | partial |
Groww | Zerodha, Kite, Angel One |
| Subscription parser | partial |
Generic parser | Platform-specific parsers |
| Notifications | pending |
None | End-to-end notification system |
Notes:
- Credit cards outside ICICI/Axis may still extract partially via the generic fallback parser, but do not have dedicated issuer-specific parsers yet.
- Some providers are recognized at sender-rule level, but parser-level extraction is still pending.





