A full-stack expense tracking application for managing personal finances with user authentication, data visualization, and automated recurring expense tracking.
- Features
- Tech Stack
- Prerequisites
- Installation
- Configuration
- Running the Application
- Project Structure
- Features Guide
- API Reference
- Database Schema
- Deployment
- Testing
- Troubleshooting
- Contributing
- License
- Multi-User Authentication - GitHub OAuth and email/password registration
- Expense Management - Add, edit, and delete expenses with categories and notes
- Recurring Expenses - Automate bills, subscriptions, and regular payments
- Budget Tracking - Set allowances with daily, weekly, or monthly cadences
- Visual Analytics - Interactive charts showing spending trends and category breakdowns
- Monthly Summaries - Detailed reports grouped by category
- Keyboard Shortcuts - Power user productivity features
- Mobile Responsive - Works seamlessly on all device sizes
- Accessible - WCAG 2.1 Level AA compliant
- Tested - 40 unit tests with 97.56% code coverage
| Technology | Purpose |
|---|---|
| Next.js 14 | React framework with App Router |
| React 18 | UI component library |
| TypeScript | Type-safe JavaScript |
| NextAuth.js | Authentication handling |
| Recharts | Data visualization |
| CSS Modules | Scoped styling |
| Technology | Purpose |
|---|---|
| Express.js | REST API server |
| TypeScript | Type-safe JavaScript |
| Prisma ORM | Database operations |
| bcrypt | Password hashing |
| node-cron | Scheduled tasks |
| JWT | API authentication |
| Technology | Purpose |
|---|---|
| PostgreSQL | Primary database |
| Supabase | Database hosting |
| Vercel | Frontend hosting |
| Railway | Backend hosting |
| Docker | Local development |
Before you begin, ensure you have the following installed:
- Node.js 18.0.0 or higher (recommended: 20.19.0)
- npm 9.0.0 or higher
- PostgreSQL 15+ (or use Supabase/Docker)
- Git for version control
You'll also need:
- A GitHub OAuth App for social login
- A Supabase account (free tier) OR local PostgreSQL
git clone https://github.com/your-username/expense-tracker.git
cd expense-trackerInstall all dependencies for root, client, and server:
npm run install:allThis runs npm install in:
- Root directory (concurrently)
/client(Next.js frontend)/server(Express backend)
Option A: Using Supabase (Recommended)
- Create a project at supabase.com
- Go to Settings → Database
- Copy the Connection pooling URL (uses port 6543)
Option B: Using Docker
docker-compose up -dThis starts a PostgreSQL container with:
- User:
expense_user - Password:
expense_password - Database:
expense_tracker - Port: 5432
Option C: Local PostgreSQL
createdb expense_trackercd prisma
npx prisma migrate deploy
npx prisma generateCreate a .env file in the root directory with the following variables:
# ============================================
# Database Configuration
# ============================================
# For Supabase: Use the connection pooler URL (port 6543)
# For Docker/Local: postgresql://expense_user:expense_password@localhost:5432/expense_tracker
DATABASE_URL="postgresql://postgres.[PROJECT]:[PASSWORD]@[REGION].pooler.supabase.com:6543/postgres"
# ============================================
# NextAuth Configuration
# ============================================
# Generate with: openssl rand -base64 32
NEXTAUTH_SECRET="your-secret-key-here"
NEXTAUTH_URL="http://localhost:3000"
# ============================================
# GitHub OAuth
# ============================================
# Create at: https://github.com/settings/developers
GITHUB_ID="your-github-client-id"
GITHUB_SECRET="your-github-client-secret"
# ============================================
# API Configuration
# ============================================
NEXT_PUBLIC_API_URL="http://localhost:4000"NextAuth Secret:
openssl rand -base64 32- Go to GitHub Developer Settings
- Click New OAuth App
- Fill in the details:
- Application name: Expense Tracker
- Homepage URL:
http://localhost:3000 - Authorization callback URL:
http://localhost:3000/api/auth/callback/github
- Copy the Client ID and generate a Client Secret
Start both frontend and backend concurrently:
npm run devThis runs:
- Frontend: http://localhost:3000
- Backend API: http://localhost:4000
# Frontend only
npm run dev:client
# Backend only
npm run dev:serverBuild and start the frontend:
cd client
npm run build
npm startBuild and start the backend:
cd server
npm run build
npm run prodexpense-tracker/
├── client/ # Next.js Frontend
│ ├── app/ # App Router pages
│ │ ├── page.js # Dashboard (home)
│ │ ├── expenses/ # Expense management
│ │ ├── recurring/ # Recurring expenses
│ │ ├── summary/ # Monthly summary
│ │ ├── categories/ # Category management
│ │ ├── login/ # Login page
│ │ ├── register/ # Registration page
│ │ └── api/auth/ # NextAuth endpoints
│ ├── components/ # Reusable UI components
│ │ ├── charts/ # Visualization (Recharts)
│ │ ├── Button/ # Button variants
│ │ ├── Card/ # Card container
│ │ ├── Modal/ # Dialog component
│ │ ├── ExpenseForm/ # Expense input form
│ │ └── ... # 20+ components
│ ├── lib/ # Utilities
│ │ ├── api-backend.ts # API client
│ │ ├── format.ts # Currency/date formatting
│ │ └── hooks/ # Custom React hooks
│ └── styles/ # Global styles
│
├── server/ # Express.js Backend
│ ├── index.ts # Main server & routes
│ ├── middleware/
│ │ └── auth.ts # JWT authentication
│ ├── lib/
│ │ └── prisma.ts # Database client
│ └── types/ # TypeScript definitions
│
├── prisma/ # Database
│ ├── schema.prisma # Data models
│ ├── migrations/ # Migration history
│ └── init.sql # Initial setup
│
├── docker-compose.yml # Local PostgreSQL
├── railway.json # Railway deployment
├── nixpacks.toml # Nixpacks config
└── package.json # Root scripts
Adding an Expense:
- Click the "Add Expense" button on the dashboard
- Enter the amount, select a category, and pick the date
- Optionally add a note for more context
- Click Save
Editing/Deleting:
- Click on any expense to view details
- Use the Edit or Delete buttons
Filtering:
- Use the date range picker to filter by period
- Select a category to filter by type
- Expenses are paginated (20 per page)
Perfect for bills, subscriptions, and regular payments:
- Go to Recurring in the navigation
- Click "Add Recurring Expense"
- Configure:
- Amount - How much it costs
- Category - Food, Utilities, etc.
- Frequency - Daily, Weekly, Monthly, or Yearly
- Start Date - When it begins
- End Date (optional) - When it stops
How It Works:
- A cron job runs every night at midnight
- It checks for recurring expenses due that day
- Automatically creates expense entries
- Updates the next due date
Set a spending budget to track your finances:
- Go to the Dashboard
- Click "Set Allowance"
- Enter your budget amount
- Choose cadence: Daily, Weekly, or Monthly
The dashboard shows:
- Your remaining balance
- Progress bar visualization
- Time until next reset
Spending Trends:
- 6-month area chart showing spending over time
- Helps identify patterns
Category Breakdown:
- Donut chart showing where your money goes
- Percentage breakdown by category
Monthly Summary:
- Go to Summary for detailed reports
- Filter by month and category
All endpoints are prefixed with http://localhost:4000/api
All protected endpoints require the X-User-Id header with the authenticated user's ID.
| Endpoint | Method | Description |
|---|---|---|
/auth/register |
POST | Register new user |
/auth/login |
POST | Login with credentials |
/users/sync |
POST | Sync OAuth user to database |
/health |
GET | Health check |
Register User:
curl -X POST http://localhost:4000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "securepassword123",
"name": "John Doe"
}'| Endpoint | Method | Description |
|---|---|---|
/expenses |
GET | List expenses (paginated) |
/expenses/:id |
GET | Get single expense |
/expenses |
POST | Create expense |
/expenses/:id |
PUT | Update expense |
/expenses/:id |
DELETE | Delete expense |
Query Parameters (GET /expenses):
page- Page number (default: 1)pageSize- Items per page (default: 20, max: 100)from- Start date (YYYY-MM-DD)to- End date (YYYY-MM-DD)category- Filter by category
Create Expense:
curl -X POST http://localhost:4000/api/expenses \
-H "Content-Type: application/json" \
-H "X-User-Id: user_id_here" \
-d '{
"amount": 25.50,
"category": "Food",
"date": "2026-01-25",
"note": "Lunch at cafe"
}'Response:
{
"item": {
"id": 1,
"amount": 25.50,
"category": "Food",
"date": "2026-01-25",
"note": "Lunch at cafe",
"createdAt": "2026-01-25T12:00:00.000Z",
"userId": "user_id_here"
}
}| Endpoint | Method | Description |
|---|---|---|
/summary |
GET | Get spending summary |
Query Parameters:
from- Start date (YYYY-MM-DD)to- End date (YYYY-MM-DD)category- Filter by category
Response:
{
"totalCents": 125000,
"total": 1250.00,
"byCategory": [
{ "category": "Food", "totalCents": 50000, "total": 500.00 },
{ "category": "Utilities", "totalCents": 75000, "total": 750.00 }
]
}| Endpoint | Method | Description |
|---|---|---|
/allowance |
GET | Get allowance settings |
/allowance |
PUT | Update allowance |
/allowance/status |
GET | Get current period status |
Update Allowance:
curl -X PUT http://localhost:4000/api/allowance \
-H "Content-Type: application/json" \
-H "X-User-Id: user_id_here" \
-d '{
"amount": 500,
"cadence": "week"
}'Cadence Options: day, week, month
| Endpoint | Method | Description |
|---|---|---|
/recurring-expenses |
GET | List all recurring |
/recurring-expenses/:id |
GET | Get single recurring |
/recurring-expenses |
POST | Create recurring |
/recurring-expenses/:id |
PUT | Update recurring |
/recurring-expenses/:id |
DELETE | Soft delete recurring |
/recurring-expenses/process |
POST | Manually trigger processing |
Create Recurring Expense:
curl -X POST http://localhost:4000/api/recurring-expenses \
-H "Content-Type: application/json" \
-H "X-User-Id: user_id_here" \
-d '{
"amount": 9.99,
"category": "Subscriptions",
"note": "Netflix",
"frequency": "monthly",
"dayOfMonth": 15,
"nextDate": "2026-02-15"
}'Frequency Options: daily, weekly, monthly, yearly
┌─────────────┐ ┌─────────────┐
│ User │────<│ Expense │
│─────────────│ │─────────────│
│ id (PK) │ │ id (PK) │
│ email │ │ userId (FK) │
│ password │ │ amountCents │
│ githubId │ │ category │
│ name │ │ date │
│ createdAt │ │ note │
│ updatedAt │ │ createdAt │
└─────────────┘ │ updatedAt │
│ └─────────────┘
│
│ ┌──────────────────┐
├───────────<│ RecurringExpense │
│ │──────────────────│
│ │ id (PK) │
│ │ userId (FK) │
│ │ amountCents │
│ │ category │
│ │ frequency │
│ │ nextDate │
│ │ endDate │
│ │ isActive │
│ └──────────────────┘
│
└───────────<┌─────────────┐
│ Allowance │
│─────────────│
│ id (PK) │
│ userId (FK) │
│ amountCents │
│ cadence │
└─────────────┘
User
- Supports both OAuth (GitHub) and email/password authentication
- Password is nullable for OAuth-only users
- One allowance per user (1:1 relationship)
Expense
- Amounts stored in cents for precision
- Indexed on
userId + dateanduserId + categoryfor fast queries
RecurringExpense
- Supports daily, weekly, monthly, yearly frequencies
nextDatetracks when the next expense should be generated- Soft delete via
isActiveflag
Allowance
- Budget settings per user
- Cadence: day, week, or month
- Push your code to GitHub
- Go to vercel.com and import your repository
- Set the Root Directory to
client - Add environment variables:
DATABASE_URL=your_supabase_url NEXTAUTH_SECRET=your_secret NEXTAUTH_URL=https://your-app.vercel.app GITHUB_ID=your_github_id GITHUB_SECRET=your_github_secret NEXT_PUBLIC_API_URL=https://your-backend.railway.app - Deploy!
- Go to railway.app and create a new project
- Connect your GitHub repository
- Railway will auto-detect the
railway.jsonconfiguration - Add environment variables:
DATABASE_URL=your_supabase_url NEXTAUTH_SECRET=your_secret PORT=4000 - Deploy!
The railway.json is pre-configured:
{
"build": { "builder": "NIXPACKS" },
"deploy": {
"startCommand": "cd server && npm start",
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 10
}
}For self-hosted deployments:
# Start PostgreSQL
docker-compose up -d
# Run migrations
cd prisma
npx prisma migrate deploy
# Build and run
cd client && npm run build && npm start &
cd server && npm run build && npm run prod &# From client directory
cd client
# Watch mode (development)
npm test
# Single run (CI)
npm run test:run
# With coverage report
npm run test:coverage
# UI mode
npm run test:ui# Run E2E tests
npm run test:e2e
# With UI
npm run test:e2e:ui
# Debug mode
npm run test:e2e:debugCurrent coverage: 97.56%
| Category | Coverage |
|---|---|
| Statements | 97.56% |
| Branches | 95.00% |
| Functions | 98.00% |
| Lines | 97.56% |
# Kill process on port 3000
lsof -ti:3000 | xargs kill -9
# Kill process on port 4000
lsof -ti:4000 | xargs kill -9- Verify
DATABASE_URLis correct - For Supabase, use the connection pooler URL (port 6543, not 5432)
- Check that your IP is allowed in Supabase settings
- "Invalid token" - Sign out and back in
- "User not found" - Check that user sync is working
- Session issues - Clear browser cookies
# Clear Next.js cache
cd client
rm -rf .next node_modules/.cache
npm install
# Regenerate Prisma client
npx prisma generate- Check server logs for cron job output
- Manually trigger:
POST /api/recurring-expenses/process - Verify server timezone matches expected behavior
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Commit your changes:
git commit -m 'Add amazing feature' - Push to the branch:
git push origin feature/amazing-feature - Open a Pull Request
- Use TypeScript for new code
- Follow existing patterns and naming conventions
- Add tests for new features
- Ensure all tests pass before submitting
This project is licensed under the MIT License.