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:
- AST rewriting at build time: third-party domains in the bundled script source are replaced with your proxy path
- Runtime intercept on the client: a plugin patches
fetch,sendBeacon,XMLHttpRequest, andImageto 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:
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:
| Tier | What's anonymised | Scripts |
|---|---|---|
| Full | IP, user agent, language, screen, timezone, hardware fingerprints | Meta Pixel, TikTok Pixel, X Pixel, Snapchat Pixel, Reddit Pixel |
| Heatmap-safe | IP, language, hardware fingerprints (preserves screen and user agent for session replay) | Google Analytics, Microsoft Clarity, Hotjar |
| IP only | IP addresses anonymised to subnet level | Plausible, 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:
| Flag | What it does |
|---|---|
ip | Anonymises 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 | Anonymises 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 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:
export default defineNuxtConfig({
scripts: {
privacy: true, // Full anonymisation for ALL scripts
}
})
Or selectively override specific flags:
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:
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:
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:
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.
export default defineNuxtConfig({
scripts: {
registry: {
// Disables both bundling AND proxying for this script
googleAnalytics: { id: 'G-XXXXXX', bundle: false },
}
}
})
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*:
{
"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.
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:
| Category | Scripts |
|---|---|
| Analytics | Google Analytics, Plausible, Cloudflare Web Analytics, Umami, Fathom, Rybbit, Databuddy, Vercel Analytics, Microsoft Clarity, Hotjar |
| Ad Pixels | Meta Pixel, TikTok Pixel, X Pixel, Snapchat Pixel, Reddit Pixel, Google AdSense |
| Video | YouTube Player, Vimeo Player |
| Utility | Intercom, Gravatar |
Proxy Only (Not Bundled)
The module can't bundle these scripts at build time, but still proxies their collection requests through your server:
| Script | How it works |
|---|---|
| PostHog | SDK installed via npm. Proxy endpoint auto-injected via apiHost config. |
| Matomo | Self-hosted; bundling breaks the script. Proxy routes registered for collection requests. |
| Carbon Ads | Ad serving script. Proxy routes registered for collection requests. |
| Lemon Squeezy | Payment 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:
| Script | Why proxy isn't supported |
|---|---|
| Google Tag Manager | GTM's core function is loading other scripts at runtime. Those runtime scripts bypass build-time rewriting. |
| 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. |
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:
| Script | Reason |
|---|---|
| Stripe | Fraud detection requires real client fingerprints |
| PayPal | Fraud detection requires real client fingerprints |
| Google reCAPTCHA | Bot detection requires real browser fingerprints |
| Google Sign-In | Auth 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:
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.
Consent Integration
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>
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 use trigger: 'manual' in your registry config and call $script.load() 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>
See the Consent Management Guide for full details on useScriptTriggerConsent().
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 | The reverse proxy is automatically disabled for SSG. Use platform rewrites or switch to server mode. See Static Hosting |
| Stale script | rm -rf .nuxt/cache/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 anonymisation uses /24 subnets, which gives city-level accuracy. Set privacy: false per-script or globally to forward exact IPs |
bundle: false disabled my proxy | For 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 working | For scripts with auto-inject (Plausible, PostHog, Umami, Rybbit, Databuddy), use proxy: false in the registry config |