A modern, enterprise-grade financial ledger and wallet management system built with Next.js. This application provides a complete solution for managing financial transactions with a focus on correctness, auditability, and resilience.
Note: This project evolved from a frontend assessment test at Mainstack into a comprehensive financial ledger system. See the About This Project section for details on the evolution.
This project started as a frontend assessment test at Mainstack and has been evolved into a comprehensive, production-ready financial ledger system. What began as a simple transaction management dashboard has been transformed into a full-featured application that demonstrates building a financial system without relying on external payment processors.
Original Assessment (Initial State)
- Basic transaction dashboard with filtering capabilities
- Simple transaction list and balance display
- Frontend-focused implementation
Current State (Enhanced)
- Complete financial ledger system with PostgreSQL backend
- Multi-currency wallet management
- Transaction lifecycle management with status transitions
- Comprehensive audit logging
- Transaction flow visualization
- Idempotency and error handling
- Real-world payment processing simulation
- Production-ready deployment configuration
- OLTP-style database transactions with ACID guarantees
- Role-Based Access Control (RBAC) for secure permission management
- Event-driven architecture for decoupled, reactive updates
This evolution showcases how a simple assessment project can be expanded into a robust, enterprise-grade application with proper architecture, database design, and real-world financial system patterns.
The goal was to create a financial ledger system that:
- Ensures Correctness: All balances are derived from transaction history, preventing discrepancies and ensuring data integrity
- Provides Auditability: Complete audit trails for all operations, making it easy to trace any transaction or system action
- Maintains Resilience: Built-in idempotency, transaction reversals, and proper error handling
- Simulates Real-World Scenarios: Mimics payment processing workflows with status transitions (pending → processing → successful/failed) without requiring external payment processors
This system demonstrates how to build a financial application that:
- Manages Multi-Currency Wallets: Support for multiple currencies with proper decimal handling and currency-specific formatting
- Tracks Transaction Lifecycles: Full support for transaction states (pending, processing, successful, failed, reversed)
- Calculates Balances Accurately: Ledger balance and available balance are calculated from transaction history, not stored values
- Prevents Duplicate Transactions: Idempotency keys ensure operations can be safely retried
- Provides Complete Audit Trails: Every action is logged for compliance and debugging
- Visualizes Transaction Flows: Interactive flow diagrams showing transaction processing steps
- Handles Manual Operations: Support for manual credits and debits for testing and administrative purposes
-
Multi-Currency Wallet Management
- Support for multiple currencies (USD, NGN, EUR, GBP, etc.)
- Currency-specific decimal precision handling
- Real-time balance calculations (ledger balance and available balance)
-
Transaction Management
- Credit and debit operations
- Withdrawal requests with VAT calculation
- Manual credit/debit for administrative purposes
- Transaction reversals
- Idempotent transaction creation
-
Transaction Lifecycle
- Status tracking: pending → processing → successful/failed
- Asynchronous status updates (simulating payment processor delays)
- Transaction flow visualization with React Flow
-
Balance Tracking
- Ledger balance: All successful credits minus all successful debits
- Available balance: Ledger balance minus pending debits
- Real-time balance charts showing transaction history
- Multi-currency balance support
-
Audit & Compliance
- Comprehensive audit logging for all operations
- Immutable transaction records
- Complete transaction history with metadata
-
User Interface
- Modern, responsive dashboard
- Advanced filtering (date range, type, status, currency)
- Transaction detail modals with flow visualization
- Real-time balance charts
- Landing page with feature showcase
- Authentication: User registration and login with session management
- Database: PostgreSQL with proper schema design and migrations
- OLTP Transactions: ACID-compliant database transactions with automatic rollback
- RBAC: Role-based access control with granular permissions
- Event-Driven Architecture: Decoupled event system for transaction lifecycle events
- API Routes: RESTful API for all operations with permission checks
- Data Validation: Zod schemas for runtime type checking
- Error Handling: Comprehensive error handling with user-friendly messages
- State Management: Zustand for global state, TanStack Query for server state
The system implements OLTP-style database transactions to ensure data consistency and atomicity:
- ACID Guarantees: All multi-step operations are wrapped in transactions
- Automatic Rollback: Failed operations automatically roll back all changes
- Deadlock Handling: Automatic retry with exponential backoff on deadlocks
- Isolation Levels: Configurable transaction isolation (default: READ COMMITTED)
All database operations that modify multiple tables (e.g., creating a transaction + audit log) are wrapped in withTransaction() to ensure atomicity.
The system implements a comprehensive RBAC system:
- Roles: Pre-defined roles (admin, user, auditor, support) with different permission sets
- Permissions: Granular permissions using
resource:actionformat (e.g.,transactions:create) - Authorization: All API routes check permissions before executing operations
- Resource Ownership: Users can only access their own resources unless they have admin permissions
The system uses an event-driven architecture for decoupled updates:
- Event Emitter: Generic event emitter for application-wide events
- Transaction Events: Events for transaction lifecycle (created, updated, reversed, failed)
- Event Listeners: Pluggable event handlers for notifications, analytics, etc.
- Async Processing: Events are emitted after database transactions commit
This architecture allows easy extension without modifying core business logic.
The technology stack was chosen for convenience, out-of-the-box tooling, optimization techniques, and ease of development to meet project deadlines.
-
Next.js 15.2.6 - React framework for production
- App Router for better performance and SEO
- Server Components and API Routes
- Built-in optimizations
-
TypeScript - For type safety and better developer experience
- Ensures code reliability and maintainability
- Provides better IDE support and autocompletion
- Runtime validation with Zod
-
PostgreSQL - Relational database
- Proper schema design with foreign keys and constraints
- Migration system for version control
- Serverless-optimized connection pooling
- Zustand - Lightweight state management solution
- Simple and intuitive API
- Minimal boilerplate compared to Redux
- Perfect for managing global application state (currency selection, UI state)
- Built-in TypeScript support
- TanStack Query (React Query) - Data fetching and caching
- Automatic background data updates
- Built-in caching and invalidation
- Optimistic updates for better UX
- Automatic retry logic
-
Tailwind CSS - Utility-first CSS framework
- Rapid UI development
- Consistent design system
- Zero-runtime CSS
- Highly customizable
-
shadcn/ui - Reusable components built with Radix UI
- Accessible by default
- Customizable and themeable
- Built on top of Tailwind CSS
- Copy-paste components for faster development
- React Flow - For transaction flow visualization
- date-fns - Date formatting and manipulation
- Zod - Schema validation
- pg - PostgreSQL client for Node.js
├── app/ # Next.js app directory
│ ├── api/ # API routes
│ │ ├── auth/ # Authentication endpoints
│ │ ├── transactions/ # Transaction endpoints
│ │ ├── wallets/ # Wallet endpoints
│ │ └── audit/ # Audit log endpoints
│ ├── dashboard/ # Dashboard pages
│ ├── login/ # Login page
│ ├── register/ # Registration page
│ └── page.tsx # Landing page
├── components/ # React components
│ ├── dashboard/ # Dashboard-specific components
│ │ ├── balance-chart/ # Balance chart components
│ │ ├── transaction-flow.tsx # Transaction flow visualization
│ │ └── ... # Other dashboard components
│ ├── auth/ # Authentication components
│ ├── landing/ # Landing page components
│ └── ui/ # Reusable UI components (shadcn/ui)
├── lib/ # Utility functions and API calls
│ ├── db/ # Database utilities
│ │ ├── migrations/ # Database migrations
│ │ ├── queries/ # Database query functions
│ │ └── index.ts # Database connection and transaction wrapper
│ ├── auth/ # Authentication and authorization
│ │ ├── permissions.ts # Permission definitions
│ │ ├── rbac.ts # RBAC authorization helpers
│ │ └── session.ts # Session management
│ ├── events/ # Event-driven architecture
│ │ ├── event-emitter.ts # Generic event emitter
│ │ ├── transaction-events.ts # Transaction-specific events
│ │ └── init.ts # Event listener initialization
│ ├── services/ # Business logic services
│ │ └── transaction-processor.ts # Async transaction processing
│ ├── api.ts # API client functions
│ └── utils/ # General utilities
├── store/ # Zustand stores
│ └── currency-store.ts # Currency selection store
└── public/ # Static assets
- Node.js 18+ and npm
- PostgreSQL database (Neon or Supabase recommended for deployment)
- Clone the repository:
git clone <repository-url>
cd mainstack- Install dependencies:
npm install- Set up environment variables:
cp .env.example .envEdit .env and add your database connection string:
DATABASE_URL=postgresql://user:password@host:port/database?sslmode=require
NODE_ENV=development- Run database migrations:
npm run migrate- Start the development server:
npm run dev- Open http://localhost:3000 in your browser.
This project was built with a focus on:
- Clean and maintainable code: Well-organized structure with clear separation of concerns
- Type safety: TypeScript throughout with runtime validation using Zod
- Performance optimization: Efficient data fetching, caching, and rendering
- User experience: Intuitive interface with real-time updates and clear feedback
- Developer experience: Easy to understand, modify, and extend
The combination of these technologies allows for rapid development while maintaining high standards of code quality and user experience.
This project uses PostgreSQL and can be deployed to Vercel. Follow these steps:
- A PostgreSQL database (choose one):
- Neon (serverless PostgreSQL, recommended)
- Supabase (PostgreSQL with additional features)
- Go to Neon Console
- Sign up or log in
- Click Create Project
- Choose a project name, region, and PostgreSQL version
- Once created, go to your project dashboard
- Navigate to Connection Details
- Copy the connection string (it will look like:
postgresql://user:password@ep-xxx.region.aws.neon.tech/dbname?sslmode=require) - Important: Make sure to copy the connection string that includes
?sslmode=requirefor secure connections
- Go to Supabase
- Sign up or log in
- Click New Project
- Fill in your project details (name, database password, region)
- Wait for the project to be created (takes a few minutes)
- Once ready, go to Project Settings → Database
- Scroll down to Connection string section
- Select URI format
- Copy the connection string (it will look like:
postgresql://postgres:[YOUR-PASSWORD]@db.xxx.supabase.co:5432/postgres) - Replace
[YOUR-PASSWORD]with your actual database password - Add
?sslmode=requireat the end if not already present
-
Go to your Vercel project dashboard
-
Navigate to Settings → Environment Variables
-
Add the following variable:
- Name:
DATABASE_URL - Value: Your PostgreSQL connection string from Neon or Supabase
- Environment: Select all (Production, Preview, Development)
- Name:
-
Click Save
You need to run migrations to set up your database schema. You have two options:
- Go to Settings → General → Build & Development Settings
- Update the Build Command to:
Note: The
npm run migrate:prod && npm run buildmigrate:prodscript uses environment variables directly (no--env-fileflag), which works perfectly with Vercel's automatic environment variable injection. - This will run migrations automatically on each deployment
-
Install Vercel CLI if you haven't:
npm i -g vercel
-
Pull environment variables:
vercel env pull .env.local
-
Run migrations (this will connect to your production database):
npm run migrate:prod
Note: Use
migrate:prodwhich reads from environment variables directly. The regularmigratecommand uses--env-file=.envfor local development.⚠️ Warning: Make sure you're connecting to the correct database (production vs development) before running migrations.
Create a one-time migration endpoint (remove after use for security):
// app/api/migrate/route.ts
import { runMigrations } from '@/lib/db/migrations/migrate';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
// Add authentication/authorization check here
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.MIGRATION_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
await runMigrations();
return NextResponse.json({ message: 'Migrations completed' });
} catch (error) {
return NextResponse.json(
{ error: 'Migration failed', details: error },
{ status: 500 }
);
}
}Then call it once:
curl -X POST https://your-app.vercel.app/api/migrate \
-H "Authorization: Bearer YOUR_MIGRATION_SECRET"-
If deploying via Git:
- Push your code to GitHub/GitLab/Bitbucket
- Connect your repository to Vercel
- Vercel will automatically deploy on push
-
If deploying via CLI:
vercel --prod
- Visit your deployed URL
- Test the database connection by visiting
/api(if you have a health check endpoint) - Try registering a new user to verify database writes work
- Error: "Database connection failed"
- Verify
DATABASE_URLis set correctly in Vercel - Check if your database allows connections from Vercel's IP ranges
- For external databases, ensure SSL is enabled (
?sslmode=require)
- Verify
-
Error: "Migration already applied"
- This is normal if migrations were run before
- The migration system is idempotent and will skip already-applied migrations
-
Error: "Cannot find migration files"
- Ensure migration files are committed to your repository
- Check that the build command has access to the
lib/db/migrationsdirectory
- If build fails during migration:
- Run migrations manually first (Option B or C above)
- Then remove the migration step from build command
- Or fix the migration script to handle errors gracefully
| Variable | Description | Required |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | Yes |
NODE_ENV |
Environment (production/development) | Optional |
The project is already configured for serverless environments:
max: 1connection per pool (important for Vercel serverless functions)- Connection timeout: 2 seconds
- Idle timeout: 30 seconds
This configuration prevents connection pool exhaustion in serverless environments.
These are some of the improvements that could be made to the project if not constrained by time:
- Custom Icon Components
- Create a dedicated
components/iconsdirectory - Implement reusable icon components using figma assets
- Add proper TypeScript types and props
- Create a dedicated
- Component Breakdown
- Split large components into smaller, focused components
- Create separate files for types and constants
- Better organization of related components
-
Memoization
- Use
useMemofor expensive computations - Implement
useCallbackfor event handlers - Add
React.memofor pure components
- Use
-
Code Splitting
- Implement dynamic imports for large components
- Use Next.js dynamic imports for route-based code splitting
- Lazy load components that are not immediately needed
- Shared Types
- Create a dedicated
typesdirectory - Use Zod for runtime type validation (partially implemented)
- Create a dedicated
- ARIA Labels
- Add proper ARIA labels to interactive elements
- Implement keyboard navigation for the Navbar
- Ensure proper color contrast
- Store Organization
- Split Zustand stores by feature
- Implement proper TypeScript types for store actions
- Add middleware for logging and persistence
- Error Boundaries
- Implement React Error Boundaries
- Add proper error states for components
- Create reusable error components
- Component Documentation
- Add JSDoc comments for complex functions
- Document prop types and usage examples
These improvements would enhance the codebase's maintainability, performance, and developer experience while following React and TypeScript best practices.
This project originated as a frontend assessment test at Mainstack and has been expanded into a comprehensive financial ledger system. It is maintained for demonstration and educational purposes, showcasing the evolution from a simple assessment project to a production-ready application.