Guides

First-Party Mode: Privacy Focused Proxy

The Problem

Every third-party script your site loads connects your users directly to external servers. Each request shares the user's IP address, and many scripts go further: the X Pixel accesses 9 browser fingerprinting APIs (including navigator.getBattery()), sets 5 tracking cookies (muc_ads, guest_id_marketing, guest_id_ads, personalization_id, guest_id), and phones home to 3 separate domains. Microsoft Clarity reads 10 fingerprinting APIs across 3 domains. Even Google Analytics at 154 KB sends data that can be correlated across sites.

Ad blockers rightfully block these requests, which breaks analytics for sites that depend on them.

How It Works

First-party mode combines three layers that work together automatically. Each script declares which capabilities it supports; the module enables them with zero config.

Bundling

During nuxt build, the module downloads third-party scripts and saves them as local assets at /_scripts/assets/[hash].js. At runtime, your server (or CDN) serves these files from your own domain instead of the original third-party CDN.

This eliminates extra DNS lookups, avoids CORS overhead, and means the initial script load never touches a third-party server. Bundled scripts use content-addressed hashes for long-term browser caching.

Reverse Proxy

Runtime requests (analytics beacons, pixel fires, tracking calls) are intercepted and forwarded through Nitro server routes at /_scripts/p/. The module rewrites URLs at two levels:

  1. AST rewriting at build time: third-party domains in the bundled script source are replaced with your proxy path
  2. Runtime intercept on the client: a plugin patches fetch, sendBeacon, XMLHttpRequest, and Image to catch any dynamic URLs the AST rewriter missed

Users never connect to third-party servers directly, so no third-party cookies are set and ad blockers don't interfere.

Anonymisation

Before forwarding proxied requests, the handler strips identifying data. IP addresses are anonymised to subnet level. Sensitive headers (cookies, auth tokens) are always removed. Additional anonymisation (user agent, screen dimensions, hardware fingerprints) varies per script based on what would break functionality.

Even with minimal anonymisation, the reverse proxy alone prevents direct browser connections, eliminates third-party cookies, and ensures requests appear same-origin.

Usage

First-party mode is auto-enabled for all scripts that support it. Adding a script to the registry enables infrastructure (proxy routes, bundling, types, composables) without auto-loading it:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    registry: {
      // Infrastructure only — use composables to load on specific pages
      googleAnalytics: { id: 'G-XXXXXX' },
      metaPixel: { id: '123456' },

      // Infrastructure + global auto-load
      plausibleAnalytics: { domain: 'mysite.com', trigger: 'onNuxtReady' },
    }
  }
})

Scripts without trigger are infrastructure only: the module prepares any supported infrastructure for that script (proxy routes, bundling, composables), but the script only loads when you call the composable in a component. Add trigger to auto-load globally.

Privacy Tiers

Every proxied script defaults to a privacy tier based on what level of anonymisation is safe for that script's functionality. Three tiers cover all scripts:

TierWhat's anonymisedScripts
FullIP, user agent, language, screen, timezone, hardware fingerprintsMeta Pixel, TikTok Pixel, X Pixel, Snapchat Pixel, Reddit Pixel
Heatmap-safeIP, language, hardware fingerprints (preserves screen and user agent for session replay)Google Analytics, Microsoft Clarity, Hotjar
IP onlyIP addresses anonymised to subnet levelPlausible, PostHog, Umami, Fathom, CF Web Analytics, Vercel Analytics, Rybbit, Databuddy, Matomo, Intercom, YouTube, Vimeo, Gravatar, Carbon Ads, Lemon Squeezy, Google AdSense

Sensitive headers (cookie, authorization) are always stripped regardless of tier.

Six Privacy Flags

Each tier maps to a combination of six flags:

FlagWhat it does
ipAnonymises IP addresses to subnet level in headers and payload params
userAgentNormalizes User-Agent to browser family + major version (e.g. Mozilla/5.0 (compatible; Chrome/131.0))
languageNormalizes Accept-Language to primary language tag
screenGeneralizes screen resolution, viewport, hardware concurrency, and device memory to common buckets
timezoneGeneralizes timezone offset and IANA timezone names
hardwareAnonymises canvas/webgl/audio fingerprints, plugin/font lists, browser versions, and device info

Tier Flag Matrix

FlagIP OnlyHeatmap-safeFull
ip
userAgent
language
screen
timezone
hardware

IP Only anonymises the IP to a /24 subnet (city-level geo accuracy). Heatmap-safe strips language and hardware fingerprints while preserving user agent and screen dimensions needed for session replay tools. Full strips everything, used for ad pixels where no analytics reporting depends on raw client data.

Global Override

Override all per-script privacy defaults at once using the top-level privacy option:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    privacy: true, // Full anonymisation for ALL scripts
  }
})

Or selectively override specific flags:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    privacy: { ip: true }, // Anonymise IP for all scripts, rest uses per-script defaults
  }
})

Per-Script Privacy Override

Override the privacy tier for a specific script by adding privacy to its registry entry:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    registry: {
      // Full anonymisation for Meta Pixel (overrides its default)
      metaPixel: { id: '123456', privacy: { ip: true, userAgent: true, screen: true } },
      // IP-only for self-hosted PostHog where you control the data
      posthog: { apiKey: 'phc_xxx', privacy: { ip: true } },
    }
  }
})

Disabling Anonymisation

Disable anonymisation per-script or globally. Routing still applies (requests still go through your server), only the data stripping is turned off:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    privacy: false, // No anonymisation for any script (routing still active)
  }
})

Opting Out

Per-Script

Disable proxying for a specific script using proxy: false in its registry config:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    registry: {
      plausibleAnalytics: { domain: 'mysite.com', proxy: false },
      googleAnalytics: { id: 'G-XXXXXX' }, // still proxied
    }
  }
})

Setting bundle: false on a bundled script also disables its proxy. The reverse proxy depends on AST URL rewriting, which requires the bundled script source. Without bundling, there are no URLs to rewrite.

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    registry: {
      // Disables both bundling AND proxying for this script
      googleAnalytics: { id: 'G-XXXXXX', bundle: false },
    }
  }
})
This does not apply to proxy-only scripts (PostHog, Matomo, etc.) that use config injection instead of URL rewriting. Those scripts can be proxied without bundling.

Static Hosting (SSG)

The reverse proxy requires a server runtime. For static deployments (nuxt generate), the proxy is automatically disabled. Scripts are still bundled and served from your domain, but runtime collection requests (analytics beacons, pixel fires) go directly to third-party servers.

If you need proxying with static hosting, configure platform-level rewrites manually. The pattern is /_scripts/p/<domain>/:path*https://<domain>/:path*:

vercel.json
{
  "rewrites": [
    { "source": "/_scripts/p/www.google-analytics.com/:path*", "destination": "https://www.google-analytics.com/:path*" },
    { "source": "/_scripts/p/www.googletagmanager.com/:path*", "destination": "https://www.googletagmanager.com/:path*" },
    { "source": "/_scripts/p/connect.facebook.net/:path*", "destination": "https://connect.facebook.net/:path*" }
  ]
}

The same pattern applies to Netlify ([[redirects]] with :splat) and Cloudflare Pages (_redirects file). Only include rewrites for scripts you use. Check Nuxt DevTools → Scripts or your Nitro server logs for the exact domains registered.

Platform-level rewrites bypass the privacy anonymisation layer. The proxy handler only runs in a Nitro server runtime.

Supported Scripts

Full First-Party (Bundled + Proxied)

These scripts are downloaded at build time, served from your domain, and have their collection requests proxied through your server:

Proxy Only (Not Bundled)

The module can't bundle these scripts at build time, but still proxies their collection requests through your server:

ScriptHow it works
PostHogSDK installed via npm. Proxy endpoint auto-injected via apiHost config.
MatomoSelf-hosted; bundling breaks the script. Proxy routes registered for collection requests.
Carbon AdsAd serving script. Proxy routes registered for collection requests.
Lemon SqueezyPayment widget. Proxy routes registered for collection requests.

Bundle Only (No Proxy)

These scripts are served from your domain but their runtime requests still go directly to third-party servers:

ScriptWhy proxy isn't supported
Google Tag ManagerGTM's core function is loading other scripts at runtime. Those runtime scripts bypass build-time rewriting.
SegmentSDK constructs API URLs dynamically, bypassing request interception.
CrispSDK loads secondary scripts and CSS at runtime from client.crisp.chat.
MixpanelNo proxy integration yet.
Bing UETNo proxy integration yet.

Bundle-only scripts still benefit from being served as first-party assets (faster loading, no CORS, reduced external connections at page load).

Excluded (Direct Loading Only)

These scripts require direct browser connections for security:

ScriptReason
StripeFraud detection requires real client fingerprints
PayPalFraud detection requires real client fingerprints
Google reCAPTCHABot detection requires real browser fingerprints
Google Sign-InAuth integrity requires direct connection

These scripts still work normally, they connect directly to the third-party server instead of routing through your domain.

Partytown (Web Worker)

Load individual scripts off the main thread by setting partytown: true per-script:

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/partytown', '@nuxt/scripts'],
  scripts: {
    registry: {
      plausibleAnalytics: { domain: 'example.com', partytown: true },
      fathomAnalytics: { site: 'XXXXX', partytown: true },
    }
  }
})

Forward arrays are auto-configured for supported scripts. You must install @nuxtjs/partytown.

GA4 has known issues with Partytown. GTM is not compatible (requires DOM access). Consider Plausible, Fathom, or Umami instead.

First-party mode controls where requests go. Consent triggers control when scripts load. Use both:

<script setup lang="ts">
const trigger = useScriptTriggerConsent()

useScriptGoogleAnalytics({
  id: 'G-XXXXXX',
  scriptOptions: { trigger }
})
</script>

For tools like OneTrust, CookieBot, or Osano, bind their consent signal to a reactive ref:

<script setup lang="ts">
const hasAnalyticsConsent = ref(false)

// Example: OneTrust callback
onMounted(() => {
  window.OneTrust?.OnConsentChanged(() => {
    hasAnalyticsConsent.value = window.OnetrustActiveGroups?.includes('C0002') ?? false
  })
})

useScriptGoogleAnalytics({
  id: 'G-XXXXXX',
  scriptOptions: {
    trigger: useScriptTriggerConsent({ consent: hasAnalyticsConsent }),
  }
})
</script>

Or use trigger: 'manual' in your registry config and call $script.load() when consent is granted:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    registry: {
      // Infrastructure only, load manually after consent
      googleAnalytics: { id: 'G-XXXXXX' },
    }
  }
})
app.vue
<script setup lang="ts">
const { $script } = useScriptGoogleAnalytics()

function onConsentGranted() {
  $script.load()
}
</script>

See the Consent Management Guide for full details on useScriptTriggerConsent().

First-party mode does not bypass GDPR consent requirements. You still need user consent before loading tracking scripts.

Troubleshooting

ProblemFix
Analytics not trackingCheck DevTools → Network for /_scripts/p/ requests. Check Nitro server logs for proxy errors
Proxy not working on static siteThe reverse proxy is automatically disabled for SSG. Use platform rewrites or switch to server mode. See Static Hosting
Stale scriptrm -rf .nuxt/cache/scripts and rebuild
Build download failsSet assets.fallbackOnSrcOnBundleFail: true to fall back to direct loading
DebuggingOpen Nuxt DevTools → Scripts to see proxy routes and privacy status
Geo accuracy reducedIP anonymisation uses /24 subnets, which gives city-level accuracy. Set privacy: false per-script or globally to forward exact IPs
bundle: false disabled my proxyFor bundled scripts, the proxy depends on AST URL rewriting which requires the bundled source. Use proxy: false instead if you only want to disable the proxy
Per-script opt-out not workingFor scripts with auto-inject (Plausible, PostHog, Umami, Rybbit, Databuddy), use proxy: false in the registry config