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:
- AST rewriting at build time: third-party domains in the bundled script source are replaced with your proxy path
- Runtime wrappers on the client: the AST transform redirects
fetch,sendBeacon,XMLHttpRequest, andImagecalls 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:
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:
| Tier | What's anonymized | Scripts |
|---|---|---|
| Full | IP, user agent, language, screen, timezone, hardware fingerprints | Meta Pixel, TikTok Pixel, X Pixel, Snapchat Pixel, Reddit Pixel, LinkedIn Insight Tag |
| Heatmap-safe | IP, language, hardware fingerprints (preserves screen and user agent for session replay) | Google Analytics, Google AdSense, Microsoft Clarity, Hotjar |
| IP only | IP addresses anonymized to subnet level | Plausible, 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:
| Flag | What it does |
|---|---|
ip | Anonymizes IP addresses to subnet level in headers and payload params |
userAgent | Normalizes User-Agent to browser family + major version (e.g. Mozilla/5.0 (compatible; Chrome/131.0)) |
language | Normalizes Accept-Language to primary language tag |
screen | Generalizes screen resolution, viewport, hardware concurrency, and device memory to common buckets |
timezone | Generalizes timezone offset and IANA timezone names |
hardware | Anonymizes canvas/webgl/audio fingerprints, plugin/font lists, browser versions, and device info |
Tier Flag Matrix
| Flag | IP Only | Heatmap-safe | Full |
|---|---|---|---|
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:
export default defineNuxtConfig({
scripts: {
privacy: true, // Full anonymization for all proxied scripts
}
})
Or selectively override specific flags:
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:
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:
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:
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:
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:
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.
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:
{
"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:
export default defineNuxtConfig({
scripts: {
security: {
secret: process.env.NUXT_SCRIPTS_PROXY_SECRET,
}
}
})
Verification Modes
The module uses two verification modes:
- URL signatures for server-rendered content. During SSR/prerender, proxy URLs include a
sigparameter: an HMAC of the path and query params. The proxy endpoint verifies the signature before forwarding. - 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+_tsparams) 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:
| Script | Endpoints |
|---|---|
| Google Maps | /_scripts/proxy/google-static-maps, /_scripts/proxy/google-maps-geocode |
| Gravatar | /_scripts/proxy/gravatar |
| Bluesky | /_scripts/embed/bluesky, /_scripts/embed/bluesky-image |
/_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
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:
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:
| Category | Scripts |
|---|---|
| Analytics | Google Analytics, Plausible, Cloudflare Web Analytics, Umami, Rybbit, Databuddy, Ahrefs Web Analytics, Vercel Analytics, Microsoft Clarity, Hotjar |
| Ad Pixels | Meta Pixel, TikTok Pixel, X Pixel, Snapchat Pixel, Reddit Pixel, LinkedIn Insight Tag, Google AdSense |
| Video | Vimeo Player |
| Utility | Intercom, Gravatar |
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:
| Script | How it works |
|---|---|
| PostHog | SDK 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:
| Script | Remaining direct traffic |
|---|---|
| Google Tag Manager | GTM's core function is loading other scripts at runtime. Those runtime scripts bypass build-time rewriting. |
| Fathom | Fathom's bot detection rejects beacons from the server's IP, so Nuxt Scripts bundles the SDK but leaves beacons direct. |
| Segment | SDK constructs API URLs dynamically, bypassing request interception. |
| Crisp | SDK loads secondary scripts and CSS at runtime from client.crisp.chat. |
| Mixpanel | No proxy integration yet. |
| Bing UET | No proxy integration yet. |
| npm | The registry entry bundles the selected package file but has no vendor-specific proxy rules. |
| YouTube Player | Nuxt 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. |
| Calendly | The 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:
| Script | Direct-loading behavior |
|---|---|
| Stripe | Stripe requires Stripe.js to load directly from js.stripe.com for PCI compliance. |
| PayPal | Nuxt Scripts loads PayPal's v6 core from PayPal's documented /web-sdk/v6/core URL; the registry declares no bundle or collection proxy. |
| Google reCAPTCHA | Nuxt 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-In | Google says self-hosted and offline copies are unsupported so clients receive security and compatibility updates. |
| Matomo | The 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 Ads | The component loads the ad script directly. Collection rewriting skips this entry because it does not bundle. |
| Lemon Squeezy | The 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.
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.
Consent Integration
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>
Third-Party Consent Managers
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:
export default defineNuxtConfig({
scripts: {
registry: {
// Infrastructure only, load manually after consent
googleAnalytics: { id: 'G-XXXXXX' },
}
}
})
<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
| Problem | Fix |
|---|---|
| Analytics not tracking | Check DevTools → Network for /_scripts/p/ requests. Check Nitro server logs for proxy errors |
| Proxy not working on static site | Static hosts do not run the Nitro proxy handler. Disable proxying, add platform rewrites, or switch to a server deployment. See Static Hosting |
| Stale script | Remove node_modules/.cache/nuxt/scripts and rebuild |
| Build download fails | Set assets.fallbackOnSrcOnBundleFail: true to fall back to direct loading |
| Debugging | Open Nuxt DevTools → Scripts to see proxy routes and privacy status |
| Geo accuracy reduced | IP anonymization uses an IPv4 /24 or IPv6 /48 prefix. Set privacy: false per script or globally to forward exact IPs |
bundle: false stopped collection proxying | Keep 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 working | For scripts with auto-inject (Plausible, PostHog, Umami, Rybbit, Databuddy), use proxy: false in the registry config |