A modern expense tracking web app built with React + Vite, powered by Firebase Authentication and Cloud Firestore.
Supports monthly budgets, multi-currency expenses with historical exchange rates, and push notification support.
-
Authentication
- Email & password
- Google Sign-In
- Password reset email
- Change password (email users only)
-
Expenses
- Add, edit, delete expenses
- Category-based organization
- Multi-currency support
- Automatic conversion using the exchange rate of the expense day
-
Dashboard
- Monthly totals
- Category breakdown with charts
- Monthly budget with over/under tracking
-
Settings
- Base currency selection
- Push notification management
- Account info & password update
-
UX
- Keyboard shortcuts (Enter = save, Esc = cancel)
- Mobile-friendly layout
- Offline-aware UI
- Frontend
- React + TypeScript
- Vite
- CSS Modules
- Backend / Services
- Firebase Authentication
- Cloud Firestore
- Express + Web Push server for notifications
- External APIs
- Frankfurter FX API (
https://api.frankfurter.dev/v1) for exchange rates
- Frankfurter FX API (
- Hosting
- Firebase Hosting (free tier)
This repository is organized as a small monorepo:
src/contains the React frontendserver/contains the Express + Web Push backend
The frontend also works without the push server, but push notifications require the backend to be running.
- Node.js 20.19+ (or 22.12+)
- Firebase project with:
- Authentication enabled
- Cloud Firestore enabled
- Email/password provider enabled
- Google provider enabled
- Test account:
- email:
test@test.test - password:
123456
- email:
- You can also create a new account directly from the Login page using email and password
- Sign-in with Google is also available
- Password reset and password change features require a valid email/password account
npm installCreate a .env.local file in the project root:
touch .env.localFill in all required values.
VITE_FIREBASE_API_KEY=your_key
VITE_FIREBASE_AUTH_DOMAIN=your_project.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=your_project_id
VITE_FIREBASE_STORAGE_BUCKET=your_project.appspot.com
VITE_FIREBASE_MESSAGING_SENDER_ID=123456789
VITE_FIREBASE_APP_ID=1:123456789:web:abcdefnpm run devOpen:
http://localhost:5173npm run build
npm run previewInstall backend dependencies:
cd server
npm installCreate server/.env:
PORT=8080
VAPID_SUBJECT=mailto:you@example.com
VAPID_PUBLIC_KEY=...
VAPID_PRIVATE_KEY=...Run the backend:
npm run devThe push server runs on:
http://localhost:8080The frontend expects this URL in src/features/notifications/push.service.ts.
Suggested test flow after starting the frontend and, if needed, the push server:
- Sign in with the test account or create a new account.
- Create at least one category from the Categories page.
- Add one or more expenses from the Expenses page.
- Open the Dashboard and verify monthly totals, category breakdown, and budget management.
- Open Settings and change the base currency.
- If the push server is running, enable notifications and send a test notification from Settings.
- Optionally install the app as a PWA and verify offline fallback / cached behavior.
firebase login
firebase init hosting- Public directory: dist
- Configure as SPA: Yes
npm run build
firebase deployYour app will be available at:
https://<project-id>.web.appEach document is owned by a user (userId field).
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function signedIn() {
return request.auth != null;
}
function isOwner() {
return signedIn() && request.auth.uid == resource.data.userId;
}
function isOwnerOnCreate() {
return signedIn() && request.auth.uid == request.resource.data.userId;
}
function keepsSameOwner() {
return request.resource.data.userId == resource.data.userId;
}
match /profiles/{uid} {
allow read, write: if signedIn() && request.auth.uid == uid;
}
match /categories/{id} {
allow create: if isOwnerOnCreate();
allow read, delete: if isOwner();
allow update: if isOwner() && keepsSameOwner();
}
match /expenses/{id} {
allow create: if isOwnerOnCreate();
allow read, delete: if isOwner();
allow update: if isOwner() && keepsSameOwner();
}
match /budgets/{id} {
allow create: if isOwnerOnCreate()
&& id == request.auth.uid + '_' + request.resource.data.month;
allow read: if signedIn()
&& (id.matches('^' + request.auth.uid + '_.*$') || isOwner());
allow delete: if isOwner();
allow update: if isOwner()
&& keepsSameOwner()
&& id == resource.data.userId + '_' + resource.data.month
&& id == request.resource.data.userId + '_' + request.resource.data.month;
}
}
}Push notifications are handled by the Express + Web Push server in server/.
The rest of the app still works even if the backend is not running, but notification subscribe/test/alert features require it.
Note: the push server base URL is currently hardcoded in
src/features/notifications/push.service.ts as:
const PUSH_SERVER_URL = 'http://localhost:8080';If you deploy the push server remotely, update that value accordingly.
If used:
- The push server must be deployed separately
- Uses VAPID keys
- Subscriptions are stored server-side per user
- The backend keeps subscriptions in memory, so restarting the server clears them
- After a push server restart, you may need to enable notifications again from Settings to re-sync the browser subscription
Example environment for push server:
PORT=8080
VAPID_SUBJECT=mailto:you@example.com
VAPID_PUBLIC_KEY=...
VAPID_PRIVATE_KEY=...- Basic responsive layout
- Input controls are usable on small screens
- Core pages are primarily optimized for desktop
- Works as a PWA when installed
The app version is read from package.json and displayed in Settings.
This project is licensed under the MIT License.
See the LICENSE file for details.
Alessandro Han
Computer Science, University of Pisa
LinkedIn: https://www.linkedin.com/in/aleh02