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: { 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

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: { domain: 'mysite.com', 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: { domain: 'mysite.com', 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

Some proxy endpoints inject server-side API keys or forward arbitrary resource requests. This includes Google Static Maps, Geocode, Gravatar, and embed image proxies. Anyone can call an unprotected endpoint directly and consume your API quota.

HMAC URL Signing

Optional HMAC signing accepts either an exact URL generated during SSR or prerender, or a request carrying a valid page token. Requests with neither credential receive a 403. The signing implementation canonicalizes each URL before generating its HMAC-SHA256 signature.

Setup

Generate a signing secret with the CLI:

npx @nuxt/scripts generate-secret

Then set it as an environment variable:

NUXT_SCRIPTS_PROXY_SECRET=<your-secret>

Or configure it directly:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    security: {
      secret: process.env.NUXT_SCRIPTS_PROXY_SECRET,
    }
  }
})

Verification Modes

The module uses two verification modes:

  1. URL signatures for server-rendered content. During SSR/prerender, proxy URLs include a sig parameter: an HMAC of the path and query params. The proxy endpoint verifies the signature before forwarding.
  2. Page tokens for client-side reactive updates. Some components recompute their proxy URL after mount (e.g. measuring element dimensions). The server embeds a short-lived token (_pt + _ts params) in the SSR payload. The token is valid for any params on any proxy path and expires after 1 hour.

Page tokens are deliberately broader than URL signatures: anyone who can read a valid token can change the parameters and signed proxy path until it expires. Treat them as short-lived authorization for the proxy group, not proof that a request matches an exact server-generated URL.

Development

In development, the module generates a secret and writes it to .env on the first run.

Production

Set NUXT_SCRIPTS_PROXY_SECRET in your deployment environment. The secret must be the same across all replicas and across build/runtime so that URLs signed at prerender time remain valid.

Without a secret, proxy endpoints remain functional but unprotected. The module logs a warning at startup when it detects signed endpoints without a secret.

Signed Endpoints

The following proxy endpoints require signing when you configure a secret:

ScriptEndpoints
Google Maps/_scripts/proxy/google-static-maps, /_scripts/proxy/google-maps-geocode
Gravatar/_scripts/proxy/gravatar
Bluesky/_scripts/embed/bluesky, /_scripts/embed/bluesky-image
Instagram/_scripts/embed/instagram, /_scripts/embed/instagram-image, /_scripts/embed/instagram-asset
X (Twitter)/_scripts/embed/x, /_scripts/embed/x-image

The generic analytics proxy does not use signing. It accepts only upstream domains registered at build time and does not inject the protected API keys used by the signed endpoints above.

Configuration Reference

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    security: {
      // HMAC secret for signing proxy URLs.
      // Falls back to process.env.NUXT_SCRIPTS_PROXY_SECRET.
      secret: undefined,
      // Auto-generate and persist a secret to .env in dev mode.
      // Set to false to disable.
      autoGenerateSecret: true,
      // Page-token lifetime in seconds (default: 3600).
      pageTokenMaxAge: 3600,
    }
  }
})

To disable proxy security entirely, set security to false:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    // No secret is resolved or auto-generated, no page token is added to the
    // SSR payload, and proxy endpoints pass requests through unverified.
    security: false,
  }
})

Disable security when you need a deterministic SSR payload, such as one used to compute a stable response etag. Without it, proxy endpoints still work but remain open to quota abuse and arbitrary requests to their allowlisted upstreams.

The shared image-proxy handler checks the initial URL's scheme and allowed hostname. Several embed image and asset routes then follow upstream redirects without checking each redirect target again. This is an implementation boundary, not evidence that a configured vendor host is exploitable: keep proxy security enabled and do not treat the initial-host allowlist as complete redirect-chain validation.

Troubleshooting

Signed URLs return 403 after deploy

The secret must be identical at build time (when URLs are signed during prerender) and at runtime (when the server verifies them). If you prerender pages, ensure NUXT_SCRIPTS_PROXY_SECRET is available in both your build environment and your deployment environment.

403 errors across multiple replicas

All server instances must share the same secret. If each replica generates its own secret, a URL signed by one instance will fail verification on another. Set NUXT_SCRIPTS_PROXY_SECRET as a shared environment variable across all replicas.

Unexpected NUXT_SCRIPTS_PROXY_SECRET in .env

The module only writes this when running nuxt dev with a signed endpoint enabled and no secret configured. If you only use client-side scripts (analytics, tracking), the module does not generate a secret. To prevent auto-generation entirely, set autoGenerateSecret: false.

Page tokens expire

Page tokens are valid for 1 hour by default. If a user leaves a tab open longer than security.pageTokenMaxAge, client-side proxy requests will start returning 403. The page will recover on the next navigation or refresh.

Proxy token changes the response payload on every request

The module injects a per-request page token into the SSR payload, so the response hash differs each request. If you compute a stable etag, set security: false to disable proxy security entirely. Proxy endpoints then pass requests through without signature verification, so only do this if you accept the wider request and redirect-validation boundaries described above.

Static Generation and SPA Mode

URL signing requires a server runtime to verify HMAC signatures. Two deployment modes cannot support signing:

nuxt generate (SSG) with static hosting: Prerendered pages contain proxy URLs, but no Nitro server exists at runtime to verify signatures or forward requests. Proxy endpoints will not work on static hosts such as GitHub Pages. If you need proxy endpoints alongside prerendered pages, deploy to a server target that supports runtime request handling; Vercel supports both static and server-rendered Nuxt deployments.

ssr: false (SPA mode): No server-side rendering means no opportunity to sign URLs or embed page tokens. The signing secret lives in server-only runtime config and cannot be accessed from the client. Proxy endpoints still function if deployed with a server, but requests will be unsigned.

The module skips signing setup and logs a build warning in both cases. In SPA mode with a deployed server, registered endpoints remain available without signature checks. A fully static host has no runtime endpoint to receive the request.

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