Project: PokerLab
PokerLab is a web application for poker training and analysis, featuring hand history review, equity calculations, and opponent profiling.
Project Overview
PokerLab bridges the gap between recreational and professional poker play by providing data-driven analysis tools. The application processes raw hand histories from major online poker platforms, calculates real-time equity via Monte Carlo simulation, tracks bankroll performance, and builds opponent behavioral profiles β all wrapped in a modern React interface backed by a Node.js API and PostgreSQL database.
Key Features
| Feature | Description | Technology |
|---|---|---|
| Hand History Importer | Parse and store hand histories from PokerStars, partypoker, and 888poker | Node.js streams, custom parsers |
| Equity Calculator | Monte Carlo simulation for hand vs hand / range vs range equity | Web Workers, statistical sampling |
| Bankroll Tracker | Session tracking, profit/loss analytics, variance visualization | Chart.js, PostgreSQL aggregates |
| Session Analytics | Performance dashboards with filtering by date, stakes, game type | React, D3.js, REST API |
| Opponent Profiling | Aggregated stats: VPIP, PFR, aggression factor, positional tendencies | Materialized views, Redis cache |
Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AWS CloudFront CDN β
β (Static assets, SSL termination) β
βββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββ
β Application Load Balancer β
ββββββββββββ¬ββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββ¬βββββββββββ
β β β
ββββββββββββΌβββββββββββ βββββββββββββΌβββββββββββββ βββββββββββΌβββββββββββ
β Frontend Service β β API Service β β Worker Service β
β (ECS Fargate) β β (ECS Fargate) β β (ECS Fargate) β
β β β β β β
β React SPA ββββββΆβ Node.js / Express β β Hand processors β
β Nginx static β β JWT auth β β Equity sim β
β serve β β Business logic β β Report generation β
βββββββββββββββββββββββ βββββββββββββ¬βββββββββββββ ββββββββββββββββββββββ
β
ββββββββββββββ΄βββββββββββββ
β β
ββββββββββΌββββββββββ βββββββββββΌβββββββββββ
β RDS PostgreSQL β β ElastiCache Redis β
β (Multi-AZ) β β (Session/cache) β
βββββββββββββββββββββ ββββββββββββββββββββββ
Technology Stack
| Layer | Technology | Version |
|---|---|---|
| Frontend | React 18, TypeScript, Vite | React ^18.2 |
| UI Components | Material-UI (MUI) | ^5.14 |
| State Management | Zustand | ^4.4 |
| Backend API | Node.js, Express, TypeScript | Node 20 LTS |
| Authentication | JWT (access + refresh tokens) | jsonwebtoken ^9 |
| Database | PostgreSQL 15 | 15.x |
| Cache | Redis | 7.x |
| Infrastructure | Terraform, AWS | Terraform ~> 1.6 |
| Containers | Docker, ECS Fargate | Docker 24.x |
| CI/CD | GitHub Actions | β |
Terraform Infrastructure
The entire AWS infrastructure is defined as code in the tf-pokerlab repository. This enables reproducible environments, peer-reviewed infrastructure changes, and disaster recovery.
VPC with Public/Private Subnets
# modules/vpc/main.tf
# Production-grade VPC with public and private subnets across 3 AZs
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-${var.environment}"
Environment = var.environment
ManagedBy = "terraform"
}
}
# Internet Gateway for public subnet access
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-igw-${var.environment}"
}
}
# Public subnets β one per AZ for ALB and NAT Gateways
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index)
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-${count.index + 1}"
Type = "public"
}
}
# Private subnets β one per AZ for ECS tasks and RDS
resource "aws_subnet" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index + length(var.availability_zones))
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${var.project_name}-private-${count.index + 1}"
Type = "private"
}
}
# NAT Gateway per AZ for outbound internet from private subnets
resource "aws_nat_gateway" "main" {
count = length(var.availability_zones)
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "${var.project_name}-nat-${count.index + 1}"
}
depends_on = [aws_internet_gateway.main]
}
resource "aws_eip" "nat" {
count = length(var.availability_zones)
domain = "vpc"
tags = {
Name = "${var.project_name}-eip-${count.index + 1}"
}
}
ECS Fargate for Containerized Services
# modules/ecs/main.tf
# ECS Cluster with Fargate launch type for serverless containers
resource "aws_ecs_cluster" "main" {
name = "${var.project_name}-${var.environment}"
setting {
name = "containerInsights"
value = "enabled"
}
tags = {
Environment = var.environment
}
}
# Frontend Task Definition
resource "aws_ecs_task_definition" "frontend" {
family = "${var.project_name}-frontend-${var.environment}"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = "256"
memory = "512"
execution_role_arn = var.ecs_execution_role_arn
container_definitions = jsonencode([{
name = "frontend"
image = "${var.ecr_repository_url}/pokerlab-frontend:${var.image_tag}"
portMappings = [{
containerPort = 80
protocol = "tcp"
}]
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = aws_cloudwatch_log_group.frontend.name
awslogs-region = var.aws_region
awslogs-stream-prefix = "ecs"
}
}
}])
}
# API Task Definition (more resources for backend processing)
resource "aws_ecs_task_definition" "api" {
family = "${var.project_name}-api-${var.environment}"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = "512"
memory = "1024"
execution_role_arn = var.ecs_execution_role_arn
task_role_arn = var.ecs_task_role_arn
container_definitions = jsonencode([{
name = "api"
image = "${var.ecr_repository_url}/pokerlab-api:${var.image_tag}"
portMappings = [{
containerPort = 3000
protocol = "tcp"
}]
environment = [
{ name = "NODE_ENV", value = var.environment },
{ name = "DB_HOST", value = var.rds_endpoint },
{ name = "REDIS_HOST", value = var.redis_endpoint },
{ name = "PORT", value = "3000" }
]
secrets = [
{ name = "DB_PASSWORD", valueFrom = var.db_secret_arn },
{ name = "JWT_SECRET", valueFrom = var.jwt_secret_arn }
]
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = aws_cloudwatch_log_group.api.name
awslogs-region = var.aws_region
awslogs-stream-prefix = "ecs"
}
}
}])
}
# ECS Service with auto-scaling
resource "aws_ecs_service" "api" {
name = "api"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.api.arn
desired_count = var.api_desired_count
launch_type = "FARGATE"
network_configuration {
subnets = var.private_subnet_ids
security_groups = [var.api_security_group_id]
assign_public_ip = false
}
load_balancer {
target_group_arn = var.api_target_group_arn
container_name = "api"
container_port = 3000
}
depends_on = [var.alb_listener]
}
# Auto-scaling policies
resource "aws_appautoscaling_target" "api" {
max_capacity = 10
min_capacity = 2
resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.api.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
resource "aws_appautoscaling_policy" "api_cpu" {
name = "api-cpu-auto-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.api.resource_id
scalable_dimension = aws_appautoscaling_target.api.scalable_dimension
service_namespace = aws_appautoscaling_target.api.service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
target_value = 70.0
scale_in_cooldown = 300
scale_out_cooldown = 60
}
}
RDS PostgreSQL
# modules/rds/main.tf
# Multi-AZ PostgreSQL with automated backups and encryption
resource "aws_db_subnet_group" "main" {
name = "${var.project_name}-${var.environment}"
subnet_ids = var.private_subnet_ids
tags = {
Name = "${var.project_name}-db-subnet-group"
}
}
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-${var.environment}"
engine = "postgres"
engine_version = "15.4"
instance_class = var.db_instance_class # db.t3.medium for prod
allocated_storage = 100
max_allocated_storage = 1000 # Autoscaling limit
storage_type = "gp3"
storage_encrypted = true
db_name = var.db_name
username = var.db_username
password = var.db_password
port = 5432
multi_az = var.environment == "production"
publicly_accessible = false
vpc_security_group_ids = [var.db_security_group_id]
db_subnet_group_name = aws_db_subnet_group.main.name
backup_retention_period = 30
backup_window = "03:00-04:00"
maintenance_window = "Mon:04:00-Mon:05:00"
delete_automated_backups = false
deletion_protection = var.environment == "production"
skip_final_snapshot = var.environment != "production"
final_snapshot_identifier = var.environment == "production" ? "${var.project_name}-final" : null
enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]
tags = {
Name = "${var.project_name}-postgres"
Environment = var.environment
}
}
CI/CD Pipeline with GitHub Actions
The pipeline implements a GitOps workflow with automated testing, security scanning, infrastructure validation, and blue/green deployment.
# .github/workflows/deploy.yml
name: Build, Test, and Deploy
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
AWS_REGION: us-east-1
ECR_REGISTRY: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com
jobs:
# -------------------------------------------------------------------------
# 1. Lint and Unit Tests
# -------------------------------------------------------------------------
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: testpass
POSTGRES_DB: pokerlab_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Unit tests
run: npm run test:ci
env:
NODE_ENV: test
DB_HOST: localhost
DB_NAME: pokerlab_test
DB_PASSWORD: testpass
REDIS_HOST: localhost
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
# -------------------------------------------------------------------------
# 2. Security Scanning
# -------------------------------------------------------------------------
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
# -------------------------------------------------------------------------
# 3. Terraform Validation
# -------------------------------------------------------------------------
terraform:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./terraform
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.6.0"
- name: Terraform fmt
run: terraform fmt -check -recursive
- name: Terraform init
run: terraform init -backend=false
- name: Terraform validate
run: terraform validate
# -------------------------------------------------------------------------
# 4. Build and Push Container Images
# -------------------------------------------------------------------------
build:
runs-on: ubuntu-latest
needs: [test, security, terraform]
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push API image
run: |
docker build -t $ECR_REGISTRY/pokerlab-api:${{ github.sha }} ./api
docker push $ECR_REGISTRY/pokerlab-api:${{ github.sha }}
- name: Build and push Frontend image
run: |
docker build -t $ECR_REGISTRY/pokerlab-frontend:${{ github.sha }} ./frontend
docker push $ECR_REGISTRY/pokerlab-frontend:${{ github.sha }}
# -------------------------------------------------------------------------
# 5. Deploy to ECS
# -------------------------------------------------------------------------
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: production
url: https://pokerlab.kuyaops.com
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Deploy to ECS
run: |
aws ecs update-service \
--cluster pokerlab-production \
--service api \
--force-new-deployment
Local Development Environment
Complete docker-compose.yml
# docker-compose.yml
# Full local development stack with hot reload and persistent data volumes
version: "3.8"
services:
# ---------------------------------------------------------------------------
# PostgreSQL Database
# ---------------------------------------------------------------------------
postgres:
image: postgres:15-alpine
container_name: pokerlab-postgres
restart: unless-stopped
environment:
POSTGRES_USER: pokerlab
POSTGRES_PASSWORD: devpassword
POSTGRES_DB: pokerlab_dev
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./database/init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U pokerlab -d pokerlab_dev"]
interval: 10s
timeout: 5s
retries: 5
# ---------------------------------------------------------------------------
# Redis Cache
# ---------------------------------------------------------------------------
redis:
image: redis:7-alpine
container_name: pokerlab-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
# ---------------------------------------------------------------------------
# Backend API
# ---------------------------------------------------------------------------
api:
build:
context: ./api
dockerfile: Dockerfile.dev
container_name: pokerlab-api
restart: unless-stopped
environment:
NODE_ENV: development
PORT: 3000
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: pokerlab_dev
DB_USER: pokerlab
DB_PASSWORD: devpassword
REDIS_HOST: redis
REDIS_PORT: 6379
JWT_SECRET: dev-jwt-secret-change-in-production
JWT_EXPIRES_IN: 15m
JWT_REFRESH_EXPIRES_IN: 7d
ports:
- "3000:3000"
volumes:
- ./api/src:/app/src:ro
- ./api/package.json:/app/package.json:ro
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
interval: 15s
timeout: 5s
retries: 3
# ---------------------------------------------------------------------------
# Frontend (React with Vite dev server)
# ---------------------------------------------------------------------------
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.dev
container_name: pokerlab-frontend
restart: unless-stopped
environment:
VITE_API_URL: http://localhost:3000
VITE_WS_URL: ws://localhost:3000
ports:
- "5173:5173"
volumes:
- ./frontend/src:/app/src
- ./frontend/public:/app/public
- ./frontend/package.json:/app/package.json:ro
depends_on:
- api
# ---------------------------------------------------------------------------
# pgAdmin for database management (optional)
# ---------------------------------------------------------------------------
pgadmin:
image: dpage/pgadmin4:latest
container_name: pokerlab-pgadmin
restart: unless-stopped
environment:
PGADMIN_DEFAULT_EMAIL: admin@pokerlab.local
PGADMIN_DEFAULT_PASSWORD: admin
ports:
- "5050:80"
volumes:
- pgadmin_data:/var/lib/pgadmin
depends_on:
- postgres
volumes:
postgres_data:
redis_data:
pgadmin_data:
Environment Setup Guide
-
Clone both repositories
# Application code git clone https://github.com/j1-medilo06/pokerlab-app.git cd pokerlab-app # Infrastructure (optional for local dev) git clone https://github.com/j1-medilo06/tf-pokerlab.git -
Start the development stack
docker-compose up -d # Wait for services to be healthy docker-compose ps # View logs docker-compose logs -f api -
Run database migrations
# API container runs migrations automatically on startup # Or manually: docker-compose exec api npx knex migrate:latest -
Seed sample data (optional)
docker-compose exec api npx knex seed:run -
Access the application
Service URL Credentials Frontend http://localhost:5173 β API http://localhost:3000 β API Health http://localhost:3000/health β pgAdmin http://localhost:5050 admin@pokerlab.local / admin PostgreSQL localhost:5432 pokerlab / devpassword Redis localhost:6379 β
Key Components Detail
Hand History Importer
The importer supports multiple poker site formats through a pluggable parser architecture. Each parser implements a common interface, normalizing hands into a unified schema.
// api/src/parsers/BaseHandParser.ts
export interface ParsedHand {
handId: string;
site: 'pokerstars' | 'partypoker' | '888poker';
gameType: 'cash' | 'tournament' | 'sng';
format: 'holdem' | 'omaha' | 'omaha5';
stakes: { smallBlind: number; bigBlind: number; currency: string };
tableName: string;
maxSeats: number;
datetime: Date;
players: ParsedPlayer[];
actions: ParsedAction[];
boardCards?: string[];
summary: HandSummary;
}
export interface ParsedPlayer {
seat: number;
name: string;
stack: number;
cards?: string[];
isHero: boolean;
}
export interface ParsedAction {
playerName: string;
street: 'preflop' | 'flop' | 'turn' | 'river' | 'showdown';
action: 'fold' | 'check' | 'call' | 'bet' | 'raise' | 'allin';
amount?: number;
position: 'SB' | 'BB' | 'UTG' | 'MP' | 'CO' | 'BTN';
}
Equity Calculator
The equity calculator uses Monte Carlo simulation to estimate win probability. For two known hands with a random board, it runs 10,000β100,000 iterations, dealing random remaining cards and evaluating the winner using the pokersolver library.
// api/src/services/EquityCalculator.ts
import * as pokersolver from 'pokersolver';
export class EquityCalculator {
private readonly HAND = pokersolver.Hand;
/**
* Calculate equity for given hand(s) against range(s) via Monte Carlo.
* @param hands Array of specific hands (e.g., ['As', 'Kd'])
* @param ranges Array of weighted range arrays
* @param board Optional known board cards
* @param iterations Number of Monte Carlo trials (default 50000)
*/
async calculateEquity(
hands: string[][],
ranges: string[][][],
board: string[] = [],
iterations: number = 50000
): Promise<EquityResult> {
const deck = this.createDeck().filter(
c => !board.includes(c) && !hands.flat().includes(c)
);
let wins = new Array(hands.length).fill(0);
let ties = new Array(hands.length).fill(0);
for (let i = 0; i < iterations; i++) {
const shuffled = this.shuffle([...deck]);
let dealtHands = hands.map(h => [...h]);
let dealtRanges = ranges.map(r => this.sampleFromRange(r, shuffled));
const allHands = [...dealtHands, ...dealtRanges];
const cardsNeeded = 5 - board.length;
const runoutBoard = [...board, ...shuffled.splice(0, cardsNeeded)];
// Evaluate all hands against the board
const evaluated = allHands.map(h => this.HAND.solve([...h, ...runoutBoard]));
const winners = this.HAND.winners(evaluated);
if (winners.length > 1) {
const tieShare = 1 / winners.length;
winners.forEach(w => {
const idx = evaluated.indexOf(w);
if (idx < hands.length) ties[idx] += tieShare;
});
} else {
const winnerIdx = evaluated.indexOf(winners[0]);
if (winnerIdx < hands.length) wins[winnerIdx]++;
}
}
return {
equities: hands.map((_, i) => ({
hand: hands[i],
winPct: (wins[i] / iterations) * 100,
tiePct: (ties[i] / iterations) * 100,
equity: ((wins[i] + ties[i]) / iterations) * 100
})),
iterations,
duration: Date.now() - startTime
};
}
}
GitHub Repositories
| Repository | Description | Link |
|---|---|---|
| pokerlab-app | React frontend + Node.js API + Docker Compose | github.com/j1-medilo06/pokerlab-app |
| tf-pokerlab | Terraform modules for AWS infrastructure | github.com/j1-medilo06/tf-pokerlab |
Deployment Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DEVELOPER β
β (git push origin main) β
ββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββΌβββββββββββ
β GitHub Actions β
β CI/CD Pipeline β
β β
β βββββββββββββββββ β
β β Lint & Test β β
β β Security Scan β β
β β TF Validate β β
β β Build Images β β
β β Push to ECR β β
β β Deploy to ECS β β
β βββββββββββββββββ β
ββββββββββββ¬βββββββββββ
β
ββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββ
β AWS PRODUCTION β
β βββββββββββββββ ββββββββββββββββ βββββββββββββββ βββββββββββββββββββ β
β β CloudFront β β ALB β β ECS Fargate β β RDS PostgreSQL β β
β β (CDN/SSL) β β (Routing) β β (API + Web) β β (Multi-AZ) β β
β βββββββββββββββ ββββββββββββββββ βββββββββββββββ βββββββββββββββββββ β
β β
β βββββββββββββββ ββββββββββββββββ βββββββββββββββ βββββββββββββββββββ β
β β ElastiCache β β CloudWatch β β S3 (Logs) β β Secrets Manager β β
β β (Redis) β β (Metrics) β β (Backups) β β (Credentials) β β
β βββββββββββββββ ββββββββββββββββ βββββββββββββββ βββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Multi-AZ deployment for high availability
- Private subnets for compute and database β no public IPs on containers
- Least-privilege IAM roles for each ECS task
- Encrypted storage at rest (RDS, EBS) and in transit (TLS 1.3)
- Container image scanning with Trivy in CI pipeline
- Database deletion protection and automated snapshots in production
- Auto-scaling based on CPU utilization (70% target)
References & Links
| Resource | Link |
|---|---|
| Application Repository | github.com/j1-medilo06/pokerlab-app |
| Terraform Repository | github.com/j1-medilo06/tf-pokerlab |
| Author GitHub | github.com/j1-medilo06 |
| Portfolio | kuyaops.com |