FinFlow is a SaaS personal finance manager that helps users track salary, allocate savings, manage bills and subscriptions, budget by category, and get AI-driven insights — all in a polished, mobile-first interface inspired by Monarch Money and Revolut.
Every month, salary flows through a fixed priority order before reaching discretionary budgets.
User-configurable percentages in settings.
Cloudflare Pages hosts the Next.js frontend as a static export. API routes run as Cloudflare Workers (Edge Runtime). No Node.js server needed.
| Layer | Choice | Reason |
|---|---|---|
| Framework | Next.js 15 | App Router, RSC, Edge Runtime — compatible with CF Pages |
| Language | TypeScript | Type safety, better DX |
| Styling | Tailwind CSS v4 | Utility-first, small bundle |
| Database | PostgreSQL via Supabase | Row-level security, realtime, auth built-in |
| ORM | Drizzle ORM | Edge-compatible (Prisma isn't for CF Workers) |
| Auth | Supabase Auth | SSO, magic link, email verify, RLS integration |
| File storage | Supabase Storage | Payslips, receipts, bill docs |
| OCR / AI | Anthropic claude-3-5-haiku | Vision + JSON extraction for docs & receipts |
| AI insights | Anthropic claude-sonnet | Spending analysis, forecasts |
| Charts | Recharts | React-native, composable, responsive |
| PDF export | @react-pdf/renderer | Runs on edge, typed React components |
| Excel/CSV | xlsx (SheetJS) | Lightweight, browser + edge support |
| Resend | Simple API, great DX, CF-compatible | |
| Hosting | Cloudflare Pages | Global CDN, free tier, Workers integration |
| CI/CD | GitHub Actions → CF Pages | Auto-deploy on push |
| Monitoring | Sentry + CF Analytics | Error tracking + edge analytics |
output: 'export' for static export + @cloudflare/next-on-pages adapter for API routes as Workers. Use edge runtime on all API routes. Database connection via Supabase REST API (not direct pg connection — Workers don't support TCP).
PostgreSQL via Supabase. All tables include created_at, updated_at. Row-level security enabled on every table keyed on user_id.
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | Supabase auth.users reference |
| text unique | ||
| full_name | text | |
| avatar_url | text | |
| currency | text | default 'USD' |
| timezone | text | default 'UTC' |
| budget_start_day | int | 1–28, default 1 |
| emergency_pct | numeric | default 5.00 |
| savings_pct | numeric | default 20.00 |
| leftover_savings_pct | numeric | default 60.00 |
| plan | text | 'free' | 'pro' |
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | |
| user_id | uuid FK | → users |
| pay_date | date | |
| gross_pay | numeric | |
| net_pay | numeric | |
| tax | numeric | |
| deductions | numeric | |
| bonus | numeric | default 0 |
| overtime | numeric | default 0 |
| employer | text | |
| source | text | 'manual' | 'payslip' |
| document_url | text | Supabase Storage path |
| ai_extracted | jsonb | raw OCR output |
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | |
| user_id | uuid FK | |
| name | text | |
| target_amount | numeric | |
| target_date | date | nullable |
| current_balance | numeric | default 0 |
| icon | text | emoji or icon name |
| color | text | hex |
| is_default | bool | receives auto-allocation |
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | |
| user_id | uuid FK | |
| name | text | e.g. "Electricity" |
| company | text | |
| amount | numeric | |
| due_day | int | day of month |
| category | text | 'rent' | 'utility' | 'insurance' | 'custom' |
| is_active | bool | default true |
| document_url | text | uploaded bill |
| reminder_days | int | days before due |
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | |
| user_id | uuid FK | |
| name | text | |
| amount | numeric | |
| billing_day | int | day of month |
| frequency | text | 'monthly' | 'yearly' | 'weekly' |
| logo_url | text | preset logo or custom |
| color | text | |
| reminder_enabled | bool | default true |
| is_active | bool | default true |
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | |
| user_id | uuid FK | |
| name | text | |
| icon | text | |
| color | text | |
| allocation_type | text | 'percentage' | 'fixed' |
| allocation_value | numeric | |
| monthly_limit | numeric | computed or manual |
| period_year | int | |
| period_month | int |
| Column | Type | Notes |
|---|---|---|
| id | uuid PK | |
| user_id | uuid FK | |
| amount | numeric | |
| category_id | uuid FK | → budget_categories |
| merchant | text | |
| date | date | |
| notes | text | nullable |
| payment_method | text | 'card' | 'cash' | 'transfer' |
| receipt_url | text | Supabase Storage |
| ai_extracted | jsonb | raw OCR output |
All routes live under /api/v1/. Edge Runtime on Cloudflare Workers. Auth via Supabase JWT in Authorization: Bearer header. RLS enforces user isolation at DB level.
A cool-toned financial palette — navy primary for trust, teal for success/savings, amber for warnings, rose for spending alerts. Light and dark mode support via CSS variables.
| Purpose | Light | Dark |
|---|---|---|
| Income / positive | #16A34A (green) | #4ADE80 |
| Expense / negative | #E11D48 (rose) | #FB7185 |
| Savings | #0D9488 (teal) | #2DD4BF |
| Emergency fund | #D97706 (amber) | #FCD34D |
| Budget warning (80%+) | #F59E0B | #FDE68A |
| Budget danger (100%+) | #E11D48 | #FB7185 |
| Primary action | #2563EB (blue) | #60A5FA |
| Role | Family | Weight | Size |
|---|---|---|---|
| Display / hero | Inter | 700 | 32–48px |
| Heading H1 | Inter | 600 | 24px |
| Heading H2 | Inter | 600 | 18px |
| Heading H3 | Inter | 500 | 15px |
| Body | Inter | 400 | 14px / 1.6 |
| Caption / label | Inter | 400 | 12px |
| Numbers (financial) | Inter Tabular | 500 | context |
| Mono / code | JetBrains Mono | 400 | 12px |
Use font-variant-numeric: tabular-nums on all financial values to prevent layout shifts.
All animations wrapped in prefers-reduced-motion media query.
| Name | Width | Layout |
|---|---|---|
| Mobile | < 640px | Single column, bottom nav |
| Tablet | 640–1024px | Collapsible sidebar + 2-col grid |
| Desktop | > 1024px | Fixed sidebar + 3–4 col dashboard |
Financial data is sensitive. Implement all items below before launch.
auth.uid() = user_id_headers file on CF PagesCloudflare Pages + Supabase scales horizontally without configuration.