Files
wehub-resource-sync 4ce4204b6c
CI / Lint (push) Failing after 2s
CI / Build (push) Has been skipped
SDK CI / PHP SDK (push) Failing after 1s
Split PHP SDK / PHP SDK tests (push) Failing after 1s
CI / Test (push) Failing after 1s
CI / Dashboard (push) Failing after 0s
SDK CI / JavaScript SDK (push) Failing after 0s
SDK CI / Python SDK (push) Failing after 2s
SDK CI / Java SDK (push) Failing after 1s
Split PHP SDK / Mirror sdk/php -> rmyndharis/openwa-php (push) Has been skipped
CI / Test (PostgreSQL migrations) (push) Failing after 7m47s
CI / Docker Build (push) Has been skipped
chore: import upstream snapshot with attribution
2026-07-13 12:24:08 +08:00

1302 lines
35 KiB
Markdown

# 14 - Migration Guide
## 14.1 Overview
This document provides a comprehensive guide for migrating OpenWA, including:
- Database migration (SQLite → PostgreSQL)
- Version upgrades (v0.1 → v0.2 → v1.0)
- Transfer session authentication state
- Rollback procedures
```mermaid
flowchart TB
subgraph Migration Types
DB[Database Migration]
VER[Version Upgrade]
ENV[Environment Migration]
end
subgraph Targets
DB --> PG[SQLite → PostgreSQL]
DB --> SCALE[Single → Multi-node]
VER --> MINOR[Minor Upgrade]
VER --> MAJOR[Major Upgrade]
ENV --> DEV[Dev → Staging]
ENV --> PROD[Staging → Production]
end
```
## 14.2 Pre-Migration Checklist
### Universal Checklist
```markdown
## Pre-Migration Checklist
### Backup
- [ ] Database backup completed
- [ ] Session auth files backed up (.wwebjs_auth/)
- [ ] Environment variables documented
- [ ] Docker volumes backed up (if applicable)
### Documentation
- [ ] Current version documented
- [ ] Active sessions list exported
- [ ] Webhook configurations exported
- [ ] API keys documented
### Communication
- [ ] Maintenance window scheduled
- [ ] Users notified
- [ ] Rollback plan prepared
- [ ] Support team briefed
### Verification
- [ ] Target environment ready
- [ ] Network connectivity tested
- [ ] Disk space sufficient (2x current size)
- [ ] New version tested in staging
```
## 14.3 Database Migration: SQLite → PostgreSQL
### When to Migrate
```mermaid
flowchart TD
A[Current Setup] --> B{Check Conditions}
B -->|Sessions > 5| C[Consider PostgreSQL]
B -->|Messages > 100K| C
B -->|Need HA| C
B -->|Concurrent writes high| C
B -->|Sessions ≤ 5| D[Stay with SQLite]
B -->|Low volume| D
C --> E[Plan Migration]
D --> F[Optimize SQLite]
```
| Condition | SQLite OK | Migrate to PostgreSQL |
| ------------------ | ---------- | --------------------- |
| Sessions | 1-5 | 6+ |
| Messages/day | < 10,000 | > 10,000 |
| Concurrent users | < 10 | > 10 |
| High Availability | Not needed | Required |
| Horizontal scaling | Not needed | Required |
### API-Based Migration (Recommended for v0.2+)
OpenWA v0.2+ includes built-in migration API endpoints that leverage the **Dual-Database Architecture**:
```bash
# Step 1: Export all Data DB tables
curl -s 'http://localhost:2785/api/infra/export-data' \
-H 'X-API-Key: YOUR_KEY' > data-backup.json
# Step 2: Change database configuration in .env or Dashboard
# From: DATABASE_TYPE=sqlite
# To: DATABASE_TYPE=postgres
# POSTGRES_BUILTIN=true
# Step 3: Restart with new configuration
docker compose --profile postgres up -d
# Step 4: Import data to new database
curl -X POST 'http://localhost:2785/api/infra/import-data' \
-H 'X-API-Key: YOUR_KEY' \
-H 'Content-Type: application/json' \
-d @data-backup.json
```
> [!NOTE]
> **Dual-Database Architecture**
>
> OpenWA separates databases:
>
> - **Main DB** (SQLite): API keys, audit logs - never migrated, always local
> - **Data DB** (Pluggable): Sessions, webhooks, messages - this is what gets migrated
>
> See [05 - Database Design: Dual-Database Architecture](./05-database-design.md#dual-database-architecture)
**Export Response Example:**
```json
{
"exportedAt": "2026-02-05T02:30:00.000Z",
"dataDbType": "sqlite",
"tables": {
"sessions": [...],
"webhooks": [...],
"messages": [...],
"messageBatches": [...]
},
"counts": {
"sessions": 5,
"webhooks": 12,
"messages": 1500,
"messageBatches": 3
}
}
```
### Storage Migration (Local ↔ S3/MinIO)
OpenWA v0.2+ supports migrating media files between storage backends:
```bash
# Step 1: Check current storage file count
curl -s 'http://localhost:2785/api/infra/storage/files/count' \
-H 'X-API-Key: YOUR_KEY'
# Response: { "storageType": "local", "count": 150, "sizeBytes": 15000000 }
# Step 2: Export all files as tar.gz
curl -s 'http://localhost:2785/api/infra/storage/export' \
-H 'X-API-Key: YOUR_KEY'
# Response: { "message": "Storage export completed", "download": "/app/data/exports/storage-export-xxx.tar.gz" }
# The archive is auto-removed after STORAGE_EXPORT_TTL_MS (default 1h), so re-import it before then.
# It is written under data/ so it survives the restart in Step 4 and stays import-able.
# Step 3: Change storage configuration
# From: STORAGE_TYPE=local
# To: STORAGE_TYPE=s3
# MINIO_BUILTIN=true # or false for external S3
# Step 4: Restart with new configuration
docker compose up -d
# Step 5: Import files to new storage
curl -X POST 'http://localhost:2785/api/infra/storage/import' \
-H 'X-API-Key: YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{"filePath": "/app/data/exports/storage-export-xxx.tar.gz"}'
```
| Scenario | Support | Method |
| ---------------------------- | ------- | ------------------------ |
| Local → Built-in MinIO | ✅ | Export → Config → Import |
| Local → External S3 | ✅ | Export → Config → Import |
| Built-in MinIO → External S3 | ✅ | Export → Config → Import |
| S3 → Local | ✅ | Export → Config → Import |
### Redis Migration (Cache)
Redis in OpenWA is used **only for caching** with TTL-based expiration. Cache data is ephemeral and automatically regenerates from the database.
**No migration API needed** - just change configuration:
```bash
# Switch from built-in to external Redis
REDIS_ENABLED=true
REDIS_BUILTIN=false # false = external Redis
REDIS_HOST=your-redis-host.com
REDIS_PORT=6379
REDIS_USERNAME=optional
REDIS_PASSWORD=optional
```
| Scenario | Support | Notes |
| ------------------------- | ------- | ---------------------------- |
| Built-in → External Redis | ✅ | Config change only |
| External → Built-in Redis | ✅ | Config change only |
| Enable → Disable Redis | ✅ | App uses memory fallback |
| Disable → Enable Redis | ✅ | Cache rebuilds automatically |
> [!TIP]
> **Cache Warm-up**: After switching Redis instances, the cache will automatically rebuild as requests come in. No data migration is necessary.
### BullMQ Migration (Queue System)
BullMQ stores job data in Redis. When switching Redis instances, pending jobs may be lost.
**Best Practice - Drain Queue Before Switching:**
```bash
# Step 1: Check queue status via Bull Board
# Visit: http://localhost:2785/admin/queues
# Step 2: Wait until MESSAGE and WEBHOOK queues are empty
# Or check via API:
curl -s 'http://localhost:2785/api/infra/status' \
-H 'X-API-Key: YOUR_KEY' | jq '.queue'
# Wait for: pending: 0
# Step 3: Change Redis configuration
REDIS_HOST=new-redis-host.com
# Step 4: Restart application
docker compose up -d
```
| Scenario | Support | Notes |
| ------------------------- | ------- | ----------------- |
| Queue Disabled → Enabled | ✅ | Config change |
| Queue Enabled → Disabled | ⚠️ | Drain queue first |
| Built-in → External Redis | ⚠️ | Drain queue first |
> [!WARNING]
> **Job Loss Prevention**: Always ensure the MESSAGE and WEBHOOK queues are empty before switching Redis instances. Check `/admin/queues` dashboard.
### Infrastructure Migration Summary
| Component | Migration Method | API Endpoint |
| ------------ | -------------------- | -------------------------------------------------------- |
| **Database** | Export/Import JSON | `/api/infra/export-data`, `/api/infra/import-data` |
| **Storage** | Export/Import tar.gz | `/api/infra/storage/export`, `/api/infra/storage/import` |
| **Redis** | Config change only | N/A (cache auto-rebuilds) |
| **BullMQ** | Drain then config | N/A (wait for empty queues) |
### Migration Script (Legacy)
```typescript
// scripts/migrate-sqlite-to-postgres.ts
import { DataSource } from 'typeorm';
import * as sqlite3 from 'sqlite3';
import { Client } from 'pg';
interface MigrationConfig {
sqlitePath: string;
postgresUrl: string;
batchSize: number;
}
interface MigrationResult {
table: string;
rowsMigrated: number;
duration: number;
errors: string[];
}
async function migrateSqliteToPostgres(config: MigrationConfig): Promise<MigrationResult[]> {
const results: MigrationResult[] = [];
// 1. Connect to both databases
console.log('🔌 Connecting to databases...');
const sqliteDb = new sqlite3.Database(config.sqlitePath);
const pgClient = new Client({ connectionString: config.postgresUrl });
await pgClient.connect();
// 2. Get list of tables
const tables = await getSqliteTables(sqliteDb);
console.log(`📋 Found ${tables.length} tables to migrate`);
// 3. Migration order (respect foreign keys)
const migrationOrder = [
'sessions',
'api_keys',
'webhooks',
'contacts',
'messages',
'media_files',
'webhook_logs',
'audit_logs',
];
// 4. Migrate each table
for (const table of migrationOrder) {
if (!tables.includes(table)) continue;
const startTime = Date.now();
const result = await migrateTable(sqliteDb, pgClient, table, config.batchSize);
result.duration = Date.now() - startTime;
results.push(result);
console.log(`✅ ${table}: ${result.rowsMigrated} rows in ${result.duration}ms`);
}
// 5. Reset sequences
await resetPostgresSequences(pgClient, migrationOrder);
// 6. Cleanup
sqliteDb.close();
await pgClient.end();
return results;
}
async function migrateTable(
sqlite: sqlite3.Database,
pg: Client,
table: string,
batchSize: number,
): Promise<MigrationResult> {
const result: MigrationResult = {
table,
rowsMigrated: 0,
duration: 0,
errors: [],
};
return new Promise(resolve => {
let offset = 0;
const processBatch = () => {
sqlite.all(`SELECT * FROM ${table} LIMIT ${batchSize} OFFSET ${offset}`, async (err, rows: any[]) => {
if (err) {
result.errors.push(err.message);
resolve(result);
return;
}
if (rows.length === 0) {
resolve(result);
return;
}
// Insert into PostgreSQL
for (const row of rows) {
try {
const columns = Object.keys(row);
const values = Object.values(row);
const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');
await pg.query(
`INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})
ON CONFLICT DO NOTHING`,
values,
);
result.rowsMigrated++;
} catch (insertErr: any) {
result.errors.push(`Row error: ${insertErr.message}`);
}
}
offset += batchSize;
processBatch();
});
};
processBatch();
});
}
async function resetPostgresSequences(pg: Client, tables: string[]): Promise<void> {
for (const table of tables) {
try {
await pg.query(`
SELECT setval(
pg_get_serial_sequence('${table}', 'id'),
COALESCE((SELECT MAX(id) FROM ${table}), 0) + 1,
false
)
`);
} catch (err) {
// Table might not have serial id
}
}
}
function getSqliteTables(db: sqlite3.Database): Promise<string[]> {
return new Promise((resolve, reject) => {
db.all("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", (err, rows: any[]) => {
if (err) reject(err);
else resolve(rows.map(r => r.name));
});
});
}
// CLI Entry point
const config: MigrationConfig = {
sqlitePath: process.env.SQLITE_PATH || './data/openwa.db',
postgresUrl: process.env.DATABASE_URL || 'postgresql://user:pass@localhost:5432/openwa',
batchSize: parseInt(process.env.BATCH_SIZE || '1000'),
};
migrateSqliteToPostgres(config)
.then(results => {
console.log('\n📊 Migration Summary:');
console.table(
results.map(r => ({
Table: r.table,
Rows: r.rowsMigrated,
'Time (ms)': r.duration,
Errors: r.errors.length,
})),
);
})
.catch(console.error);
```
### Step-by-Step Migration
```bash
# Step 1: Stop OpenWA
docker compose down
# Step 2: Backup current data
cp -r ./data ./data-backup-$(date +%Y%m%d)
docker exec openwa-db pg_dump -U postgres openwa > backup.sql
# Step 3: Setup PostgreSQL (if not exists)
docker compose -f docker-compose.postgres.yml up -d postgres
# Step 4: Run migration script
npx ts-node scripts/migrate-sqlite-to-postgres.ts
# Step 5: Update environment
export DATABASE_ADAPTER=postgresql
export DATABASE_URL=postgresql://user:pass@localhost:5432/openwa
# Step 6: Verify migration
psql $DATABASE_URL -c "SELECT COUNT(*) FROM sessions;"
psql $DATABASE_URL -c "SELECT COUNT(*) FROM messages;"
# Step 7: Start with PostgreSQL
docker compose -f docker-compose.postgres.yml up -d
# Step 8: Verify functionality
curl http://localhost:2785/health
```
### Verification Queries
```sql
-- Compare row counts
-- Run on both SQLite and PostgreSQL
-- Sessions
SELECT 'sessions' as table_name, COUNT(*) as count FROM sessions
UNION ALL
SELECT 'messages', COUNT(*) FROM messages
UNION ALL
SELECT 'contacts', COUNT(*) FROM contacts
UNION ALL
SELECT 'webhooks', COUNT(*) FROM webhooks
UNION ALL
SELECT 'api_keys', COUNT(*) FROM api_keys;
-- Verify foreign key integrity
SELECT m.id, m.session_id
FROM messages m
LEFT JOIN sessions s ON m.session_id = s.id
WHERE s.id IS NULL;
-- Check for data integrity
SELECT session_id, COUNT(*) as msg_count
FROM messages
GROUP BY session_id
ORDER BY msg_count DESC
LIMIT 10;
```
## 14.4 Session Auth State Transfer
### Understanding Session Auth
```
Session Auth Structure:
.wwebjs_auth/
├── session-{sessionId}/
│ ├── Default/
│ │ ├── IndexedDB/
│ │ ├── Local Storage/
│ │ └── Session Storage/
│ └── ... (Chrome profile data)
```
### Transfer Methods
#### Method 1: Direct File Copy (Same Host)
```bash
#!/bin/bash
# transfer-session.sh
SOURCE_DIR="/old-server/data/.wwebjs_auth"
TARGET_DIR="/new-server/data/.wwebjs_auth"
SESSION_ID=$1
if [ -z "$SESSION_ID" ]; then
echo "Usage: ./transfer-session.sh <session-id>"
exit 1
fi
# Stop both instances
echo "⏹️ Stopping services..."
ssh old-server "docker compose down"
ssh new-server "docker compose down"
# Copy session data
echo "📦 Copying session data..."
rsync -avz --progress \
"old-server:${SOURCE_DIR}/session-${SESSION_ID}/" \
"${TARGET_DIR}/session-${SESSION_ID}/"
# Copy database record
echo "📄 Exporting session record..."
ssh old-server "sqlite3 /data/openwa.db \
\"SELECT * FROM sessions WHERE id='${SESSION_ID}'\" \
-csv" > session_record.csv
# Import to new database
echo "📥 Importing session record..."
ssh new-server "sqlite3 /data/openwa.db \
\".import session_record.csv sessions\""
# Start new server
echo "▶️ Starting new server..."
ssh new-server "docker compose up -d"
echo "✅ Session ${SESSION_ID} transferred successfully"
```
#### Method 2: Export/Import via API
```typescript
// Export session from old server
// GET /api/sessions/{id}/export
interface SessionExport {
session: {
id: string;
name: string;
phoneNumber: string;
status: string;
config: object;
};
authData: string; // Base64 encoded tar.gz of auth folder
webhooks: WebhookConfig[];
contacts: Contact[];
}
// api/sessions/export.controller.ts
@Get(':id/export')
@UseGuards(ApiKeyGuard)
async exportSession(@Param('id') id: string): Promise<SessionExport> {
const session = await this.sessionService.findOne(id);
// Create tar.gz of auth folder
const authPath = path.join(this.authDir, `session-${id}`);
const tarBuffer = await this.createTarGz(authPath);
return {
session: {
id: session.id,
name: session.name,
phoneNumber: session.phoneNumber,
status: 'exported',
config: session.config,
},
authData: tarBuffer.toString('base64'),
webhooks: await this.webhookService.findBySession(id),
contacts: await this.contactService.findBySession(id),
};
}
// Import session to new server
// POST /api/sessions/import
@Post('import')
@UseGuards(ApiKeyGuard)
async importSession(@Body() data: SessionExport): Promise<Session> {
// 1. Create session record
const session = await this.sessionService.create({
id: data.session.id,
name: data.session.name,
config: data.session.config,
});
// 2. Extract auth data
const authBuffer = Buffer.from(data.authData, 'base64');
const authPath = path.join(this.authDir, `session-${session.id}`);
await this.extractTarGz(authBuffer, authPath);
// 3. Import webhooks
for (const webhook of data.webhooks) {
await this.webhookService.create({
...webhook,
sessionId: session.id,
});
}
// 4. Import contacts
for (const contact of data.contacts) {
await this.contactService.create({
...contact,
sessionId: session.id,
});
}
// 5. Initialize session (will use existing auth)
await this.whatsappService.initSession(session.id);
return session;
}
```
### Bulk Session Transfer
```typescript
// scripts/bulk-transfer-sessions.ts
interface TransferConfig {
sourceUrl: string;
sourceApiKey: string;
targetUrl: string;
targetApiKey: string;
sessionIds: string[] | 'all';
}
async function bulkTransferSessions(config: TransferConfig): Promise<void> {
const axios = require('axios');
// Get session list
let sessionIds = config.sessionIds;
if (sessionIds === 'all') {
const response = await axios.get(`${config.sourceUrl}/api/sessions`, {
headers: { 'X-API-Key': config.sourceApiKey },
});
sessionIds = response.data.map((s: any) => s.id);
}
console.log(`📋 Transferring ${sessionIds.length} sessions...`);
for (const sessionId of sessionIds) {
try {
console.log(`\n🔄 Processing session: ${sessionId}`);
// 1. Stop session on source
await axios.post(
`${config.sourceUrl}/api/sessions/${sessionId}/stop`,
{},
{ headers: { 'X-API-Key': config.sourceApiKey } },
);
// 2. Export from source
const exportResponse = await axios.get(`${config.sourceUrl}/api/sessions/${sessionId}/export`, {
headers: { 'X-API-Key': config.sourceApiKey },
});
// 3. Import to target
await axios.post(`${config.targetUrl}/api/sessions/import`, exportResponse.data, {
headers: { 'X-API-Key': config.targetApiKey },
});
console.log(`✅ Session ${sessionId} transferred`);
} catch (error: any) {
console.error(`❌ Failed to transfer ${sessionId}: ${error.message}`);
}
}
}
```
## 14.5 Version Upgrade Guide
### Upgrade Matrix
```mermaid
flowchart LR
V01[v0.1.x] -->|Direct| V02[v0.2.x]
V02 -->|Direct| V10[v1.0.x]
V01 -->|"Via v0.2"| V10
V10 -->|Direct| V11[v1.1.x]
V11 -->|Direct| V12[v1.2.x]
```
| From | To | Migration Type | Downtime |
| ------ | ------ | ------------------------- | --------- |
| v0.1.x | v0.2.x | Schema migration | 5-10 min |
| v0.2.x | v1.0.x | Schema + Config migration | 10-15 min |
| v0.1.x | v1.0.x | Must go via v0.2.x | 15-25 min |
| v1.x | v1.y | Usually automatic | < 5 min |
### v0.1 → v0.2 Migration
#### Breaking Changes in v0.2
```yaml
breaking_changes:
api:
- path: /api/session → /api/sessions (plural)
- path: /api/send → /api/sessions/{id}/messages
- auth: Basic Auth → API Key
config:
- DATABASE_PATH → DATABASE_URL (for PostgreSQL)
- WEBHOOK_URL → Managed via API
database:
- New tables: api_keys, webhook_logs
- Modified: sessions (added config JSON column)
- Modified: messages (added metadata column)
```
#### Migration Steps
```bash
#!/bin/bash
# upgrade-v01-to-v02.sh
set -e
echo "🚀 Upgrading OpenWA v0.1.x → v0.2.x"
# 1. Backup
echo "📦 Creating backup..."
BACKUP_DIR="./backups/v01-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BACKUP_DIR"
cp -r ./data "$BACKUP_DIR/"
cp .env "$BACKUP_DIR/"
# 2. Stop current version
echo "⏹️ Stopping v0.1..."
docker compose down
# 3. Run database migrations
echo "🔄 Running migrations..."
docker run --rm \
-v $(pwd)/data:/app/data \
-e DATABASE_URL=sqlite:///app/data/openwa.db \
ghcr.io/rmyndharis/openwa:0.2.0 \
npm run migration:run:prod # the prod image strips ts-node/TS source — use :prod
# 4. Migrate configuration
echo "⚙️ Migrating configuration..."
cat > .env.new << 'EOF'
# OpenWA v0.2.x Configuration
# Database (unchanged if using SQLite)
DATABASE_ADAPTER=sqlite
DATABASE_URL=sqlite:./data/openwa.db
# New in v0.2: API Key Authentication
API_KEY_ENABLED=true
# Generate with: openssl rand -hex 32
API_KEY=your-generated-api-key
# Webhook (now managed via API, but can set default)
DEFAULT_WEBHOOK_URL=${WEBHOOK_URL:-}
# Other settings migrated from v0.1
NODE_ENV=${NODE_ENV:-production}
PORT=${PORT:-3000}
LOG_LEVEL=${LOG_LEVEL:-info}
EOF
# Merge with existing .env
source .env
envsubst < .env.new > .env.migrated
mv .env.migrated .env
rm .env.new
# 5. Start new version
echo "▶️ Starting v0.2..."
docker compose pull
docker compose up -d
# 6. Wait for health
echo "⏳ Waiting for health check..."
sleep 10
curl -f http://localhost:2785/health || exit 1
# 7. Create API key for existing integrations
echo "🔑 Creating API key..."
docker exec openwa npm run cli -- create-api-key --name "migrated-key"
echo "✅ Upgrade complete!"
echo ""
echo "⚠️ IMPORTANT: Update your API clients:"
echo " - Change /api/session to /api/sessions"
echo " - Change /api/send to /api/sessions/{id}/messages"
echo " - Add X-API-Key header to all requests"
```
### v0.2 → v1.0 Migration
#### Breaking Changes in v1.0
```yaml
breaking_changes:
api:
- Groups API restructured
- Webhook payload format v2
- Rate limiting enforced
config:
- ENGINE_TYPE required (default: whatsapp-web.js; also accepts: baileys)
- STORAGE_ADAPTER required (default: local)
database:
- New tables: groups, group_participants, channels
- sessions.status enum expanded
```
#### Migration Steps
```bash
#!/bin/bash
# upgrade-v02-to-v10.sh
set -e
echo "🚀 Upgrading OpenWA v0.2.x → v1.0.0"
# Pre-flight checks
CURRENT_VERSION=$(docker inspect ghcr.io/rmyndharis/openwa --format '{{.Config.Labels.version}}' 2>/dev/null || echo "unknown")
echo "Current version: $CURRENT_VERSION"
# 1. Comprehensive backup
echo "📦 Creating comprehensive backup..."
BACKUP_DIR="./backups/v02-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BACKUP_DIR"
# Backup database
if [ "$DATABASE_ADAPTER" = "postgresql" ]; then
pg_dump $DATABASE_URL > "$BACKUP_DIR/database.sql"
else
cp ./data/openwa.db "$BACKUP_DIR/"
fi
# Backup auth sessions
cp -r ./data/.wwebjs_auth "$BACKUP_DIR/"
# Backup config
cp .env "$BACKUP_DIR/"
cp docker-compose.yml "$BACKUP_DIR/"
# 2. Export webhook configurations (per session, new format in v1.0)
echo "📤 Exporting webhooks..."
for sessionId in $(curl -s -H "X-API-Key: $API_KEY" \
http://localhost:2785/api/sessions | jq -r '.data[].id'); do
curl -s -H "X-API-Key: $API_KEY" \
"http://localhost:2785/api/sessions/${sessionId}/webhooks" \
> "$BACKUP_DIR/webhooks_${sessionId}.json"
done
# 3. Stop services
echo "⏹️ Stopping v0.2..."
docker compose down
# 4. Update configuration
echo "⚙️ Updating configuration..."
cat >> .env << 'EOF'
# New in v1.0
ENGINE_TYPE=whatsapp-web.js # default (Chromium-based); set to "baileys" for browser-free engine
STORAGE_ADAPTER=local
CACHE_ADAPTER=memory
# Webhook v2 format (recommended)
WEBHOOK_VERSION=2
EOF
# 5. Run database migrations
echo "🔄 Running migrations..."
docker run --rm \
-v $(pwd)/data:/app/data \
--env-file .env \
ghcr.io/rmyndharis/openwa:1.0.0 \
npm run migration:run
# 6. Migrate webhooks to new format
echo "🔄 Migrating webhooks..."
docker run --rm \
-v $(pwd)/data:/app/data \
-v "$BACKUP_DIR/webhooks.json:/tmp/webhooks.json" \
--env-file .env \
ghcr.io/rmyndharis/openwa:1.0.0 \
npm run cli -- migrate-webhooks /tmp/webhooks.json
# 7. Start new version
echo "▶️ Starting v1.0..."
sed -i 's/:0.2./:1.0./g' docker-compose.yml
docker compose pull
docker compose up -d
# 8. Health check
echo "⏳ Waiting for health check..."
for i in {1..30}; do
if curl -sf http://localhost:2785/health > /dev/null; then
echo "✅ Health check passed"
break
fi
sleep 2
done
# 9. Verify sessions
echo "🔍 Verifying sessions..."
curl -s -H "X-API-Key: $API_KEY" \
http://localhost:2785/api/sessions | jq '.[] | {id, status}'
echo ""
echo "✅ Upgrade to v1.0.0 complete!"
echo ""
echo "📝 Post-upgrade tasks:"
echo " 1. Update webhook consumers for v2 payload format"
echo " 2. Test all active sessions"
echo " 3. Review new rate limits"
echo " 4. Check Groups API if used"
```
## 14.6 Rollback Procedures
### Quick Rollback (< 24 hours)
```bash
#!/bin/bash
# rollback.sh
BACKUP_DIR=$1
TARGET_VERSION=$2
if [ -z "$BACKUP_DIR" ] || [ -z "$TARGET_VERSION" ]; then
echo "Usage: ./rollback.sh <backup-dir> <target-version>"
echo "Example: ./rollback.sh ./backups/v02-20260215-120000 0.2.0"
exit 1
fi
echo "🔄 Rolling back to v${TARGET_VERSION}..."
# 1. Stop current
docker compose down
# 2. Restore database
echo "📥 Restoring database..."
if [ -f "$BACKUP_DIR/database.sql" ]; then
# PostgreSQL
psql $DATABASE_URL < "$BACKUP_DIR/database.sql"
else
# SQLite
cp "$BACKUP_DIR/openwa.db" ./data/
fi
# 3. Restore auth sessions
echo "📥 Restoring auth sessions..."
rm -rf ./data/.wwebjs_auth
cp -r "$BACKUP_DIR/.wwebjs_auth" ./data/
# 4. Restore configuration
echo "📥 Restoring configuration..."
cp "$BACKUP_DIR/.env" .
cp "$BACKUP_DIR/docker-compose.yml" .
# 5. Start old version
echo "▶️ Starting v${TARGET_VERSION}..."
docker compose pull
docker compose up -d
# 6. Verify
sleep 10
curl -f http://localhost:2785/health && echo "✅ Rollback successful"
```
### Rollback Decision Tree
```mermaid
flowchart TD
A[Issue Detected] --> B{Severity?}
B -->|Critical| C[Immediate Rollback]
B -->|High| D{Fix Available?}
B -->|Medium| E{Can Wait?}
D -->|Yes| F[Apply Hotfix]
D -->|No| C
E -->|Yes| G[Schedule Fix]
E -->|No| D
C --> H{Backup Age?}
H -->|< 24h| I[Full Rollback]
H -->|> 24h| J[Partial Rollback + Data Merge]
F --> K[Monitor]
I --> K
J --> K
G --> K
```
## 14.7 Environment Migration
### Development → Staging
```yaml
# environments/staging.yml
migration:
source: development
target: staging
steps:
- name: Export sessions (anonymized)
command: |
npm run cli -- export-sessions \
--anonymize \
--output /tmp/sessions-staging.json
- name: Reset database
command: |
npm run migration:fresh
- name: Import test sessions
command: |
npm run cli -- import-sessions /tmp/sessions-staging.json
- name: Configure staging webhooks
command: |
npm run cli -- set-webhook \
--url https://staging-webhook.example.com/openwa \
--events message,status
- name: Set staging limits
command: |
npm run cli -- set-limits \
--max-sessions 10 \
--rate-limit 100/min
```
### Staging → Production
```yaml
# environments/production.yml
migration:
source: staging
target: production
pre_checks:
- name: Staging tests passed
command: npm run test:e2e -- --tag staging
required: true
- name: Performance benchmarks
command: npm run benchmark
threshold: 'p99 < 500ms'
- name: Security scan
command: npm audit --production
required: true
steps:
- name: Blue-green deployment
type: blue-green
config:
health_check: /health
switch_after: 60s
rollback_on_error: true
- name: Gradual traffic shift
type: canary
config:
initial_percentage: 10
increment: 10
interval: 5m
success_threshold: 99%
```
## 14.8 Data Export/Import
### Full Export
```typescript
// scripts/full-export.ts
interface ExportOptions {
outputDir: string;
includeMedia: boolean;
includeLogs: boolean;
compress: boolean;
}
async function fullExport(options: ExportOptions): Promise<void> {
const fs = require('fs-extra');
const archiver = require('archiver');
const exportDir = path.join(options.outputDir, `export-${Date.now()}`);
await fs.ensureDir(exportDir);
// 1. Export database tables
console.log('📊 Exporting database...');
const tables = ['sessions', 'messages', 'contacts', 'webhooks', 'api_keys'];
for (const table of tables) {
const data = await db.query(`SELECT * FROM ${table}`);
await fs.writeJson(path.join(exportDir, `${table}.json`), data, { spaces: 2 });
}
// 2. Export auth sessions
console.log('🔐 Exporting auth sessions...');
await fs.copy('./data/.wwebjs_auth', path.join(exportDir, 'auth'));
// 3. Export media (optional)
if (options.includeMedia) {
console.log('📁 Exporting media files...');
await fs.copy('./data/media', path.join(exportDir, 'media'));
}
// 4. Export logs (optional)
if (options.includeLogs) {
console.log('📝 Exporting logs...');
await fs.copy('./logs', path.join(exportDir, 'logs'));
}
// 5. Export configuration (sanitized)
console.log('⚙️ Exporting configuration...');
const config = {
version: process.env.npm_package_version,
exportedAt: new Date().toISOString(),
settings: {
DATABASE_ADAPTER: process.env.DATABASE_ADAPTER,
STORAGE_ADAPTER: process.env.STORAGE_ADAPTER,
ENGINE_TYPE: process.env.ENGINE_TYPE,
},
};
await fs.writeJson(path.join(exportDir, 'config.json'), config, { spaces: 2 });
// 6. Compress (optional)
if (options.compress) {
console.log('🗜️ Compressing export...');
const output = fs.createWriteStream(`${exportDir}.tar.gz`);
const archive = archiver('tar', { gzip: true });
archive.pipe(output);
archive.directory(exportDir, false);
await archive.finalize();
await fs.remove(exportDir);
console.log(`✅ Export complete: ${exportDir}.tar.gz`);
} else {
console.log(`✅ Export complete: ${exportDir}`);
}
}
```
### Full Import
```typescript
// scripts/full-import.ts
interface ImportOptions {
inputPath: string;
mergeStrategy: 'replace' | 'merge' | 'skip-existing';
dryRun: boolean;
}
async function fullImport(options: ImportOptions): Promise<void> {
const fs = require('fs-extra');
const tar = require('tar');
let importDir = options.inputPath;
// Extract if compressed
if (options.inputPath.endsWith('.tar.gz')) {
importDir = options.inputPath.replace('.tar.gz', '-extracted');
await tar.extract({
file: options.inputPath,
cwd: importDir,
});
}
// Validate export
const configPath = path.join(importDir, 'config.json');
if (!(await fs.pathExists(configPath))) {
throw new Error('Invalid export: config.json not found');
}
const exportConfig = await fs.readJson(configPath);
console.log(`📦 Importing from v${exportConfig.version}`);
console.log(`📅 Exported at: ${exportConfig.exportedAt}`);
if (options.dryRun) {
console.log('🔍 DRY RUN - No changes will be made');
}
// Import order matters (foreign keys)
const importOrder = ['sessions', 'api_keys', 'webhooks', 'contacts', 'messages'];
for (const table of importOrder) {
const dataPath = path.join(importDir, `${table}.json`);
if (!(await fs.pathExists(dataPath))) continue;
const data = await fs.readJson(dataPath);
console.log(`📥 Importing ${table}: ${data.length} records`);
if (!options.dryRun) {
await importTable(table, data, options.mergeStrategy);
}
}
// Import auth sessions
const authPath = path.join(importDir, 'auth');
if (await fs.pathExists(authPath)) {
console.log('🔐 Importing auth sessions...');
if (!options.dryRun) {
await fs.copy(authPath, './data/.wwebjs_auth', {
overwrite: options.mergeStrategy === 'replace',
});
}
}
console.log('✅ Import complete');
}
```
## 14.9 Troubleshooting Migration Issues
### Common Issues
| Issue | Cause | Solution |
| ------------------------ | ---------------------- | ---------------------------- |
| Session not reconnecting | Auth data corrupted | Re-scan QR code |
| Foreign key errors | Wrong import order | Use provided import order |
| Duplicate key errors | Existing data conflict | Use merge strategy |
| Permission denied | File ownership | `chown -R 1000:1000 ./data` |
| Out of memory | Large export | Increase Docker memory limit |
### PostgreSQL: boot crash-loop after upgrading a `DATABASE_SYNCHRONIZE=true` deployment
**Symptom:** after upgrade, the container crash-loops on boot. `docker logs` shows one of:
- `column "id" is of type uuid but default expression is of type character varying`
- `foreign key constraint ... cannot be implemented ... incompatible types: character varying and uuid`
**Cause:** a deployment previously bootstrapped with `DATABASE_SYNCHRONIZE=true` on PostgreSQL has native `uuid` `id`/FK columns (TypeORM derives them from `@PrimaryGeneratedColumn('uuid')`), while the migration chain assumes `varchar`. The two are incompatible, and migrations run unconditionally on the Postgres data connection (`migrationsRun: true`), so boot cannot complete (issue #690).
**Fix (automatic for most deployments):** OpenWA ships a guard migration (`NormalizeSynchronizeUuidColumns`, ordered before the first collision) that converts the affected `uuid` columns to `varchar` on the next boot. For small-to-medium databases this is transparent — upgrade and restart.
**Large-database maintenance window:** the conversion rewrites `messages` and `message_batches` in full under an exclusive lock. If either table is large (millions of rows) and your orchestrator's liveness/readiness grace is tight, run the migration against the stopped app during a planned window:
```bash
docker compose down
DATABASE_TYPE=postgres DATABASE_HOST=... DATABASE_USERNAME=... \
DATABASE_PASSWORD=... DATABASE_NAME=openwa npm run migration:run
docker compose up -d
```
(The CLI runner does not impose a statement timeout; the migration lifts it via `SET LOCAL`.)
`DATABASE_SYNCHRONIZE=true` on PostgreSQL is unsupported for production. Leave it unset (the default `false`) and let migrations manage the schema.
### Debug Commands
```bash
# Check database integrity
sqlite3 ./data/openwa.db "PRAGMA integrity_check;"
# Verify auth session files
ls -la ./data/.wwebjs_auth/session-*/
# Check file permissions
stat ./data/openwa.db
stat ./data/.wwebjs_auth
# Verify PostgreSQL connection
psql $DATABASE_URL -c "SELECT version();"
# Check migration status
npm run migration:status
# Force re-run specific migration
npm run migration:run -- --name CreateApiKeysTable
```
---
<div align="center">
[← 13 - Horizontal Scaling Guide](./13-horizontal-scaling.md) · [Documentation Index](./README.md) · [Next: 15 - Project Roadmap →](./15-project-roadmap.md)
</div>