Skip to content

Infrastructure

Overview

The Oktuple infrastructure is designed to support high availability, scalability, and performance for the project management platform. The system uses modern cloud-native technologies and follows best practices for production deployments.

Infrastructure Components

Core Services

PMN Service

  • Application Server: Node.js with Hono framework
  • Database: PostgreSQL with Prisma ORM
  • Cache: Redis for session and data caching
  • Search: Elasticsearch for full-text search
  • File Storage: AWS S3 or compatible storage

Syncer Service

  • Data Sync Engine: PostgreSQL logical replication
  • Search Indexing: Elasticsearch and Typesense
  • Queue Management: Redis with BullMQ
  • WebSocket Client: Real-time communication

Shared Infrastructure

  • Load Balancer: Nginx or cloud load balancer
  • Monitoring: Sentry for error tracking
  • Logging: Structured logging with log aggregation
  • Security: Casdoor for authentication

Deployment Architecture

Containerization

Docker Configuration

dockerfile
# PMN Dockerfile
FROM node:18-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

FROM node:18-alpine AS runtime
WORKDIR /app

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./

EXPOSE 3000
CMD ["npm", "start"]

Docker Compose

yaml
# docker-compose.yml
version: "3.8"

services:
  pmn:
    build: ./pmn
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://user:pass@postgres:5432/oktuple
      - REDIS_URL=redis://redis:6379
      - ELASTICSEARCH_URL=http://elasticsearch:9200
    depends_on:
      - postgres
      - redis
      - elasticsearch
    volumes:
      - ./uploads:/app/uploads

  syncer:
    build: ./syncer
    environment:
      - NODE_ENV=production
      - POSTGRES_HOST=postgres
      - REDIS_URL=redis://redis:6379
      - ELASTICSEARCH_NODE=http://elasticsearch:9200
    depends_on:
      - postgres
      - redis
      - elasticsearch

  postgres:
    image: postgres:15
    environment:
      - POSTGRES_DB=oktuple
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init:/docker-entrypoint-initdb.d
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
    ports:
      - "9200:9200"
    volumes:
      - elasticsearch_data:/usr/share/elasticsearch/data

  typesense:
    image: typesense/typesense:0.25.1
    environment:
      - TYPESENSE_API_KEY=xyz
      - TYPESENSE_DATA_DIR=/data
    ports:
      - "8108:8108"
    volumes:
      - typesense_data:/data

volumes:
  postgres_data:
  redis_data:
  elasticsearch_data:
  typesense_data:

Environment Configuration

Environment Variables

bash
# PMN Environment
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://user:pass@localhost:5432/oktuple
REDIS_URL=redis://localhost:6379
ELASTICSEARCH_URL=http://localhost:9200
JWT_SECRET=your-secret-key
CASDOOR_URL=https://casdoor.example.com
SENTRY_DSN=https://your-sentry-dsn
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_REGION=us-east-1
AWS_S3_BUCKET=oktuple-uploads

# Syncer Environment
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=oktuple
POSTGRES_USER=user
POSTGRES_PASSWORD=pass
TIMESCALE_HOST=localhost
TIMESCALE_PORT=5432
TIMESCALE_DB=timescale
TIMESCALE_USER=user
TIMESCALE_PASSWORD=pass
ELASTICSEARCH_NODE=http://localhost:9200
ELASTICSEARCH_USERNAME=elastic
ELASTICSEARCH_PASSWORD=password
REDIS_URL=redis://localhost:6379
TYPESENSE_HOST=localhost
TYPESENSE_PORT=8108
TYPESENSE_PROTOCOL=http
TYPESENSE_API_KEY=your-api-key

Database Infrastructure

PostgreSQL Setup

Database Configuration

sql
-- Enable logical replication
ALTER SYSTEM SET wal_level = logical;
ALTER SYSTEM SET max_replication_slots = 10;
ALTER SYSTEM SET max_wal_senders = 10;
ALTER SYSTEM SET max_connections = 200;
ALTER SYSTEM SET shared_buffers = '256MB';
ALTER SYSTEM SET effective_cache_size = '1GB';
ALTER SYSTEM SET maintenance_work_mem = '64MB';
ALTER SYSTEM SET checkpoint_completion_target = 0.9;
ALTER SYSTEM SET wal_buffers = '16MB';
ALTER SYSTEM SET default_statistics_target = 100;

-- Create replication user
CREATE USER replicator REPLICATION LOGIN PASSWORD 'repl_password';

-- Create publication for logical replication
CREATE PUBLICATION oktuple_pub FOR ALL TABLES;

-- Create replication slot
SELECT pg_create_logical_replication_slot('oktuple_syncer_slot', 'pgoutput');

Performance Optimization

sql
-- Create indexes for performance
CREATE INDEX CONCURRENTLY idx_enode_parent_id ON enode(parent_id);
CREATE INDEX CONCURRENTLY idx_enode_type ON enode(type);
CREATE INDEX CONCURRENTLY idx_issue_enode_id ON issue(enode_id);
CREATE INDEX CONCURRENTLY idx_issue_type ON issue(type);
CREATE INDEX CONCURRENTLY idx_people_enode_id ON people(enode_id);

-- JSON field indexes
CREATE INDEX CONCURRENTLY idx_enode_meta ON enode USING GIN (meta);
CREATE INDEX CONCURRENTLY idx_issue_meta ON issue USING GIN (meta);
CREATE INDEX CONCURRENTLY idx_issue_tags ON issue USING GIN (tags);

-- Composite indexes
CREATE INDEX CONCURRENTLY idx_enode_parent_type ON enode(parent_id, type);
CREATE INDEX CONCURRENTLY idx_issue_enode_type ON issue(enode_id, type);

Redis Configuration

Redis Setup

bash
# redis.conf
bind 0.0.0.0
port 6379
timeout 300
tcp-keepalive 60
maxmemory 512mb
maxmemory-policy allkeys-lru
save 900 1
save 300 10
save 60 10000

Redis Cluster (Optional)

yaml
# redis-cluster.yml
version: "3.8"
services:
  redis-node-1:
    image: redis:7-alpine
    command: redis-server --port 7001 --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly yes
    ports:
      - "7001:7001"
    volumes:
      - redis_node_1:/data

  redis-node-2:
    image: redis:7-alpine
    command: redis-server --port 7002 --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly yes
    ports:
      - "7002:7002"
    volumes:
      - redis_node_2:/data

  redis-node-3:
    image: redis:7-alpine
    command: redis-server --port 7003 --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly yes
    ports:
      - "7003:7003"
    volumes:
      - redis_node_3:/data

Search Infrastructure

Elasticsearch Setup

Elasticsearch Configuration

yaml
# elasticsearch.yml
cluster.name: oktuple-cluster
node.name: oktuple-node-1
network.host: 0.0.0.0
http.port: 9200
discovery.type: single-node
xpack.security.enabled: false
xpack.monitoring.enabled: false
xpack.watcher.enabled: false

# Performance settings
indices.memory.index_buffer_size: 30%
indices.queries.cache.size: 10%
indices.fielddata.cache.size: 10%

Index Templates

json
{
  "index_patterns": ["oktuple-*"],
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0,
    "refresh_interval": "1s",
    "analysis": {
      "analyzer": {
        "oktuple_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": ["lowercase", "stop", "snowball"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "oktuple_analyzer",
        "search_analyzer": "oktuple_analyzer"
      },
      "content": {
        "type": "text",
        "analyzer": "oktuple_analyzer"
      },
      "tags": {
        "type": "keyword"
      },
      "created_at": {
        "type": "date"
      }
    }
  }
}

Typesense Setup

Typesense Configuration

bash
# Typesense startup
typesense-server --data-dir=/data \
  --api-key=your-api-key \
  --enable-cors \
  --log-level=info \
  --port=8108 \
  --host=0.0.0.0

Collection Schema

json
{
  "name": "oktuple_issues",
  "fields": [
    { "name": "id", "type": "string" },
    { "name": "title", "type": "string" },
    { "name": "description", "type": "string" },
    { "name": "type", "type": "string" },
    { "name": "status", "type": "string" },
    { "name": "enode_id", "type": "string" },
    { "name": "created_at", "type": "int64" },
    { "name": "updated_at", "type": "int64" }
  ],
  "default_sorting_field": "created_at"
}

Load Balancing and Scaling

Nginx Configuration

Reverse Proxy Setup

nginx
# nginx.conf
upstream pmn_backend {
    server pmn:3000;
    server pmn:3001;
    server pmn:3002;
}

upstream syncer_backend {
    server syncer:3000;
    server syncer:3001;
}

server {
    listen 80;
    server_name oktuple.example.com;

    # PMN API
    location /api/ {
        proxy_pass http://pmn_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket support
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # Syncer endpoints
    location /syncer/ {
        proxy_pass http://syncer_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Static files
    location /uploads/ {
        alias /var/www/uploads/;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Health checks
    location /health {
        access_log off;
        return 200 "healthy\n";
        add_header Content-Type text/plain;
    }
}

Horizontal Scaling

PMN Service Scaling

yaml
# pmn-service.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pmn-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: pmn
  template:
    metadata:
      labels:
        app: pmn
    spec:
      containers:
        - name: pmn
          image: oktuple/pmn:latest
          ports:
            - containerPort: 3000
          env:
            - name: NODE_ENV
              value: "production"
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: oktuple-secrets
                  key: database-url
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5

Monitoring and Observability

Health Checks

Application Health

typescript
// Health check endpoints
app.get("/health", async (c) => {
  try {
    // Check database connection
    await prisma.$queryRaw`SELECT 1`;

    // Check Redis connection
    await redis.ping();

    // Check Elasticsearch
    await elasticsearch.ping();

    return c.json({
      status: "healthy",
      timestamp: new Date().toISOString(),
      services: {
        database: "connected",
        redis: "connected",
        elasticsearch: "connected",
      },
    });
  } catch (error) {
    return c.json(
      {
        status: "unhealthy",
        timestamp: new Date().toISOString(),
        error: error.message,
      },
      500
    );
  }
});

app.get("/health/ready", async (c) => {
  // Readiness check - can the service handle requests?
  try {
    await prisma.$queryRaw`SELECT 1`;
    return c.json({ status: "ready" });
  } catch (error) {
    return c.json({ status: "not_ready" }, 503);
  }
});

Logging Configuration

Structured Logging

typescript
// Logger configuration
import pino from "pino";

const logger = pino({
  level: process.env.LOG_LEVEL || "info",
  formatters: {
    level: (label) => {
      return { level: label };
    },
    log: (object) => {
      return object;
    },
  },
  timestamp: pino.stdTimeFunctions.isoTime,
  base: {
    service: "oktuple-pmn",
    version: process.env.npm_package_version,
  },
});

// Usage in services
export class EnodeService {
  async createEnode(data: CreateEnodeData): Promise<ENode> {
    logger.info("Creating ENode", {
      userId: data.owner_id,
      type: data.type,
      parentId: data.parent_id,
    });

    try {
      const enode = await this.prisma.enode.create({ data });

      logger.info("ENode created successfully", {
        enodeId: enode.id,
        userId: data.owner_id,
      });

      return enode;
    } catch (error) {
      logger.error("Failed to create ENode", {
        error: error.message,
        userId: data.owner_id,
        data,
      });
      throw error;
    }
  }
}

Metrics Collection

Performance Metrics

typescript
// Metrics collection
export class MetricsCollector {
  private metrics = {
    requests: 0,
    errors: 0,
    responseTime: 0,
    activeConnections: 0,
  };

  recordRequest(duration: number): void {
    this.metrics.requests++;
    this.metrics.responseTime += duration;
  }

  recordError(): void {
    this.metrics.errors++;
  }

  setActiveConnections(count: number): void {
    this.metrics.activeConnections = count;
  }

  getMetrics(): any {
    return {
      ...this.metrics,
      averageResponseTime:
        this.metrics.requests > 0
          ? this.metrics.responseTime / this.metrics.requests
          : 0,
      errorRate:
        this.metrics.requests > 0
          ? this.metrics.errors / this.metrics.requests
          : 0,
    };
  }
}

// Metrics endpoint
app.get("/metrics", (c) => {
  const metrics = metricsCollector.getMetrics();
  return c.json(metrics);
});

Security Configuration

Authentication and Authorization

Casdoor Integration

typescript
// Casdoor configuration
export class CasdoorService {
  private client: CasdoorSDK;

  constructor() {
    this.client = new CasdoorSDK({
      clientId: process.env.CASDOOR_CLIENT_ID!,
      clientSecret: process.env.CASDOOR_CLIENT_SECRET!,
      certificate: process.env.CASDOOR_CERTIFICATE!,
      orgName: process.env.CASDOOR_ORG_NAME!,
      appName: process.env.CASDOOR_APP_NAME!,
      endpoint: process.env.CASDOOR_ENDPOINT!,
    });
  }

  async validateToken(token: string): Promise<any> {
    try {
      const claims = await this.client.parseJwtToken(token);
      return claims;
    } catch (error) {
      throw new Error("Invalid token");
    }
  }

  async getUserInfo(userId: string): Promise<any> {
    try {
      const user = await this.client.getUser(userId);
      return user;
    } catch (error) {
      throw new Error("User not found");
    }
  }
}

SSL/TLS Configuration

HTTPS Setup

nginx
# SSL configuration
server {
    listen 443 ssl http2;
    server_name oktuple.example.com;

    ssl_certificate /etc/ssl/certs/oktuple.crt;
    ssl_certificate_key /etc/ssl/private/oktuple.key;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options DENY always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-XSS-Protection "1; mode=block" always;

    # Rest of configuration...
}

Backup and Recovery

Database Backup

Automated Backups

bash
#!/bin/bash
# backup.sh

BACKUP_DIR="/backups"
DATE=$(date +%Y%m%d_%H%M%S)
DB_NAME="oktuple"

# Create backup directory
mkdir -p $BACKUP_DIR

# Database backup
pg_dump -h localhost -U postgres -d $DB_NAME > $BACKUP_DIR/oktuple_$DATE.sql

# Compress backup
gzip $BACKUP_DIR/oktuple_$DATE.sql

# Keep only last 7 days of backups
find $BACKUP_DIR -name "oktuple_*.sql.gz" -mtime +7 -delete

# Upload to S3 (optional)
aws s3 cp $BACKUP_DIR/oktuple_$DATE.sql.gz s3://oktuple-backups/

Backup Restoration

bash
#!/bin/bash
# restore.sh

BACKUP_FILE=$1
DB_NAME="oktuple"

if [ -z "$BACKUP_FILE" ]; then
    echo "Usage: $0 <backup_file>"
    exit 1
fi

# Stop application
systemctl stop oktuple-pmn

# Restore database
gunzip -c $BACKUP_FILE | psql -h localhost -U postgres -d $DB_NAME

# Start application
systemctl start oktuple-pmn

echo "Restore completed successfully"

Disaster Recovery

High Availability Setup

Database Replication

sql
-- Primary database
-- Enable WAL archiving
ALTER SYSTEM SET archive_mode = on;
ALTER SYSTEM SET archive_command = 'test ! -f /var/lib/postgresql/archive/%f && cp %p /var/lib/postgresql/archive/%f';

-- Standby database
-- Create recovery.conf
standby_mode = 'on'
primary_conninfo = 'host=primary_host port=5432 user=replicator password=repl_password'
restore_command = 'cp /var/lib/postgresql/archive/%f %p'
archive_cleanup_command = 'pg_archivecleanup /var/lib/postgresql/archive %r'

Load Balancer Failover

nginx
# Primary and backup upstreams
upstream pmn_primary {
    server pmn-primary:3000;
}

upstream pmn_backup {
    server pmn-backup:3000;
}

# Health check and failover
server {
    listen 80;
    server_name oktuple.example.com;

    location /api/ {
        proxy_pass http://pmn_primary;
        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
        proxy_next_upstream_tries 3;
        proxy_next_upstream_timeout 10s;
    }
}