Skip to content

coder11125/SpendsWise

Repository files navigation

SpendsWise

A personal finance tracker built with Svelte 5, TypeScript, Express, and MongoDB. Includes an AI finance assistant powered by Groq, real-time sync via Pusher, recurring transactions, receipt OCR, and multi-currency support. Deployed on Vercel.

Features

  • Transactions — Add, edit, and delete income and expense entries with category, date, currency, family member, and notes
  • Recurring transactions — Set up daily / weekly / biweekly / monthly / yearly templates; entries are auto-generated by a server-side scheduler
  • Dashboard — Summary cards, category pie chart, trend chart, budget progress bars, upcoming recurring transactions, and recent entries
  • AI assistant — Chat with your financial data, parse expenses from natural language ("spent 450 on lunch"), and OCR receipts (single or bulk up to 10 images)
  • Budget goals — Per-category spending limits with green → amber → red progress bars
  • Multi-currency — 150+ currencies, live exchange rates, per-transaction currency, unified display currency
  • Family members — Tag transactions by household member
  • Import / Export — CSV import (up to 500 rows) and CSV export
  • Real-time sync — Pusher WebSocket events with 5-minute polling fallback
  • Dark mode — System-aware toggle persisted to localStorage
  • Mobile — Responsive layout with FAB quick-add button
  • Google OAuth — Sign in with Google in addition to email/password

Project layout

SpendsWise/
├── index.html                  # Vite frontend entry
├── src/                        # Svelte 5 SPA
│   ├── app.ts                  # Client entry point
│   ├── App.svelte              # Root component — router, modals, layout
│   ├── app.css                 # Tailwind v4 via @import "tailwindcss"
│   ├── types.ts                # Shared TypeScript interfaces
│   ├── lib/
│   │   ├── state.svelte.ts     # Reactive state (Svelte 5 runes)
│   │   ├── api.ts              # API wrapper + all endpoint functions
│   │   ├── currency.ts         # Currency formatting/conversion + live rates
│   │   ├── calculations.svelte.ts  # Async summary calculations
│   │   ├── utils.ts            # Date helpers, trend data, image compression
│   │   ├── charts.ts           # Canvas pie chart + trend chart renderers
│   │   └── constants.ts        # Currencies, colors, category icons
│   ├── components/             # UI components (modals, forms, cards, charts)
│   └── views/                  # Dashboard, IncomeView, ExpenseView, AccountView
├── api/
│   └── index.ts                # Vercel serverless entry (re-exports Express app)
├── server/                     # Express + TypeScript API
│   └── src/
│       ├── app.ts              # Express app setup — middleware, routes, scheduler
│       ├── index.ts            # Local dev entry (listens on PORT)
│       ├── config.ts           # Environment variable loading
│       ├── db.ts               # MongoDB connection
│       ├── lib/
│       │   ├── pusher.ts       # Pusher real-time notifications
│       │   └── recurringScheduler.ts  # 60s scheduler for recurring entries
│       ├── middleware/         # auth, csrf, asyncHandler, groqRateLimiter, passport
│       ├── models/             # Expense, User Mongoose schemas
│       └── routes/             # auth, expenses, familyMembers, ai, currency
├── vercel.json
├── package.json                # Frontend deps + scripts
└── server/package.json         # Backend deps + scripts

Getting started

Prerequisites

1. Frontend

npm install
npm run dev        # Vite dev server with HMR (proxies /api → localhost:4000)
npm run build      # production build → dist/
npm run preview    # preview production build locally

2. Backend

cd server
npm install
cp .env.example .env
# Fill in the required values (see Environment variables below)
npm run dev        # ts-node-dev with auto-reload

Generate secrets:

node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"

Health check: GET http://localhost:4000/health

Environment variables

Set these in server/.env for local development, or in your Vercel dashboard for production.

Variable Required Description
MONGODB_URI yes MongoDB connection string
JWT_SECRET yes Long random string for signing JWTs
CSRF_SECRET yes Long random string for signing CSRF tokens (separate from JWT_SECRET)
GOOGLE_CLIENT_ID no Google OAuth — from Google Cloud Console → APIs & Services → Credentials
GOOGLE_CLIENT_SECRET no Google OAuth client secret
GOOGLE_CALLBACK_URL no Default: http://localhost:5173/api/auth/google/callback
FRONTEND_URL no Default: http://localhost:5173 (must match Vite dev server or Vercel URL)
GROQ_API_KEY no Enables AI features — get one free at console.groq.com
GROQ_MODEL no Groq text model (default: llama-3.3-70b-versatile)
GROQ_VISION_API_KEY no Separate Groq key for receipt OCR — falls back to GROQ_API_KEY
GROQ_VISION_MODEL no Groq vision model (default: meta-llama/llama-4-scout-17b-16e-instruct)
AI_DAILY_LIMIT no Max AI requests per user per day (default: 50)
AI_MONTHLY_LIMIT no Max AI requests per user per month (default: 500)
PUSHER_APP_ID no Pusher app ID — enables real-time sync
PUSHER_KEY no Pusher public key (used by server and client)
PUSHER_SECRET no Pusher secret (server only)
PUSHER_CLUSTER no Pusher cluster (e.g. us2, eu, ap1)
CURRENCY_API_KEY no exchangerate-api.com key — uses demo key if not set

API

Session is managed via an HttpOnly cookie (sw_session). All state-changing requests must include a valid CSRF token in the x-csrf-token header — fetch one from GET /api/auth/csrf first.

Method Path Auth Description
GET /health no Health check
GET /api/auth/csrf no Get CSRF token; sets sw_csrf cookie
POST /api/auth/register no { email, password }
POST /api/auth/login no { email, password }
POST /api/auth/logout no Clears session cookie
GET /api/auth/me yes Current user profile
PUT /api/auth/password yes { currentPassword, newPassword }
GET /api/auth/google no Start Google OAuth flow
GET /api/auth/google/callback no Google OAuth callback
GET /api/expenses yes List all expenses (sorted by date desc)
POST /api/expenses yes { amount, category, type, note?, date?, currency?, familyMember?, recurrence? }
PUT /api/expenses/:id yes Update any subset of expense fields
DELETE /api/expenses/:id yes Delete single expense
DELETE /api/expenses yes { confirm: true } — delete all expenses
GET /api/expenses/recurring yes List active recurring templates
PUT /api/expenses/:id/recurring yes { frequency, endDate, isActive, nextDueDate }
POST /api/expenses/bulk yes { rows: [...] } — bulk create up to 500 rows
GET /api/family-members yes List family members
POST /api/family-members yes { name }
DELETE /api/family-members yes { name }
GET /api/currency/rates yes ?base={currency} — live conversion rates
GET /api/ai/quota yes Remaining daily/monthly AI quota
POST /api/ai/chat yes { message, history? } — chat with expense context
POST /api/ai/parse yes { text } — natural language → structured expense
POST /api/ai/parse-receipt yes { imageData } — base64 receipt image → expense fields
POST /api/ai/parse-receipts-bulk yes { images: [...] } — batch OCR, up to 10 images

Data model

Expense

{
  userId: ObjectId,
  type: "income" | "expense",
  amount: Number (0–1e12),
  category: String,
  currency: String (default "USD"),
  familyMember: String,
  note: String,
  date: Date,
  recurrence: {                    // null for non-recurring
    frequency: "daily" | "weekly" | "biweekly" | "monthly" | "yearly",
    nextDueDate: Date,
    endDate: Date | null,
    isActive: Boolean
  },
  timestamps: true
}

Recurring templates carry the recurrence subdocument. Auto-generated instances are plain expenses with no recurrence field.

User

{
  email: String (unique),
  passwordHash: String,
  googleId: String (optional),
  familyMembers: [String],
  aiUsage: { dailyCount, monthlyCount, dailyDate, monthlyDate },
  tokenVersion: Number,
  timestamps: true
}

Security

Control Detail
CSRF protection Double-submit cookie pattern. GET /api/auth/csrf issues a token; all POST/PUT/DELETE must send it as x-csrf-token.
Session cookie HttpOnly, SameSite=Strict, Secure (production).
Token revocation tokenVersion on the User document. Password change increments it, invalidating all live sessions instantly.
Rate limiting Auth: 10 req / 15 min / IP. Expenses: 200 req / 15 min / IP.
Content-Type enforcement Non-JSON bodies on state-changing requests rejected with 415.
Password rules 12–72 characters, requires uppercase, lowercase, digit, and special character. bcrypt cost 12.
NoSQL injection express-mongo-sanitize strips $ and . operator keys from all request bodies.
Security headers Helmet (HSTS, X-Content-Type-Options, etc.) on every response.
ObjectId validation All :id params validated with ObjectId.isValid() before DB queries.
Google OAuth isolation Email and Google ID lookups run separately to prevent account takeover.

Deployment (Vercel)

The project deploys as a monorepo on Vercel:

  • Frontend — Vite builds the Svelte SPA to dist/; static files served by Vercel CDN.
  • Backendapi/index.ts is the serverless entry; all /api/* and /health requests route there.
{
  "buildCommand": "npm run build",
  "outputDirectory": "dist",
  "routes": [
    { "src": "/api/(.*)", "dest": "api/index.ts" },
    { "src": "/health", "dest": "api/index.ts" },
    { "handle": "filesystem" },
    { "src": "/(.*)", "dest": "/index.html" }
  ]
}

MongoDB Atlas note: Add 0.0.0.0/0 to Network Access so Vercel's dynamic IPs can connect.

License

MIT

About

The Global Budget Tracker

Topics

Resources

License

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors