Skip to main content
Guides

First-Party Mode: Privacy-Focused Proxy

Why Proxy Third-Party Scripts?

Third-party requests connect your visitors' browsers directly to vendor servers, exposing IP addresses and browser signals that can contribute to a fingerprint. The current Nuxt Scripts audit snapshot shows why counting script tags is not enough: integrations can set several cookies, contact multiple domains, and inspect device data after their loader runs.

Ad blockers can stop these requests, leaving gaps in analytics that depend on them.

How First-Party Mode Works

For supported registry scripts, first-party mode can bundle the SDK, proxy its runtime requests, and anonymize data before forwarding it. Each registry definition declares which of these capabilities the script supports.

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.

The initial request no longer reaches the original host or pays its DNS and connection setup costs. Content-addressed filenames also let browsers cache bundled scripts long term.

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 wrappers on the client: the AST transform redirects fetch, sendBeacon, XMLHttpRequest, and Image calls in supported bundled SDKs through Nuxt Scripts wrappers, which proxy dynamically constructed URLs

Supported script and collection requests use same-origin URLs, so cookies set on those responses are not third-party cookies. Host-based blocking rules are less likely to match them, although path-based or behavioral rules can still block requests.

Anonymization

By default, the proxy anonymizes IP addresses to subnet level and removes sensitive headers such as cookies and auth tokens before forwarding a request. Each script's privacy tier determines whether it also changes the user agent, screen dimensions, and hardware fingerprints.

Even with minimal anonymization, requests routed through the reverse proxy appear same-origin and do not expose the browser's direct connection to the upstream service.

The privacy transformer deliberately preserves analytics identifiers and user data fields such as uid, cid, email, and phone; SDKs may hash some of those values before sending them. First-party mode reduces selected network and fingerprinting data. It does not make an identified analytics payload anonymous.

Usage

Registry scripts use their supported first-party capabilities by default. Adding an entry prepares its proxy routes, bundled assets, types, and composables without loading the script:

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: { scriptId: 'YOUR_SCRIPT_ID', 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

Each proxied script has a default privacy tier chosen around the data its SDK needs:

TierWhat's anonymizedScripts
FullIP, user agent, language, screen, timezone, hardware fingerprintsMeta Pixel, TikTok Pixel, X Pixel, Snapchat Pixel, Reddit Pixel, LinkedIn Insight Tag
Heatmap-safeIP, language, hardware fingerprints (preserves screen and user agent for session replay)Google Analytics, Google AdSense, Microsoft Clarity, Hotjar
IP onlyIP addresses anonymized to subnet levelPlausible, PostHog, Umami, Cloudflare Web Analytics, Vercel Analytics, Rybbit, Databuddy, Matomo, Ahrefs Web Analytics, Intercom, YouTube, Vimeo, Gravatar, Calendly

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

Six Privacy Flags

Each tier selects from six privacy flags. The privacy transformer source defines the exact header, query, and payload fields each flag changes:

FlagWhat it does
ipAnonymizes 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
hardwareAnonymizes 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 zeroes the final IPv4 octet (/24) or keeps the first 48 bits of an IPv6 address. This reduces precision but does not guarantee a particular geolocation accuracy. Heatmap-safe normalizes language and hardware fingerprint data while preserving user agent and screen dimensions used by session replay tools. Full applies all six privacy transforms.

Global Override

Use the top-level privacy option to replace the defaults for every proxied script:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    privacy: true, // Full anonymization for all proxied scripts
  }
})

Or selectively override specific flags:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    privacy: { ip: true }, // Anonymize IP on proxied traffic; direct requests are unaffected
  }
})

Per-Script Privacy Override

Add privacy to a registry entry to override one script:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    registry: {
      // Apply all privacy transforms to Plausible instead of its IP-only default
      plausibleAnalytics: { scriptId: 'YOUR_SCRIPT_ID', privacy: true },
      // IP-only for self-hosted PostHog where you control the data
      posthog: { apiKey: 'phc_xxx', privacy: { ip: true } },
    }
  }
})

Disabling Anonymization

Set privacy: false per script or globally to stop anonymizing data. Requests still pass through your server:

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

Hiding Hostnames

By default, a proxy path includes the third-party hostname, for example /_scripts/p/us.i.posthog.com/e/. With a self-hosted service, that can expose an internal domain such as /_scripts/p/analytics.internal.example.com/api/send. A verbatim hostname also makes the request easier for ad blockers and network observers to classify.

Use proxy.alias to replace the hostname segment with an alias.

Set alias: true to auto-generate a short opaque alias per domain:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    proxy: {
      alias: true, // /_scripts/p/a1b2c3d4/e/
    }
  }
})

Or map specific domains to custom aliases. Domains not listed keep their verbatim hostname:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    proxy: {
      alias: {
        'us.i.posthog.com': 'ph',
        'analytics.internal.example.com': 'a',
      }
    }
  }
})

Aliases apply everywhere a proxy path is produced: build-time URL rewrites, auto-injected endpoints (such as PostHog's apiHost), runtime-intercepted requests, and Partytown worker requests. The server handler resolves the alias back to the real domain before forwarding upstream.

Aliases change only the hostname segment. Set the top-level prefix option, for example prefix: '/_t', to change /_scripts/p.

Aliases keep the real hostname out of request URLs. If an SDK builds its collection URL at runtime, the host may still appear in the client JavaScript. Aliasing changes the network-visible path; it does not obfuscate your bundle.

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: { scriptId: 'YOUR_SCRIPT_ID', proxy: false },
      googleAnalytics: { id: 'G-XXXXXX' }, // still proxied
    }
  }
})

For scripts whose collection requests depend on AST URL rewriting, setting bundle: false also prevents those requests from using the proxy. Without a bundled script source, there are no SDK URLs or calls for the transform to rewrite.

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    registry: {
      // Disables bundling and the proxy rewrites that depend on it
      googleAnalytics: { id: 'G-XXXXXX', bundle: false },
    }
  }
})

PostHog receives the proxy endpoint through SDK config, so it can proxy collection requests without bundling. Matomo does not: its normal main-thread loader remains direct unless you use the separate Partytown routing path described below.

Static Hosting (SSG)

The reverse proxy requires a server runtime. A fully static deployment serves the output of nuxt generate without a Nitro process to handle /_scripts/p/**. Nuxt Scripts warns for known static presets, but it does not rewrite proxy URLs to their third-party origins. Disable proxying for affected scripts or use a host that supports external-origin rewrites. For example, Vercel rewrites accept /:path* captures and external destinations:

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*" }
  ]
}

Netlify proxy rewrites use a 200 rule such as /_scripts/p/www.google-analytics.com/* https://www.google-analytics.com/:splat 200. Cloudflare Pages is different: its _redirects proxy rules support only relative destinations, not external domains. Use a Pages Function or Worker if you need this proxy on a static Cloudflare Pages deployment. Only configure domains your site uses; Nuxt DevTools → Scripts and Nitro logs show the registered set.

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

Proxy Endpoint Security

Embed, avatar, and analytics proxy routes are public resources. They do not contain server-side API keys. Each route accepts only the upstream hosts and request shapes declared by its integration.

Runtime proxy fetches validate the initial upstream URL and every redirect target before requesting it. Direct local, private, link-local, and reserved targets are rejected on every runtime; Node deployments also validate and pin DNS results before opening the socket. Image routes reject active content types such as HTML and SVG. The Instagram embed route restricts post and stylesheet hosts, then sanitizes the returned fragment before client rendering.

Nuxt Scripts does not proxy Google Maps requests. Static Maps loads from Google with the public browser key, while location lookup uses the Maps JavaScript Places service. Apply website and API restrictions to the key, then configure Google Cloud quotas to cap spend. See Google Maps Platform security guidance.

If public embed traffic needs request limits, configure them at your deployment edge or add application middleware. Nitro 2 route rules do not provide a portable rate limiter.

Supported Scripts

The groups below follow the capability declarations in the Nuxt Scripts registry.

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 (Package-backed)

The module doesn't bundle PostHog's SDK because it comes from a package dependency, but it can still send collection requests through your server:

ScriptHow it works
PostHogSDK installed as posthog-js. Proxy endpoint auto-injected via apiHost config.

Bundled, with Third-Party Runtime Traffic

These integrations serve their main loader from your domain, but some runtime requests still go directly to third-party servers:

ScriptRemaining direct traffic
Google Tag ManagerGTM's core function is loading other scripts at runtime. Those runtime scripts bypass build-time rewriting.
FathomFathom's bot detection rejects beacons from the server's IP, so Nuxt Scripts bundles the SDK but leaves beacons direct.
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.
npmThe registry entry bundles the selected package file but has no vendor-specific proxy rules.
YouTube PlayerNuxt Scripts bundles the API loader, but the facade supplies a direct youtube.com or youtube-nocookie.com iframe host after the bundle transform runs. The iframe and its internal requests remain third-party.
CalendlyThe widget script and assets can use first-party routing, but booking iframes and their traffic load directly from calendly.com.

Bundling avoids the main loader's connection to its original host, but it does not make all runtime traffic first-party.

Direct Loading (No Active Bundle or Collection Proxy)

These integrations load their SDKs and runtime traffic directly from their configured hosts:

ScriptDirect-loading behavior
StripeStripe requires Stripe.js to load directly from js.stripe.com for PCI compliance.
PayPalNuxt Scripts loads PayPal's v6 core from PayPal's documented /web-sdk/v6/core URL; the registry declares no bundle or collection proxy.
Google reCAPTCHANuxt Scripts loads api.js or enterprise.js directly from Google, or from recaptcha.net; the registry declares no bundle or collection proxy. Google separately recommends loading reCAPTCHA early because more page context improves its assessment.
Google Sign-InGoogle says self-hosted and offline copies are unsupported so clients receive security and compatibility updates.
MatomoThe main-thread SDK is not bundled, so its normal requests keep using the configured Matomo host. With partytown: true, Nuxt Scripts can route matching worker requests through Partytown's resolveUrl; the Partytown limitations below still apply.
Carbon AdsThe component loads the ad script directly. Collection rewriting skips this entry because it does not bundle.
Lemon SqueezyThe composable loads the payment widget directly. Collection rewriting skips this entry because it does not bundle.

Google Maps, SpeedCurve LUX, and Usercentrics also load directly because they do not declare bundle or collection-proxy capabilities.

Partytown (Web Worker)

Load individual scripts in a Partytown web worker by setting partytown: true per script. The trigger below causes the registry plugin to call the composable globally; it does not delay the Partytown tag, which is written into the server-rendered HTML.

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/partytown', '@nuxt/scripts'],
  scripts: {
    registry: {
      plausibleAnalytics: { scriptId: 'YOUR_SCRIPT_ID', partytown: true, trigger: 'onNuxtReady' },
    }
  }
})

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

The current Partytown path keeps only the script src. It skips other attributes, registry initialization hooks, trigger timing, and the usual composable proxy/context API. A declared Partytown capability means Nuxt Scripts knows the forwarding and routing configuration; it does not guarantee that an integration which depends on data-* attributes or clientInit will initialize correctly. Verify the generated tag and vendor traffic.

The Partytown tracker has an open report of dropped GA4 sessions when Google Tag Manager runs in a worker. Nuxt Scripts does not declare Partytown support for GTM. Test analytics delivery before moving GA4 to Partytown.

First-party mode controls where requests go. Consent triggers control when main-thread scripts load. The current Partytown path ignores trigger timing, so do not rely on a Nuxt Scripts consent trigger to gate a Partytown tag.

<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 keep the registry entry infrastructure-only and call load() on the composable result 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>

The Consent Management guide covers useScriptTriggerConsent() and vendor-specific controls.

Routing a request through your domain does not settle the consent question. For EU deployments, the EDPB's final Article 5(3) technical-scope guidance covers tracking URLs, pixels, and JavaScript as well as cookies. Assess each script and jurisdiction before loading it.

Troubleshooting

ProblemFix
Analytics not trackingCheck DevTools → Network for /_scripts/p/ requests. Check Nitro server logs for proxy errors
Proxy not working on static siteStatic hosts do not run the Nitro proxy handler. Disable proxying, add platform rewrites, or switch to a server deployment. See Static Hosting
Stale scriptRemove node_modules/.cache/nuxt/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 anonymization uses an IPv4 /24 or IPv6 /48 prefix. Set privacy: false per script or globally to forward exact IPs
bundle: false stopped collection proxyingKeep bundling enabled for scripts whose proxy support depends on AST rewriting. Auto-injected integrations such as PostHog do not require bundling; main-thread Matomo remains direct
Per-script opt-out not workingFor scripts with auto-inject (Plausible, PostHog, Umami, Rybbit, Databuddy), use proxy: false in the registry config
Was this page helpful?