Security Best Practices
1. Credential Management
✅ Good: Use Service Role Keys in Backend OnlyCopy
// 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
}
}
});
Copy
// 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:Copy
-- 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:Copy
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:Copy
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 PaginationCopy
// 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);
}
Copy
// 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:Copy
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:Copy
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:Copy
-- 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:Copy
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:Copy
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
};
}
}
}