• Skip to main content
  • Skip to navigation
  • Skip to search
    Petunia™
    FeaturesPricingIntegrationsAboutContact
    Log inStart free trialSign up
    Loading
    Petunia™

    Reimagining customer communication for the modern business.

    Product

    • Features
    • Pricing
    • Integrations
    • Roadmap
    • What's New

    Resources

    • Help Center
    • Documentation
    • Guides
    • API Reference
    • Community
    • Support

    Company

    • About Us
    • Careers
    • Blog
    • Press
    • Contact

    © 2026 Gray Group International LLC. All rights reserved.·
    Made by gardenpatch 🌱

    Privacy PolicyTerms of ServiceCookie Policy

    Petunia™ is a trademark of Gray Group International LLC. The Petunia name, brand, product design, and content are proprietary. Unauthorized use, imitation, or copying is prohibited.

    Documentation

    CALL_RECORDINGS

    docs/CALL_RECORDINGS.md
    Docs homeGuidesSupport
    Quick links
    Start here
    How the docs are organized.
    Environment setup
    Configure env + run locally.
    Unified Inbox
    Inbox concepts & behavior.
    Voice AI setup
    Providers, Twilio, testing.
    Pricing model
    Source-of-truth pricing.
    Operations runbook
    How to operate safely.

    Call Recording Storage Architecture

    Overview

    Petunia's call recording system stores voice call recordings from multiple providers (Twilio, RingCentral, Retell, Cartesia) in a unified architecture. Call recordings are referenced by URL in the database but stored externally by the voice providers. This document covers the storage patterns, retention policies, compliance considerations, and cleanup procedures.

    Status: Recording URL storage and playback UI implemented. Recordings are stored by voice providers (Twilio, Retell, etc.) and accessed via provider URLs. S3 upload infrastructure exists in lib/services/file-upload.ts for future self-hosted storage migration. Recording consent tracking and GDPR deletion are not yet implemented.

    Architecture

    Current Implementation

    ┌─────────────────────┐
    │  Voice Providers    │
    │  (Twilio, RC, etc)  │
    └──────────┬──────────┘
               │ recordingUrl
               ▼
    ┌─────────────────────┐
    │   CallRecord Model  │
    │ - recordingUrl      │ ────▶ External URL (provider-hosted)
    │ - transcriptUrl     │ ────▶ External URL (provider-hosted)
    │ - duration          │
    │ - status            │
    └─────────────────────┘
               │
               ▼
    ┌─────────────────────┐
    │  ExtractedData      │
    │ - transcriptions    │
    │ - metadata          │
    └─────────────────────┘
    

    Database Schema

    CallRecord Model

    Location: /Users/tiago/Documents/Projects/petunia/prisma/schema.prisma (lines 151-184)

    model CallRecord {
      id               String          @id @default(cuid())
      callId           String          @unique
      contactId        String
      businessId       String?
      portalId         String?
      status           String
      direction        String          // inbound, outbound
      phoneNumber      String
      duration         Int?
      recordingUrl     String?         // External URL to call recording
      transcriptUrl    String?         // External URL to transcript
      agentId          String?
      voiceId          String?
      callbackUrl      String?
      webhookUrl       String?
      isAutomated      Boolean         @default(false)
      initiatedAt      DateTime        @default(now())
      answeredAt       DateTime?
      endedAt          DateTime?
      provider         String?         // twilio, ringcentral, retell, cartesia
      providerMetadata Json?           // Provider-specific data
      createdAt        DateTime        @default(now())
      updatedAt        DateTime        @updatedAt
    
      Contact          Contact         @relation(fields: [contactId], references: [id])
      Portal           Portal?         @relation("PortalCallRecords", fields: [portalId], references: [id])
      Message          Message[]
      ExtractedData    ExtractedData[]
    
      @@index([contactId])
      @@index([businessId])
      @@index([portalId])
      @@index([provider])
    }
    

    ExtractedData Model

    Stores transcriptions and other extracted information from calls:

    model ExtractedData {
      id           String     @id @default(cuid())
      callRecordId String
      dataType     String     // voice_extraction, transcription, etc.
      data         String     @db.Text  // JSON stringified data
      confidence   Float      @default(0)
      createdAt    DateTime   @default(now())
      updatedAt    DateTime   @updatedAt
      CallRecord   CallRecord @relation(fields: [callRecordId], references: [id], onDelete: Cascade)
    
      @@index([callRecordId])
      @@index([dataType])
    }
    

    Storage Patterns

    Provider-Hosted Recordings

    Currently, all call recordings remain hosted by the voice provider:

    Twilio

    • Storage: Twilio-hosted, S3-backed
    • URL Format: https://api.twilio.com/2010-04-01/Accounts/{AccountSid}/Recordings/{RecordingSid}.mp3
    • Access: Requires Twilio credentials for authentication
    • Retention: Twilio's default is indefinite, but can be configured
    • Source: /Users/tiago/Documents/Projects/petunia/lib/services/voice/callLogAdapters/twilioCallLogAdapter.ts

    RingCentral

    • Storage: RingCentral-hosted
    • URL Format: Platform-specific URLs from RingCentral API
    • Access: OAuth token required
    • Retention: RingCentral's default retention policy
    • Webhook: /Users/tiago/Documents/Projects/petunia/app/api/communications/ringcentral/webhooks/route.ts (line 305)

    Retell

    • Storage: Retell-hosted
    • URL Format: Retell-specific CDN URLs
    • Access: API key authentication
    • Provider: /Users/tiago/Documents/Projects/petunia/lib/services/voice/retellProvider.ts

    Cartesia

    • Storage: Cartesia-hosted
    • URL Format: Cartesia Line API URLs
    • Access: API key authentication
    • Adapter: /Users/tiago/Documents/Projects/petunia/lib/services/voice/callLogAdapters/cartesiaCallLogAdapter.ts

    Future: Self-Hosted Storage (Not Yet Implemented)

    Planned Architecture (from feature documentation):

    ┌─────────────────────┐
    │  Voice Provider     │
    │  Webhook            │
    └──────────┬──────────┘
               │ Download recording
               ▼
    ┌─────────────────────┐
    │   Upload Handler    │
    │ /api/recordings/    │
    │      upload         │
    └──────────┬──────────┘
               │
               ▼
    ┌─────────────────────┐
    │   S3 / Supabase     │
    │   Storage Bucket    │
    │   petunia-recordings│
    └─────────────────────┘
               │
               ▼
    ┌─────────────────────┐
    │   CallRecord        │
    │   recordingUrl      │ ────▶ CDN URL or Presigned URL
    └─────────────────────┘
    

    Infrastructure Available:

    • S3 Client configured in /Users/tiago/Documents/Projects/petunia/lib/services/file-upload.ts
    • AWS credentials support via AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
    • Presigned URL generation capability via @aws-sdk/s3-request-presigner
    • Default bucket: petunia-uploads (configurable via S3_BUCKET)

    Retention Policies

    GDPR Compliance Framework

    Implementation: /Users/tiago/Documents/Projects/petunia/lib/services/gdpr-compliance.ts

    Default Retention Periods

    export const DATA_RETENTION_POLICIES: Record<string, RetentionPolicy> = {
      // Behavior analytics: 90 days
      behaviorAnalytics: {
        days: 90,
        description: 'User behavior analytics and interaction patterns',
        tables: ['user_behavior_events', 'user_behavior_analytics'],
        exceptions: ['aggregated_metrics'],
      },
    
      // Session data: 30 days
      sessionData: {
        days: 30,
        description: 'Session logs and temporary interaction data',
        tables: ['user_sessions', 'session_events'],
        exceptions: [],
      },
    
      // ML training data: 180 days
      mlTrainingData: {
        days: 180,
        description: 'Machine learning model training data',
        tables: ['profile_request_history', 'ml_training_examples'],
        exceptions: ['model_metrics'],
      },
    
      // Audit logs: 365 days
      auditLogs: {
        days: 365,
        description: 'Security and compliance audit logs',
        tables: ['audit_logs', 'data_access_logs'],
        exceptions: [],
      },
    };
    

    Call Recording Retention

    Current Status: No specific retention policy implemented for call recordings.

    Recommended Policy (based on industry best practices):

    Recording TypeRetention PeriodJustification
    Automated calls (AI)90 daysAnalytics and quality improvement
    Customer service calls180 daysDispute resolution and training
    Sales calls1 yearContract verification and compliance
    Compliance-sensitive7 yearsLegal/regulatory requirements
    Voicemail recordings30 daysStandard telephony retention

    Implementation Needed:

    1. Add retentionPolicy field to CallRecord model
    2. Create retention enforcement in GDPR compliance service
    3. Add automated cleanup job

    GDPR Data Retention Infrastructure

    Database Tables: /Users/tiago/Documents/Projects/petunia/supabase/migrations/20250706_gdpr_compliance_tables.sql

    -- Data retention metadata
    CREATE TABLE IF NOT EXISTS data_retention_log (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      table_name TEXT NOT NULL,
      records_deleted INTEGER NOT NULL DEFAULT 0,
      retention_days INTEGER NOT NULL,
      cutoff_date TIMESTAMPTZ NOT NULL,
      execution_time_ms INTEGER,
      error_message TEXT,
      created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
    
      INDEX idx_retention_log_created_at ON data_retention_log(created_at DESC)
    );
    

    Enforcement Cron: /Users/tiago/Documents/Projects/petunia/app/api/cron/data-retention/route.ts

    • Schedule: Daily at 3 AM PST
    • Monitoring: Sentry monitor data-retention-daily
    • Authentication: HMAC signature or CRON_SECRET
    • Action: Calls GDPRComplianceService.applyRetentionPolicies()

    Compliance Considerations

    GDPR (General Data Protection Regulation)

    Implemented Features:

    1. Consent Tracking (user_consent table)

      • Records user consent for data processing
      • Tracks consent types and withdrawals
      • Logs IP address and user agent (optional for privacy)
    2. Audit Trail (gdpr_audit_log table)

      • Tracks all GDPR-related actions:
        • data_export - User data export requests
        • data_deletion - User data deletion requests
        • consent_update - Consent preference changes
        • data_access - Data access requests
      • Records action metadata and IP addresses
    3. Data Deletion

      • Automatic scheduling on user account deletion
      • 30-day grace period before permanent deletion
      • Cascading deletion via onDelete: Cascade in schema

    Not Yet Implemented for Call Recordings:

    • Call recording consent tracking
    • Recording-specific data export
    • Automated recording deletion after retention period
    • Subject Access Request (SAR) for call recordings

    CCPA (California Consumer Privacy Act)

    Required Capabilities (partially implemented):

    1. Right to Know: Users can request what personal data is collected

      • ✅ GDPR audit log tracks data access
      • ❌ No specific call recording inventory endpoint
    2. Right to Delete: Users can request deletion of personal data

      • ✅ GDPR deletion framework exists
      • ❌ Not extended to call recordings
    3. Right to Opt-Out: Users can opt out of sale of personal data

      • ⚠️ Not applicable (Petunia doesn't sell data)
    4. Non-Discrimination: Can't discriminate against users who exercise privacy rights

      • ✅ Implemented in auth and access control

    Additional Compliance Requirements

    Recording Consent Laws (US State-Level)

    Two-Party Consent States:

    • California, Connecticut, Florida, Illinois, Maryland, Massachusetts, Michigan, Montana, Nevada, New Hampshire, Pennsylvania, Washington

    Requirements:

    • Must obtain consent from ALL parties before recording
    • Must provide audible beep or verbal notification
    • Violation can result in criminal penalties

    Implementation Status: ❌ Not implemented

    • No consent prompt before recording
    • No "this call is being recorded" notification
    • No state-specific logic

    Recommendation: Add consent tracking to CallRecord:

    model CallRecord {
      // ... existing fields ...
      recordingConsent   Boolean?       // Did user consent to recording?
      consentMethod      String?        // verbal, beep, written
      consentTimestamp   DateTime?      // When consent was obtained
      consentMetadata    Json?          // State-specific consent data
    }
    

    Payment Card Industry (PCI) Compliance

    If processing payment information over phone:

    • Requirement: Must not store credit card numbers in recordings
    • Implementation: ⚠️ Not specifically addressed
    • Recommendation:
      • Pause recording during payment capture
      • Use DTMF masking for card numbers
      • Add PCI compliance flags to CallRecord

    Health Insurance Portability and Accountability Act (HIPAA)

    If handling protected health information (PHI):

    • Requirement: Encrypted storage, access controls, audit trails
    • Implementation: ⚠️ Partial (audit trails exist, encryption not specific to recordings)
    • Recommendation:
      • Add PHI flag to CallRecord
      • Require additional encryption for PHI recordings
      • Implement Business Associate Agreements (BAA) with providers

    Access Controls

    Database-Level Security

    Row-Level Security (RLS): Enabled on GDPR tables

    -- User consent: Users can only see their own consent records
    CREATE POLICY "Users can view own consent records" ON user_consent
      FOR SELECT USING (auth.uid() = user_id);
    
    -- GDPR audit log: Users can view their own audit records
    CREATE POLICY "Users can view own GDPR audit records" ON gdpr_audit_log
      FOR SELECT USING (auth.uid() = user_id);
    
    -- Data retention log: Only service role can access
    CREATE POLICY "Service role only for retention logs" ON data_retention_log
      FOR ALL USING (auth.role() = 'service_role');
    

    CallRecord Access Control: Enforced in API routes

    • Location: /Users/tiago/Documents/Projects/petunia/app/api/calls/[callId]/route.ts
    • Method: Client-scoped queries via Contact.clientId
    // Example from GET /api/calls/[callId]
    const callRecord = await prisma.callRecord.findFirst({
      where: {
        callId: paramResult.data.callId,
        Contact: { clientId },  // Enforces client isolation
      },
      // ... includes ...
    });
    

    API Authentication

    Authentication Methods:

    1. User Sessions: Auth.js session-based authentication
    2. CRON Jobs: HMAC signature + CRON_SECRET header
    3. Webhooks: Provider-specific verification (Twilio, RingCentral)

    Implementation: /Users/tiago/Documents/Projects/petunia/lib/middleware/security/internal-api-auth.ts

    Provider-Specific Access

    Twilio:

    • Recordings protected by Account SID + Auth Token
    • URL-based access requires authentication
    • Recording SID is not guessable

    RingCentral:

    • OAuth 2.0 token required for recording access
    • Token rotation handled by connection service
    • Stored in ConnectionIntegration.credentials

    Retell/Cartesia:

    • API key authentication
    • Keys stored securely in environment or database
    • Resolved via apiKeyResolver service

    Retrieval and Playback

    Current Implementation

    UI Component: /Users/tiago/Documents/Projects/petunia/components/inbox/CallMessageItem.tsx

    // Audio playback handler (lines 109-120)
    if (!call.recordingUrl) {
      toast({ title: 'No recording available', variant: 'destructive' });
      return;
    }
    
    const audio = new Audio(call.recordingUrl);
    audio.play();
    
    // Download link (lines 288-304)
    {call.recordingUrl && (
      <a href={call.recordingUrl} target="_blank" rel="noopener noreferrer" download>
        Download Recording
      </a>
    )}
    

    API Endpoint: /Users/tiago/Documents/Projects/petunia/app/api/calls/[callId]/route.ts

    • Returns recordingUrl in response
    • No presigned URL generation
    • Direct link to provider-hosted recording

    Issues with Current Approach

    1. No Access Control: Direct URLs bypass API authentication
    2. Provider Dependency: Recordings unavailable if provider changes URL scheme
    3. No Expiration: URLs may expose recordings indefinitely
    4. No Analytics: Can't track who accessed which recordings
    5. No Transcoding: Can't convert formats or apply compression

    Recommended Implementation (Not Yet Built)

    Presigned URL Pattern:

    // GET /api/recordings/[recordingId]/playback
    export async function GET(request: NextRequest, { params }: { params: { recordingId: string } }) {
      const session = await auth();
      if (!session?.user) return unauthorized();
    
      // Verify user has access to this recording
      const callRecord = await prisma.callRecord.findFirst({
        where: {
          id: params.recordingId,
          Contact: { clientId: session.user.clientId }
        }
      });
    
      if (!callRecord?.recordingUrl) return notFound();
    
      // If self-hosted, generate presigned URL
      if (callRecord.recordingUrl.startsWith('s3://')) {
        const s3Client = new S3Client({ /* ... */ });
        const command = new GetObjectCommand({
          Bucket: 'petunia-recordings',
          Key: callRecord.recordingUrl.replace('s3://petunia-recordings/', '')
        });
    
        const presignedUrl = await getSignedUrl(s3Client, command, {
          expiresIn: 3600 // 1 hour
        });
    
        return NextResponse.json({ url: presignedUrl });
      }
    
      // If provider-hosted, proxy or redirect
      return NextResponse.redirect(callRecord.recordingUrl);
    }
    

    Benefits:

    • Time-limited access (URLs expire)
    • Audit trail of who accessed what
    • Provider abstraction
    • Client isolation enforced

    Cleanup Procedures

    Manual Cleanup

    Delete Single Call Record:

    # DELETE /api/calls/[callId]
    curl -X DELETE https://app.petunia.gardenpatch.xyz/api/calls/{callId} \
      -H "Authorization: Bearer $SESSION_TOKEN"
    

    Implementation: /Users/tiago/Documents/Projects/petunia/app/api/calls/[callId]/route.ts (lines 347-443)

    • Deletes related messages first
    • Then deletes CallRecord
    • Uses transaction for atomicity
    • Does NOT delete external recording from provider
    await prisma.$transaction([
      prisma.message.deleteMany({
        where: { callRecordId: existing.id }
      }),
      prisma.callRecord.delete({
        where: { callId: paramResult.data.callId }
      })
    ]);
    

    Automated Cleanup

    GDPR Data Retention Cron:

    • Endpoint: /Users/tiago/Documents/Projects/petunia/app/api/cron/data-retention/route.ts
    • Schedule: Daily at 3 AM PST
    • Auth: HMAC signature or CRON_SECRET header
    • Service: GDPRComplianceService.applyRetentionPolicies()

    Vercel Cron Configuration (recommended):

    // vercel.json
    {
      "crons": [{
        "path": "/api/cron/data-retention",
        "schedule": "0 3 * * *"  // 3 AM daily
      }]
    }
    

    Missing: Recording-Specific Cleanup

    What's Needed:

    1. Retention Policy Enforcement:
    // Example implementation needed
    async function cleanupExpiredRecordings() {
      const cutoffDate = new Date();
      cutoffDate.setDate(cutoffDate.getDate() - 90); // 90 days
    
      const expiredRecordings = await prisma.callRecord.findMany({
        where: {
          endedAt: { lt: cutoffDate },
          recordingUrl: { not: null }
        }
      });
    
      for (const record of expiredRecordings) {
        // 1. Delete from provider (if possible)
        await deleteProviderRecording(record.provider, record.recordingUrl);
    
        // 2. Clear URL from database
        await prisma.callRecord.update({
          where: { id: record.id },
          data: { recordingUrl: null, transcriptUrl: null }
        });
    
        // 3. Log the deletion
        await logRetentionAction('call_recordings', record.id);
      }
    }
    
    1. Provider-Specific Deletion:
    async function deleteProviderRecording(provider: string, recordingUrl: string) {
      switch (provider) {
        case 'twilio':
          // Delete via Twilio API
          const recordingSid = extractSidFromUrl(recordingUrl);
          await twilioClient.recordings(recordingSid).remove();
          break;
    
        case 'ringcentral':
          // Delete via RingCentral API
          await ringcentralClient.delete(recordingUrl);
          break;
    
        case 's3':
          // Delete from S3 bucket
          await s3Client.send(new DeleteObjectCommand({
            Bucket: 'petunia-recordings',
            Key: extractKeyFromUrl(recordingUrl)
          }));
          break;
      }
    }
    

    Security Best Practices

    Storage Security

    Current State:

    • ✅ Provider-hosted recordings use provider security
    • ❌ No encryption at rest enforcement
    • ❌ No encryption in transit verification
    • ⚠️ Credentials stored in database (encrypted in ConnectionIntegration.credentials)

    Recommendations:

    1. Encryption at Rest (for self-hosted storage):
    // Use AWS S3 server-side encryption
    await s3Client.send(new PutObjectCommand({
      Bucket: 'petunia-recordings',
      Key: recordingKey,
      Body: recordingBuffer,
      ServerSideEncryption: 'AES256', // or 'aws:kms'
      Metadata: {
        'x-amz-meta-call-id': callId,
        'x-amz-meta-encrypted': 'true'
      }
    }));
    
    1. Encryption in Transit:
    • Enforce HTTPS for all recording URLs
    • Reject HTTP URLs from providers
    • Use TLS 1.2+ for provider API calls
    1. Key Management:
    • Rotate provider API keys quarterly
    • Use AWS KMS for S3 encryption keys
    • Store credentials in environment variables, not database
    • Use secrets management service (AWS Secrets Manager, HashiCorp Vault)

    Access Logging

    Current Implementation: Partial via GDPR audit log

    Recommended Enhancement:

    model RecordingAccessLog {
      id              String   @id @default(cuid())
      callRecordId    String
      userId          String
      action          String   // viewed, downloaded, deleted
      ipAddress       String?
      userAgent       String?
      accessedAt      DateTime @default(now())
    
      CallRecord      CallRecord @relation(fields: [callRecordId], references: [id])
    
      @@index([callRecordId])
      @@index([userId])
      @@index([accessedAt])
    }
    

    Compliance Monitoring

    Recommendations:

    1. Regular Audits:
    -- Find recordings without retention policy
    SELECT id, callId, initiatedAt, provider
    FROM "CallRecord"
    WHERE "recordingUrl" IS NOT NULL
      AND "endedAt" < NOW() - INTERVAL '90 days'
    ORDER BY "endedAt" DESC;
    
    -- Count recordings by age
    SELECT
      DATE_TRUNC('month', "endedAt") AS month,
      COUNT(*) AS recording_count,
      COUNT(*) FILTER (WHERE "recordingUrl" IS NOT NULL) AS with_recording
    FROM "CallRecord"
    GROUP BY month
    ORDER BY month DESC;
    
    1. Compliance Dashboard Metrics:
    • Total recordings stored
    • Recordings past retention period
    • Missing consent records
    • Provider distribution
    • Storage cost trends
    1. Alerts:
    • Recording retention SLA violations
    • Unusual access patterns
    • Failed deletions
    • Provider quota warnings

    Migration Path

    From Provider-Hosted to Self-Hosted

    Phase 1: Parallel Storage (1-2 weeks)

    1. Implement upload handler
    2. Create S3 bucket and configure access
    3. Update webhook handlers to upload to both provider and S3
    4. Add s3RecordingUrl field to CallRecord
    5. Test with subset of portals

    Phase 2: Migration (2-4 weeks)

    1. Backfill existing recordings from provider to S3
    2. Update playback endpoints to use S3 URLs
    3. Monitor for issues
    4. Gradually increase percentage of S3-backed recordings

    Phase 3: Deprecation (4-8 weeks)

    1. Stop writing provider URLs for new recordings
    2. Delete old recordings from providers
    3. Update recordingUrl to point to S3
    4. Remove provider URL fallback logic

    Phase 4: Optimization (ongoing)

    1. Implement retention policies
    2. Add transcoding for size optimization
    3. Enable CDN for global access
    4. Implement tiered storage (hot/cold/archive)

    Cost Considerations

    Provider Costs

    Twilio:

    • Recording storage: $0.0025/minute/month
    • Transcription: $0.05/minute
    • Bandwidth: $0.02/MB

    RingCentral:

    • Included in subscription
    • Unlimited storage for Enterprise plans
    • Additional storage: ~$50/user/year

    Retell/Cartesia:

    • Pricing varies by plan
    • Typically included in per-minute usage

    Self-Hosted Costs (AWS S3)

    Storage:

    • Standard: $0.023/GB/month
    • Infrequent Access: $0.0125/GB/month (after 30 days)
    • Glacier: $0.004/GB/month (after 90 days)

    Bandwidth:

    • First 10 TB/month: $0.09/GB
    • Next 40 TB/month: $0.085/GB

    Requests:

    • PUT: $0.005 per 1,000 requests
    • GET: $0.0004 per 1,000 requests

    Example Calculation (1000 calls/month, 5 min avg, 1 MB/min):

    Storage: 1000 calls × 5 min × 1 MB × $0.023/GB = $0.115/month
    Requests: 1000 uploads + 2000 downloads × $0.005/1000 = $0.015/month
    Bandwidth: 1000 × 5 MB × $0.09/GB × 1/1024 = $0.44/month
    Total: ~$0.57/month (vs. Twilio: ~$12.50/month)
    

    Monitoring and Alerts

    Metrics to Track

    1. Storage Metrics:

      • Total recordings count
      • Total storage used (GB)
      • Average recording size
      • Recordings by provider
      • Growth rate (recordings/day)
    2. Access Metrics:

      • Playback requests/day
      • Download requests/day
      • Failed access attempts
      • Average access time
    3. Compliance Metrics:

      • Recordings past retention period
      • Missing consent records
      • Pending deletion requests
      • Audit log coverage
    4. Cost Metrics:

      • Storage cost/month
      • Bandwidth cost/month
      • Provider cost/month
      • Cost per recording

    Recommended Alerts

    // Example Sentry alert configuration
    Sentry.captureMessage('Recording retention SLA violated', {
      level: 'warning',
      tags: {
        type: 'compliance',
        policy: 'retention'
      },
      extra: {
        expiredCount: 150,
        oldestRecording: '2024-01-15'
      }
    });
    

    Alert Conditions:

    • Recordings > 90 days old without deletion
    • Failed recording uploads
    • Storage quota > 80%
    • Unusual access patterns (>100 downloads/hour)
    • Provider API errors

    Testing Recommendations

    Unit Tests

    1. Recording Access Control:
    describe('CallRecord API - Access Control', () => {
      it('should deny access to recording from different client', async () => {
        const response = await fetch(`/api/calls/${otherClientCallId}`, {
          headers: { Authorization: `Bearer ${userToken}` }
        });
        expect(response.status).toBe(404);
      });
    });
    
    1. Retention Policy:
    describe('GDPR Compliance - Retention', () => {
      it('should delete recordings older than retention period', async () => {
        await seedOldCallRecords(100); // 100 days old
        await applyRetentionPolicies();
        const remaining = await prisma.callRecord.count({
          where: { recordingUrl: { not: null } }
        });
        expect(remaining).toBe(0);
      });
    });
    

    Integration Tests

    1. End-to-End Recording Flow:
    describe('Call Recording Flow', () => {
      it('should store recording URL from Twilio webhook', async () => {
        const webhookPayload = {
          CallSid: 'CA123',
          RecordingUrl: 'https://api.twilio.com/recordings/RE123.mp3',
          CallStatus: 'completed'
        };
    
        await fetch('/api/webhooks/twilio', {
          method: 'POST',
          body: new URLSearchParams(webhookPayload)
        });
    
        const callRecord = await prisma.callRecord.findFirst({
          where: { callId: 'CA123' }
        });
    
        expect(callRecord?.recordingUrl).toBe(webhookPayload.RecordingUrl);
      });
    });
    

    Load Tests

    1. Concurrent Playback:
    // Test 100 concurrent recording playback requests
    import { test } from '@playwright/test';
    
    test('concurrent recording playback', async ({ browser }) => {
      const contexts = await Promise.all(
        Array.from({ length: 100 }, () => browser.newContext())
      );
    
      const results = await Promise.all(
        contexts.map(async (context) => {
          const page = await context.newPage();
          const start = Date.now();
          await page.goto(`/calls/${callId}`);
          await page.click('button[aria-label="Play recording"]');
          return Date.now() - start;
        })
      );
    
      const avgLatency = results.reduce((a, b) => a + b) / results.length;
      expect(avgLatency).toBeLessThan(2000); // < 2 seconds
    });
    

    Implementation Checklist

    Phase 1: Foundation (Immediate)

    • Document current provider recording behavior
    • Add recording consent fields to CallRecord model
    • Create recording access audit log table
    • Implement presigned URL endpoint for existing provider URLs
    • Add recording access logging

    Phase 2: Compliance (1-2 weeks)

    • Implement consent capture before recording
    • Add state-specific consent logic (two-party states)
    • Create GDPR data export endpoint for recordings
    • Extend retention policies to call recordings
    • Add automated cleanup job for expired recordings

    Phase 3: Self-Hosting (2-4 weeks)

    • Create S3 bucket for recordings (or Supabase Storage)
    • Implement webhook recording upload handler
    • Build recording migration service
    • Add CDN distribution
    • Create recording transcoding pipeline

    Phase 4: Optimization (4-8 weeks)

    • Implement tiered storage (hot/cold/archive)
    • Add recording compression
    • Create cost monitoring dashboard
    • Optimize bandwidth usage
    • Add recording analytics

    Related Documentation

    • Call Log Ingestion System
    • GDPR Compliance Service
    • Data Retention Cron Job
    • File Upload Service
    • Database Schema

    Frequently Asked Questions

    Q: Are call recordings automatically deleted? A: Currently, no. Recordings remain with the provider indefinitely unless manually deleted. A retention policy system exists for other data types but hasn't been extended to call recordings.

    Q: Can users download their call recordings? A: Yes, if a recordingUrl exists, users can access it via the UI (CallMessageItem component) or API (GET /api/calls/[callId]). However, access control relies on client-scoped queries, not presigned URLs.

    Q: How are recordings stored? A: Currently, all recordings are stored by the voice provider (Twilio, RingCentral, etc.). Only the URL is stored in the Petunia database.

    Q: Is recording consent tracked? A: No, this feature is not yet implemented. You should add consent tracking before deploying to two-party consent states.

    Q: What happens when a user deletes their account? A: The GDPR compliance system schedules data deletion after a 30-day grace period, but call recordings are not yet included in this process.

    Q: How much does call recording storage cost? A: It depends on the provider. See the "Cost Considerations" section above. Self-hosting on S3 is typically 10-20x cheaper than provider storage.

    Q: Can we migrate existing recordings to self-hosted storage? A: Yes, but this requires building a migration service to download recordings from providers and upload to S3/Supabase Storage. See "Migration Path" section.

    Q: Are recordings encrypted? A: Provider-hosted recordings use the provider's encryption. For self-hosted storage, you must enable server-side encryption (AES-256 or KMS).

    Support and Troubleshooting

    Common Issues:

    1. Recording URL returns 403 Forbidden:

      • Provider credentials may have expired
      • Check ConnectionIntegration.credentials for the portal
      • Verify OAuth token refresh is working (RingCentral)
    2. Recording not created:

      • Verify recording is enabled in provider settings
      • Check webhook delivery (Twilio, RingCentral)
      • Review call status (some call statuses don't generate recordings)
    3. Playback fails in browser:

      • Recording format may not be browser-compatible (codec issue)
      • CORS headers may be missing from provider
      • Use download link as fallback

    Logging:

    // Enable debug logging for call recordings
    const logger = createLogger('call-recordings', { level: 'debug' });
    
    logger.debug('Recording URL received', {
      callId,
      provider,
      recordingUrl,
      hasTranscript: !!transcriptUrl
    });
    

    Contact:

    • Engineering Team: support@gardenpatch.xyz
    • Security/Compliance: support@gardenpatch.xyz
    • Customer Support: support@gardenpatch.xyz
    On this page
    OverviewArchitectureCurrent ImplementationDatabase SchemaStorage PatternsProvider-Hosted RecordingsFuture: Self-Hosted Storage (Not Yet Implemented)Retention PoliciesGDPR Compliance FrameworkCall Recording RetentionGDPR Data Retention InfrastructureCompliance ConsiderationsGDPR (General Data Protection Regulation)CCPA (California Consumer Privacy Act)Additional Compliance RequirementsAccess ControlsDatabase-Level SecurityAPI AuthenticationProvider-Specific AccessRetrieval and PlaybackCurrent ImplementationIssues with Current ApproachRecommended Implementation (Not Yet Built)Cleanup ProceduresManual CleanupAutomated CleanupMissing: Recording-Specific CleanupSecurity Best PracticesStorage SecurityAccess LoggingCompliance MonitoringMigration Path