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

SettingEffect
Require ConsentShows cookieconsent banner, blocks non-essential scripts until user consents
Consent LoggingLogs each user’s consent choices to consent_logs table (for GDPR auditing)
Send conversions without consentSends 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

FieldFormatWhere to find
Pixel ID10-20 digit numberMeta Events Manager → Settings
CAPI TokenAccess token stringEvents Manager → Settings → Generate Access Token
Test Event CodeTEST12345Events Manager → Test Events (for debugging)
Enable CAPICheckboxEnables server-side conversion tracking

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

User ActionPixel EventGTM Event
Views product pageViewContentview_item
Clicks “Buy”InitiateCheckoutbegin_checkout
Enters payment infoAddPaymentInfoadd_payment_info
Completes purchasePurchasepurchase
Signs up / free productLeadgenerate_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

FieldFormatExample
Container IDGTM-XXXXXXXGTM-ABC123
Server Container URLhttps://... (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

FieldFormatDefault
Website IDUUID
Script URLHTTPS URLhttps://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.


ColumnTypePurpose
gtm_container_idTEXTGTM Container ID
gtm_server_container_urlTEXTGTM Server URL
facebook_pixel_idTEXTMeta Pixel ID
facebook_capi_tokenTEXTMeta CAPI access token
facebook_test_event_codeTEXTMeta test event code
fb_capi_enabledBOOLEANEnable server-side conversions
send_conversions_without_consentBOOLEANAllow CAPI without consent
umami_website_idTEXTUmami Website UUID
umami_script_urlTEXTUmami script URL
cookie_consent_enabledBOOLEANShow cookieconsent banner
consent_logging_enabledBOOLEANLog consent to DB
sellf_licenseTEXTLicense key (unrelated)
ColumnTypePurpose
idUUIDPrimary key
nameTEXTDisplay name
script_locationTEXThead or body
script_contentTEXTScript code or URL
categoryTEXTessential, analytics, or marketing
is_activeBOOLEANEnable/disable
cookie_consent_enabledBOOLEANWhether cookieconsent manages this script
ColumnTypePurpose
idUUIDPrimary key
user_idUUIDAuthenticated user (nullable)
anonymous_idTEXTAnonymous visitor ID
ip_addressTEXTVisitor IP
user_agentTEXTBrowser User-Agent
consentsJSONB{"service-name": true/false}
consent_versionTEXTConfig version
created_atTIMESTAMPTZTimestamp

ScenarioGTMGTM ServerPixelCAPIUmamiConsent
Meta Pixel only--YesYes-Yes
Privacy-focused (no Big Tech)----YesYes
Full Google ecosystemYes-YesYes-Yes
Adblocker resistantYesYesYesYes-Yes
All featuresYesYesYesYesYesYes
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.


FilePurpose
src/components/TrackingProvider.tsxScript injection + cookieconsent configuration
src/components/IntegrationsForm.tsxAdmin UI for all integrations settings
src/lib/actions/integrations.tsServer actions (get/update config, CRUD scripts)
src/lib/validations/integrations.tsInput validation for all tracking IDs/URLs
src/lib/tracking/client.tsClient-side event tracking + consent checks
src/lib/tracking/server.tsServer-side CAPI tracking
src/app/api/consent/route.tsConsent logging endpoint
src/app/api/tracking/fb-capi/route.tsFacebook CAPI proxy endpoint
src/app/layout.tsxRoot layout (fetches config, renders providers)
tests/helpers/consent.tsTest helper for bypassing consent banner