Custom Client

Implement your own consent storage solution with a custom client for ultimate flexibility and control

The custom client mode gives you complete control over how consent decisions are stored and retrieved, allowing for integration with any storage backend or custom logic.

Key Characteristics

  • Maximum flexibility - Design a storage solution that meets your specific requirements
  • Full integration - Connect with existing user data systems or authentication services
  • Complete control - Implement custom caching, batching, or synchronization mechanisms
  • Advanced use cases - Support for complex scenarios like multi-tenant applications

The custom client mode is the most flexible but also the most complex implementation option. Only use it if you need complete control over consent handling.

Implementation

Install the Package

npm install @c15t/react

Create Custom Endpoint Handlers

You must implement all three core handlers: showConsentBanner, setConsent, and verifyConsent.

First, create handlers for the required endpoints:

lib/consent-handlers.ts
import type {
  SetConsentRequestBody,
  SetConsentResponse,
  ShowConsentBannerResponse,
  VerifyConsentRequestBody,
  VerifyConsentResponse,
} from '@c15t/backend';
import type { EndpointHandlers } from '@c15t/react';
 
// Custom storage implementation (could be localStorage, IndexedDB, etc.)
const consentStorage = {
  getItem: (key: string) => {
    if (typeof window === 'undefined') return null;
    return localStorage.getItem(key);
  },
  setItem: (key: string, value: string) => {
    if (typeof window === 'undefined') return;
    localStorage.setItem(key, value);
  }
};
 
export const createCustomHandlers = (): EndpointHandlers => {
  return {
    // Handler for checking if banner should be shown
    showConsentBanner: async () => {
      const hasConsent = Boolean(consentStorage.getItem('custom-consent'));
      
      return {
        data: {
          showConsentBanner: !hasConsent,
          jurisdiction: { code: 'EU', message: 'European Union' },
          location: { countryCode: 'DE', regionCode: null }
        },
        error: null,
        ok: true,
        response: null
      };
    },
    
    // Handler for saving consent preferences
    setConsent: async (options) => {
      const body = options?.body as SetConsentRequestBody;
      
      // Store the consent preferences
      consentStorage.setItem(
        'custom-consent',
        JSON.stringify({
          timestamp: new Date().toISOString(),
          preferences: body?.preferences || {},
        })
      );
      
      return {
        data: { success: true },
        error: null,
        ok: true,
        response: null
      };
    },
    
    // Handler for verifying consent
    verifyConsent: async (options) => {
      const body = options?.body as VerifyConsentRequestBody;
      const storedConsent = consentStorage.getItem('custom-consent');
      
      // Parse stored consent or create default response
      let response: VerifyConsentResponse = {
        valid: false,
        requiredConsent: body?.requiredConsent || [],
        missingConsent: body?.requiredConsent || []
      };
      
      if (storedConsent) {
        const parsedConsent = JSON.parse(storedConsent);
        const preferences = parsedConsent.preferences || {};
        
        // Check if all required consent items are present and accepted
        const missingConsent = (body?.requiredConsent || []).filter(
          item => !preferences[item] || preferences[item] !== true
        );
        
        response = {
          valid: missingConsent.length === 0,
          requiredConsent: body?.requiredConsent || [],
          missingConsent
        };
      }
      
      return {
        data: response,
        error: null,
        ok: true,
        response: null
      };
    }
  };
};

Configure the Provider with Custom Client

app/layout.tsx
import { 
  ConsentManagerDialog,
  ConsentManagerProvider,
  CookieBanner,
  type ConsentManagerOptions
} from '@c15t/react';
import { createCustomHandlers } from '../lib/consent-handlers';
 
export default function Layout({ children }: { children: React.ReactNode }) {
  const options: ConsentManagerOptions = {
    mode: 'custom',
    endpointHandlers: createCustomHandlers(),
    // Optional event callbacks
    callbacks: {
      onConsentSet: (response) => {
        console.log('Consent has been saved');
      }
    }
  };
 
  return (
    <ConsentManagerProvider options={options}>
      {children}
      <CookieBanner />
      <ConsentManagerDialog />
    </ConsentManagerProvider>
  );
}

Custom Handler Interface

Each handler must return a Promise that resolves to a ResponseContext object with the appropriate data structure.

Each handler function must implement the EndpointHandler interface, which returns a Promise resolving to a ResponseContext object:

type EndpointHandler<ResponseType, BodyType, QueryType> = (
  options?: FetchOptions<ResponseType, BodyType, QueryType>
) => Promise<ResponseContext<ResponseType>>;
 
interface ResponseContext<T> {
  data: T | null;
  error: {
    message: string;
    status: number;
    code: string;
    cause?: unknown;
  } | null;
  ok: boolean;
  response: Response | null;
}

Required Endpoint Handlers

Your custom client must implement three core endpoint handlers:

1. showConsentBanner

Determines if the consent banner should be displayed to the user.

showConsentBanner: EndpointHandler<ShowConsentBannerResponse>;
 
// Return type structure
interface ShowConsentBannerResponse {
  showConsentBanner: boolean;
  jurisdiction: {
    code: string;
    message: string;
  };
  location: {
    countryCode: string;
    regionCode: string | null;
  };
}

2. setConsent

Saves user consent preferences.

setConsent: EndpointHandler<SetConsentResponse, SetConsentRequestBody>;
 
// Request body structure
interface SetConsentRequestBody {
  preferences: Record<string, boolean>;
  // Additional custom fields can be included
}
 
// Response structure
interface SetConsentResponse {
  success: boolean;
}

3. verifyConsent

Checks if existing consent meets required criteria.

verifyConsent: EndpointHandler<VerifyConsentResponse, VerifyConsentRequestBody>;
 
// Request body structure
interface VerifyConsentRequestBody {
  requiredConsent: string[];
}
 
// Response structure
interface VerifyConsentResponse {
  valid: boolean;
  requiredConsent: string[];
  missingConsent: string[];
}

Advanced Features

Dynamic Handlers

Dynamic handlers allow you to extend the client's functionality beyond the core consent operations.

You can register additional custom endpoint handlers for specialized functionality:

import { useConsentManager } from '@c15t/react';
 
function MyComponent() {
  const { client } = useConsentManager();
  
  useEffect(() => {
    if (client && 'registerHandler' in client) {
      // Register a custom endpoint handler
      client.registerHandler('customEndpoint', async (options) => {
        // Custom implementation
        return {
          data: { customData: 'example' },
          error: null,
          ok: true,
          response: null
        };
      });
    }
  }, [client]);
  
  return <div>My component</div>;
}

Error Handling

Proper error handling is essential for custom clients to ensure your application behaves predictably.

Custom handlers should handle errors and return appropriate response contexts:

const errorHandler: EndpointHandler = async () => {
  try {
    // Operation that might fail
    throw new Error('Something went wrong');
  } catch (error) {
    return {
      data: null,
      error: {
        message: error instanceof Error ? error.message : String(error),
        status: 500,
        code: 'CUSTOM_ERROR',
        cause: error
      },
      ok: false,
      response: null
    };
  }
};

Integration with External Systems

Custom clients can seamlessly integrate with third-party consent management systems:

const createThirdPartyHandlers = (): EndpointHandlers => {
  return {
    showConsentBanner: async () => {
      // Call third-party API to check if banner should be shown
      const response = await thirdPartyClient.checkConsentStatus();
      
      return {
        data: {
          showConsentBanner: response.needsConsent,
          jurisdiction: { code: response.region, message: response.regionName },
          location: { countryCode: response.country, regionCode: null }
        },
        error: null,
        ok: true,
        response: null
      };
    },
    
    // Other handlers...
  };
};

Use Cases

Integration with Existing Systems

If your organization already has a consent management system, custom handlers provide a bridge between it and c15t's UI components.

// Integrate with an existing in-house consent system
const createIntegrationHandlers = (): EndpointHandlers => {
  return {
    showConsentBanner: async () => {
      const consentStatus = await existingConsentSystem.getStatus();
      return {
        data: {
          showConsentBanner: consentStatus.requiresBanner,
          jurisdiction: consentStatus.jurisdiction,
          location: consentStatus.location
        },
        error: null,
        ok: true,
        response: null
      };
    },
    
    // Other handlers
  };
};
// Create handlers for A/B testing different consent UIs
const createABTestHandlers = (): EndpointHandlers => {
  // Randomly assign user to test group
  const testGroup = Math.random() > 0.5 ? 'A' : 'B';
  
  return {
    showConsentBanner: async () => {
      // Log the test group for analytics
      analytics.logGroup(testGroup);
      
      return {
        data: {
          showConsentBanner: true,
          jurisdiction: { code: 'EU', message: 'European Union' },
          location: { countryCode: 'DE', regionCode: null },
          // Include test group in the response data
          testGroup
        },
        error: null,
        ok: true,
        response: null
      };
    },
    
    // Other handlers
  };
};

Hybrid Storage Approach

A hybrid approach can give you the best of both worlds: local storage for reliability and remote storage for analytics.

// Store data both locally and remotely when connection is available
const createHybridHandlers = (): EndpointHandlers => {
  return {
    setConsent: async (options) => {
      const body = options?.body;
      
      // Always save locally first
      localStorage.setItem('hybrid-consent', JSON.stringify({
        timestamp: new Date().toISOString(),
        preferences: body?.preferences
      }));
      
      // Try to save remotely if online
      if (navigator.onLine) {
        try {
          await fetch('/api/external-consent', {
            method: 'POST',
            body: JSON.stringify(body)
          });
        } catch (error) {
          // Queue for retry later
          const queue = JSON.parse(localStorage.getItem('sync-queue') || '[]');
          queue.push({ type: 'setConsent', data: body });
          localStorage.setItem('sync-queue', JSON.stringify(queue));
        }
      }
      
      return {
        data: { success: true },
        error: null,
        ok: true,
        response: null
      };
    },
    
    // Other handlers
  };
};
export const externalCMPStorage = {
  async getConsent(consentId) {
    // Map to your CMP's purpose ID if needed
    const purposeId = mapToPurposeId(consentId);
    return externalCMP.getPurposeConsent(purposeId);
  },
  
  async getConsents() {
    // Get all consents from external CMP
    const purposes = await externalCMP.getAllPurposeConsents();
    
    // Transform to c15t format
    return purposes.map(purpose => ({
      id: mapFromPurposeId(purpose.id),
      status: purpose.consent,
      timestamp: purpose.timestamp
    }));
  },
  
  async setConsent(consentId, value) {
    const purposeId = mapToPurposeId(consentId);
    await externalCMP.setPurposeConsent(purposeId, value.status);
    return { success: true };
  }
};
 
function mapToPurposeId(c15tId) {
  const mapping = {
    'marketing': 'purpose-1',
    'analytics': 'purpose-2',
    // Add your mappings
  };
  return mapping[c15tId] || c15tId;
}
 
function mapFromPurposeId(purposeId) {
  const mapping = {
    'purpose-1': 'marketing',
    'purpose-2': 'analytics',
    // Add your mappings
  };
  return mapping[purposeId] || purposeId;
}

When to Use Custom Client

Consider using the custom client mode when:

  • You need to integrate with an existing user data system
  • You have complex consent storage requirements
  • You want to implement advanced features like batching or caching
  • You're integrating with an external consent management platform
  • You need to support multi-tenant applications
  • You're implementing a hybrid online/offline strategy

For simpler use cases, consider Server-Based Storage or Offline Mode instead.

Next Steps

c15t.com