CCPA Compliance for ChatGPT Apps in California

The California Consumer Privacy Act (CCPA), amended by the California Privacy Rights Act (CPRA) in 2023, establishes the most stringent data privacy regulations in the United States. For ChatGPT app developers serving California residents, non-compliance carries severe penalties: $2,500 per violation and $7,500 for intentional violations. With potential class-action lawsuits enabling damages of $100-$750 per consumer per incident, the financial risks are substantial.

CCPA compliance isn't just about avoiding penalties—it's about building trust with California's 39 million residents and establishing privacy-first practices that scale nationally. The law applies to businesses that collect personal information from California residents and meet any of these thresholds:

  • Annual gross revenue exceeding $25 million
  • Buy, sell, or share personal information of 100,000+ California residents or households
  • Derive 50%+ of annual revenue from selling or sharing personal information

If your ChatGPT app serves California users—regardless of where your business is located—compliance is mandatory. This guide provides production-ready code for implementing every CCPA requirement, from consumer rights portals to opt-out mechanisms, ensuring your app meets California's gold-standard privacy law.


Consumer Rights Under CCPA

The CCPA grants California consumers five fundamental rights regarding their personal information. Your ChatGPT app must provide mechanisms to honor each right within 45 days of a verified request (extendable to 90 days with notice):

1. Right to Know

Consumers can request disclosure of:

  • Categories of personal information collected (e.g., identifiers, geolocation, biometric data)
  • Specific pieces of personal information collected
  • Sources from which data was collected
  • Business purposes for collection
  • Third parties with whom data is shared

2. Right to Delete

Consumers can request deletion of their personal information, subject to exceptions (e.g., legal compliance, fraud prevention, internal research).

3. Right to Opt-Out of Sale

Consumers can prohibit the sale or sharing of their personal information. Under CCPA, "sale" includes transferring data for monetary consideration or any valuable consideration, including cross-context behavioral advertising.

4. Right to Correct

Added by CPRA 2023, consumers can request correction of inaccurate personal information.

5. Right to Non-Discrimination

Businesses cannot discriminate against consumers for exercising CCPA rights (e.g., denying services, charging different prices, providing lower quality).

Production-Ready Consumer Rights Portal

Here's a complete React implementation for handling all CCPA rights:

// components/CCPAConsumerRightsPortal.tsx
import React, { useState } from 'react';

interface CCPARequest {
  requestType: 'know' | 'delete' | 'optOut' | 'correct';
  email: string;
  firstName: string;
  lastName: string;
  description?: string;
  verificationCode?: string;
}

const CCPAConsumerRightsPortal: React.FC = () => {
  const [request, setRequest] = useState<CCPARequest>({
    requestType: 'know',
    email: '',
    firstName: '',
    lastName: ''
  });
  const [step, setStep] = useState<'form' | 'verify' | 'complete'>('form');
  const [requestId, setRequestId] = useState<string | null>(null);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    try {
      const response = await fetch('/api/ccpa/request', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(request)
      });

      if (!response.ok) throw new Error('Request submission failed');

      const { requestId: id } = await response.json();
      setRequestId(id);
      setStep('verify');

      // Send verification email
      await sendVerificationEmail(request.email, id);
    } catch (error) {
      console.error('CCPA request error:', error);
      alert('Failed to submit request. Please try again.');
    }
  };

  const handleVerify = async (e: React.FormEvent) => {
    e.preventDefault();

    try {
      const response = await fetch('/api/ccpa/verify', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          requestId,
          verificationCode: request.verificationCode
        })
      });

      if (!response.ok) throw new Error('Verification failed');

      setStep('complete');
    } catch (error) {
      console.error('Verification error:', error);
      alert('Invalid verification code. Please try again.');
    }
  };

  const sendVerificationEmail = async (email: string, id: string) => {
    await fetch('/api/email/send-verification', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, requestId: id })
    });
  };

  return (
    <div className="ccpa-portal">
      <h1>California Consumer Privacy Rights</h1>

      {step === 'form' && (
        <form onSubmit={handleSubmit}>
          <div className="form-group">
            <label htmlFor="requestType">Request Type</label>
            <select
              id="requestType"
              value={request.requestType}
              onChange={(e) => setRequest({ ...request, requestType: e.target.value as any })}
              required
            >
              <option value="know">Right to Know</option>
              <option value="delete">Right to Delete</option>
              <option value="optOut">Right to Opt-Out of Sale</option>
              <option value="correct">Right to Correct</option>
            </select>
          </div>

          <div className="form-group">
            <label htmlFor="email">Email Address</label>
            <input
              type="email"
              id="email"
              value={request.email}
              onChange={(e) => setRequest({ ...request, email: e.target.value })}
              required
            />
          </div>

          <div className="form-row">
            <div className="form-group">
              <label htmlFor="firstName">First Name</label>
              <input
                type="text"
                id="firstName"
                value={request.firstName}
                onChange={(e) => setRequest({ ...request, firstName: e.target.value })}
                required
              />
            </div>

            <div className="form-group">
              <label htmlFor="lastName">Last Name</label>
              <input
                type="text"
                id="lastName"
                value={request.lastName}
                onChange={(e) => setRequest({ ...request, lastName: e.target.value })}
                required
              />
            </div>
          </div>

          {request.requestType === 'correct' && (
            <div className="form-group">
              <label htmlFor="description">Description of Inaccurate Information</label>
              <textarea
                id="description"
                value={request.description || ''}
                onChange={(e) => setRequest({ ...request, description: e.target.value })}
                rows={4}
                required
              />
            </div>
          )}

          <button type="submit" className="btn-primary">Submit Request</button>
        </form>
      )}

      {step === 'verify' && (
        <form onSubmit={handleVerify}>
          <p>We've sent a verification code to <strong>{request.email}</strong>.</p>

          <div className="form-group">
            <label htmlFor="verificationCode">Verification Code</label>
            <input
              type="text"
              id="verificationCode"
              value={request.verificationCode || ''}
              onChange={(e) => setRequest({ ...request, verificationCode: e.target.value })}
              required
            />
          </div>

          <button type="submit" className="btn-primary">Verify Identity</button>
        </form>
      )}

      {step === 'complete' && (
        <div className="confirmation">
          <h2>Request Submitted Successfully</h2>
          <p>Your request ID: <strong>{requestId}</strong></p>
          <p>We will respond within <strong>45 days</strong> as required by CCPA.</p>
        </div>
      )}
    </div>
  );
};

export default CCPAConsumerRightsPortal;

This portal implements identity verification required by CCPA Section 1798.185(a)(7), ensuring requests come from the actual consumer.


Privacy Notice Requirements

CCPA mandates a comprehensive Privacy Notice accessible to consumers at or before data collection. Your notice must disclose:

  • Categories of personal information collected in the past 12 months
  • Sources from which information is collected
  • Business or commercial purposes for collection
  • Categories of third parties with whom information is shared
  • Consumer rights under CCPA
  • How to submit requests

At-or-Before Collection Notice

For ChatGPT apps, this means displaying privacy information before the first conversation or signup:

<!-- components/CCPAPrivacyNotice.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Privacy Notice - California Residents | YourApp</title>
</head>
<body>
  <div class="privacy-notice">
    <h1>California Consumer Privacy Notice</h1>
    <p><strong>Effective Date:</strong> January 1, 2026</p>

    <section>
      <h2>Personal Information We Collect</h2>
      <p>In the past 12 months, we have collected the following categories of personal information from California residents:</p>

      <table>
        <thead>
          <tr>
            <th>Category</th>
            <th>Examples</th>
            <th>Collected</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>A. Identifiers</td>
            <td>Name, email address, IP address, device ID</td>
            <td>YES</td>
          </tr>
          <tr>
            <td>B. Personal Information (Cal. Civ. Code § 1798.80)</td>
            <td>Name, address, telephone number</td>
            <td>YES</td>
          </tr>
          <tr>
            <td>C. Protected Classifications</td>
            <td>Age, gender, race (NOT collected)</td>
            <td>NO</td>
          </tr>
          <tr>
            <td>D. Commercial Information</td>
            <td>Subscription plans, purchase history</td>
            <td>YES</td>
          </tr>
          <tr>
            <td>E. Biometric Information</td>
            <td>Fingerprints, voiceprints (NOT collected)</td>
            <td>NO</td>
          </tr>
          <tr>
            <td>F. Internet Activity</td>
            <td>Chat history, search queries, browsing behavior</td>
            <td>YES</td>
          </tr>
          <tr>
            <td>G. Geolocation Data</td>
            <td>IP-based location (city/state level)</td>
            <td>YES</td>
          </tr>
          <tr>
            <td>H. Sensory Data</td>
            <td>Audio recordings (NOT collected)</td>
            <td>NO</td>
          </tr>
          <tr>
            <td>I. Professional Information</td>
            <td>Job title, employer (if provided)</td>
            <td>YES</td>
          </tr>
          <tr>
            <td>J. Education Information</td>
            <td>Student records (NOT collected)</td>
            <td>NO</td>
          </tr>
          <tr>
            <td>K. Inferences</td>
            <td>User preferences, predicted interests</td>
            <td>YES</td>
          </tr>
        </tbody>
      </table>
    </section>

    <section>
      <h2>Sources of Personal Information</h2>
      <ul>
        <li><strong>Directly from you:</strong> Account registration, chat conversations, profile settings</li>
        <li><strong>Automatically:</strong> Usage analytics, cookies, device information</li>
        <li><strong>Third-party services:</strong> OAuth providers (Google, Microsoft), payment processors (Stripe)</li>
      </ul>
    </section>

    <section>
      <h2>Business Purposes for Collection</h2>
      <p>We use your personal information for:</p>
      <ul>
        <li><strong>Service delivery:</strong> Powering ChatGPT app conversations, generating responses</li>
        <li><strong>Account management:</strong> User authentication, subscription billing</li>
        <li><strong>Improvement:</strong> Analytics, bug fixes, feature development</li>
        <li><strong>Legal compliance:</strong> Fraud prevention, security, regulatory requirements</li>
        <li><strong>Marketing:</strong> Product updates, newsletters (opt-in only)</li>
      </ul>
    </section>

    <section>
      <h2>Third Parties We Share With</h2>
      <ul>
        <li><strong>Service Providers:</strong> Cloud hosting (Firebase), email delivery (Azure Communication Services), payment processing (Stripe)</li>
        <li><strong>Analytics Providers:</strong> Google Analytics, Firebase Analytics</li>
        <li><strong>AI Model Providers:</strong> OpenAI (for ChatGPT API)</li>
      </ul>
    </section>

    <section>
      <h2>Your California Privacy Rights</h2>
      <p>California residents have the right to:</p>
      <ul>
        <li><strong>Know:</strong> Request disclosure of personal information collected</li>
        <li><strong>Delete:</strong> Request deletion of personal information</li>
        <li><strong>Opt-Out:</strong> Prohibit sale or sharing of personal information</li>
        <li><strong>Correct:</strong> Request correction of inaccurate information</li>
        <li><strong>Non-Discrimination:</strong> Exercise rights without penalty</li>
      </ul>

      <p><a href="/ccpa-rights-portal">Submit a CCPA Request</a></p>
      <p><a href="/do-not-sell">Do Not Sell or Share My Personal Information</a></p>
    </section>

    <section>
      <h2>Contact Us</h2>
      <p>For CCPA-related questions:</p>
      <ul>
        <li><strong>Email:</strong> privacy@makeaihq.com</li>
        <li><strong>Phone:</strong> 1-800-PRIVACY</li>
        <li><strong>Address:</strong> 123 Privacy Street, San Francisco, CA 94102</li>
      </ul>
    </section>
  </div>
</body>
</html>

This notice satisfies CCPA Section 1798.100(a)(1), providing comprehensive disclosure at collection.


Do Not Sell Implementation

CCPA requires a "Do Not Sell My Personal Information" link on your homepage. For ChatGPT apps, "sale" includes sharing conversation data with analytics providers or using it for targeted advertising.

Global Privacy Control (GPC) Support

The CPRA mandates honoring opt-out preference signals like Global Privacy Control (GPC). This browser signal automatically communicates a user's opt-out preference:

// lib/ccpa/doNotSell.ts
interface DoNotSellPreference {
  userId?: string;
  ipAddress: string;
  preference: 'opt-out' | 'opt-in';
  source: 'manual' | 'gpc' | 'cookie';
  timestamp: Date;
}

class DoNotSellManager {
  private static COOKIE_NAME = 'ccpa_opt_out';
  private static COOKIE_DURATION = 365 * 24 * 60 * 60 * 1000; // 1 year

  /**
   * Detect Global Privacy Control signal from browser
   */
  static detectGPC(): boolean {
    // @ts-ignore - GPC is a navigator property
    return navigator.globalPrivacyControl === true;
  }

  /**
   * Check if user has opted out of sale/sharing
   */
  static async hasOptedOut(userId?: string): Promise<boolean> {
    // Check GPC signal first (highest priority)
    if (this.detectGPC()) {
      await this.recordOptOut({ source: 'gpc', userId });
      return true;
    }

    // Check cookie preference
    const cookieValue = this.getCookie(this.COOKIE_NAME);
    if (cookieValue === 'true') {
      return true;
    }

    // Check database preference (for authenticated users)
    if (userId) {
      const userPreference = await this.getDatabasePreference(userId);
      return userPreference?.preference === 'opt-out';
    }

    return false;
  }

  /**
   * Record opt-out preference
   */
  static async recordOptOut(options: {
    source: 'manual' | 'gpc' | 'cookie';
    userId?: string;
  }): Promise<void> {
    const preference: DoNotSellPreference = {
      userId: options.userId,
      ipAddress: await this.getClientIP(),
      preference: 'opt-out',
      source: options.source,
      timestamp: new Date()
    };

    // Set cookie (for unauthenticated users)
    this.setCookie(this.COOKIE_NAME, 'true', this.COOKIE_DURATION);

    // Store in database (for authenticated users)
    if (options.userId) {
      await this.saveDatabasePreference(preference);
    }

    // Log for CCPA compliance records
    await this.logCCPAAction('opt-out', preference);
  }

  /**
   * Record opt-in preference (user consents to sale/sharing)
   */
  static async recordOptIn(userId?: string): Promise<void> {
    const preference: DoNotSellPreference = {
      userId,
      ipAddress: await this.getClientIP(),
      preference: 'opt-in',
      source: 'manual',
      timestamp: new Date()
    };

    // Clear cookie
    this.setCookie(this.COOKIE_NAME, 'false', 0);

    // Update database
    if (userId) {
      await this.saveDatabasePreference(preference);
    }

    // Log action
    await this.logCCPAAction('opt-in', preference);
  }

  // Helper methods
  private static getCookie(name: string): string | null {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
    return null;
  }

  private static setCookie(name: string, value: string, maxAge: number): void {
    document.cookie = `${name}=${value}; path=/; max-age=${maxAge}; secure; samesite=strict`;
  }

  private static async getDatabasePreference(userId: string): Promise<DoNotSellPreference | null> {
    const response = await fetch(`/api/ccpa/preference/${userId}`);
    if (!response.ok) return null;
    return response.json();
  }

  private static async saveDatabasePreference(preference: DoNotSellPreference): Promise<void> {
    await fetch('/api/ccpa/preference', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(preference)
    });
  }

  private static async getClientIP(): Promise<string> {
    try {
      const response = await fetch('https://api.ipify.org?format=json');
      const data = await response.json();
      return data.ip;
    } catch {
      return 'unknown';
    }
  }

  private static async logCCPAAction(action: string, data: any): Promise<void> {
    await fetch('/api/ccpa/log', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ action, data, timestamp: new Date() })
    });
  }
}

export default DoNotSellManager;

This implementation honors GPC signals automatically while maintaining manual opt-out mechanisms for non-GPC browsers.


Verification Processes

CCPA requires reasonable verification of consumer identity before fulfilling requests. The law specifies different verification standards based on request sensitivity:

  • Right to Know (Categories): One-step verification (email confirmation)
  • Right to Know (Specific Pieces): Two-step verification (email + additional identifier)
  • Right to Delete: Two-step verification
  • Right to Correct: Two-step verification

Production-Ready Verification System

// lib/ccpa/verification.ts
import crypto from 'crypto';

interface VerificationLevel {
  requestType: 'know-categories' | 'know-specific' | 'delete' | 'correct';
  steps: number;
  identifiers: string[];
}

class CCPAVerificationSystem {
  private static VERIFICATION_LEVELS: VerificationLevel[] = [
    {
      requestType: 'know-categories',
      steps: 1,
      identifiers: ['email']
    },
    {
      requestType: 'know-specific',
      steps: 2,
      identifiers: ['email', 'phone_or_account_details']
    },
    {
      requestType: 'delete',
      steps: 2,
      identifiers: ['email', 'phone_or_account_details']
    },
    {
      requestType: 'correct',
      steps: 2,
      identifiers: ['email', 'phone_or_account_details']
    }
  ];

  /**
   * Generate verification code (6-digit numeric)
   */
  static generateVerificationCode(): string {
    return crypto.randomInt(100000, 999999).toString();
  }

  /**
   * Send verification email
   */
  static async sendVerificationEmail(
    email: string,
    requestId: string,
    code: string
  ): Promise<void> {
    await fetch('/api/email/send', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        to: email,
        subject: 'CCPA Request Verification Code',
        html: `
          <h2>Verify Your CCPA Request</h2>
          <p>Your verification code is: <strong>${code}</strong></p>
          <p>Request ID: ${requestId}</p>
          <p>This code expires in 15 minutes.</p>
          <p>If you did not submit this request, please ignore this email.</p>
        `
      })
    });
  }

  /**
   * Verify step 1: Email verification
   */
  static async verifyStep1(
    requestId: string,
    providedCode: string
  ): Promise<boolean> {
    const stored = await this.getStoredVerificationCode(requestId);

    if (!stored) {
      throw new Error('Verification code not found or expired');
    }

    if (stored.code !== providedCode) {
      // Log failed attempt
      await this.logVerificationAttempt(requestId, 'step1', false);
      return false;
    }

    // Mark step 1 complete
    await this.markStepComplete(requestId, 1);
    await this.logVerificationAttempt(requestId, 'step1', true);
    return true;
  }

  /**
   * Verify step 2: Additional identifier verification
   */
  static async verifyStep2(
    requestId: string,
    additionalIdentifier: { type: 'phone' | 'account_detail'; value: string }
  ): Promise<boolean> {
    const request = await this.getRequest(requestId);

    if (!request) {
      throw new Error('Request not found');
    }

    // Check if step 1 is complete
    if (!request.step1Complete) {
      throw new Error('Step 1 verification required first');
    }

    // Verify additional identifier matches user records
    const isValid = await this.validateAdditionalIdentifier(
      request.email,
      additionalIdentifier
    );

    if (!isValid) {
      await this.logVerificationAttempt(requestId, 'step2', false);
      return false;
    }

    // Mark step 2 complete
    await this.markStepComplete(requestId, 2);
    await this.logVerificationAttempt(requestId, 'step2', true);
    return true;
  }

  /**
   * Handle authorized agent requests
   */
  static async verifyAuthorizedAgent(
    requestId: string,
    agentDocumentation: {
      signedAuthorization: File;
      powerOfAttorney?: File;
    }
  ): Promise<boolean> {
    // Store documentation for manual review
    await this.storeAgentDocumentation(requestId, agentDocumentation);

    // Flag for manual review (CCPA allows up to 45 days)
    await this.flagForManualReview(requestId, 'authorized_agent');

    return true; // Pending manual review
  }

  // Helper methods
  private static async getStoredVerificationCode(
    requestId: string
  ): Promise<{ code: string; expiresAt: Date } | null> {
    const response = await fetch(`/api/ccpa/verification/${requestId}`);
    if (!response.ok) return null;
    return response.json();
  }

  private static async markStepComplete(
    requestId: string,
    step: number
  ): Promise<void> {
    await fetch(`/api/ccpa/request/${requestId}/step`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ step, complete: true })
    });
  }

  private static async logVerificationAttempt(
    requestId: string,
    step: string,
    success: boolean
  ): Promise<void> {
    await fetch('/api/ccpa/log', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        action: 'verification_attempt',
        requestId,
        step,
        success,
        timestamp: new Date()
      })
    });
  }

  private static async getRequest(requestId: string): Promise<any> {
    const response = await fetch(`/api/ccpa/request/${requestId}`);
    if (!response.ok) return null;
    return response.json();
  }

  private static async validateAdditionalIdentifier(
    email: string,
    identifier: { type: string; value: string }
  ): Promise<boolean> {
    const response = await fetch('/api/ccpa/validate-identifier', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, identifier })
    });

    return response.ok;
  }

  private static async storeAgentDocumentation(
    requestId: string,
    documentation: any
  ): Promise<void> {
    const formData = new FormData();
    formData.append('requestId', requestId);
    formData.append('signedAuthorization', documentation.signedAuthorization);

    if (documentation.powerOfAttorney) {
      formData.append('powerOfAttorney', documentation.powerOfAttorney);
    }

    await fetch('/api/ccpa/agent-documentation', {
      method: 'POST',
      body: formData
    });
  }

  private static async flagForManualReview(
    requestId: string,
    reason: string
  ): Promise<void> {
    await fetch(`/api/ccpa/request/${requestId}/flag`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ reason, flaggedAt: new Date() })
    });
  }
}

export default CCPAVerificationSystem;

This system implements CCPA's graduated verification requirements, ensuring appropriate security for each request type.


Record-Keeping Requirements

CCPA Section 1798.185(a)(8) requires businesses to maintain records of consumer requests and responses for at least 24 months. For ChatGPT apps, this includes:

  • Request logs: Type, date received, date completed, verification method
  • Response logs: Action taken, data disclosed, data deleted
  • Metrics reporting: Annual statistics on requests received and fulfilled

CCPA Request Logger

// lib/ccpa/logger.ts
import { Firestore } from 'firebase-admin/firestore';

interface CCPARequestLog {
  requestId: string;
  requestType: 'know' | 'delete' | 'optOut' | 'correct';
  email: string;
  submittedAt: Date;
  verifiedAt?: Date;
  completedAt?: Date;
  responseDeliveredAt?: Date;
  status: 'pending' | 'verified' | 'completed' | 'denied';
  denialReason?: string;
  verificationMethod: 'email' | 'two-factor' | 'authorized_agent';
  dataDisclosed?: string[]; // Categories or specific pieces
  dataDeleted?: string[]; // Categories deleted
  notes?: string;
}

class CCPALogger {
  private db: Firestore;
  private static COLLECTION = 'ccpa_request_logs';
  private static RETENTION_PERIOD = 24 * 30 * 24 * 60 * 60 * 1000; // 24 months

  constructor(firestore: Firestore) {
    this.db = firestore;
  }

  /**
   * Log new CCPA request
   */
  async logRequest(log: Omit<CCPARequestLog, 'requestId'>): Promise<string> {
    const docRef = this.db.collection(CCPALogger.COLLECTION).doc();

    const requestLog: CCPARequestLog = {
      requestId: docRef.id,
      ...log,
      submittedAt: new Date(),
      status: 'pending'
    };

    await docRef.set(requestLog);
    return docRef.id;
  }

  /**
   * Update request status
   */
  async updateStatus(
    requestId: string,
    status: CCPARequestLog['status'],
    additionalData?: Partial<CCPARequestLog>
  ): Promise<void> {
    const updates: any = { status, ...additionalData };

    if (status === 'verified') {
      updates.verifiedAt = new Date();
    } else if (status === 'completed') {
      updates.completedAt = new Date();
    }

    await this.db
      .collection(CCPALogger.COLLECTION)
      .doc(requestId)
      .update(updates);
  }

  /**
   * Generate annual metrics report (required by CCPA)
   */
  async generateAnnualReport(year: number): Promise<{
    totalRequests: number;
    requestsByType: Record<string, number>;
    averageResponseTime: number; // Days
    deniedRequests: number;
    denialReasons: Record<string, number>;
  }> {
    const startDate = new Date(year, 0, 1);
    const endDate = new Date(year, 11, 31, 23, 59, 59);

    const snapshot = await this.db
      .collection(CCPALogger.COLLECTION)
      .where('submittedAt', '>=', startDate)
      .where('submittedAt', '<=', endDate)
      .get();

    const logs: CCPARequestLog[] = snapshot.docs.map(doc => doc.data() as CCPARequestLog);

    const report = {
      totalRequests: logs.length,
      requestsByType: this.countByType(logs),
      averageResponseTime: this.calculateAverageResponseTime(logs),
      deniedRequests: logs.filter(log => log.status === 'denied').length,
      denialReasons: this.countDenialReasons(logs)
    };

    return report;
  }

  /**
   * Delete logs older than 24 months (retention period)
   */
  async deleteExpiredLogs(): Promise<number> {
    const cutoffDate = new Date(Date.now() - CCPALogger.RETENTION_PERIOD);

    const snapshot = await this.db
      .collection(CCPALogger.COLLECTION)
      .where('submittedAt', '<', cutoffDate)
      .get();

    const batch = this.db.batch();
    snapshot.docs.forEach(doc => batch.delete(doc.ref));

    await batch.commit();
    return snapshot.size;
  }

  // Helper methods
  private countByType(logs: CCPARequestLog[]): Record<string, number> {
    return logs.reduce((acc, log) => {
      acc[log.requestType] = (acc[log.requestType] || 0) + 1;
      return acc;
    }, {} as Record<string, number>);
  }

  private calculateAverageResponseTime(logs: CCPARequestLog[]): number {
    const completedLogs = logs.filter(log => log.completedAt);

    if (completedLogs.length === 0) return 0;

    const totalDays = completedLogs.reduce((sum, log) => {
      const days = Math.floor(
        (log.completedAt!.getTime() - log.submittedAt.getTime()) /
        (24 * 60 * 60 * 1000)
      );
      return sum + days;
    }, 0);

    return Math.round(totalDays / completedLogs.length);
  }

  private countDenialReasons(logs: CCPARequestLog[]): Record<string, number> {
    return logs
      .filter(log => log.status === 'denied' && log.denialReason)
      .reduce((acc, log) => {
        const reason = log.denialReason!;
        acc[reason] = (acc[reason] || 0) + 1;
        return acc;
      }, {} as Record<string, number>);
  }
}

export default CCPALogger;

This logger maintains comprehensive records for CCPA audits and generates annual metrics reports automatically.


Conclusion

CCPA compliance for ChatGPT apps requires a multi-layered approach: consumer rights portals with robust verification, comprehensive privacy notices, opt-out mechanisms honoring GPC signals, and meticulous record-keeping. The penalties for non-compliance—$2,500 per violation plus potential class-action damages—make this a business-critical priority.

The production code in this guide implements every CCPA requirement, from two-factor verification to 24-month log retention. By deploying these systems, your ChatGPT app not only avoids California's harsh penalties but also builds trust with privacy-conscious consumers nationwide.

Ready to build CCPA-compliant ChatGPT apps without coding? Start your free trial at MakeAIHQ.com →


Related Resources

Security & Compliance

  • ChatGPT App Security Best Practices - Complete security architecture for OpenAI Apps SDK
  • GDPR Compliance for ChatGPT Apps - EU privacy law implementation guide
  • Data Encryption for ChatGPT Apps - End-to-end encryption strategies
  • Privacy by Design for AI Apps - Proactive privacy engineering

User Rights & Moderation

  • Content Moderation Integration - OpenAI Moderation API best practices
  • User Rights API Implementation - Building consumer rights portals

Industry Applications

  • Legal Services ChatGPT App - Privacy-critical legal tech implementation

External Resources