🔗 Live App: spendsmart.gokulmithran.com
Most personal finance apps on the internet fall into one of two camps: either they're pretty UIs with a Firebase backend and zero security thinking, or they're enterprise monsters that take 6 months to set up. SpendSmart is neither.
I built SpendSmart as a production-grade, full-stack personal finance tracker with the same security and performance patterns you'd find in a real fintech product — deployed, live, and free to use.
Here's the deep dive into what I built, how I built it, and why the technical decisions matter.
🧩 What is SpendSmart?
SpendSmart is a web-based personal finance application that lets users:
- Track transactions (income & expenses) with category tagging and filtering
- Set budgets per spending category and get visual progress tracking
- Create savings goals with contributions, withdrawals, and milestone tracking
- View detailed reports — monthly breakdowns, annual overviews, daily spending trends, category donut charts, CSV export, and auto-generated insights
- Manage multiple accounts (bank accounts, wallets, credit cards)
- Receive push notifications for budget alerts and goal milestones
- Install as a PWA — works offline and behaves like a native mobile app
🏗️ Tech Stack at a Glance
| Layer | Technology |
|---|---|
| Frontend | React 18 + TypeScript + Vite 7 |
| Styling | Tailwind CSS 4 + Radix UI primitives |
| State | TanStack React Query v5 |
| Charts | Recharts (Bar, Line, Pie, Donut) |
| Animations | Framer Motion |
| Forms | React Hook Form + Zod validation |
| PWA | Workbox (vite-plugin-pwa) + Service Worker |
| Backend | Spring Boot 3.5 + Java 21 |
| Database | PostgreSQL 15 |
| ORM | Spring Data JPA + MapStruct DTOs |
| Auth | Spring Security + OAuth2 (Google) |
| Rate Limiting | Bucket4j + Caffeine Cache |
| Notifications | Web Push (RFC 8291) + BouncyCastle |
| Observability | Micrometer + Prometheus + Spring Actuator |
| API Docs | Springdoc OpenAPI (Swagger UI) |
| Secrets | AWS Parameter Store |
| Infra | Docker + Docker Compose |
| CI/CD | GitHub Actions (AWS + GCP deploy pipelines) |
| CDN/Hosting | Cloudflare |
🔐 Security — Not an Afterthought
This is where SpendSmart diverges from most side-project expense trackers. I treated security like a first-class citizen from Day 1.
OAuth2 with Session-Based Auth (No JWTs)
Most tutorials push JWT-based auth for SPAs. I went with session-based authentication with Spring Security OAuth2 instead. Why?
- Server-side session invalidation — if a user logs out, the session is dead immediately. No waiting for token expiry.
-
CSRF protection built-in via CookieCsrfTokenRepository with
SameSite=None; Securecookies in production -
Session fixation protection via
migrateSession() - Single-session enforcement — one session per user, period
http.sessionManagement(session -> session
.sessionFixation().migrateSession()
.maximumSessions(1));
Production Security Headers
The prod security config sets a tight CSP, disables iframes, enforces HSTS with preload, and strips referrers:
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp.policyDirectives(
"default-src 'self'; script-src 'self'; ..."))
.frameOptions(FrameOptionsConfig::deny)
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)
.preload(true))
.referrerPolicy(referrer -> referrer
.policy(ReferrerPolicy.NO_REFERRER)));
Per-Endpoint Rate Limiting with Bucket4j
I didn't slap a global rate limiter and call it a day. SpendSmart uses a policy-based rate limiting system where different API endpoint groups have different limits:
| Endpoint Group | Limit | Key Strategy |
|---|---|---|
Auth (/oauth2/, /login/) |
5 req/min | IP-based |
| Dashboard | 120 req/min | User-based |
| Transactions | 60 req/min | User-based |
| Budgets | 30 req/min | User-based |
| Goals | 20 req/min | User-based |
| Default API | 100 req/min | User-based |
Auth endpoints are rate-limited by IP (to prevent brute-force attacks on login flows), while all other endpoints are rate-limited by authenticated user. The policies are backed by Caffeine in-memory cache for near-zero-latency bucket lookups.
📊 Reports & Insights — More Than Just Charts
The Reports module isn't a simple "show me a pie chart" screen. It provides:
- Annual overview — stacked bar chart comparing income vs. expenses across all 12 months
- Monthly summary cards — income, expenses, net savings, savings rate percentage with month-over-month change percentages
- Daily spending trend — line chart showing spending patterns within a month
- Category breakdown — interactive donut chart with percentage splits
- Top spending categories — ranked list with progress bars
- Budget performance — shows how each budget performed for the month
- Auto-generated insights — the backend computes contextual financial insights
- CSV export — one-click download of monthly transaction data
All of this is computed server-side in a single optimized query pass with batch fetching to avoid N+1 problems:
private Map<Long, BigDecimal> batchFetchSpentAmounts(List<Budget> budgets, User user) {
List<Long> categoryIds = budgets.stream()
.map(b -> b.getCategory().getId())
.distinct().collect(Collectors.toList());
List<Object[]> results = transactionRepository
.findTotalSpentPerCategory(user, categoryIds, start, end);
return results.stream().collect(Collectors.toMap(
r -> (Long) r[0], r -> (BigDecimal) r[1]));
}
📱 PWA with Real Push Notifications
SpendSmart isn't just "responsive" — it's a full Progressive Web App:
- Workbox-powered service worker with precaching and offline navigation
- Web Push notifications using RFC 8291 (VAPID keys + BouncyCastle encryption)
- Backend has a dedicated notification subsystem with:
-
NotificationDispatcher— routes notifications -
NotificationEventPublisher— event-driven architecture -
NotificationRateLimiter— prevents notification spam -
PushNotificationService— sends encrypted push payloads
-
The service worker handles push events and notification clicks with deep-link routing:
self.addEventListener('push', (event) => {
const data = event.data.json();
self.registration.showNotification(data.title, {
body: data.body,
icon: '/favicon.png',
vibrate: [100, 50, 100],
data: { url: data.url || '/' }
});
});
🎨 Frontend Architecture
React Query for Server State
No Redux. No Zustand. TanStack React Query v5 handles all server state — with automatic caching, background refetching, optimistic updates, and stale-while-revalidate patterns. The result is a UI that feels instant.
Component Architecture
- Radix UI primitives (Dialog, Select, Popover, AlertDialog) for accessibility-first components
- Framer Motion for page transitions and micro-animations
- Recharts for responsive data visualizations
- React Hook Form + Zod for type-safe form validation with zero re-renders
- Custom undo-action hook — delete operations show a toast with an "Undo" option before actually firing the API call
Context-driven Theming
Four React Contexts power the UX:
-
AuthContext— authentication state -
CurrencyContext— locale-aware currency formatting -
ThemeContext— dark/light mode with system preference detection -
SidebarContext— responsive sidebar state
☁️ Deployment & Infrastructure
- Dockerized backend + frontend + PostgreSQL via Docker Compose
- GitHub Actions CI/CD with separate workflows for AWS and GCP deployments
- AWS Parameter Store for secret management (no .env files in production)
- Cloudflare for CDN, DDoS protection, and analytics
- Spring Boot Actuator + Prometheus metrics endpoint for production observability
💡 What I'd Do Differently
- Add Redis — Caffeine works great for single-instance rate limiting, but for horizontal scaling I'd swap to Redis-backed Bucket4j
- Implement recurring transactions — auto-detect salary, rent, subscriptions
- Add bank connectivity — integrate with Plaid or a similar aggregator
- Mobile app — the PWA is solid, but a React Native companion would unlock background sync and native widgets
🔗 Try It Out
Live: spendsmart.gokulmithran.com
Login with Google, add a few transactions, and see how it feels. It's free, your data is yours, and no credit card is needed.
If this was helpful, drop a ❤️ and follow for more full-stack deep dives. Questions? Hit me in the comments!
Tags: #webdev #react #java #springboot
Top comments (1)
Solid architecture decisions throughout - the session-based auth over JWTs is a deliberate choice most side projects skip, and laying out the rate limiting policy per endpoint is genuinely useful to see.
One thing caught my eye in the "What I'd Do Differently" section - bank connectivity via Plaid. I went the opposite direction on my finance app (money-me.com) - manual entry only, no bank linking. Curious whether you've had any users say they'd prefer not connecting bank credentials? My experience is there's a real segment who want full control over what data exists, and Plaid's data sharing model puts some people off regardless of how well it's secured on your end.
Nice work shipping this - the deployment setup with separate AWS/GCP CI pipelines is more robust than most side projects get.