Skip to main content

Security Best Practices

1. Credential Management

✅ Good: Use Service Role Keys in Backend Only
// Backend/API route only
const client = new WhatsAppClient({
  accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
  phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
  storage: {
    enabled: true,
    provider: 'supabase',
    options: {
      url: process.env.SUPABASE_URL!,
      apiKey: process.env.SUPABASE_SERVICE_ROLE_KEY! // ✅ Service Role Key
    }
  }
});
❌ Avoid: Exposing Service Keys in Frontend
// NEVER do this in frontend code
const client = new WhatsAppClient({
  storage: {
    options: {
      apiKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' // ❌ Hardcoded in frontend
    }
  }
});

2. Row Level Security (RLS)

Enable RLS in Supabase for additional security:
-- Enable RLS
ALTER TABLE whatsapp_messages ENABLE ROW LEVEL SECURITY;
ALTER TABLE whatsapp_conversations ENABLE ROW LEVEL SECURITY;

-- Create policy for service role
CREATE POLICY "Service role has full access"
ON whatsapp_messages
FOR ALL
TO service_role
USING (true)
WITH CHECK (true);

-- Create policy for authenticated users (if needed)
CREATE POLICY "Users can read their own messages"
ON whatsapp_messages
FOR SELECT
TO authenticated
USING (from_phone = auth.jwt() ->> 'phone' OR to_phone = auth.jwt() ->> 'phone');

3. Data Encryption

Encrypt sensitive message content:
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

class EncryptionService {
  private algorithm = 'aes-256-gcm';
  private key: Buffer;

  constructor() {
    this.key = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex');
  }

  encrypt(text: string): { encrypted: string; iv: string; authTag: string } {
    const iv = randomBytes(16);
    const cipher = createCipheriv(this.algorithm, this.key, iv);

    let encrypted = cipher.update(text, 'utf8', 'hex');
    encrypted += cipher.final('hex');

    return {
      encrypted,
      iv: iv.toString('hex'),
      authTag: cipher.getAuthTag().toString('hex')
    };
  }

  decrypt(encrypted: string, iv: string, authTag: string): string {
    const decipher = createDecipheriv(
      this.algorithm,
      this.key,
      Buffer.from(iv, 'hex')
    );

    decipher.setAuthTag(Buffer.from(authTag, 'hex'));

    let decrypted = decipher.update(encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');

    return decrypted;
  }
}

// Use with message transformer
const encryptionService = new EncryptionService();

const encryptTransformer: MessageTransformer = {
  name: 'encrypt',
  transform: async (message) => {
    if (message.content.text) {
      const { encrypted, iv, authTag } = encryptionService.encrypt(
        message.content.text
      );

      return {
        ...message,
        content: {
          ...message.content,
          text: encrypted,
          _encryption: { iv, authTag }
        }
      };
    }
    return message;
  }
};

4. Access Control

Implement role-based access control:
enum Role {
  ADMIN = 'admin',
  AGENT = 'agent',
  VIEWER = 'viewer'
}

class AccessControl {
  private userRoles = new Map<string, Role>();

  async canAccessConversation(
    userId: string,
    phoneNumber: string
  ): Promise<boolean> {
    const role = this.userRoles.get(userId);

    if (role === Role.ADMIN) {
      return true;
    }

    if (role === Role.AGENT) {
      // Check if agent is assigned to this customer
      return await this.isAssignedAgent(userId, phoneNumber);
    }

    return false;
  }

  async canExportData(userId: string): Promise<boolean> {
    const role = this.userRoles.get(userId);
    return role === Role.ADMIN;
  }

  private async isAssignedAgent(
    userId: string,
    phoneNumber: string
  ): Promise<boolean> {
    // Check assignment from database
    return true;
  }
}

// Usage
const accessControl = new AccessControl();

async function getConversation(userId: string, phoneNumber: string) {
  if (!await accessControl.canAccessConversation(userId, phoneNumber)) {
    throw new Error('Access denied');
  }

  return client.getConversation(phoneNumber);
}

Performance Optimization

1. Query Optimization

✅ Good: Use Pagination
// Paginate large result sets
async function* paginateConversation(phoneNumber: string) {
  const pageSize = 100;
  let offset = 0;
  let hasMore = true;

  while (hasMore) {
    const result = await client.getConversation(phoneNumber, {
      limit: pageSize,
      offset
    });

    yield result.messages;

    hasMore = result.messages.length === pageSize;
    offset += pageSize;
  }
}

// Usage
for await (const messages of paginateConversation('+1234567890')) {
  await processMessages(messages);
}
❌ Avoid: Loading Everything at Once
// Don't do this
const allMessages = await client.getConversation('+1234567890', {
  limit: 1000000 // ❌ Will crash or be very slow
});

2. Caching Strategy

Implement multi-level caching:
import { LRUCache } from 'lru-cache';

class CachedStorageService {
  // In-memory cache (fast, temporary)
  private memoryCache = new LRUCache<string, any>({
    max: 500,
    ttl: 5 * 60 * 1000 // 5 minutes
  });

  // Redis cache (shared, persistent)
  private redis: Redis;

  constructor(redis: Redis) {
    this.redis = redis;
  }

  async getConversation(phoneNumber: string, options: any) {
    const cacheKey = `conv:${phoneNumber}:${JSON.stringify(options)}`;

    // Level 1: Check memory cache
    let result = this.memoryCache.get(cacheKey);
    if (result) {
      console.log('✅ Memory cache hit');
      return result;
    }

    // Level 2: Check Redis cache
    const cached = await this.redis.get(cacheKey);
    if (cached) {
      console.log('✅ Redis cache hit');
      result = JSON.parse(cached);
      this.memoryCache.set(cacheKey, result);
      return result;
    }

    // Level 3: Query database
    console.log('📊 Database query');
    result = await client.getConversation(phoneNumber, options);

    // Update caches
    this.memoryCache.set(cacheKey, result);
    await this.redis.setex(cacheKey, 300, JSON.stringify(result)); // 5 min TTL

    return result;
  }

  async invalidateCache(phoneNumber: string) {
    // Clear all cache entries for this phone number
    const pattern = `conv:${phoneNumber}:*`;
    const keys = await this.redis.keys(pattern);

    for (const key of keys) {
      await this.redis.del(key);
      this.memoryCache.delete(key);
    }
  }
}

3. Batch Operations

Process messages in batches:
class BatchProcessor {
  private batch: StorageMessage[] = [];
  private batchSize = 100;
  private flushInterval = 5000; // 5 seconds
  private timer: NodeJS.Timeout | null = null;

  constructor(private adapter: IStorageAdapter) {
    this.startTimer();
  }

  async addMessage(message: StorageMessage): Promise<void> {
    this.batch.push(message);

    if (this.batch.length >= this.batchSize) {
      await this.flush();
    }
  }

  private startTimer(): void {
    this.timer = setInterval(async () => {
      if (this.batch.length > 0) {
        await this.flush();
      }
    }, this.flushInterval);
  }

  private async flush(): Promise<void> {
    if (this.batch.length === 0) return;

    const messages = [...this.batch];
    this.batch = [];

    try {
      await this.adapter.saveMessages(messages);
      console.log(`✅ Flushed ${messages.length} messages`);
    } catch (error) {
      console.error('❌ Batch flush failed:', error);
      // Re-add failed messages to batch
      this.batch.unshift(...messages);
    }
  }

  async destroy(): Promise<void> {
    if (this.timer) {
      clearInterval(this.timer);
    }
    await this.flush();
  }
}

4. Database Indexing

Create optimal indexes:
-- Supabase/PostgreSQL indexes

-- Composite index for common queries
CREATE INDEX idx_messages_phone_timestamp
ON whatsapp_messages(from_phone, to_phone, timestamp DESC);

-- Index for status updates
CREATE INDEX idx_messages_status_update
ON whatsapp_messages(whatsapp_message_id, status)
WHERE status IN ('sent', 'delivered', 'read');

-- Partial index for recent messages (last 90 days)
CREATE INDEX idx_messages_recent
ON whatsapp_messages(timestamp DESC)
WHERE timestamp > CURRENT_DATE - INTERVAL '90 days';

-- GIN index for JSONB content search
CREATE INDEX idx_messages_content_gin
ON whatsapp_messages USING GIN (content);

Monitoring and Observability

1. Storage Metrics

Track storage performance:
class StorageMetrics {
  private metrics = {
    queriesTotal: 0,
    queriesSuccess: 0,
    queriesError: 0,
    avgQueryTime: 0,
    cacheHits: 0,
    cacheMisses: 0
  };

  async trackQuery<T>(
    operation: string,
    fn: () => Promise<T>
  ): Promise<T> {
    const startTime = Date.now();
    this.metrics.queriesTotal++;

    try {
      const result = await fn();

      this.metrics.queriesSuccess++;
      this.updateAvgQueryTime(Date.now() - startTime);

      console.log(`✅ ${operation} completed in ${Date.now() - startTime}ms`);

      return result;
    } catch (error) {
      this.metrics.queriesError++;
      console.error(`❌ ${operation} failed:`, error);
      throw error;
    }
  }

  private updateAvgQueryTime(duration: number): void {
    const n = this.metrics.queriesSuccess;
    this.metrics.avgQueryTime =
      (this.metrics.avgQueryTime * (n - 1) + duration) / n;
  }

  getMetrics() {
    return {
      ...this.metrics,
      successRate: (this.metrics.queriesSuccess / this.metrics.queriesTotal) * 100,
      errorRate: (this.metrics.queriesError / this.metrics.queriesTotal) * 100,
      cacheHitRate: (this.metrics.cacheHits / (this.metrics.cacheHits + this.metrics.cacheMisses)) * 100
    };
  }
}

// Usage
const metrics = new StorageMetrics();

const conversation = await metrics.trackQuery(
  'getConversation',
  () => client.getConversation('+1234567890')
);

2. Health Checks

Implement storage health checks:
class StorageHealthCheck {
  async check(): Promise<HealthStatus> {
    const checks = {
      connection: await this.checkConnection(),
      write: await this.checkWrite(),
      read: await this.checkRead(),
      performance: await this.checkPerformance()
    };

    const healthy = Object.values(checks).every(c => c.healthy);

    return {
      healthy,
      checks,
      timestamp: new Date()
    };
  }

  private async checkConnection(): Promise<CheckResult> {
    try {
      const connected = client.isStorageEnabled();
      return {
        healthy: connected,
        message: connected ? 'Connected' : 'Not connected'
      };
    } catch (error) {
      return {
        healthy: false,
        message: error.message
      };
    }
  }

  private async checkWrite(): Promise<CheckResult> {
    try {
      const testMessage = {
        whatsappMessageId: `health_check_${Date.now()}`,
        // ... other fields
      };

      await client.storage.saveMessage(testMessage);

      return {
        healthy: true,
        message: 'Write successful'
      };
    } catch (error) {
      return {
        healthy: false,
        message: `Write failed: ${error.message}`
      };
    }
  }

  private async checkRead(): Promise<CheckResult> {
    try {
      const result = await client.getConversation('+0000000000', {
        limit: 1
      });

      return {
        healthy: true,
        message: 'Read successful'
      };
    } catch (error) {
      return {
        healthy: false,
        message: `Read failed: ${error.message}`
      };
    }
  }

  private async checkPerformance(): Promise<CheckResult> {
    const startTime = Date.now();

    try {
      await client.getConversation('+0000000000', { limit: 10 });

      const duration = Date.now() - startTime;
      const healthy = duration < 1000; // Should complete in < 1 second

      return {
        healthy,
        message: `Query took ${duration}ms`,
        metrics: { duration }
      };
    } catch (error) {
      return {
        healthy: false,
        message: error.message
      };
    }
  }
}

Next Steps

I