Finora is a personal expense tracker built as a modern full-stack TypeScript application. It lets you log daily expenses, browse them by day / month / year, and visualise your monthly spending — all with a clean, responsive UI.
Live Preview: https://finora-two.vercel.app/
This is the v2 rewrite of the original Finora codebase, modernised to match today's state-of-the-art stack while preserving the original coding and commenting patterns.
- 🔐 Email/password authentication with bcrypt hashing + JWT stored in an httpOnly cookie
- 🌐 Google OAuth 2.0 login via Passport (optional)
- 📅 Add, edit & delete expenses (inline editing on desktop, popover editing on mobile)
- 🗓️ Day / Month / Year summary views with running totals
- 📊 Recharts bar chart of yearly spending
- 🎨 Themed UI (Tailwind v4) with the original Finora palette (forest-mint, soft-teal, matcha…)
- ⚡ Server state handled by TanStack Query (auto-refetch after every mutation)
- 🛡️ Zod validation on both client and server
| Layer | Technology |
|---|---|
| Frontend | React 19, TypeScript, Vite 8, Tailwind CSS 4, React Router 7 |
| Data fetching | TanStack Query, Axios |
| Forms | React Hook Form + Zod |
| State | Zustand (client state) |
| Charts | Recharts |
| Backend | Express 5, TypeScript, tsx |
| Database | MongoDB + Mongoose 9 |
| Auth | JWT (httpOnly cookie), Passport Google OAuth 2.0 |
| Security | Helmet, httpOnly cookies, CORS, environment-validated config |
Finora/
├── finora/ # Frontend (Vite + React)
│ └── src/
│ ├── api/ # Axios instance
│ ├── components/ # Feature + UI components
│ ├── hooks/ # TanStack Query hooks (queries & mutations)
│ ├── lib/ # Utilities
│ ├── pages/ # Route pages
│ ├── schema/ # Zod schemas (client)
│ ├── services/ # API service functions
│ ├── stores/ # Zustand stores
│ └── types/ # Shared TypeScript types
└── finora-api/ # Backend (Express REST API)
└── src/
├── auth/ # Passport Google OAuth strategy
├── config/ # env validation
├── controllers/ # Route handlers
├── db/ # MongoDB connection
├── middlewares/ # auth, validation, error handling
├── models/ # Mongoose schemas
├── routes/ # Express routers
├── schema/ # Zod schemas (server)
├── types/ # Global type augmentations
└── utils/ # asyncHandler, token helpers
- Node.js >= 20.19 (Vite 8 requirement) or 22.12+
- MongoDB running locally (or an Atlas cluster)
cd finora-api
cp .env.example .env.local
# edit .env.local with your values
npm install
npm run dev # starts API on http://localhost:3000Minimum env vars: JWT_SECRET, SESSION_SECRET, LOCAL_MONGODB_URI. Google OAuth vars are optional (required only for "Login with Google").
cd finora
cp .env.example .env
npm install
npm run dev # starts Vite dev server on http://localhost:5173Open http://localhost:5173 and you're good to go.
| Script | Description |
|---|---|
npm run dev |
Start Vite dev server |
npm run build |
Type-check + production build |
npm run lint |
ESLint check |
npm run typecheck |
TypeScript check only |
npm run preview |
Preview the production build |
| Script | Description |
|---|---|
npm run dev |
Start API with tsx watch |
npm run build |
Compile TypeScript to dist/ |
npm start |
Run the compiled server |
npm run typecheck |
TypeScript check only |
Base URL (dev): http://localhost:3000
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /user/register |
– | Register + auto login |
| POST | /user/login |
– | Login |
| DELETE | /user/logout |
– | Logout (clears cookie) |
| GET | /user/profile |
✅ | Current user profile |
| PATCH | /user/update |
✅ | Update profile (password required) |
| GET | /user/authenticate/me |
✅ | Restore session |
| GET | /user/:id |
✅ | Get user by id |
| POST | /expense/create |
✅ | Create expense |
| PATCH | /expense/update/:id |
✅ | Update expense (owner only) |
| DELETE | /expense/delete/:id |
✅ | Delete expense (owner only) |
| GET | /expense/by-date |
✅ | Expenses for a date |
| GET | /expense/monthly |
✅ | Summary: day → total |
| GET | /expense/yearly |
✅ | Summary: month → total |
| GET | /auth/google |
– | Google OAuth start |
| GET | /auth/google/callback |
– | Google OAuth callback |
All authenticated routes read the JWT from the token httpOnly cookie.
- User registers / logs in → server signs a JWT (1-day expiry) and sets it as an httpOnly cookie (
Secure+SameSite=Nonein production,Laxin development). Registration auto-logs-in. - Frontend calls
/user/authenticate/meon boot to restore the session into the Zustand auth store. - Protected routes (
/dashboard,/user/:id,/update-info) redirect to/loginwhen unauthenticated. - Logging out clears the cookie and resets the TanStack Query cache.
The goal of v2 was: update to state-of-the-art tech, fix broken UI/logic/code, preserve the original coding & commenting patterns.
- Vite 6 → 8, React 19.2, TypeScript 5.9, Tailwind 4.3
- React Router DOM 7 (kept
createBrowserRouter) - Added TanStack Query v5 for all server state
- Zustand now only holds pure client state (auth session + selected date)
- Zod 3 → 4, date-fns 2 → 4, react-day-picker 8 → 10, Recharts 2.15
- Backend: Express 5.2, Mongoose 8 → 9, bcrypt → bcryptjs, ts-node/nodemon → tsx
- Added Helmet, compression, centralized error handler, and zod server-side validation
- Frontend: lists did not refresh after create/update/delete → mutations now invalidate queries
- Frontend: avatar fallback never rendered for users without a profile image
- Frontend: untracked mobile edit popover never prefilled / closed → now prefills and closes on submit
- Frontend:
DashboardChartused hardcoded data → now shows real yearly spending - Backend:
secure: truecookie always set (broke local http dev) → only in production - Backend:
name.trim() === nullvalidation bug → proper zod validation - Backend:
getMonthlyExpencestypo →getMonthlyExpenses - Backend: missing error-handler middleware → centralized error handler + 404 handler
- Backend:
@/path aliases broke the compilednode dist/index.jsstart → relative imports - Backend: Google OAuth strategy imported
..//models/userand referenced a missinggoogleIdfield → fixed - Backend: session serialization stored the whole user object → stores only the id
- Backend: Google OAuth credentials were required at boot even when unused → strategy now optional
- Build the frontend with
npm run build→ output indist/(serve statically, e.g. nginx / Netlify / Vercel). - Set
NODE_ENV=production,VITE_MODE=production,VITE_API_URL,MONGODB_URI,JWT_SECRET,SESSION_SECRET,CLIENT_URL. - Production cookies use
Secure+SameSite=None; the API must run behind HTTPS. - API is a standard Node server:
npm run build && npm start.
This project is licensed under the MIT License.