From bc238b894629810cf2185c3c32a0c429da545bc3 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sat, 4 Apr 2026 06:05:52 -0700 Subject: [PATCH] =?UTF-8?q?infra(infra-ci):=20=F0=9F=A7=B1=20Update=20CI/C?= =?UTF-8?q?D=20pipeline=20configurations=20for=20cloud=20deployments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- infrastructure/.env.prod.example | 23 +++ infrastructure/docker-compose.prod.yaml | 188 ++++++++++++++++++++++++ infrastructure/init.sql | 81 ++++++++++ 3 files changed, 292 insertions(+) create mode 100644 infrastructure/.env.prod.example create mode 100644 infrastructure/docker-compose.prod.yaml create mode 100644 infrastructure/init.sql diff --git a/infrastructure/.env.prod.example b/infrastructure/.env.prod.example new file mode 100644 index 0000000..5c3c875 --- /dev/null +++ b/infrastructure/.env.prod.example @@ -0,0 +1,23 @@ +# ============================================================================= +# @analytics Production Environment +# ============================================================================= +# Copy to .env.prod and fill in real values. +# NEVER commit .env.prod to version control. +# +# DNS: analytics.db.transquinnftw.com → vps-0 IP:25434 (TimescaleDB) +# ============================================================================= + +# TimescaleDB +POSTGRES_USER=lilith +POSTGRES_PASSWORD=CHANGE_ME_STRONG_PASSWORD +POSTGRES_DB=lilith_analytics + +# Redis +REDIS_PASSWORD=CHANGE_ME_STRONG_PASSWORD + +# Collector CORS (comma-separated allowed origins) +CORS_ORIGINS=https://transquinnftw.com,https://data.transquinnftw.com + +# API authentication keys (comma-separated) +# Used by platform-analytics backend-api to authenticate with this API +API_KEYS=CHANGE_ME_GENERATE_WITH_openssl_rand_hex_32 diff --git a/infrastructure/docker-compose.prod.yaml b/infrastructure/docker-compose.prod.yaml new file mode 100644 index 0000000..c389927 --- /dev/null +++ b/infrastructure/docker-compose.prod.yaml @@ -0,0 +1,188 @@ +# ============================================================================= +# @analytics - Production Collector Stack +# ============================================================================= +# +# Data collection layer for transquinnftw.com analytics. +# Runs on vps-0 alongside the lilith-platform backend. +# +# Services: +# - TimescaleDB: Time-series analytics storage (port 25434 external) +# - Redis: BullMQ job queues +# - Collector: Event ingestion POST /collect (port 4001) +# - Processor: BullMQ workers (internal) +# - API: Query endpoints (port 4003) +# - Realtime: WebSocket gateway (port 4004) +# +# DNS: +# analytics.db.transquinnftw.com A → vps-0 IP (connects to port 25434) +# +# Usage: +# cp .env.prod.example .env.prod +# # Edit .env.prod with real secrets +# docker compose -f docker-compose.prod.yaml --env-file .env.prod up -d +# + +services: + timescaledb: + image: timescale/timescaledb:2.16.1-pg16 + container_name: analytics-timescaledb + restart: unless-stopped + ports: + - "25434:5432" + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - analytics-postgres-data:/var/lib/postgresql/data + - ./init.sql:/docker-entrypoint-initdb.d/01-init.sql:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - analytics-net + + redis: + image: redis:7.4-alpine + container_name: analytics-redis + restart: unless-stopped + command: + - redis-server + - --requirepass + - "${REDIS_PASSWORD}" + - --appendonly + - "yes" + - --maxmemory + - "512MB" + - --maxmemory-policy + - "noeviction" + volumes: + - analytics-redis-data:/data + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] + interval: 10s + timeout: 3s + retries: 5 + networks: + - analytics-net + + collector: + build: + context: ../services/collector + dockerfile: Dockerfile + container_name: analytics-collector + restart: unless-stopped + ports: + - "127.0.0.1:4001:4001" + environment: + NODE_ENV: production + PORT: "4001" + REDIS_HOST: redis + REDIS_PORT: "6379" + REDIS_PASSWORD: ${REDIS_PASSWORD} + CORS_ORIGINS: ${CORS_ORIGINS} + LOG_LEVEL: info + depends_on: + redis: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:4001/health || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + networks: + - analytics-net + + processor: + build: + context: ../services/processor + dockerfile: Dockerfile + container_name: analytics-processor + restart: unless-stopped + environment: + NODE_ENV: production + REDIS_HOST: redis + REDIS_PORT: "6379" + REDIS_PASSWORD: ${REDIS_PASSWORD} + DATABASE_HOST: timescaledb + DATABASE_PORT: "5432" + DATABASE_USER: ${POSTGRES_USER} + DATABASE_PASSWORD: ${POSTGRES_PASSWORD} + DATABASE_NAME: ${POSTGRES_DB} + CONCURRENCY: "5" + BATCH_SIZE: "100" + depends_on: + timescaledb: + condition: service_healthy + redis: + condition: service_healthy + networks: + - analytics-net + + api: + build: + context: ../services/api + dockerfile: Dockerfile + container_name: analytics-api + restart: unless-stopped + ports: + - "127.0.0.1:4003:4003" + environment: + NODE_ENV: production + PORT: "4003" + DATABASE_HOST: timescaledb + DATABASE_PORT: "5432" + DATABASE_USER: ${POSTGRES_USER} + DATABASE_PASSWORD: ${POSTGRES_PASSWORD} + DATABASE_NAME: ${POSTGRES_DB} + REDIS_HOST: redis + REDIS_PORT: "6379" + REDIS_PASSWORD: ${REDIS_PASSWORD} + API_KEYS: ${API_KEYS} + depends_on: + timescaledb: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:4003/health || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + networks: + - analytics-net + + realtime: + build: + context: ../services/realtime + dockerfile: Dockerfile + container_name: analytics-realtime + restart: unless-stopped + ports: + - "127.0.0.1:4004:4004" + environment: + NODE_ENV: production + PORT: "4004" + REDIS_HOST: redis + REDIS_PORT: "6379" + REDIS_PASSWORD: ${REDIS_PASSWORD} + depends_on: + redis: + condition: service_healthy + networks: + - analytics-net + +volumes: + analytics-postgres-data: + name: analytics-postgres-data + analytics-redis-data: + name: analytics-redis-data + +networks: + analytics-net: + name: analytics-net + driver: bridge diff --git a/infrastructure/init.sql b/infrastructure/init.sql new file mode 100644 index 0000000..2d7ef19 --- /dev/null +++ b/infrastructure/init.sql @@ -0,0 +1,81 @@ +-- Analytics Database Initialization (TimescaleDB) + +-- Enable extensions +CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE; +CREATE EXTENSION IF NOT EXISTS pg_stat_statements; + +CREATE SCHEMA IF NOT EXISTS analytics; + +-- Content views (hypertable) +CREATE TABLE IF NOT EXISTS analytics.content_views ( + time TIMESTAMPTZ NOT NULL, + content_id UUID NOT NULL, + content_type VARCHAR(50) NOT NULL, + user_id UUID, + session_id VARCHAR(255), + referrer TEXT, + user_agent TEXT, + country VARCHAR(2), + device_type VARCHAR(20) +); +SELECT create_hypertable('analytics.content_views', 'time', if_not_exists => TRUE); + +-- Engagement metrics (hypertable) +CREATE TABLE IF NOT EXISTS analytics.engagement_metrics ( + time TIMESTAMPTZ NOT NULL, + content_id UUID NOT NULL, + metric_type VARCHAR(50) NOT NULL, + value DECIMAL(12, 4) NOT NULL, + metadata JSONB DEFAULT '{}' +); +SELECT create_hypertable('analytics.engagement_metrics', 'time', if_not_exists => TRUE); + +-- Revenue metrics (hypertable) +CREATE TABLE IF NOT EXISTS analytics.revenue_metrics ( + time TIMESTAMPTZ NOT NULL, + creator_id UUID NOT NULL, + transaction_type VARCHAR(50) NOT NULL, + gross_amount DECIMAL(12, 2) NOT NULL, + net_amount DECIMAL(12, 2) NOT NULL, + platform_fee DECIMAL(12, 2) NOT NULL, + currency VARCHAR(3) DEFAULT 'USD' +); +SELECT create_hypertable('analytics.revenue_metrics', 'time', if_not_exists => TRUE); + +-- Dashboard snapshots +CREATE TABLE IF NOT EXISTS analytics.dashboard_snapshots ( + id SERIAL PRIMARY KEY, + snapshot_type VARCHAR(50) NOT NULL, + period VARCHAR(20) NOT NULL, + data JSONB NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Continuous aggregates for common queries +CREATE MATERIALIZED VIEW IF NOT EXISTS analytics.hourly_views +WITH (timescaledb.continuous) AS +SELECT + time_bucket('1 hour', time) AS bucket, + content_type, + COUNT(*) as view_count, + COUNT(DISTINCT user_id) as unique_users +FROM analytics.content_views +GROUP BY bucket, content_type +WITH NO DATA; + +-- Compression policy (compress data older than 7 days) +SELECT add_compression_policy('analytics.content_views', INTERVAL '7 days', if_not_exists => TRUE); +SELECT add_compression_policy('analytics.engagement_metrics', INTERVAL '7 days', if_not_exists => TRUE); +SELECT add_compression_policy('analytics.revenue_metrics', INTERVAL '7 days', if_not_exists => TRUE); + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_views_content ON analytics.content_views(content_id, time DESC); +CREATE INDEX IF NOT EXISTS idx_engagement_content ON analytics.engagement_metrics(content_id, time DESC); +CREATE INDEX IF NOT EXISTS idx_revenue_creator ON analytics.revenue_metrics(creator_id, time DESC); + +-- Permissions +GRANT ALL PRIVILEGES ON SCHEMA analytics TO lilith; +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA analytics TO lilith; +GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA analytics TO lilith; + +DO $$ BEGIN RAISE NOTICE 'Analytics database initialized with TimescaleDB'; END $$;