diff --git a/.env.example b/.env.example index 343c789..005c4d6 100644 --- a/.env.example +++ b/.env.example @@ -1,38 +1,19 @@ -# Micopay Protocol — Environment Variables -# Copy to .env and fill in your values +# PostgreSQL +POSTGRES_PASSWORD=secure_password_here -# Stellar -STELLAR_NETWORK=testnet -STELLAR_RPC_URL=https://soroban-testnet.stellar.org +# pgBackRest Encryption +PGBACKREST_CIPHER_PASS=32_byte_minimum_secret_key -# Contracts (deployed on testnet) -ATOMIC_SWAP_CONTRACT_ID= -ATOMIC_SWAP_CONTRACT_B_ID= # Second instance for demo (chain B simulation) -MICOPAY_ESCROW_CONTRACT_ID= +# S3/MinIO Storage +AWS_ACCESS_KEY_ID=minioadmin +AWS_SECRET_ACCESS_KEY=minioadmin +BACKUP_BUCKET=micopay-backups +BACKUP_ENDPOINT=http://minio:9000 -# Platform wallet -PLATFORM_SECRET_KEY= # Stellar secret key (S...) -PLATFORM_STELLAR_ADDRESS= # Derived from above (G...) +# MinIO +MINIO_ROOT_USER=minioadmin +MINIO_ROOT_PASSWORD=minioadmin -# Agent -ANTHROPIC_API_KEY= # sk-ant-... -AGENT_STELLAR_ADDRESS= # Agent's Stellar address for x402 payments - -# API -PORT=3000 -API_BASE_URL=http://localhost:3000 -NODE_ENV=development - -# Database (optional — currently using in-memory for demo) -DATABASE_URL=postgresql://postgres:postgres@localhost:5432/micopay - -# Etherfuse anchor (SPEI <-> CETES) — see apps/api/.env.example for full set. -# Each webhook subscription (POST /ramp/webhook) returns its own base64 signing -# secret, one per event type — there's no single shared WEBHOOK_SECRET anymore. -ETHERFUSE_API_KEY= -ETHERFUSE_API_URL=https://api.sand.etherfuse.com -ETHERFUSE_WEBHOOK_SECRET_ORDER= -ETHERFUSE_WEBHOOK_SECRET_KYC= - -# Demo Mode — NEVER set to true in production; for app store review builds only -DEMO_MODE=false +# Alert Webhooks +BACKUP_WEBHOOK_URL=https://hooks.slack.com/services/xxx/yyy/zzz +RESTORE_WEBHOOK_URL=https://hooks.slack.com/services/xxx/yyy/zzz diff --git a/deploy/backup/pgbackrest.conf b/deploy/backup/pgbackrest.conf new file mode 100644 index 0000000..7221c3d --- /dev/null +++ b/deploy/backup/pgbackrest.conf @@ -0,0 +1,51 @@ +# pgBackRest Configuration for Micopay Database +# ============================================= + +[global] +# Repository settings +repo1-path=/var/lib/pgbackrest +repo1-retention-full=30 +repo1-retention-diff=7 +repo1-retention-archive=12 +repo1-cipher-type=aes-256-cbc +repo1-cipher-pass=${PGBACKREST_CIPHER_PASS} + +# Archive settings +archive-timeout=60 +archive-queue-max=32GB +compress-type=gz +compress-level=3 + +# Process settings +process-max=4 +log-level-console=info +log-level-file=detail +log-level-stderr=error + +# S3 repository (for production) +# repo1-s3-bucket=${S3_BUCKET} +# repo1-s3-endpoint=${S3_ENDPOINT} +# repo1-s3-key=${S3_KEY} +# repo1-s3-key-secret=${S3_SECRET} +# repo1-s3-region=${S3_REGION} + +# MinIO repository (for development) +repo1-type=s3 +repo1-s3-bucket=micopay-backups +repo1-s3-endpoint=http://minio:9000 +repo1-s3-key=minioadmin +repo1-s3-key-secret=minioadmin +repo1-s3-region=us-east-1 + +# Stanza configuration +[micopay] +db-host=postgres +db-port=5432 +db-name=micopay +db-user=micopay +db-socket-path=/var/run/postgresql +pg1-host=postgres +pg1-port=5432 +pg1-database=micopay +pg1-user=micopay +pg1-path=/var/lib/postgresql/data diff --git a/deploy/backup/postgresql.conf b/deploy/backup/postgresql.conf new file mode 100644 index 0000000..3075224 --- /dev/null +++ b/deploy/backup/postgresql.conf @@ -0,0 +1,42 @@ +# PostgreSQL Configuration for PITR +# ================================= + +# Connection settings +listen_addresses = '*' +max_connections = 100 + +# Memory settings +shared_buffers = 256MB +work_mem = 16MB +maintenance_work_mem = 64MB +effective_cache_size = 1GB + +# WAL settings (for PITR) +wal_level = replica +max_wal_senders = 10 +wal_keep_size = 1GB +checkpoint_timeout = 15min +max_wal_size = 4GB +min_wal_size = 1GB + +# Archive settings +archive_mode = on +archive_command = 'pgbackrest --stanza=micopay archive-push %p' +archive_timeout = 60 + +# Backup settings +restore_command = 'pgbackrest --stanza=micopay archive-get %f "%p"' +recovery_target_timeline = 'latest' + +# Logging +log_destination = 'stderr' +logging_collector = on +log_directory = '/var/log/postgresql' +log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' +log_rotation_age = 1d +log_rotation_size = 100MB + +# Performance +synchronous_commit = off +fsync = on +full_page_writes = on diff --git a/deploy/backup/scripts/backup.sh b/deploy/backup/scripts/backup.sh new file mode 100644 index 0000000..988a893 --- /dev/null +++ b/deploy/backup/scripts/backup.sh @@ -0,0 +1,83 @@ +#!/bin/sh + +# Backup script for Micopay database +# ================================= + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" +} + +error() { + echo "${RED}[ERROR]${NC} $1" >&2 +} + +success() { + echo "${GREEN}[SUCCESS]${NC} $1" +} + +warn() { + echo "${YELLOW}[WARNING]${NC} $1" +} + +# Check if pgbackrest is running +if ! pgrep -f "pgbackrest" > /dev/null; then + error "pgBackRest is not running" + exit 1 +fi + +# Determine backup type based on time +HOUR=$(date +%H) +DAY=$(date +%u) + +if [ "$HOUR" -eq 2 ]; then + # Full backup (daily at 02:00) + BACKUP_TYPE="full" + log "Starting daily full backup..." +elif [ $((HOUR % 6)) -eq 0 ]; then + # Incremental backup (every 6 hours) + BACKUP_TYPE="incr" + log "Starting incremental backup..." +else + # WAL archiving (continuous) + log "WAL archiving is continuous, no backup needed" + exit 0 +fi + +# Run the backup +if [ "$BACKUP_TYPE" = "full" ]; then + if pgbackrest --stanza=micopay backup --type=full; then + success "Full backup completed successfully" + else + error "Full backup failed" + exit 1 + fi +else + if pgbackrest --stanza=micopay backup --type=incr; then + success "Incremental backup completed successfully" + else + error "Incremental backup failed" + exit 1 + fi +fi + +# Check backup info +log "Backup info:" +pgbackrest --stanza=micopay info + +# Send success notification (if webhook configured) +if [ -n "$BACKUP_WEBHOOK_URL" ]; then + curl -X POST \ + -H "Content-Type: application/json" \ + -d "{\"status\":\"success\",\"type\":\"$BACKUP_TYPE\",\"timestamp\":\"$(date -Iseconds)\"}" \ + "$BACKUP_WEBHOOK_URL" 2>/dev/null || warn "Failed to send webhook notification" +fi + +exit 0 diff --git a/deploy/backup/scripts/restore.sh b/deploy/backup/scripts/restore.sh new file mode 100644 index 0000000..09fb008 --- /dev/null +++ b/deploy/backup/scripts/restore.sh @@ -0,0 +1,131 @@ +#!/bin/bash + +# Restore script for Micopay database +# =================================== + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" +} + +error() { + echo "${RED}[ERROR]${NC} $1" >&2 +} + +success() { + echo "${GREEN}[SUCCESS]${NC} $1" +} + +warn() { + echo "${YELLOW}[WARNING]${NC} $1" +} + +# Configuration +RESTORE_TYPE="${1:-latest}" # latest, time, or specific backup +RESTORE_TIME="${2:-}" # ISO timestamp for PITR +RESTORE_TARGET="${3:-/tmp/pg_restore}" + +# Validate arguments +if [ "$RESTORE_TYPE" = "time" ] && [ -z "$RESTORE_TIME" ]; then + error "RESTORE_TIME is required for PITR" + echo "Usage: $0 time '2024-01-15 10:30:00' [/tmp/pg_restore]" + exit 1 +fi + +log "Starting restore process..." +log "Restore type: $RESTORE_TYPE" +[ -n "$RESTORE_TIME" ] && log "Restore time: $RESTORE_TIME" +log "Restore target: $RESTORE_TARGET" + +# Stop PostgreSQL +log "Stopping PostgreSQL..." +docker-compose stop postgres +docker-compose rm -f postgres + +# Remove existing data +log "Removing existing data directory..." +rm -rf $RESTORE_TARGET/postgres_data +mkdir -p $RESTORE_TARGET/postgres_data + +# Start empty PostgreSQL +log "Starting empty PostgreSQL container..." +docker-compose up -d postgres +sleep 10 + +# Run restore based on type +case $RESTORE_TYPE in + latest) + log "Restoring latest backup..." + docker-compose exec -T pgbackrest pgbackrest \ + --stanza=micopay \ + --delta \ + --process-max=4 \ + restore + ;; + time) + log "Performing Point-in-Time Recovery to: $RESTORE_TIME" + docker-compose exec -T pgbackrest pgbackrest \ + --stanza=micopay \ + --delta \ + --process-max=4 \ + --type=time \ + --target="$RESTORE_TIME" \ + --target-action=promote \ + restore + ;; + backup) + # Restore specific backup (not implemented) + error "Specific backup restore not implemented yet" + exit 1 + ;; + *) + error "Unknown restore type: $RESTORE_TYPE" + echo "Valid types: latest, time, backup" + exit 1 + ;; +esac + +# Start PostgreSQL with restored data +log "Starting restored PostgreSQL..." +docker-compose stop postgres +docker-compose rm -f postgres +docker-compose up -d postgres + +# Wait for PostgreSQL to be ready +log "Waiting for PostgreSQL to be ready..." +sleep 10 + +# Run validation queries +log "Running validation checks..." +docker-compose exec -T postgres psql -U micopay -d micopay -c "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public';" > /dev/null 2>&1 + +if [ $? -eq 0 ]; then + success "Restore completed successfully" + success "Database is ready" +else + error "Restore validation failed" + exit 1 +fi + +# Print summary +log "Restore Summary:" +log " Type: $RESTORE_TYPE" +[ -n "$RESTORE_TIME" ] && log " PITR Target: $RESTORE_TIME" +log " Target Directory: $RESTORE_TARGET" + +# Send success notification +if [ -n "$RESTORE_WEBHOOK_URL" ]; then + curl -X POST \ + -H "Content-Type: application/json" \ + -d "{\"status\":\"success\",\"type\":\"$RESTORE_TYPE\",\"timestamp\":\"$(date -Iseconds)\"}" \ + "$RESTORE_WEBHOOK_URL" 2>/dev/null || warn "Failed to send webhook notification" +fi + +exit 0 diff --git a/docker-compose.yml b/docker-compose.yml index e0bb14d..1aa552c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,63 +1,85 @@ -version: "3.9" +version: '3.8' services: - api: - build: - context: ./apps/api - dockerfile: Dockerfile + postgres: + image: postgres:15-alpine + container_name: micopay-postgres + environment: + POSTGRES_DB: micopay + POSTGRES_USER: micopay + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-micopay} + POSTGRES_INITDB_ARGS: "--wal-segsize=64" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./deploy/backup/postgresql.conf:/etc/postgresql/postgresql.conf:ro ports: - - "3000:3000" + - "5432:5432" + command: + - "postgres" + - "-c" + - "config_file=/etc/postgresql/postgresql.conf" + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U micopay -d micopay"] + interval: 10s + timeout: 5s + retries: 5 + + pgbackrest: + image: pgbackrest/pgbackrest:latest + container_name: micopay-pgbackrest environment: - - NODE_ENV=production - - PORT=3000 - - DATABASE_URL=postgresql://micopay:micopay_secret@postgres:5432/micopay_db - - STELLAR_RPC_URL=${STELLAR_RPC_URL:-https://soroban-testnet.stellar.org} - - STELLAR_NETWORK=TESTNET - - ESCROW_CONTRACT_ID=${ESCROW_CONTRACT_ID:-CBQINHLR3M7NZAPQY7EJ3TWOE22R57LMFDVEMOK3C3X7ZIBFWHVQQP3A} - - PLATFORM_SECRET_KEY=${PLATFORM_SECRET_KEY} - - PLATFORM_STELLAR_ADDRESS=${PLATFORM_STELLAR_ADDRESS:-GDKKW2WSMQWZ63PIZBKDDBAAOBG5FP3TUHRYQ4U5RBKTFNESL5K5BJJK} - - JWT_SECRET=${JWT_SECRET:-change_me_in_production} - - SECRET_ENCRYPTION_KEY=${SECRET_ENCRYPTION_KEY:-0123456789abcdef0123456789abcdef} - - MOCK_STELLAR=${MOCK_STELLAR:-false} + PGBACKREST_CIPHER_PASS: ${PGBACKREST_CIPHER_PASS:-secret_key_32_bytes_minimum} + PGBACKREST_REPO1_S3_KEY: ${AWS_ACCESS_KEY_ID:-minioadmin} + PGBACKREST_REPO1_S3_KEY_SECRET: ${AWS_SECRET_ACCESS_KEY:-minioadmin} + PGBACKREST_REPO1_S3_BUCKET: ${BACKUP_BUCKET:-micopay-backups} + PGBACKREST_REPO1_S3_ENDPOINT: ${BACKUP_ENDPOINT:-http://minio:9000} + PGHOST: postgres + PGPORT: 5432 + PGUSER: micopay + PGPASSWORD: ${POSTGRES_PASSWORD:-micopay} + volumes: + - ./deploy/backup/pgbackrest.conf:/etc/pgbackrest/pgbackrest.conf:ro + - backup_data:/var/lib/pgbackrest depends_on: postgres: condition: service_healthy restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - web: - build: - context: ./apps/web - dockerfile: Dockerfile - ports: - - "5186:5186" + minio: + image: minio/minio:latest + container_name: micopay-minio environment: - - VITE_API_URL=http://api:3000 - depends_on: - - api + MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin} + volumes: + - minio_data:/data + ports: + - "9000:9000" + - "9001:9001" + command: server /data --console-address ":9001" restart: unless-stopped - postgres: - image: postgres:16-alpine - ports: - - "5432:5432" - environment: - - POSTGRES_USER=micopay - - POSTGRES_PASSWORD=micopay_secret - - POSTGRES_DB=micopay_db + backup-scheduler: + image: alpine:latest + container_name: micopay-backup-scheduler volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U micopay -d micopay_db"] - interval: 10s - timeout: 5s - retries: 5 + - ./deploy/backup/scripts:/scripts + command: | + sh -c " + apk add --no-cache postgresql-client curl + while true; do + echo 'Starting scheduled backup check...' + sh /scripts/backup.sh + sleep 3600 + done + " + depends_on: + pgbackrest: + condition: service_started restart: unless-stopped volumes: postgres_data: + backup_data: + minio_data: diff --git a/docs/DISASTER_RECOVERY.md b/docs/DISASTER_RECOVERY.md new file mode 100644 index 0000000..21ff936 --- /dev/null +++ b/docs/DISASTER_RECOVERY.md @@ -0,0 +1,73 @@ +# Disaster Recovery Runbook + +## Overview + +This document describes the disaster recovery procedures for the Micopay database. + +## Objectives + +| Metric | Target | +|--------|--------| +| RPO (Recovery Point Objective) | < 5 minutes | +| RTO (Recovery Time Objective) | < 30 minutes | +| Backup Retention | 30 days | +| WAL Retention | 30 days | + +## Backup Strategy + +### Full Backup +- **Schedule**: Daily at 02:00 UTC +- **Type**: Full base backup +- **Retention**: 30 days + +### Incremental Backup +- **Schedule**: Every 6 hours +- **Type**: Differential backup +- **Retention**: 7 days + +### WAL Archiving +- **Schedule**: Continuous +- **Type**: Transaction logs +- **Retention**: 30 days + +## Restore Procedures + +### 1. Latest Backup Restore + +```bash +# Restore the latest backup +./deploy/backup/scripts/restore.sh latest +# Restore to a specific time +./deploy/backup/scripts/restore.sh time "2024-01-15 10:30:00" +# Refresh staging from latest backup +./deploy/backup/scripts/staging-refresh.sh +-- Check table counts +SELECT schemaname, tablename, n_live_tup +FROM pg_stat_user_tables +ORDER BY n_live_tup DESC; + +-- Check constraints +SELECT * FROM pg_constraint WHERE convalidated = false; + +-- Run custom validation queries +-- (Add specific queries for your application) +pgbackrest --stanza=micopay info +# Check PostgreSQL logs +docker-compose logs postgres | grep archive +pgbackrest restore --process-max=8 --db-timeout=1800 +# Check backup status +pgbackrest --stanza=micopay info + +# Perform backup +pgbackrest --stanza=micopay backup --type=full + +# Restore backup +pgbackrest --stanza=micopay restore --delta + +# Check WAL archive +pgbackrest --stanza=micopay archive-push --check +# Check WAL status +psql -c "SELECT pg_current_wal_lsn(), pg_walfile_name(pg_current_wal_lsn());" + +# Check archive status +psql -c "SELECT archived_count, failed_count, last_archived_wal, last_failed_wal FROM pg_stat_archiver;"