# CouchDB Migration Guide ## Overview This guide provides comprehensive documentation for migrating the Adopt-a-Street application from MongoDB to CouchDB. This migration improves scalability, provides better offline capabilities, and simplifies deployment. ## Migration Benefits ### Why CouchDB? 1. **Better Scalability**: CouchDB's master-master replication allows for easier scaling across multiple nodes 2. **Offline-First**: Built-in sync capabilities enable offline functionality 3. **Simpler Deployment**: No complex schema migrations required 4. **HTTP API**: Native REST API simplifies client-server communication 5. **Document Validation**: Built-in validation functions ensure data integrity 6. **MapReduce Views**: Powerful querying capabilities for complex data analysis ## Architecture Changes ### Before (MongoDB) ``` Backend (Node.js/Express) ├── Mongoose ODM ├── MongoDB Database └── Schema Definitions ``` ### After (CouchDB) ``` Backend (Node.js/Express) ├── Nano Client ├── CouchDB Database └── Document Models ``` ## Migration Steps ### Phase 1: Setup CouchDB #### 1. Install CouchDB ```bash # Ubuntu/Debian sudo apt-get install couchdb # macOS brew install couchdb # Docker docker run -d -p 5984:5984 --name couchdb couchdb:latest ``` #### 2. Configure CouchDB ```bash # Create admin user curl -X PUT http://localhost:5984/_config/admins/admin -d '"password"' # Create database curl -X PUT http://admin:password@localhost:5984/adopt-a-street ``` #### 3. Install Nano Client ```bash cd backend bun add nano ``` ### Phase 2: Update Backend Configuration #### 1. Environment Variables Update `.env` file: ```env # Remove MongoDB # MONGO_URI=mongodb://localhost:27017/adopt-a-street # Add CouchDB COUCHDB_URL=http://localhost:5984 COUCHDB_DB_NAME=adopt-a-street COUCHDB_USERNAME=admin COUCHDB_PASSWORD=password ``` #### 2. Create CouchDB Service Create `backend/services/couchdbService.js`: ```javascript const nano = require('nano')( `${process.env.COUCHDB_URL}/${process.env.COUCHDB_DB_NAME}` ); class CouchDBService { constructor() { this.db = nano; } async create(doc) { return await this.db.insert(doc); } async get(id) { return await this.db.get(id); } async update(id, doc) { const existing = await this.get(id); doc._rev = existing._rev; return await this.db.insert(doc); } async delete(id) { const doc = await this.get(id); return await this.db.destroy(id, doc._rev); } async find(view, params = {}) { return await this.db.view('design_doc', view, params); } async all(params = {}) { return await this.db.list(params); } } module.exports = new CouchDBService(); ``` ### Phase 3: Migrate Data Models #### 1. User Model Create `backend/models/User.js`: ```javascript const couchdbService = require('../services/couchdbService'); class User { static async create(userData) { const user = { _id: `user:${userData.email}`, type: 'user', name: userData.name, email: userData.email, password: userData.password, // Hashed points: 0, isPremium: false, adoptedStreets: [], completedTasks: [], earnedBadges: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; return await couchdbService.create(user); } static async findById(id) { try { return await couchdbService.get(id); } catch (error) { if (error.statusCode === 404) return null; throw error; } } static async findByEmail(email) { const result = await couchdbService.find('by_email', { key: email }); return result.rows.length > 0 ? result.rows[0].value : null; } static async update(id, updateData) { const user = await this.findById(id); if (!user) throw new Error('User not found'); Object.assign(user, updateData); user.updatedAt = new Date().toISOString(); return await couchdbService.update(id, user); } static async delete(id) { return await couchdbService.delete(id); } } module.exports = User; ``` #### 2. Street Model Create `backend/models/Street.js`: ```javascript const couchdbService = require('../services/couchdbService'); class Street { static async create(streetData) { const street = { _id: `street:${Date.now()}:${Math.random().toString(36).substr(2, 9)}`, type: 'street', name: streetData.name, location: streetData.location, // GeoJSON description: streetData.description, status: 'active', adoptedBy: null, adoptionDate: null, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; return await couchdbService.create(street); } static async findById(id) { try { return await couchdbService.get(id); } catch (error) { if (error.statusCode === 404) return null; throw error; } } static async findAll(params = {}) { const result = await couchdbService.find('all_streets', params); return result.rows.map(row => row.value); } static async findNearby(coordinates, maxDistance = 1000) { const result = await couchdbService.find('nearby_streets', { lat: coordinates[1], lon: coordinates[0], radius: maxDistance }); return result.rows.map(row => row.value); } static async update(id, updateData) { const street = await this.findById(id); if (!street) throw new Error('Street not found'); Object.assign(street, updateData); street.updatedAt = new Date().toISOString(); return await couchdbService.update(id, street); } static async delete(id) { return await couchdbService.delete(id); } } module.exports = Street; ``` ### Phase 4: Create Design Documents #### 1. Create Design Document Create `backend/couchdb/design-doc.json`: ```json { "_id": "_design/adopt_a_street", "views": { "by_email": { "map": "function(doc) { if (doc.type === 'user' && doc.email) { emit(doc.email, doc); } }" }, "all_streets": { "map": "function(doc) { if (doc.type === 'street') { emit(doc._id, doc); } }" }, "streets_by_user": { "map": "function(doc) { if (doc.type === 'street' && doc.adoptedBy) { emit(doc.adoptedBy, doc); } }" }, "tasks_by_street": { "map": "function(doc) { if (doc.type === 'task' && doc.street) { emit(doc.street, doc); } }" }, "posts_by_user": { "map": "function(doc) { if (doc.type === 'post' && doc.user) { emit(doc.user, doc); } }" }, "events_by_date": { "map": "function(doc) { if (doc.type === 'event' && doc.date) { emit(doc.date, doc); } }" }, "nearby_streets": { "map": "function(doc) { if (doc.type === 'street' && doc.location && doc.location.coordinates) { emit([doc.location.coordinates[1], doc.location.coordinates[0]], doc); } }" } }, "validate_doc_update": "function(newDoc, oldDoc, userCtx) { if (newDoc.type && !['user', 'street', 'task', 'post', 'event', 'reward', 'report'].includes(newDoc.type)) { throw({forbidden: 'Invalid document type'}); } }" } ``` #### 2. Install Design Document ```bash curl -X PUT http://admin:password@localhost:5984/adopt-a-street/_design/adopt_a_street \ -H "Content-Type: application/json" \ -d @backend/couchdb/design-doc.json ``` ### Phase 5: Update Routes #### 1. Auth Routes Update `backend/routes/auth.js`: ```javascript const User = require('../models/User'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); // Register router.post('/register', async (req, res) => { try { const { name, email, password } = req.body; // Check if user exists const existingUser = await User.findByEmail(email); if (existingUser) { return res.status(400).json({ msg: 'User already exists' }); } // Hash password const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash(password, salt); // Create user const user = await User.create({ name, email, password: hashedPassword }); // Create JWT const token = jwt.sign( { userId: user._id }, process.env.JWT_SECRET, { expiresIn: '24h' } ); res.json({ token, user: { id: user._id, name: user.name, email: user.email } }); } catch (err) { console.error(err.message); res.status(500).send('Server error'); } }); // Login router.post('/login', async (req, res) => { try { const { email, password } = req.body; // Find user const user = await User.findByEmail(email); if (!user) { return res.status(400).json({ msg: 'Invalid credentials' }); } // Check password const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) { return res.status(400).json({ msg: 'Invalid credentials' }); } // Create JWT const token = jwt.sign( { userId: user._id }, process.env.JWT_SECRET, { expiresIn: '24h' } ); res.json({ token, user: { id: user._id, name: user.name, email: user.email } }); } catch (err) { console.error(err.message); res.status(500).send('Server error'); } }); ``` ### Phase 6: Data Migration Script #### 1. Create Migration Script Create `scripts/migrate-to-couchdb.js`: ```javascript const mongoose = require('mongoose'); const nano = require('nano')('http://localhost:5984/adopt-a-street'); // MongoDB Models (old) const User = require('../backend/models/User'); const Street = require('../backend/models/Street'); const Task = require('../backend/models/Task'); const Post = require('../backend/models/Post'); const Event = require('../backend/models/Event'); const Reward = require('../backend/models/Reward'); const Report = require('../backend/models/Report'); async function migrateUsers() { console.log('Migrating users...'); const users = await User.find(); for (const user of users) { const couchUser = { _id: `user:${user.email}`, type: 'user', name: user.name, email: user.email, password: user.password, points: user.points || 0, isPremium: user.isPremium || false, adoptedStreets: user.adoptedStreets || [], completedTasks: user.completedTasks || [], earnedBadges: user.earnedBadges || [], createdAt: user.createdAt, updatedAt: user.updatedAt }; await nano.insert(couchUser); } console.log(`Migrated ${users.length} users`); } async function migrateStreets() { console.log('Migrating streets...'); const streets = await Street.find(); for (const street of streets) { const couchStreet = { _id: `street:${street._id}`, type: 'street', name: street.name, location: street.location, description: street.description, status: street.status || 'active', adoptedBy: street.adoptedBy, adoptionDate: street.adoptionDate, createdAt: street.createdAt, updatedAt: street.updatedAt }; await nano.insert(couchStreet); } console.log(`Migrated ${streets.length} streets`); } // Add similar functions for other models... async function runMigration() { try { // Connect to MongoDB await mongoose.connect('mongodb://localhost:27017/adopt-a-street'); // Run migrations await migrateUsers(); await migrateStreets(); // await migrateTasks(); // await migratePosts(); // await migrateEvents(); // await migrateRewards(); // await migrateReports(); console.log('Migration completed successfully!'); process.exit(0); } catch (error) { console.error('Migration failed:', error); process.exit(1); } } runMigration(); ``` #### 2. Run Migration ```bash cd scripts node migrate-to-couchdb.js ``` ### Phase 7: Update Tests #### 1. Update Test Setup Update `backend/__tests__/setup.js`: ```javascript const { CouchDBMem } = require('@couchdb/test-helpers'); let couchdb; beforeAll(async () => { couchdb = new CouchDBMem(); await couchdb.start(); // Create test database await couchdb.createDb('adopt-a-street'); // Install design document const designDoc = require('../../couchdb/design-doc.json'); await couchdb.insertDoc('adopt-a-street', designDoc); }); afterAll(async () => { await couchdb.stop(); }); beforeEach(async () => { // Clean up test data await couchdb.clearDb('adopt-a-street'); }); ``` #### 2. Update Test Helpers Update `backend/__tests__/utils/testHelpers.js`: ```javascript const User = require('../../models/User'); const Street = require('../../models/Street'); const Task = require('../../models/Task'); const Post = require('../../models/Post'); const Event = require('../../models/Event'); const Reward = require('../../models/Reward'); const Report = require('../../models/Report'); const jwt = require('jsonwebtoken'); async function createTestUser(userData = {}) { const defaultUser = { name: 'Test User', email: 'test@example.com', password: 'password123' }; const user = await User.create({ ...defaultUser, ...userData }); const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET); return { user, token }; } async function createTestStreet(streetData = {}) { const defaultStreet = { name: 'Test Street', location: { type: 'Point', coordinates: [-74.0060, 40.7128] }, description: 'A test street' }; return await Street.create({ ...defaultStreet, ...streetData }); } // Add similar functions for other models... module.exports = { createTestUser, createTestStreet, createTestTask, createTestPost, createTestEvent, createTestReward, createTestReport }; ``` ## Deployment Considerations ### 1. Docker Configuration Update `docker-compose.yml`: ```yaml version: '3.8' services: couchdb: image: couchdb:latest ports: - "5984:5984" environment: - COUCHDB_USER=admin - COUCHDB_PASSWORD=password volumes: - couchdb_data:/opt/couchdb/data networks: - adopt-a-street backend: build: ./backend ports: - "5000:5000" environment: - COUCHDB_URL=http://couchdb:5984 - COUCHDB_DB_NAME=adopt-a-street - COUCHDB_USER=admin - COUCHDB_PASSWORD=password depends_on: - couchdb networks: - adopt-a-street volumes: couchdb_data: networks: adopt-a-street: driver: bridge ``` ### 2. Kubernetes Deployment Update `deploy/k8s/couchdb-statefulset.yaml`: ```yaml apiVersion: apps/v1 kind: StatefulSet metadata: name: couchdb spec: serviceName: couchdb replicas: 1 selector: matchLabels: app: couchdb template: metadata: labels: app: couchdb spec: containers: - name: couchdb image: couchdb:latest ports: - containerPort: 5984 env: - name: COUCHDB_USER valueFrom: secretKeyRef: name: couchdb-secret key: username - name: COUCHDB_PASSWORD valueFrom: secretKeyRef: name: couchdb-secret key: password volumeMounts: - name: couchdb-data mountPath: /opt/couchdb/data volumeClaimTemplates: - metadata: name: couchdb-data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 1Gi ``` ## Performance Optimizations ### 1. Indexing Create additional indexes for common queries: ```javascript // Add to design document "indexes": { "streets_by_status": { "map": "function(doc) { if (doc.type === 'street') { emit(doc.status, doc); } }" }, "tasks_by_assignee": { "map": "function(doc) { if (doc.type === 'task' && doc.assignedTo) { emit(doc.assignedTo, doc); } }" }, "events_by_organizer": { "map": "function(doc) { if (doc.type === 'event' && doc.organizer) { emit(doc.organizer, doc); } }" } } ``` ### 2. Caching Implement Redis caching for frequently accessed data: ```javascript const redis = require('redis'); const client = redis.createClient(); async function getCachedUser(id) { const cached = await client.get(`user:${id}`); if (cached) return JSON.parse(cached); const user = await User.findById(id); await client.setex(`user:${id}`, 3600, JSON.stringify(user)); return user; } ``` ## Monitoring and Maintenance ### 1. Health Checks ```javascript // Add to backend/routes/health.js router.get('/couchdb', async (req, res) => { try { const response = await couchdbService.db.info(); res.json({ status: 'healthy', couchdb: response }); } catch (error) { res.status(500).json({ status: 'unhealthy', error: error.message }); } }); ``` ### 2. Backup Strategy ```bash # Create backup script #!/bin/bash DATE=$(date +%Y%m%d_%H%M%S) BACKUP_DIR="/backups/couchdb" # Create backup curl -X GET http://admin:password@localhost:5984/adopt-a-street/_all_docs?include_docs=true \ -o "$BACKUP_DIR/backup_$DATE.json" # Compress backup gzip "$BACKUP_DIR/backup_$DATE.json" # Clean old backups (keep last 7 days) find $BACKUP_DIR -name "backup_*.json.gz" -mtime +7 -delete ``` ## Troubleshooting ### Common Issues 1. **Connection Errors** - Check CouchDB is running: `curl http://localhost:5984` - Verify credentials in environment variables - Check network connectivity 2. **Document Conflicts** - Use revision numbers when updating documents - Implement conflict resolution strategies - Consider using bulk operations for multiple updates 3. **Performance Issues** - Add appropriate indexes - Use pagination for large result sets - Consider view caching for frequently accessed data 4. **Migration Failures** - Check MongoDB connection - Verify data format compatibility - Run migration in smaller batches ## Rollback Plan If you need to rollback to MongoDB: 1. **Stop Application** ```bash docker-compose down ``` 2. **Restore MongoDB Data** ```bash mongorestore --db adopt-a-street /path/to/mongodb/backup ``` 3. **Update Configuration** - Restore original `.env` file - Revert code changes to use Mongoose 4. **Restart Application** ```bash docker-compose up -d ``` ## Conclusion This migration guide provides a comprehensive approach to migrating from MongoDB to CouchDB. The key benefits include improved scalability, offline capabilities, and simplified deployment. Take time to test thoroughly in a staging environment before deploying to production. For additional support: - [CouchDB Documentation](https://docs.couchdb.org/) - [Nano Client Documentation](https://github.com/apache/couchdb-nano) - [Community Forums](https://couchdb.apache.org/#mailing-lists) --- **Migration Date**: 2025-11-03 **CouchDB Version**: 3.3+ **Node.js Version**: 18+ **Test Coverage**: Maintained at 55%+