Skip to content

Cookie Consent & Tracking Setup

Sellf ships with a built-in cookie consent system powered by vanilla-cookieconsent v3 (orestbida). It gates Google Tag Manager and Meta Pixel (incl. Conversions API) behind opt-in consent and wires Google Consent Mode V2. Umami Analytics runs cookieless and is not gated — see below. All settings live in the admin panel; no env vars required.

Compliance is configuration, not just code. GDPR/CCPA regulate behaviour, not which library you use. cookieconsent is a tool — the regulatory posture below depends on you keeping the defaults Sellf ships with.

  1. Default ALL OFF. Categories analytics and marketing are opt-in (mode: 'opt-in', only necessary is enabled:true readOnly:true). No tracking script may fire before the visitor explicitly accepts.
  2. Every tracking script tagged. GTM + Meta Pixel render as type="text/plain" data-category=... data-service=.... cookieconsent rewrites them to executable scripts only on opt-in. Any future tracking script you add MUST follow this convention or it will run unconditionally and break compliance.
  3. Informed copy. The banner names each provider that is actually configured and its storage horizon (e.g. “Google Tag Manager — up to 24 months”, “Meta Pixel — ~7 days”). Generic copy (“we use cookies”) does not satisfy the GDPR Art. 7 “freely given, specific and informed” standard.
  4. Re-consent. The marketing footer (LandingFooter) carries a “Cookie preferences” button (data-cc="show-preferencesModal") so visitors can withdraw consent as easily as they gave it (GDPR Art. 7(3)). Add the same button to any other globally visible chrome you ship.
  5. Cookie policy in /privacy. Sellf does NOT ship a privacy or cookie policy. /privacy and /terms redirect to admin-supplied PRIVACY_URL / TERMS_URL. You are responsible for publishing a cookie inventory (name, host, lifetime, purpose) there — the banner alone is not enough.

Umami exception. Umami runs in cookieless mode (no client cookies, anonymous visitor hashing). Under ePrivacy Art. 5(3) + GDPR recital 30, purely anonymous analytics that store nothing on the device are exempt from consent. Sellf therefore loads the Umami script unconditionally and it is not listed in the consent banner.


Admin Panel (/dashboard/integrations)
integrations_config table (singleton row, id=1)
get_public_integrations_config() RPC (public, safe subset of config)
Root Layout (server component) fetches config
├─► TrackingProvider (injects scripts + cookieconsent)
└─► TrackingConfigProvider (React context for event tracking hooks)

All configuration is stored in the database and managed through the admin UI at /dashboard/integrations. No environment variables are needed for consent or tracking.


  1. Go to /dashboard/integrationsConsents tab
  2. Enable “Require Consent” → Save
  3. (Optional) Add tracking IDs in Analytics or Marketing tabs
  4. Done — cookieconsent banner appears on all pages

Admin Panel: /dashboard/integrations → Consents tab

Setting Effect
Require Consent Shows cookieconsent banner, blocks non-essential scripts until user consents
Consent Logging Logs each user’s consent choices to consent_logs table (for GDPR auditing)
Send conversions without consent Sends Purchase/Lead events via CAPI even if user declines cookies (legitimate interest)

When consent is enabled, tracking scripts are injected with type="text/plain" with data-category and data-service attributes. cookieconsent intercepts these and only converts them to executable JavaScript after the user consents to that specific service.

Admin Panel: /dashboard/integrations → Marketing tab

Field Format Where to find
Pixel ID 10-20 digit number Meta Events Manager → Settings
CAPI Token Access token string Events Manager → Settings → Generate Access Token
Test Event Code TEST12345 Events Manager → Test Events (for debugging)
Enable CAPI Checkbox Enables server-side conversion tracking

What gets tracked (client-side, requires consent):

User Action Pixel Event GTM Event
Views product page ViewContent view_item
Clicks “Buy” InitiateCheckout begin_checkout
Enters payment info AddPaymentInfo add_payment_info
Completes purchase Purchase purchase
Signs up / free product Lead generate_lead

What gets tracked (server-side via CAPI):

  • Purchase and Lead events only
  • Bypasses adblockers
  • Can work without cookie consent (see Conversions Without Consent)
  • Deduplication with client-side Pixel via shared event_id

Admin Panel: /dashboard/integrations → Analytics tab

Field Format Example
Container ID GTM-XXXXXXX GTM-ABC123
Server Container URL https://... (optional) https://gtm.yourdomain.com

The Server Container URL is for GTM Server-Side Tagging — it proxies tracking requests through your domain, bypassing most adblockers. Requires separate infrastructure (e.g., Stape.io or self-hosted).

Google Consent Mode V2 is automatically configured:

  • Defaults to denied for all consent types before cookieconsent loads
  • Updates to granted when user accepts via cookieconsent callback

Admin Panel: /dashboard/integrations → Analytics tab

Field Format Default
Website ID UUID
Script URL HTTPS URL https://cloud.umami.is/script.js

Umami is a privacy-focused alternative to Google Analytics. Sign up at umami.is.

Admin Panel: /dashboard/integrations → Script Manager tab

Add any third-party script with:

  • Location: head or body
  • Category: essential (no consent needed), analytics, or marketing (consent required)
  • Content: Raw <script> content or external URL
  • Active toggle: Enable/disable without deleting

Non-essential scripts are automatically managed by cookieconsent — they only execute after user consent for the matching purpose.

Admin Panel: /dashboard/integrations → Consents tab → “Send conversions without cookie consent”

When enabled:

  • Purchase and Lead events are sent to Facebook CAPI even if the user declines cookies
  • Uses hashed email + IP for matching (no _fbc/_fbp cookies)
  • Legal basis: legitimate interest (GDPR Article 6(1)(f))
  • All other events (ViewContent, InitiateCheckout, etc.) still require consent

Important: Your Privacy Policy must mention server-side conversion tracking under legitimate interest.

When enabled, every interaction with the cookieconsent banner is logged via POST /api/consent:

{
"anonymous_id": "uuid",
"consents": { "google-tag-manager": true, "facebook-pixel": false },
"consent_version": "1"
}

Stored in consent_logs table with IP, User-Agent, and timestamp. Rate limited to 30 requests/minute per IP.


User visits site
→ TrackingProvider renders in root layout
→ cookieconsent loads + shows banner (if no sellf_consent cookie)
→ Scripts injected as type="text/plain" (blocked)
User clicks "Accept All"
→ cookieconsent sets cookie: sellf_consent = {"categories":["necessary","analytics","marketing"], "services":{"analytics":["gtm"],"marketing":["pixel"]}, ...}
→ cookieconsent converts scripts from text/plain → text/javascript (executes them)
→ cookieconsent callback updates Google Consent Mode: analytics_storage → "granted"
→ POST /api/consent logs the choice (if consent_logging_enabled)
User clicks "Decline" / closes banner
→ Cookie set with all services = false
→ Scripts remain as text/plain (never execute)
→ CAPI still sends Purchase/Lead (if send_conversions_without_consent = true)

TrackingProvider (src/components/TrackingProvider.tsx) injects scripts in this order:

  1. Google Consent Mode V2 defaults (always, before everything else)
  2. cookieconsent config + library (if cookie_consent_enabled)
  3. GTM script (managed by cookieconsent if consent enabled)
  4. Meta Pixel script (managed by cookieconsent if consent enabled)
  5. Umami script (managed by cookieconsent if consent enabled)
  6. Custom scripts (managed by cookieconsent based on category)

All IDs are validated before injection to prevent XSS:

  • GTM: /^GTM-[A-Z0-9]{1,10}$/i
  • Pixel: /^\d{10,20}$/
  • Umami: UUID format
  • URLs: Must start with https://

When GTM is enabled, the following is set before GTM loads:

gtag('consent', 'default', {
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'analytics_storage': 'denied',
'wait_for_update': 500
});

After user interacts with cookieconsent, the callback updates consent:

gtag('consent', 'update', {
'analytics_storage': consent['google-tag-manager'] ? 'granted' : 'denied',
'ad_storage': consent['facebook-pixel'] ? 'granted' : 'denied',
'ad_user_data': consent['facebook-pixel'] ? 'granted' : 'denied',
'ad_personalization': consent['facebook-pixel'] ? 'granted' : 'denied',
});

Facebook Conversions API sends events server-to-server, bypassing adblockers:

Client: trackEvent('purchase', data)
→ POST /api/tracking/fb-capi
→ Server checks: fb_capi_enabled? capi_token exists?
→ Server checks: hasConsent OR send_conversions_without_consent?
→ If yes: POST https://graph.facebook.com/v18.0/{pixel_id}/events

With consent: sends full data including _fbc, _fbp cookies for better matching. Without consent: sends hashed email + IP only (lower match rate but still valuable).

Client-side tracking (src/lib/tracking/client.ts) provides:

trackEvent(eventName, data, config)

Before sending, it checks cookieconsent consent:

  • hasFacebookConsent() → reads sellf_consent cookie for facebook-pixel
  • hasGTMConsent() → reads sellf_consent cookie for google-tag-manager

Events are pushed to:

  1. GTM dataLayer (if GTM enabled + consent)
  2. fbq('track', ...) (if Pixel enabled + consent)
  3. /api/tracking/fb-capi (if CAPI enabled, consent rules apply server-side)

Deduplication: each event gets a UUID event_id shared between Pixel and CAPI.


Column Type Purpose
gtm_container_id TEXT GTM Container ID
gtm_server_container_url TEXT GTM Server URL
facebook_pixel_id TEXT Meta Pixel ID
facebook_capi_token TEXT Meta CAPI access token
facebook_test_event_code TEXT Meta test event code
fb_capi_enabled BOOLEAN Enable server-side conversions
send_conversions_without_consent BOOLEAN Allow CAPI without consent
umami_website_id TEXT Umami Website UUID
umami_script_url TEXT Umami script URL
cookie_consent_enabled BOOLEAN Show cookieconsent banner
consent_logging_enabled BOOLEAN Log consent to DB
sellf_license TEXT License key (unrelated)
Column Type Purpose
id UUID Primary key
name TEXT Display name
script_location TEXT head or body
script_content TEXT Script code or URL
category TEXT essential, analytics, or marketing
is_active BOOLEAN Enable/disable
cookie_consent_enabled BOOLEAN Whether cookieconsent manages this script
Column Type Purpose
id UUID Primary key
user_id UUID Authenticated user (nullable)
anonymous_id TEXT Anonymous visitor ID
ip_address TEXT Visitor IP
user_agent TEXT Browser User-Agent
consents JSONB {"service-name": true/false}
consent_version TEXT Config version
created_at TIMESTAMPTZ Timestamp

Scenario GTM GTM Server Pixel CAPI Umami Consent
Meta Pixel only - - Yes Yes - Yes
Privacy-focused (no Big Tech) - - - - Yes Yes
Full Google ecosystem Yes - Yes Yes - Yes
Adblocker resistant Yes Yes Yes Yes - Yes
All features Yes Yes Yes Yes Yes Yes
No tracking (consent banner only) - - - - - Yes

Visit your site in an incognito window — the cookieconsent banner should appear at the bottom.

Browser DevTools → Application → Cookies → look for sellf_consent:

{"google-tag-manager":true,"facebook-pixel":true,"umami-analytics":true}

DevTools → Elements → search for text/plain:

  • Before consent: tracking scripts have type="text/plain"
  • After consent: scripts have type="text/javascript" (or no type attribute)

Events Manager → Test Events → enter your Test Event Code → trigger events on your site. Events should appear within seconds.

SELECT anonymous_id, consents, created_at
FROM consent_logs
ORDER BY created_at DESC
LIMIT 10;

Browser DevTools → Console → filter for [tracking] or [fb-capi] log messages.


File Purpose
src/components/TrackingProvider.tsx Script injection + cookieconsent configuration
src/components/IntegrationsForm.tsx Admin UI for all integrations settings
src/lib/actions/integrations.ts Server actions (get/update config, CRUD scripts)
src/lib/validations/integrations.ts Input validation for all tracking IDs/URLs
src/lib/tracking/client.ts Client-side event tracking + consent checks
src/lib/tracking/server.ts Server-side CAPI tracking
src/app/api/consent/route.ts Consent logging endpoint
src/app/api/tracking/fb-capi/route.ts Facebook CAPI proxy endpoint
src/app/layout.tsx Root layout (fetches config, renders providers)
tests/helpers/consent.ts Test helper for bypassing consent banner