Building a Consent Management Platform

A robust consent management platform (CMP) forms the foundation of compliant cookie usage. While commercial CMPs exist, understanding how to build one illuminates the technical requirements and enables better integration with custom applications. The CMP must handle consent collection, storage, enforcement, and documentation while providing seamless user experience.

// Comprehensive Cookie Consent Management Platform
class CookieConsentManager {
  constructor(config = {}) {
    this.config = {
      cookieName: 'cookie_consent',
      cookieDomain: window.location.hostname,
      cookieExpiry: 365,
      defaultRegion: 'US',
      geoIpEndpoint: '/api/geoip',
      consentEndpoint: '/api/privacy/consent',
      cookieCategories: {
        necessary: {
          name: 'Necessary',
          description: 'Essential for website functionality',
          required: true,
          cookies: ['session_id', 'csrf_token', 'auth_token']
        },
        analytics: {
          name: 'Analytics',
          description: 'Help us understand how visitors interact with our website',
          required: false,
          cookies: ['_ga', '_gid', '_gat', 'amplitude_id'],
          scripts: ['google-analytics', 'amplitude']
        },
        marketing: {
          name: 'Marketing',
          description: 'Used to deliver personalized advertisements',
          required: false,
          cookies: ['_fbp', 'fr', 'ads_session'],
          scripts: ['facebook-pixel', 'google-ads']
        },
        functional: {
          name: 'Functional',
          description: 'Enable enhanced functionality and personalization',
          required: false,
          cookies: ['language', 'theme', 'timezone'],
          localStorage: ['user_preferences', 'saved_searches']
        }
      },
      ...config
    };
    
    this.consent = null;
    this.region = null;
    this.initialized = false;
  }

  // Initialize consent management
  async init() {
    // Detect user region for appropriate consent flow
    this.region = await this.detectRegion();
    
    // Load existing consent
    this.consent = this.loadConsent();
    
    // Apply consent or show banner
    if (this.consent && !this.consentExpired()) {
      await this.applyConsent();
    } else {
      await this.showConsentBanner();
    }
    
    // Block scripts until consent is given
    this.interceptScriptLoading();
    
    // Monitor for consent changes
    this.setupConsentMonitoring();
    
    this.initialized = true;
  }

  // Detect user region for appropriate consent flow
  async detectRegion() {
    try {
      // Check for cached region
      const cachedRegion = sessionStorage.getItem('user_region');
      if (cachedRegion) return cachedRegion;
      
      // Call GeoIP service
      const response = await fetch(this.config.geoIpEndpoint);
      const data = await response.json();
      
      // Map country to privacy region
      const region = this.mapCountryToRegion(data.country);
      sessionStorage.setItem('user_region', region);
      
      return region;
    } catch (error) {
      console.error('Failed to detect region:', error);
      return this.config.defaultRegion;
    }
  }

  // Map country codes to privacy regions
  mapCountryToRegion(country) {
    const regions = {
      EU: ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 
           'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 
           'RO', 'SK', 'SI', 'ES', 'SE', 'GB'],
      CA: ['US-CA'], // California
      BR: ['BR'],    // Brazil (LGPD)
      US: ['US']     // Other US states
    };
    
    for (const [region, countries] of Object.entries(regions)) {
      if (countries.includes(country)) return region;
    }
    
    return 'OTHER';
  }

  // Show appropriate consent banner based on region
  async showConsentBanner() {
    const bannerConfig = this.getRegionBannerConfig();
    const banner = this.createBanner(bannerConfig);
    
    // Add to page
    document.body.appendChild(banner);
    
    // Animate entrance
    requestAnimationFrame(() => {
      banner.classList.add('visible');
    });
    
    // Focus management for accessibility
    const firstButton = banner.querySelector('button');
    if (firstButton) firstButton.focus();
    
    // Prevent page interaction until consent decision
    if (bannerConfig.blockingRequired) {
      this.createBlockingOverlay();
    }
  }

  // Get region-specific banner configuration
  getRegionBannerConfig() {
    const configs = {
      EU: {
        type: 'opt-in',
        blockingRequired: true,
        showRejectAll: true,
        defaultSelections: {
          necessary: true,
          analytics: false,
          marketing: false,
          functional: false
        },
        text: {
          title: 'We value your privacy',
          description: 'We use cookies to enhance your browsing experience, analyze traffic, and personalize content. By clicking "Accept All", you consent to our use of cookies.',
          acceptAll: 'Accept All',
          rejectAll: 'Reject All',
          customize: 'Customize',
          save: 'Save Preferences'
        }
      },
      CA: {
        type: 'opt-out',
        blockingRequired: false,
        showRejectAll: false,
        defaultSelections: {
          necessary: true,
          analytics: true,
          marketing: true,
          functional: true
        },
        text: {
          title: 'Cookie Notice',
          description: 'We use cookies and similar technologies. You can manage your preferences or opt-out of data sales.',
          acceptAll: 'OK',
          customize: 'Manage Preferences',
          optOut: 'Do Not Sell My Personal Information'
        }
      }
    };
    
    return configs[this.region] || configs.US;
  }

  // Create consent banner DOM
  createBanner(config) {
    const banner = document.createElement('div');
    banner.className = 'cookie-consent-banner';
    banner.setAttribute('role', 'dialog');
    banner.setAttribute('aria-label', 'Cookie consent');
    banner.setAttribute('aria-modal', 'true');
    
    banner.innerHTML = `
      <div class="consent-content">
        <h2 id="consent-title">${config.text.title}</h2>
        <p id="consent-description">${config.text.description}</p>
        
        <div class="consent-options" id="consent-options" style="display: none;">
          ${Object.entries(this.config.cookieCategories).map(([key, category]) => `
            <label class="consent-option ${category.required ? 'required' : ''}">
              <input type="checkbox" 
                     name="consent_${key}" 
                     value="${key}"
                     ${category.required ? 'checked disabled' : ''}
                     ${config.defaultSelections[key] ? 'checked' : ''}
                     aria-describedby="desc_${key}">
              <span class="option-name">${category.name}</span>
              <span class="option-description" id="desc_${key}">${category.description}</span>
            </label>
          `).join('')}
        </div>
        
        <div class="consent-actions">
          <button class="btn-primary" onclick="cookieConsent.acceptAll()">
            ${config.text.acceptAll}
          </button>
          ${config.showRejectAll ? `
            <button class="btn-secondary" onclick="cookieConsent.rejectAll()">
              ${config.text.rejectAll}
            </button>
          ` : ''}
          <button class="btn-text" onclick="cookieConsent.showOptions()">
            ${config.text.customize}
          </button>
          ${config.text.optOut ? `
            <a href="/privacy/do-not-sell" class="opt-out-link">
              ${config.text.optOut}
            </a>
          ` : ''}
        </div>
      </div>
    `;
    
    return banner;
  }

  // Handle accept all action
  async acceptAll() {
    const consent = {};
    Object.keys(this.config.cookieCategories).forEach(category => {
      consent[category] = true;
    });
    
    await this.saveConsent(consent);
    this.closeBanner();
  }

  // Handle reject all action
  async rejectAll() {
    const consent = {};
    Object.keys(this.config.cookieCategories).forEach(category => {
      consent[category] = this.config.cookieCategories[category].required;
    });
    
    await this.saveConsent(consent);
    this.closeBanner();
  }

  // Show detailed options
  showOptions() {
    const optionsDiv = document.getElementById('consent-options');
    const actionsDiv = document.querySelector('.consent-actions');
    
    optionsDiv.style.display = 'block';
    
    // Update buttons
    actionsDiv.innerHTML = `
      <button class="btn-primary" onclick="cookieConsent.savePreferences()">
        Save Preferences
      </button>
      <button class="btn-secondary" onclick="cookieConsent.hideOptions()">
        Cancel
      </button>
    `;
  }

  // Save user preferences
  async savePreferences() {
    const consent = {};
    const checkboxes = document.querySelectorAll('.consent-options input[type="checkbox"]');
    
    checkboxes.forEach(checkbox => {
      const category = checkbox.value;
      consent[category] = checkbox.checked;
    });
    
    await this.saveConsent(consent);
    this.closeBanner();
  }

  // Save consent with all required metadata
  async saveConsent(consent) {
    const consentRecord = {
      id: this.generateConsentId(),
      timestamp: new Date().toISOString(),
      consent: consent,
      region: this.region,
      consentType: this.getRegionBannerConfig().type,
      cookieVersion: '1.0',
      source: 'banner',
      ipHash: await this.hashIP(),
      userAgent: navigator.userAgent
    };
    
    // Save to cookie
    this.setCookie(this.config.cookieName, JSON.stringify(consentRecord), {
      expires: this.config.cookieExpiry,
      domain: this.config.cookieDomain,
      sameSite: 'Lax',
      secure: true
    });
    
    // Save to server for compliance records
    try {
      await fetch(this.config.consentEndpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(consentRecord)
      });
    } catch (error) {
      console.error('Failed to save consent to server:', error);
    }
    
    // Apply consent immediately
    this.consent = consentRecord;
    await this.applyConsent();
  }

  // Apply consent by controlling cookies and scripts
  async applyConsent() {
    // Remove non-consented cookies
    this.removeNonConsentedCookies();
    
    // Enable/disable scripts based on consent
    this.controlScripts();
    
    // Update third-party services
    this.updateThirdPartyServices();
    
    // Dispatch consent event for other components
    window.dispatchEvent(new CustomEvent('cookieConsentUpdated', {
      detail: this.consent
    }));
  }

  // Remove cookies for non-consented categories
  removeNonConsentedCookies() {
    const allCookies = this.getAllCookies();
    
    Object.entries(this.config.cookieCategories).forEach(([category, config]) => {
      if (!this.hasConsent(category) && !config.required) {
        config.cookies?.forEach(cookieName => {
          // Remove exact matches and pattern matches
          allCookies.forEach(cookie => {
            if (cookie.name === cookieName || cookie.name.startsWith(cookieName)) {
              this.deleteCookie(cookie.name);
            }
          });
        });
        
        // Clear localStorage items
        config.localStorage?.forEach(key => {
          localStorage.removeItem(key);
        });
      }
    });
  }

  // Control script loading based on consent
  controlScripts() {
    // Enable consented scripts
    document.querySelectorAll('script[data-consent-category]').forEach(script => {
      const category = script.getAttribute('data-consent-category');
      if (this.hasConsent(category)) {
        // Create new script element to trigger execution
        const newScript = document.createElement('script');
        Array.from(script.attributes).forEach(attr => {
          if (attr.name !== 'type') {
            newScript.setAttribute(attr.name, attr.value);
          }
        });
        newScript.textContent = script.textContent;
        script.parentNode.replaceChild(newScript, script);
      }
    });
  }

  // Intercept dynamic script loading
  interceptScriptLoading() {
    const originalAppend = Element.prototype.appendChild;
    const self = this;
    
    Element.prototype.appendChild = function(element) {
      if (element.tagName === 'SCRIPT' && element.src) {
        const category = self.getScriptCategory(element.src);
        if (category && !self.hasConsent(category)) {
          console.log(`Blocked script loading: ${element.src} (requires ${category} consent)`);
          return element;
        }
      }
      return originalAppend.call(this, element);
    };
  }

  // Determine script category from URL
  getScriptCategory(url) {
    const scriptMappings = {
      'google-analytics.com': 'analytics',
      'googletagmanager.com': 'analytics',
      'facebook.com': 'marketing',
      'doubleclick.net': 'marketing',
      'amplitude.com': 'analytics'
    };
    
    for (const [domain, category] of Object.entries(scriptMappings)) {
      if (url.includes(domain)) {
        return category;
      }
    }
    
    return null;
  }

  // Check if category has consent
  hasConsent(category) {
    if (!this.consent) return false;
    return this.consent.consent[category] === true;
  }

  // Update third-party services based on consent
  updateThirdPartyServices() {
    // Google Analytics
    if (typeof gtag !== 'undefined') {
      if (this.hasConsent('analytics')) {
        gtag('consent', 'update', {
          'analytics_storage': 'granted'
        });
      } else {
        gtag('consent', 'update', {
          'analytics_storage': 'denied'
        });
      }
    }
    
    // Facebook Pixel
    if (typeof fbq !== 'undefined') {
      if (!this.hasConsent('marketing')) {
        fbq('dataProcessingOptions', ['LDU'], 1, 1000);
      } else {
        fbq('dataProcessingOptions', []);
      }
    }
  }

  // Cookie utility functions
  setCookie(name, value, options = {}) {
    let cookie = `${name}=${encodeURIComponent(value)}`;
    
    if (options.expires) {
      const date = new Date();
      date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000));
      cookie += `; expires=${date.toUTCString()}`;
    }
    
    if (options.domain) {
      cookie += `; domain=${options.domain}`;
    }
    
    cookie += `; path=${options.path || '/'}`;
    
    if (options.secure) {
      cookie += '; secure';
    }
    
    if (options.sameSite) {
      cookie += `; samesite=${options.sameSite}`;
    }
    
    document.cookie = cookie;
  }

  getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) {
      return decodeURIComponent(parts.pop().split(';').shift());
    }
    return null;
  }

  deleteCookie(name) {
    this.setCookie(name, '', { expires: -1 });
    // Try deleting with different domain variations
    this.setCookie(name, '', { expires: -1, domain: `.${window.location.hostname}` });
    this.setCookie(name, '', { expires: -1, domain: window.location.hostname });
  }

  getAllCookies() {
    return document.cookie.split(';').map(cookie => {
      const [name, value] = cookie.trim().split('=');
      return { name, value: decodeURIComponent(value || '') };
    }).filter(cookie => cookie.name);
  }
}

// Initialize consent management
const cookieConsent = new CookieConsentManager();
document.addEventListener('DOMContentLoaded', () => cookieConsent.init());