Appearance
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-keyDatabase 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 10000Redis 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:/dataSearch 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.0Collection 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: 5Monitoring 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;
}
}