A full-stack personal finance app built with the PERN stack (PostgreSQL, Express, React, Node.js). Track income, log expenses, manage savings goals, and sign in with Google.
| Layer | Technology |
|---|---|
| Frontend | React 18, Ant Design, React Router v6 |
| Backend | Node.js, Express |
| Database | PostgreSQL, TypeORM |
| Auth | JWT (httpOnly cookie), Google OAuth 2.0 |
| Security | helmet, express-rate-limit, express-validator |
Make sure you have these installed before starting:
- Node.js v18 or higher
- PostgreSQL v14 or higher
- npm (comes with Node.js)
- A Google Cloud project with OAuth 2.0 credentials (for Google login — see step below)
Budget-Tracker-App/
├── client/
│ └── budget-tracker-app/ ← React frontend
└── server/ ← Express backend
git clone <your-repo-url>
cd Budget-Tracker-App- Open
psql(or pgAdmin) and create a new database:
CREATE DATABASE "Budget-Tracker-App";- Note your PostgreSQL username, password, host (
localhost), and port (5432). You'll need these in the next step.
Skip this if you don't need Google login. The rest of the app works without it.
- Go to Google Cloud Console
- Create a new project (or use an existing one)
- Navigate to APIs & Services → Credentials
- Click Create Credentials → OAuth 2.0 Client ID
- Application type: Web application
- Add
http://localhost:3000to Authorized JavaScript origins - Copy the Client ID — you'll use it in both
.envfiles below
cd server
cp .env.example .envOpen server/.env and fill in your values:
PORT=3001
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres # your PostgreSQL username
DB_PASSWORD=your_password # your PostgreSQL password
DB_NAME=Budget-Tracker-App
JWT_SECRET=some_long_random_string_here # any secret, min 32 chars
CORS_ORIGIN=http://localhost:3000
NODE_ENV=development
GOOGLE_CLIENT_ID= # paste your Google OAuth Client ID hereGenerating a strong JWT_SECRET:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Copy the output and paste it as the value for JWT_SECRET.
# still inside server/
npm installThis creates all the tables in your PostgreSQL database:
npm run migration:runYou should see output like:
query: CREATE TABLE "users" ...
query: CREATE TABLE "incomes" ...
query: CREATE TABLE "expenses" ...
query: CREATE TABLE "savings" ...
Migration 1000000000000-InitialSchema has been executed successfully.
If you see ECONNREFUSED, PostgreSQL isn't running. Start it first:
- macOS:
brew services start postgresql - Windows: Start the PostgreSQL service from Services panel
- Linux:
sudo systemctl start postgresql
npm run devThe server starts on http://localhost:3001. You should see:
Server is running on port 3001
To verify it's healthy:
curl http://localhost:3001/health
# → {"status":"ok"}Open a new terminal:
cd client/budget-tracker-app
cp .env.example .env.localOpen client/budget-tracker-app/.env.local and fill in:
REACT_APP_API_URL=http://localhost:3001
REACT_APP_GOOGLE_CLIENT_ID= # same Client ID from Step 3# still inside client/budget-tracker-app/
npm installnpm startThe app opens at http://localhost:3000.
Run through these in order to verify everything works end-to-end:
- Sign up — go to
/user/signup, create an account with name, email, password. Should redirect to the dashboard. - Login — log out, go to
/user/login, log in with the same credentials. Should redirect to the dashboard. - JWT cookie set — open browser DevTools → Application → Cookies →
localhost:3000. You should see atokencookie withHttpOnlychecked and no value visible. - Private route — while logged out, navigate directly to
http://localhost:3000/user/dashboard/1. Should redirect to/user/login. - Logout — click logout. Cookie should be cleared. Navigating to dashboard should redirect to login.
- Google login (if configured) — click "Login with Google", complete OAuth flow, should land on the dashboard.
- Add expense — click Add Expense, fill in name, amount, type, date. Should appear in the table without a page reload.
- Edit expense — click the edit icon on a row, change a field, save. Row should update.
- Delete expense — click the delete icon. Row should disappear from the table without a page reload.
- Add income — same flow as expenses with type (Regular / One-Time / Passive).
- Edit income — verify changes persist.
- Delete income — row removed from table without reload.
- Add saving — enter name, target amount, deadline.
- Edit saving — modify and save.
- Delete saving — row removed.
- Total income / expenses show correct sums.
- Current Balance = total income this month − total expenses this month.
- Monthly cards update when you add/delete income or expenses.
- Go to Edit Profile. You should only see Name and Email fields — no
roleorpasswordfields. - Update name. Should save and reflect immediately.
- Cross-user access — sign up as User A (note their ID from the URL). Sign up as User B in an incognito window. While logged in as User B, paste
http://localhost:3000/user/dashboard/<User A's ID>in the address bar. Should get a 403 error, not User A's data. - API without cookie — open a new terminal and run:
Should return
curl http://localhost:3001/expense/get-expenses/1
{"error":"Unauthorized"}, not expense data. - Security headers — run:
Response should include
curl -I http://localhost:3001/health
X-Frame-Options,X-Content-Type-Options,X-DNS-Prefetch-Controlheaders. - Rate limiting — run this 11 times quickly on the login route:
The 11th request should return
for i in $(seq 1 11); do curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:3001/user/login -H "Content-Type: application/json" -d '{"email":"a@b.com","password":"wrong"}'; done
429.
| Script | Description |
|---|---|
npm run dev |
Start with nodemon (auto-restart on file changes) |
npm start |
Start without nodemon (production-style) |
npm run migration:run |
Apply pending database migrations |
npm run migration:generate |
Generate a new migration from entity changes |
| Script | Description |
|---|---|
npm start |
Start development server on port 3000 |
npm run build |
Create optimized production build |
ECONNREFUSED when running migration
PostgreSQL is not running. Start it first (see Step 6).
invalid signature or jwt malformed errors
The JWT_SECRET in .env was changed after users logged in. Clear your browser cookies and log in again.
Google login shows error or does nothing
- Check that
REACT_APP_GOOGLE_CLIENT_IDin.env.localmatches the server'sGOOGLE_CLIENT_IDin.env - Ensure
http://localhost:3000is listed in your Google Cloud Console under Authorized JavaScript origins
Cannot find module on server start
Run npm install inside the server/ directory.
Blank page or white screen on the client
Open browser DevTools → Console. If it says a component crashed, refresh — the ErrorBoundary will catch it and show a recovery button.
Port already in use
# Find and kill the process on port 3001
lsof -ti:3001 | xargs kill -9
# Or for port 3000
lsof -ti:3000 | xargs kill -9