A modern, full-stack personal finance management application with comprehensive financial analytics, budgeting tools, and real-time data synchronization.
Features β’ Tech Stack β’ Getting Started β’ Architecture β’ API Documentation
- Overview
- Features
- Tech Stack
- Getting Started
- Project Structure
- Architecture
- API Integration
- Authentication
- State Management
- Performance
- Deployment
- Contributing
BankDash is a comprehensive personal finance management platform that helps users track expenses, manage budgets, set financial goals, and gain insights into their spending habits. Built with Next.js 14 (App Router) and TypeScript, it features a beautiful, responsive UI and integrates with a robust NestJS backend.
- Full-Stack Integration: Seamlessly connected with NestJS backend API
- Real-Time Sync: Automatic data synchronization with PostgreSQL (Supabase)
- Modern Architecture: Server Components, Server Actions, and API Routes
- Type-Safe: End-to-end TypeScript with shared DTOs
- Production-Ready: Optimized for performance and scalability
- Multi-Account Support: Manage checking, savings, credit cards, and investment accounts
- Transaction Tracking: Create, edit, and categorize transactions with advanced filtering
- Budget Management: Set monthly budgets per category with real-time progress tracking
- Bill Tracking: Never miss a payment with bill reminders and payment history
- Financial Goals: Set savings goals and track progress with automatic calculations
- Analytics Dashboard: Visualize spending patterns with interactive charts
- Spending Analysis: Breakdown by category, merchant, and time period
- Trends: Identify spending patterns and seasonal variations
- Cash Flow: Monitor income vs expenses with predictive analytics
- Category Insights: Deep dive into spending habits per category
- Comparative Reports: Month-over-month and year-over-year comparisons
- Financial Health Score: AI-powered insights (coming soon)
- JWT Authentication: Secure token-based authentication
- Automatic Token Refresh: Seamless session management
- HTTP-Only Cookies: Enhanced security for sensitive data
- Protected Routes: Server-side and client-side route protection
- Password Reset: Secure password recovery flow
- Dark Mode: Full dark mode support with system preference detection
- Responsive Design: Mobile-first approach, works on all devices
- Animations: Smooth transitions with Framer Motion
- Virtual Scrolling: Optimized performance for large transaction lists
- Loading States: Skeleton loaders and suspense boundaries
- Error Handling: Comprehensive error boundaries and toast notifications
- Next.js 14.2 - React framework with App Router
- React 18.3 - UI library with Server Components
- TypeScript 5.0 - Type safety
- Tailwind CSS 3.4 - Utility-first CSS framework
- @tanstack/react-query 5.28 - Server state management
- Automatic caching and background refetching
- Optimistic updates
- Pagination and infinite scroll support
- Zustand 5.0 - Lightweight client state
- Axios 1.6 - HTTP client with interceptors
- Recharts 3.3 - Chart library for analytics
- Framer Motion 11.0 - Animation library
- Lucide React 0.552 - Icon library
- Sonner 1.4 - Toast notifications
- @tanstack/react-virtual 3.2 - Virtual scrolling
- React Hook Form 7.51 - Form management
- Zod 3.22 - Schema validation
- @hookform/resolvers 3.3 - Form validation integration
- date-fns 4.1 - Date manipulation and formatting
- Vitest 1.3 - Unit testing framework
- @testing-library/react 14.2 - Component testing
- TypeScript ESLint - Linting
- Prettier - Code formatting
- Node.js: Version 18.0 or higher
- npm: Version 9.0 or higher
- Backend: NestJS API running on port 3000 (Backend Repo)
- Clone the repository
git clone <repository-url>
cd Financial-banking-analytics- Install dependencies
npm install- Environment Setup
Create a .env.local file in the root directory:
# API Configuration - NestJS Backend
NEXT_PUBLIC_API_URL=http://localhost:3000/api/v1
API_URL=http://localhost:3000/api/v1
NEXT_PUBLIC_API_TIMEOUT=30000
# App Configuration
NEXT_PUBLIC_APP_URL=http://localhost:3001
NEXT_PUBLIC_APP_NAME=BankDash
# Feature Flags
NEXT_PUBLIC_ENABLE_AI_INSIGHTS=false
NEXT_PUBLIC_ENABLE_BANK_SYNC=false
NEXT_PUBLIC_ENABLE_MULTI_CURRENCY=false
NEXT_PUBLIC_ENABLE_REAL_TIME=false
# Development
NODE_ENV=development- Start the development server
npm run devThe application will be available at http://localhost:3001
Email: demo@example.com
Password: Demo@12345
# Development
npm run dev # Start development server on port 3001
npm run build # Build for production
npm start # Start production server
npm run lint # Run ESLint
npm run type-check # Check TypeScript types
# Testing
npm test # Run tests
npm run test:ui # Run tests with UI
npm run test:coverage # Generate coverage reportFinancial-banking-analytics/
βββ app/ # Next.js App Router
β βββ (auth)/ # Authentication routes (route group)
β β βββ login/
β β β βββ page.tsx # Login page
β β βββ register/
β β β βββ page.tsx # Registration page
β β βββ forgot-password/
β β β βββ page.tsx # Password recovery
β β βββ layout.tsx # Auth layout
β β
β βββ (dashboard)/ # Protected dashboard routes
β β βββ dashboard/ # Main dashboard
β β βββ transactions/ # Transaction management
β β βββ accounts/ # Account management
β β βββ budgets/ # Budget planning
β β βββ bills/ # Bill tracking
β β βββ goals/ # Financial goals
β β βββ analytics/ # Analytics & insights
β β βββ reports/ # Report generation
β β βββ planning/ # Financial planning tools
β β βββ layout.tsx # Dashboard layout (Sidebar, Header)
β β
β βββ actions/ # Server Actions
β β βββ auth.ts # Authentication actions
β β
β βββ api/ # API Routes
β β βββ auth/
β β βββ login/route.ts
β β βββ register/route.ts
β β βββ logout/route.ts
β β
β βββ globals.css # Global styles
β βββ layout.tsx # Root layout
β βββ page.tsx # Home page (redirects to dashboard)
β
βββ components/ # React components
β βββ ui/ # Reusable UI components
β β βββ Button.tsx
β β βββ Card.tsx
β β βββ Input.tsx
β β βββ Skeleton.tsx
β β
β βββ forms/ # Form components
β β βββ AccountForm.tsx
β β βββ TransactionForm.tsx
β β βββ BudgetForm.tsx
β β βββ GoalForm.tsx
β β
β βββ animations/ # Animation wrappers
β β βββ FadeIn.tsx
β β βββ SlideIn.tsx
β β βββ ScaleIn.tsx
β β βββ StaggerChildren.tsx
β β
β βββ Header.tsx # Top navigation
β βββ Sidebar.tsx # Side navigation
β βββ StatCard.tsx # Statistics card
β βββ TransactionCard.tsx # Transaction list item
β βββ ChartContainer.tsx # Chart wrapper
β βββ VirtualTransactionList.tsx # Optimized transaction list
β βββ EmptyState.tsx # Empty state placeholder
β βββ ErrorBoundary.tsx # Error boundary
β βββ ThemeProvider.tsx # Dark mode provider
β βββ QueryProvider.tsx # React Query provider
β βββ Toaster.tsx # Toast notifications
β
βββ services/ # API service layer
β βββ api/
β βββ client.ts # Axios instance with interceptors
β βββ auth.service.ts # Authentication API
β βββ accounts.service.ts # Accounts CRUD
β βββ transactions.service.ts # Transactions CRUD
β βββ budgets.service.ts # Budgets CRUD
β βββ bills.service.ts # Bills CRUD
β βββ goals.service.ts # Goals CRUD
β βββ categories.service.ts # Categories CRUD
β βββ analytics.service.ts # Analytics & insights
β
βββ hooks/ # Custom React hooks
β βββ useAuth.ts # Authentication hooks
β βββ useAccounts.ts # Account queries/mutations
β βββ useTransactions.ts # Transaction queries/mutations
β βββ useBudgets.ts # Budget queries/mutations
β βββ useBills.ts # Bill queries/mutations
β βββ useGoals.ts # Goal queries/mutations
β βββ useCategories.ts # Category queries/mutations
β βββ useAnalytics.ts # Analytics queries
β βββ useDebounce.ts # Debounce utility hook
β
βββ types/ # TypeScript type definitions
β βββ index.ts # Domain models
β βββ dto.ts # Data Transfer Objects (shared with backend)
β
βββ lib/ # Utilities and configurations
β βββ queryClient.ts # React Query configuration
β βββ store.ts # Zustand store
β βββ featureFlags.ts # Feature flags
β βββ validations/ # Zod schemas
β β βββ auth.ts
β β βββ account.ts
β β βββ transaction.ts
β βββ utils.ts # Utility functions
β
βββ utils/ # Helper utilities
β βββ currency.ts # Currency formatting
β βββ date.ts # Date formatting
β
βββ middleware.ts # Next.js middleware (auth check)
βββ tailwind.config.js # Tailwind configuration
βββ next.config.js # Next.js configuration
βββ tsconfig.json # TypeScript configuration
βββ vitest.config.ts # Vitest configuration
βββ .env.local # Environment variables
βββ package.json # Dependencies and scripts
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Browser β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Next.js Frontend (Port 3001) β β
β β β β
β β ββββββββββββββββ ββββββββββββββββ βββββββββββββββ β β
β β β UI Layer β β Server β β Server β β β
β β β (Components) β β Components β β Actions β β β
β β ββββββββ¬ββββββββ ββββββββ¬ββββββββ ββββββββ¬βββββββ β β
β β β β β β β
β β βββββββββββββββββββ΄βββββββββββββββββββ β β
β β β β β
β β βββββββββββββββββββ΄βββββββββββββββββββ β β
β β β β β β
β β ββββββββΌβββββββββ βββββββββββββΌβββββ β β
β β β React Query β β Service Layer β β β
β β β (TanStack) ββββββββββββββββ€ (Axios) β β β
β β βββββββββββββββββ ββββββββββ¬ββββββββ β β
β β β β β
β βββββββββββββββββββββββββββββββββββββββββββββΌβββββββββββ β
β β β
ββββββββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββ
β
HTTP/REST β
β
ββββββββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββ
β NestJS Backend (Port 3000) β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β API Layer β β
β β ββββββββββββββ ββββββββββββββ βββββββββββββββ β β
β β βControllers β β Services β β Repositoriesβ β β
β β βββββββ¬βββββββ βββββββ¬βββββββ ββββββββ¬βββββββ β β
β β β β β β β
β β ββββββββββββββββββ΄βββββββββββββββββ β β
β β β β β
β βββββββββββββββββββββββββββΌββββββββββββββββββββββββββ β
β β β
β βββββββββββββββββββββββββββΌββββββββββββββββββββββββββ β
β β TypeORM / PostgreSQL β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββ
β
β SQL/TCP
β
βββββββββββββββββββββββββββββββββββββββββΌββββββββββββββββββββ
β Supabase PostgreSQL β
β (Cloud Database) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- User Interaction β UI Component
- Component β React Query Hook
- Hook β Service Layer (Axios)
- Service β Backend API (NestJS)
- Backend β TypeORM β PostgreSQL
- Response β Service β Hook β Component β UI
1. User submits login form
2. Frontend calls auth.service.login()
3. Service sends POST /api/v1/auth/login
4. Backend validates credentials
5. Backend returns { user, accessToken, refreshToken }
6. Frontend stores tokens in:
- localStorage (client-side access)
- HTTP-only cookies (server-side access)
7. Subsequent requests include Authorization: Bearer <token>
8. On 401 error, automatic token refresh is triggered
9. If refresh fails, redirect to /login
The application uses a singleton Axios instance with interceptors for authentication and error handling:
// services/api/client.ts
const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
})
// Request interceptor - Add auth token
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('accessToken')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// Response interceptor - Handle token refresh
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// Attempt token refresh
const refreshed = await refreshToken()
if (refreshed) {
// Retry original request
return apiClient(error.config)
}
// Redirect to login
window.location.href = '/login'
}
return Promise.reject(error)
}
)All API calls go through dedicated service modules:
// services/api/transactions.service.ts
class TransactionsService {
async getAll(filters: TransactionFiltersDTO): Promise<TransactionDTO[]> {
const response = await apiClient.get('/transactions', { params: filters })
return response.data
}
async create(data: CreateTransactionDTO): Promise<TransactionDTO> {
const response = await apiClient.post('/transactions', data)
return response.data
}
// ... other methods
}
export const transactionsService = TransactionsService.getInstance()| Method | Endpoint | Description |
|---|---|---|
| Authentication | ||
| POST | /auth/login |
User login |
| POST | /auth/register |
User registration |
| POST | /auth/logout |
User logout |
| POST | /auth/refresh |
Refresh access token |
| POST | /auth/forgot-password |
Request password reset |
| POST | /auth/reset-password |
Reset password |
| GET | /auth/me |
Get current user |
| Accounts | ||
| GET | /accounts |
List all accounts |
| GET | /accounts/:id |
Get account by ID |
| POST | /accounts |
Create new account |
| PATCH | /accounts/:id |
Update account |
| DELETE | /accounts/:id |
Delete account |
| POST | /accounts/transfer |
Transfer between accounts |
| GET | /accounts/:id/balance-history |
Get balance history |
| Transactions | ||
| GET | /transactions |
List transactions (with filters) |
| GET | /transactions/:id |
Get transaction by ID |
| POST | /transactions |
Create transaction |
| PATCH | /transactions/:id |
Update transaction |
| DELETE | /transactions/:id |
Delete transaction |
| POST | /transactions/import |
Import transactions (CSV/OFX) |
| POST | /transactions/export |
Export transactions |
| GET | /transactions/stats |
Get transaction statistics |
| Budgets | ||
| GET | /budgets |
List all budgets |
| GET | /budgets/:id |
Get budget by ID |
| POST | /budgets |
Create budget |
| PATCH | /budgets/:id |
Update budget |
| DELETE | /budgets/:id |
Delete budget |
| GET | /budgets/:id/progress |
Get budget progress |
| Bills | ||
| GET | /bills |
List all bills |
| GET | /bills/:id |
Get bill by ID |
| POST | /bills |
Create bill |
| PATCH | /bills/:id |
Update bill |
| DELETE | /bills/:id |
Delete bill |
| POST | /bills/:id/pay |
Mark bill as paid |
| GET | /bills/upcoming |
Get upcoming bills |
| Goals | ||
| GET | /goals |
List all goals |
| GET | /goals/:id |
Get goal by ID |
| POST | /goals |
Create goal |
| PATCH | /goals/:id |
Update goal |
| DELETE | /goals/:id |
Delete goal |
| POST | /goals/:id/contribute |
Add contribution to goal |
| Categories | ||
| GET | /categories |
List all categories |
| GET | /categories/:id |
Get category by ID |
| POST | /categories |
Create category |
| PATCH | /categories/:id |
Update category |
| DELETE | /categories/:id |
Delete category |
| GET | /categories/spending |
Get spending by category |
| Analytics | ||
| GET | /analytics/overview |
Get financial overview |
| GET | /analytics/cash-flow |
Get cash flow analysis |
| GET | /analytics/trends |
Get spending trends |
| GET | /analytics/spending-by-category |
Category breakdown |
The application uses JWT tokens for authentication:
- Access Token: Short-lived (15 minutes), used for API requests
- Refresh Token: Long-lived (7 days), used to obtain new access tokens
- localStorage: For client-side JavaScript access
- HTTP-only Cookies: For server-side rendering and enhanced security
Routes are protected using Next.js middleware:
// middleware.ts
export function middleware(request: NextRequest) {
const token = request.cookies.get('accessToken')
if (!token && !isPublicRoute(request.nextUrl.pathname)) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}- Minimum 8 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one number
- At least one special character
React Query handles all server state with automatic caching, background refetching, and optimistic updates.
// lib/queryClient.ts
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false,
retry: 3,
},
},
})// Hierarchical query keys for efficient cache management
const queryKeys = {
auth: ['auth'] as const,
user: () => [...queryKeys.auth, 'user'] as const,
accounts: ['accounts'] as const,
accountList: () => [...queryKeys.accounts, 'list'] as const,
accountDetail: (id: string) => [...queryKeys.accounts, 'detail', id] as const,
transactions: ['transactions'] as const,
transactionList: (filters: any) =>
[...queryKeys.transactions, 'list', filters] as const,
transactionDetail: (id: string) =>
[...queryKeys.transactions, 'detail', id] as const,
}// hooks/useTransactions.ts
export function useTransactions(filters: TransactionFiltersDTO) {
return useQuery({
queryKey: queryKeys.transactionList(filters),
queryFn: () => transactionsService.getAll(filters),
staleTime: 5 * 60 * 1000,
})
}
export function useCreateTransaction() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: CreateTransactionDTO) =>
transactionsService.create(data),
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({
queryKey: queryKeys.transactions
})
toast.success('Transaction created successfully')
},
onError: (error) => {
toast.error(error.message)
},
})
}Lightweight client-side state for UI preferences and filters:
// lib/store.ts
interface FilterState {
selectedAccount: string | null
dateRange: { start: Date; end: Date }
selectedCategories: string[]
searchQuery: string
setSelectedAccount: (id: string | null) => void
setDateRange: (range: { start: Date; end: Date }) => void
// ... other actions
}
export const useFilterStore = create<FilterState>()(
persist(
(set) => ({
selectedAccount: null,
dateRange: { start: startOfMonth(new Date()), end: new Date() },
selectedCategories: [],
searchQuery: '',
setSelectedAccount: (id) => set({ selectedAccount: id }),
setDateRange: (range) => set({ dateRange: range }),
// ... other implementations
}),
{ name: 'filter-storage' }
)
)Most pages use React Server Components for optimal performance:
// app/(dashboard)/dashboard/page.tsx - Server Component
export default async function DashboardPage() {
// Data fetching happens on the server
// No JavaScript sent to the client for this component
return (
<div>
<DashboardStats /> {/* Server Component */}
<RecentTransactions /> {/* Server Component */}
<InteractiveChart /> {/* 'use client' - Client Component */}
</div>
)
}Webpack is configured for optimal bundle splitting:
// next.config.js
webpack: (config, { isServer }) => {
if (!isServer) {
config.optimization.splitChunks = {
chunks: 'all',
cacheGroups: {
recharts: {
test: /[\\/]node_modules[\\/](recharts|d3-.*)[\\/]/,
name: 'recharts',
priority: 20,
},
reactQuery: {
test: /[\\/]node_modules[\\/](@tanstack[\\/]react-query)[\\/]/,
name: 'react-query',
priority: 15,
},
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 10,
},
},
}
}
return config
}Large transaction lists use virtual scrolling for optimal performance:
// components/VirtualTransactionList.tsx
import { useVirtualizer } from '@tanstack/react-virtual'
export function VirtualTransactionList({ transactions }: Props) {
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: transactions.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 80,
overscan: 5,
})
return (
<div ref={parentRef} className="h-[600px] overflow-auto">
<div style={{ height: `${virtualizer.getTotalSize()}px` }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<TransactionCard
key={virtualItem.key}
transaction={transactions[virtualItem.index]}
style={{
transform: `translateY(${virtualItem.start}px)`,
}}
/>
))}
</div>
</div>
)
}Next.js Image component with optimal configuration:
// next.config.js
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
}- SWC Minification: Faster than Terser
- Tree Shaking: Remove unused code
- Console Log Removal: Automatically removed in production
- Source Maps: Disabled in production for smaller bundle size
- Backend API deployed and accessible
- PostgreSQL database (Supabase recommended)
- Environment variables configured
- Connect Repository
vercel- Configure Environment Variables
Add all .env.local variables to Vercel project settings:
NEXT_PUBLIC_API_URLAPI_URLNEXT_PUBLIC_APP_URL- Other feature flags
- Deploy
vercel --prod# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
EXPOSE 3001
CMD ["npm", "start"]docker build -t bankdash-frontend .
docker run -p 3001:3001 bankdash-frontend| Variable | Description | Required |
|---|---|---|
NEXT_PUBLIC_API_URL |
Backend API URL | Yes |
API_URL |
Backend API URL (server-side) | Yes |
NEXT_PUBLIC_APP_URL |
Frontend URL | Yes |
NEXT_PUBLIC_API_TIMEOUT |
API timeout in milliseconds | No |
NEXT_PUBLIC_ENABLE_AI_INSIGHTS |
Enable AI features | No |
NEXT_PUBLIC_ENABLE_BANK_SYNC |
Enable bank synchronization | No |
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with UI
npm run test:ui
# Generate coverage report
npm run test:coveragetests/
βββ unit/
β βββ components/
β βββ hooks/
β βββ utils/
βββ integration/
β βββ api/
βββ e2e/
βββ flows/
// tests/unit/components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from '@/components/ui/Button'
describe('Button', () => {
it('renders correctly', () => {
render(<Button>Click me</Button>)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
it('handles click events', () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click me</Button>)
fireEvent.click(screen.getByText('Click me'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
})Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
- Follow TypeScript best practices
- Use ESLint and Prettier for code formatting
- Write tests for new features
- Update documentation as needed
- Follow conventional commit messages
This project is licensed under the MIT License - see the LICENSE file for details.
- Next.js - The React Framework
- Tailwind CSS - Utility-first CSS
- TanStack Query - Powerful data synchronization
- Recharts - Chart library
- Lucide - Beautiful icons
For issues, questions, or contributions:
- Open an issue on GitHub
- Contact the maintainers
- Check the documentation
- AI-powered insights and recommendations
- Real-time notifications with WebSockets
- Multi-currency support
- Bank account integration (Plaid)
- Mobile app (React Native)
- Advanced reporting (PDF generation)
- Receipt OCR scanning
- Collaborative budgets
- Tax optimization tools
- Investment tracking
Built with β€οΈ using Next.js, TypeScript, and Tailwind CSS