# Redis Migration Guide ## Overview This guide covers migrating from in-memory KV storage to Redis for multi-instance SaaS deployments. The current in-memory session storage works well for single-instance deployments but requires centralized storage for horizontal scaling. ### When to Migrate **Current Setup (Sufficient For):** - Single-instance deployments - Development environments - Small-scale production deployments **Redis Required For:** - Multiple server instances behind load balancer - Container orchestration (Kubernetes, Docker Swarm) - High-availability SaaS deployments - Session persistence across server restarts ## What Needs Migration ### Critical Data (Must Migrate) - **Sessions** (`sessionsKV`): Essential for authentication across instances - **CSRF tokens**: Stored within session objects - **Rate limiting data**: Currently in-memory Map in `rate-limiter.ts` ### Optional Data (Can Remain File-based) - Bookings, treatments, reviews, gallery photos - Can stay in file-based KV for now - Migrate later if performance becomes an issue ## Redis Setup ### Installation Options #### Self-hosted Redis ```bash # Docker docker run -d -p 6379:6379 redis:alpine # Ubuntu/Debian sudo apt update sudo apt install redis-server # macOS (Homebrew) brew install redis brew services start redis ``` #### Managed Services - **Redis Cloud**: Managed Redis service - **AWS ElastiCache**: Redis-compatible cache - **Azure Cache for Redis**: Microsoft's managed service - **DigitalOcean Managed Databases**: Redis option available ### Configuration Add to `.env`: ```bash # Redis Configuration (required for multi-instance deployments) REDIS_URL=redis://localhost:6379 REDIS_TLS_ENABLED=false # Set to true for production REDIS_PASSWORD=your_redis_password # Optional ``` ## Code Changes ### 1. Install Redis Client ```bash pnpm add ioredis pnpm add -D @types/ioredis ``` ### 2. Create Redis KV Adapter Create `src/server/lib/create-redis-kv.ts`: ```typescript import Redis from 'ioredis'; interface RedisKVStore { getItem(key: string): Promise; setItem(key: string, value: T): Promise; removeItem(key: string): Promise; getAllItems(): Promise; subscribe(): AsyncIterable; } export function createRedisKV(namespace: string): RedisKVStore { const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379', { retryDelayOnFailover: 100, enableReadyCheck: false, maxRetriesPerRequest: null, lazyConnect: true, }); const prefix = `${namespace}:`; return { async getItem(key: string): Promise { const value = await redis.get(`${prefix}${key}`); return value ? JSON.parse(value) : null; }, async setItem(key: string, value: T): Promise { await redis.set(`${prefix}${key}`, JSON.stringify(value)); }, async removeItem(key: string): Promise { await redis.del(`${prefix}${key}`); }, async getAllItems(): Promise { const keys = await redis.keys(`${prefix}*`); if (keys.length === 0) return []; const values = await redis.mget(...keys); return values .filter(v => v !== null) .map(v => JSON.parse(v!)); }, async* subscribe(): AsyncIterable { const pubsub = new Redis(process.env.REDIS_URL || 'redis://localhost:6379'); await pubsub.subscribe(`${namespace}:changes`); for await (const message of pubsub) { if (message[0] === 'message' && message[1] === `${namespace}:changes`) { yield; } } } }; } // Helper to notify subscribers of changes export async function notifyRedisChanges(namespace: string): Promise { const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379'); await redis.publish(`${namespace}:changes`, 'update'); await redis.quit(); } ``` ### 3. Update Session Storage In `src/server/lib/auth.ts`: ```typescript import { createRedisKV } from './create-redis-kv.js'; // Replace this line: // export const sessionsKV = createKV("sessions"); // With this: export const sessionsKV = createRedisKV("sessions"); ``` ### 4. Update Rate Limiter In `src/server/lib/rate-limiter.ts`: ```typescript import Redis from 'ioredis'; const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379', { retryDelayOnFailover: 100, enableReadyCheck: false, maxRetriesPerRequest: null, lazyConnect: true, }); export async function checkBookingRateLimit( email?: string, ip?: string ): Promise<{ allowed: boolean; resetTime?: number }> { const now = Date.now(); const windowMs = 15 * 60 * 1000; // 15 minutes const maxRequests = 3; const results = await Promise.all([ email ? checkRateLimit(`booking:email:${email}`, maxRequests, windowMs) : { allowed: true }, ip ? checkRateLimit(`booking:ip:${ip}`, maxRequests, windowMs) : { allowed: true } ]); return { allowed: results.every(r => r.allowed), resetTime: results.find(r => !r.allowed)?.resetTime }; } async function checkRateLimit( key: string, maxRequests: number, windowMs: number ): Promise<{ allowed: boolean; resetTime?: number }> { const now = Date.now(); const windowStart = now - windowMs; // Use Redis sorted set for sliding window const pipeline = redis.pipeline(); // Remove old entries pipeline.zremrangebyscore(key, 0, windowStart); // Count current entries pipeline.zcard(key); // Add current request pipeline.zadd(key, now, `${now}-${Math.random()}`); // Set expiry pipeline.expire(key, Math.ceil(windowMs / 1000)); const results = await pipeline.exec(); const currentCount = results?.[1]?.[1] as number || 0; return { allowed: currentCount < maxRequests, resetTime: currentCount >= maxRequests ? windowStart + windowMs : undefined }; } ``` ### 5. Environment Configuration Update `.env.example`: ```bash # Redis Configuration (optional - required for multi-instance SaaS deployments) # For single-instance deployments, the default in-memory storage is sufficient # See docs/redis-migration.md for migration guide REDIS_URL=redis://localhost:6379 REDIS_TLS_ENABLED=false # Enable for production REDIS_PASSWORD=your_redis_password # Optional ``` ## Migration Strategy ### Development Testing 1. **Start Redis locally**: ```bash docker run -d -p 6379:6379 redis:alpine ``` 2. **Test session functionality**: - Login/logout - Session validation - CSRF token generation/validation - Session rotation 3. **Test rate limiting**: - Verify rate limit enforcement - Check sliding window behavior 4. **Simulate multi-instance**: - Run multiple server instances - Verify session sharing works ### Staging Deployment 1. **Deploy Redis in staging** 2. **Run single instance first** to verify functionality 3. **Scale to multiple instances** and test session sharing 4. **Monitor Redis memory usage** and connection pool ### Production Rollout #### Option A: Blue/Green Deployment 1. Deploy new version with Redis to green environment 2. Test thoroughly with production-like data 3. Switch traffic to green environment 4. **Note**: Existing sessions will be lost (users need to re-login) #### Option B: Gradual Migration 1. Deploy Redis alongside existing system 2. Write to both stores temporarily (dual-write) 3. Read from Redis first, fallback to in-memory 4. After verification period, remove in-memory store ## Monitoring & Maintenance ### Key Metrics - **Redis memory usage**: Monitor `used_memory` and `used_memory_peak` - **Connection pool**: Track active connections and pool utilization - **Session operations**: Monitor session creation/validation latency - **Rate limit hits**: Track rate limit enforcement effectiveness ### Backup Strategy - **Enable Redis persistence**: Configure RDB snapshots or AOF logging - **Regular backups**: Backup Redis data regularly - **Session data**: Ephemeral, but rate limit data should be backed up ### Scaling Considerations - **Redis Cluster**: For horizontal scaling across multiple nodes - **Redis Sentinel**: For high availability with automatic failover - **Connection pooling**: Configure appropriate pool sizes ## Rollback Plan If issues arise, you can revert to in-memory storage: 1. **Remove Redis imports** and revert to `createKV` 2. **Remove Redis environment variables** 3. **Redeploy** with in-memory storage 4. **Note**: All sessions will be lost (users need to re-login) ## Cost Considerations ### Redis Hosting Costs - **Self-hosted**: Server costs + maintenance - **Managed services**: - Redis Cloud: ~$7/month for 30MB - AWS ElastiCache: ~$15/month for t3.micro - Azure Cache: ~$16/month for Basic tier ### Memory Requirements **Session storage**: Minimal - Example: 1000 concurrent users × 1KB per session = ~1MB - CSRF tokens add minimal overhead **Rate limiting**: Negligible - Sliding window data is automatically cleaned up - Minimal memory footprint per IP/email ## Alternative Solutions ### Sticky Sessions - Load balancer routes user to same instance - **Pros**: Simpler implementation - **Cons**: Less resilient, harder to scale ### Database-backed Sessions - Use PostgreSQL/MySQL instead of Redis - **Pros**: No additional infrastructure - **Cons**: Higher latency, more database load ### JWT Tokens - Stateless authentication tokens - **Pros**: No server-side session storage needed - **Cons**: No server-side session invalidation, different security model ## References - [Session Management Guide](session-management.md) - [Rate Limiting Documentation](rate-limiting.md) - [Redis Documentation](https://redis.io/documentation) - [ioredis Library](https://github.com/luin/ioredis) - [Redis Best Practices](https://redis.io/docs/manual/admin/)