A scalable, event-sourced distributed ledger system built with Turborepo monorepo architecture, featuring event sourcing with EventStoreDB (Kurrent DB), CQRS pattern implementation, and microservices for handling financial transactions, projections, and real-time updates.
PayIt is a modern financial transaction processing platform that leverages event sourcing and CQRS (Command Query Responsibility Segregation) patterns to provide a robust, scalable, and auditable distributed ledger system. The system is designed to handle high-throughput financial operations while maintaining complete transaction history and ensuring data consistency across distributed services.
- β Event Sourcing - Complete audit trail of all transactions with EventStoreDB as the single source of truth
- β CQRS Architecture - Separated read and write models for optimized performance and scalability
- β Distributed Microservices - Independent services for transactions, projections, and business logic
- β Real-time Projections - Automatically updated read models from event streams
- β Eventual Consistency - Reliable data synchronization across all services
- β Message-Driven Communication - Asynchronous processing with RabbitMQ
- β High Availability - Redis-backed caching and session management
- β Type-Safe Development - Shared TypeScript types across the monorepo
- β OAuth Integration - Secure authentication with Google OAuth
- β Developer-Friendly - Modern tooling with Turborepo, Prisma, and Next.js
PayIt is ideal for building:
- Payment Processing Systems - Handle transactions with complete audit trails
- Digital Wallets - Manage user balances and transaction histories
- Banking Applications - Core banking functionality with event sourcing
- Financial Ledgers - Double-entry bookkeeping and reconciliation
- Cryptocurrency Platforms - Track blockchain transactions and wallet states
- Accounting Systems - Immutable financial records and reporting
- E-commerce Platforms - Order processing and payment management
This project uses Turborepo as a monorepo management tool with the following structure:
payit/
βββ apps/
β βββ http-server/ # Main HTTP API server
β βββ projection-worker/ # Reads events and updates read models
β βββ service-worker/ # Handles business logic and services
β βββ transaction-worker/ # Processes financial transactions
β βββ web/ # Frontend Next.js application
βββ packages/
β βββ eslint-config/ # Shared ESLint configuration
β βββ postgre-db/ # PostgreSQL database client (read models)
β βββ rabbitmq/ # RabbitMQ client utilities
β βββ redisclient/ # Redis client utilities
β βββ types/ # Shared TypeScript types
β βββ typescript-config/ # Shared TypeScript configuration
β βββ ui/ # Shared UI components
βββ docker-compose.yml
PayIt implements the CQRS (Command Query Responsibility Segregation) and Event Sourcing patterns:
- EventStoreDB (Kurrent DB): Stores all events as the source of truth
- Projection Worker: Subscribes to event streams and builds read models in PostgreSQL
- Transaction Worker: Processes and validates financial transactions
- Service Worker: Handles business logic and orchestrates workflows
- HTTP Server: Provides REST API for commands and queries
- PostgreSQL: Stores read-optimized projections for queries
- RabbitMQ: Message broker for async communication between services
- Redis: Caching layer and session management
- Node.js (v18 or higher)
- Docker and Docker Compose
- Git
- pnpm (recommended) or npm/yarn
# Fork the repository on GitHub, then clone your fork
git clone https://github.com/YOUR_USERNAME/payit.git
cd payit# Install pnpm globally if you haven't already
npm install -g pnpm
# Install all dependencies
pnpm installThe project includes a docker-compose.yml file that sets up all required services:
- EventStoreDB (Kurrent DB) - Event store database
- PostgreSQL - Read model database
- Redis - Cache and session store
- RabbitMQ - Message broker
# Start all services in detached mode
docker-compose up -d
# Check if all services are running
docker-compose ps
# View logs for all services
docker-compose logs -f
# View logs for specific service
docker-compose logs -f eventstoredb
# Stop all services
docker-compose down
# Stop and remove volumes (clean slate)
docker-compose down -v# Access EventStoreDB UI
# Open browser: http://localhost:2113
# Default credentials: admin / changeit- Go to Google Cloud Console
- Create a new project or select an existing one
- Enable required APIs:
- Navigate to "APIs & Services" > "Library"
- Search for "Google+ API" and enable it
- Create OAuth 2.0 credentials:
- Go to "APIs & Services" > "Credentials"
- Click "Create Credentials" > "OAuth client ID"
- Choose "Web application"
- Add authorized redirect URIs:
http://localhost:3002/api/auth/callback/google
- Add authorized JavaScript origins:
http://localhost:3000http://localhost:3002
- Save and copy the Client ID and Client Secret
The Docker Compose file includes Redis. Connection details:
- Host: localhost
- Port: 6379
- Username: default
- Password: (set in docker-compose.yml)
If you prefer a cloud Redis instance:
- Sign up for Redis Cloud or Upstash
- Create a new Redis database
- Copy the connection details:
- Host
- Port
- Password
- Username (usually 'default')
The Docker Compose file includes PostgreSQL for read models. Connection details:
- Host: localhost
- Port: 5432
- Database: payit_db
- Username: payit_user
- Password: payit_password
Database URL format:
postgresql://payit_user:payit_password@localhost:5432/payit_db
EventStoreDB is included in Docker Compose and serves as the event store:
- Host: localhost
- HTTP Port: 2113
- TCP Port: 1113
- Connection String:
esdb://localhost:2113?tls=false
Kurrent DB URL format:
esdb://admin:changeit@localhost:2113?tls=false
Create .env files in the respective application directories:
NEXT_PUBLIC_FRONTEND_URL="http://localhost:3000"
NEXT_PUBLIC_API_URL="http://localhost:3002"
GOOGLE_CLIENT_ID='your_google_client_id_here'
GOOGLE_CLIENT_SECRET='your_google_client_secret_here'
GOOGLE_REDIRECT_URI="http://localhost:3002/api/auth/callback/google"NEXT_PUBLIC_FRONTEND_URL="http://localhost:3000"
TRANSACTION_WORKER_URL='http://transaction-worker:3008'
SERVICE_WORKER_URL='http://service-worker:3007'
GOOGLE_CLIENT_ID='your_google_client_id_here'
GOOGLE_CLIENT_SECRET='your_google_client_secret_here'
GOOGLE_REDIRECT_URI="http://localhost:3002/api/auth/callback/google"
secret_key='your_jwt_secret_key_here'
access_key='your_access_key_here'
# Redis Configuration
REDIS_USERNAME='default'
REDIS_PASSWORD='your_redis_password_here'
REDIS_HOST='localhost'
REDIS_PORT='6379'
# PostgreSQL (Read Models)
DATABASE_URL="postgresql://payit_user:payit_password@localhost:5432/payit_db"
# EventStoreDB (Kurrent DB)
KURRENT_DB_URL="esdb://admin:changeit@localhost:2113?tls=false"
# RabbitMQ
RABBITMQ_CLUSTER_URL="amqp://payit_user:payit_password@localhost:5672/"TRANSACTION_WORKER_URL='http://transaction-worker:3008'
# PostgreSQL (Read Models)
DATABASE_URL="postgresql://payit_user:payit_password@localhost:5432/payit_db"
# EventStoreDB (Kurrent DB)
KURRENT_DB_URL="esdb://admin:changeit@localhost:2113?tls=false"
# RabbitMQ
RABBITMQ_CLUSTER_URL="amqp://payit_user:payit_password@localhost:5672/"
# Redis
REDIS_USERNAME='default'
REDIS_PASSWORD='your_redis_password_here'
REDIS_HOST='localhost'
REDIS_PORT='6379'# PostgreSQL (Read Models)
DATABASE_URL="postgresql://payit_user:payit_password@localhost:5432/payit_db"
# EventStoreDB (Kurrent DB) - Source of events
KURRENT_DB_URL="esdb://admin:changeit@localhost:2113?tls=false"
# RabbitMQ
RABBITMQ_CLUSTER_URL="amqp://payit_user:payit_password@localhost:5672/"
# Redis
REDIS_USERNAME='default'
REDIS_PASSWORD='your_redis_password_here'
REDIS_HOST='localhost'
REDIS_PORT='6379'SERVICE_WORKER_URL='http://service-worker:3007'
# PostgreSQL (Read Models)
DATABASE_URL="postgresql://payit_user:payit_password@localhost:5432/payit_db"
# EventStoreDB (Kurrent DB)
KURRENT_DB_URL="esdb://admin:changeit@localhost:2113?tls=false"
# RabbitMQ
RABBITMQ_CLUSTER_URL="amqp://payit_user:payit_password@localhost:5672/"
# Cloudinary (Optional)
CLOUDINARY_NAME='your_cloudinary_name'
API_KEY='your_cloudinary_api_key'
API_SECRET='your_cloudinary_api_secret'
# Redis
REDIS_USERNAME='default'
REDIS_PASSWORD='your_redis_password_here'
REDIS_HOST='localhost'
REDIS_PORT='6379'# Navigate to the database package
cd packages/postgre-db
# Generate Prisma client
pnpm prisma generate
# Return to root
cd ../..# Navigate to the database package
cd packages/postgre-db
# Run migrations to create read model tables
pnpm prisma migrate dev --name init
# Or push schema without creating migration files
pnpm prisma db push
# Return to root
cd ../..# Open Prisma Studio (database GUI)
cd packages/postgre-db
pnpm prisma studioThis will open Prisma Studio at http://localhost:5555 where you can view and edit your read models.
cd packages/postgre-db
pnpm prisma db seed
cd ../..# Run all applications in development mode
pnpm dev
# Run specific application
pnpm dev --filter=web
pnpm dev --filter=http-server
pnpm dev --filter=transaction-worker
pnpm dev --filter=projection-worker
pnpm dev --filter=service-worker# Build all applications
pnpm build
# Build specific application
pnpm build --filter=web
pnpm build --filter=http-server# Start all applications
pnpm start
# Start specific application
pnpm start --filter=web| Service | Port | Description |
|---|---|---|
| Web (Frontend) | 3000 | Next.js web application |
| HTTP Server | 3002 | Main REST API server |
| Service Worker | 3007 | Business logic processor |
| Transaction Worker | 3008 | Transaction processor |
| Projection Worker | 3009 | Event projection builder |
| PostgreSQL | 5432 | Read model database |
| Redis | 6379 | Cache and session store |
| RabbitMQ | 5672 | Message broker |
| RabbitMQ Management | 15672 | RabbitMQ web UI |
| EventStoreDB HTTP | 2113 | Event store HTTP API |
| EventStoreDB TCP | 1113 | Event store TCP connection |
User Request β HTTP Server β Transaction Worker β EventStoreDB
β
Event Persisted
β
Event Published
User Request β HTTP Server β PostgreSQL (Read Models)
EventStoreDB β Projection Worker β Transform Event β Update PostgreSQL
β β
Subscription Read Models Updated
# Run tests for all packages
pnpm test
# Run tests for specific package
pnpm test --filter=transaction-worker
# Run tests in watch mode
pnpm test:watch
# Run integration tests
pnpm test:integration# Check for linting errors
pnpm lint
# Fix linting errors
pnpm lint:fix
# Format code
pnpm format
# Clean all node_modules and build artifacts
pnpm clean
# Type check all packages
pnpm typecheck# View Prisma Studio (database GUI)
cd packages/postgre-db && pnpm prisma studio
# Reset database (WARNING: Deletes all data)
cd packages/postgre-db && pnpm prisma migrate reset
# Create a new migration
cd packages/postgre-db && pnpm prisma migrate dev --name your_migration_name# Access EventStoreDB CLI inside Docker
docker exec -it eventstoredb /bin/bash
# Check EventStoreDB status
curl http://localhost:2113/stats# View logs for all services
docker-compose logs -f
# View logs for specific service
docker-compose logs -f transaction-worker
# Restart specific service
docker-compose restart postgres
# Check resource usage
docker stats# Check Docker logs
docker-compose logs
# Restart all services
docker-compose restart
# Rebuild specific service
docker-compose up -d --build transaction-worker# Find process using port (example: port 3000)
# On Mac/Linux:
lsof -i :3000
# On Windows:
netstat -ano | findstr :3000
# Kill the process
kill -9 <PID>-
Verify EventStoreDB is running:
docker-compose ps eventstoredb
-
Check EventStoreDB logs:
docker-compose logs eventstoredb
-
Access EventStoreDB UI:
- URL:
http://localhost:2113 - Default credentials:
admin/changeit
- URL:
-
Test connection:
curl http://localhost:2113/stats
# Regenerate Prisma Client
cd packages/postgre-db
pnpm prisma generate
# Reset database and re-run migrations
pnpm prisma migrate reset-
Ensure RabbitMQ is running:
docker-compose ps rabbitmq
-
Check RabbitMQ management UI:
- URL:
http://localhost:15672 - Username:
payit_user - Password:
payit_password
- URL:
-
View RabbitMQ logs:
docker-compose logs rabbitmq
# Test Redis connection
docker exec -it redis redis-cli
> AUTH your_password
> PING
# Should return PONG
# Check Redis logs
docker-compose logs redis-
Check if Projection Worker is running:
docker-compose ps projection-worker
-
Verify EventStoreDB subscription:
- Open EventStoreDB UI at
http://localhost:2113 - Check "Persistent Subscriptions" section
- Open EventStoreDB UI at
-
Check worker logs:
docker-compose logs projection-worker
-
Manually trigger projection rebuild (if needed):
# This depends on your implementation curl -X POST http://localhost:3009/rebuild-projections
- UI Dashboard:
http://localhost:2113 - Stats Endpoint:
http://localhost:2113/stats - Health Check:
http://localhost:2113/health/live
- Management UI:
http://localhost:15672 - View queues, exchanges, and message rates
- Monitor consumer connections
# View all application logs
pnpm logs
# View specific service logs
docker-compose logs -f http-server
docker-compose logs -f transaction-worker
docker-compose logs -f projection-worker-
Environment Variables:
- Never commit
.envfiles to version control - Use strong, unique passwords for all services
- Rotate secrets regularly
- Never commit
-
EventStoreDB:
- Change default admin password
- Enable TLS in production
- Configure proper access control lists (ACLs)
-
API Security:
- Implement rate limiting
- Use JWT tokens with short expiration
- Validate all inputs
-
Database:
- Use read-only credentials where possible
- Enable connection encryption
- Regular backups of EventStoreDB and PostgreSQL
- Turborepo Documentation
- EventStoreDB Documentation
- CQRS Pattern
- Event Sourcing
- Next.js Documentation
- Prisma Documentation
- RabbitMQ Tutorials
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
We follow Conventional Commits:
feat:New featurefix:Bug fixdocs:Documentation changesstyle:Code style changes (formatting, etc.)refactor:Code refactoringtest:Adding or updating testschore:Maintenance tasks