Two distinct mechanisms for first-party routing:
- Build: Downloads script, rewrites hardcoded URLs via AST using domain-to-proxy mappings derived from
proxy-configs.tsdomains[], applies SDK-specificpostProcesspatches - Client: Intercept plugin wraps
fetch/sendBeacon/XHR/Image.srcvia__nuxtScripts— any non-same-origin URL is automatically proxied through/_scripts/p/<host><path> - Server: Nitro handler at
/_scripts/p/**extracts the domain from the path, reconstructs the upstream URL, and proxies with privacy transforms
- Build:
autoInjecton the PostHog proxy config setsapiHost→/_scripts/p/us.i.posthog.com - Client: SDK natively uses the injected endpoint — no interception needed
- Server: Same Nitro proxy handler
PostHog is the only true Path B script — it uses npm mode (src: false, no script to download/rewrite).
The AST rewriter transforms API calls in downloaded third-party scripts:
fetch(url)→__nuxtScripts.fetch(url)navigator.sendBeacon(url)→__nuxtScripts.sendBeacon(url)new XMLHttpRequest→new __nuxtScripts.XMLHttpRequestnew Image→new __nuxtScripts.Image
The intercept plugin defines __nuxtScripts with a simple proxyUrl function:
function proxyUrl(url) {
const parsed = new URL(url, location.origin)
if (parsed.origin !== location.origin)
return `${location.origin + proxyPrefix}/${parsed.host}${parsed.pathname}${parsed.search}`
return url
}No domain allowlist or rule matching needed. Only AST-rewritten third-party scripts call __nuxtScripts, so any non-same-origin URL is safe to proxy.
The server handler extracts the domain from the path (e.g. /_scripts/p/www.google-analytics.com/g/collect → https://www.google-analytics.com/g/collect) and looks up per-domain privacy config.
Some Path A scripts also define autoInject on their proxy config to set SDK endpoint config:
- Plausible:
endpoint→/_scripts/p/plausible.io/api/event - Umami:
hostUrl→/_scripts/p/cloud.umami.is - Rybbit:
analyticsHost→/_scripts/p/app.rybbit.io/api - Databuddy:
apiUrl→/_scripts/p/basket.databuddy.cc
Auto-inject respects per-script proxy: false opt-out (see "Per-script opt-out" below).
Some SDKs have quirks that require targeted regex patches after AST rewriting. These are defined as postProcess functions directly on each script's ProxyConfig:
- Rybbit: SDK derives API host from
document.currentScript.src.split("/script.js")[0]— breaks when bundled to/_scripts/assets/<hash>.js. Regex replaces the split expression with the proxy path. - Fathom: SDK checks
src.indexOf("cdn.usefathom.com") < 0to detect self-hosted mode and overrides the tracker URL. Regex neutralizes this check.
Note: Google Analytics previously needed postProcess regex patches for dynamically constructed collect URLs. This is no longer needed since the runtime intercept plugin catches all non-same-origin URLs at the sendBeacon/fetch call site.
Proxy config keys match registry keys directly — no indirection layer. A script's registryKey is used to look up its proxy config from proxy-configs.ts.
The only exception is googleAdsense which sets proxy: 'googleAnalytics' to share GA's proxy config.
Scripts can opt out of first-party mode at three levels:
Scripts without the proxy capability in registry.ts are never proxied. Used for scripts that require fingerprinting for core functionality:
- Stripe, PayPal: Fraud detection requires real client IP and browser fingerprints
- Google reCAPTCHA: Bot detection requires real fingerprints
- Google Sign-In: Auth integrity requires direct connection
These scripts also have scriptBundling: false to prevent AST rewriting.
Users can opt out per-script in nuxt.config.ts:
scripts.registry.plausibleAnalytics = { domain: 'mysite.com', proxy: false }
Using flat config syntax:
scripts.registry.plausibleAnalytics = { domain: 'mysite.com', proxy: false }
This skips domain registration, auto-inject, and AST rewriting for that script. Important for scripts with autoInject (Plausible, PostHog, Umami, Rybbit, Databuddy) since autoInject runs at module setup before transforms.
useScriptPlausibleAnalytics({
scriptOptions: { proxy: false }
})This only affects AST rewriting (the transform plugin skips proxy rewrites for the bundled script). It does not undo autoInject config changes, since those run at module setup before transforms. For scripts with autoInject, use the config-level opt-out instead.
- Add a
domains[]entry inproxy-configs.tswith the script's domains and a privacy preset - Add a registry entry in
registry.tswith a matchingregistryKey - Done — the transform plugin derives rewrite rules from domains as
{ from: domain, to: proxyPrefix/domain }
For npm-mode scripts (no download), define autoInject to configure the SDK's endpoint field.
For scripts that need fingerprinting (payments, CAPTCHA, auth), omit the proxy capability and set scriptBundling: false in the registry entry.
Four presets in proxy-configs.ts cover all proxy-enabled scripts:
| Preset | Flags | Used by |
|---|---|---|
PRIVACY_NONE |
all false | (not currently assigned to any script) |
PRIVACY_FULL |
all true | Meta, TikTok, X, Snap, Reddit |
PRIVACY_HEATMAP |
ip, language, hardware | GA, Clarity, Hotjar |
PRIVACY_IP_ONLY |
ip only | PostHog, Plausible, Umami, Rybbit, Databuddy, Fathom, CF Web Analytics, Vercel, Matomo, Carbon Ads, Lemon Squeezy, Intercom, Gravatar, YouTube, Vimeo |
Note: GTM, Segment, Crisp, Mixpanel, and Bing UET are bundle-only (no proxy capability), so no privacy transforms are applied.
| Config Key | Registry Scripts | Privacy | Mechanism |
|---|---|---|---|
googleAnalytics |
googleAnalytics, googleAdsense | PRIVACY_HEATMAP |
Path A |
metaPixel |
metaPixel | PRIVACY_FULL |
Path A |
tiktokPixel |
tiktokPixel | PRIVACY_FULL |
Path A |
xPixel |
xPixel | PRIVACY_FULL |
Path A |
snapchatPixel |
snapchatPixel | PRIVACY_FULL |
Path A |
redditPixel |
redditPixel | PRIVACY_FULL |
Path A |
clarity |
clarity | PRIVACY_HEATMAP |
Path A |
hotjar |
hotjar | PRIVACY_HEATMAP |
Path A |
posthog |
posthog | PRIVACY_IP_ONLY |
Path B (npm-only) + autoInject |
plausibleAnalytics |
plausibleAnalytics | PRIVACY_IP_ONLY |
Path A + autoInject |
umamiAnalytics |
umamiAnalytics | PRIVACY_IP_ONLY |
Path A + autoInject |
rybbitAnalytics |
rybbitAnalytics | PRIVACY_IP_ONLY |
Path A + autoInject + postProcess |
databuddyAnalytics |
databuddyAnalytics | PRIVACY_IP_ONLY |
Path A + autoInject |
fathomAnalytics |
fathomAnalytics | PRIVACY_IP_ONLY |
Path A + postProcess |
cloudflareWebAnalytics |
cloudflareWebAnalytics | PRIVACY_IP_ONLY |
Path A |
vercelAnalytics |
vercelAnalytics | PRIVACY_IP_ONLY |
Path A |
matomoAnalytics |
matomoAnalytics | PRIVACY_IP_ONLY |
Path A |
carbonAds |
carbonAds | PRIVACY_IP_ONLY |
Path A |
lemonSqueezy |
lemonSqueezy | PRIVACY_IP_ONLY |
Path A |
youtubePlayer |
youtubePlayer | PRIVACY_IP_ONLY |
Path A |
vimeoPlayer |
vimeoPlayer | PRIVACY_IP_ONLY |
Path A |
intercom |
intercom | PRIVACY_IP_ONLY |
Path A |
gravatar |
gravatar | PRIVACY_IP_ONLY |
Path A |
googleTagManager |
googleTagManager | n/a | Bundle only |
segment |
segment | n/a | Bundle only |
crisp |
crisp | n/a | Bundle only |
| Script | Reason |
|---|---|
stripe |
Fraud detection requires real fingerprints |
paypal |
Fraud detection requires real fingerprints |
googleRecaptcha |
Bot detection requires real fingerprints |
googleSignIn |
Auth integrity requires direct connection |
Each proxy config declares domains[] — the list of third-party domains that script communicates with. The transform plugin derives rewrite rules at build time as { from: domain, to: proxyPrefix/domain }. The server handler extracts the domain from the proxy path and forwards to the upstream.
The intercept plugin proxies any non-same-origin URL. No domain allowlist or rule matching is required because __nuxtScripts wrappers are only injected into AST-rewritten third-party scripts. Regular app code uses native fetch/sendBeacon and is unaffected.
The server handler extracts the target domain directly from the proxy path (/_scripts/p/<host>/<path>) and looks up privacy config by domain. Unrecognized domains default to full anonymization (fail-closed).
module.ts calls two functions:
setupFirstParty(config, resolvePath)— registers the proxy handler unconditionally (handler rejects unknown domains at runtime). ReturnsFirstPartyConfig.- In
modules:done: resolves capabilities for each configured script viaresolveCapabilities(), then callsfinalizeFirstParty({...})which builds proxy configs from the registry, collects domain privacy mappings, applies auto-injects, registers the intercept plugin, and populates runtimeConfig. Respects per-entryproxy: falseopt-out.
The transform plugin receives the pre-built proxyConfigs map via options — direct lookup per-script, no rebuilding.
Registry entry normalization (true/'mock'/object/array → [input, scriptOptions?] tuple) is handled once at module setup by normalizeRegistryConfig() (src/normalize.ts). All downstream consumers (env defaults, template plugin, auto-inject, partytown) receive a single shape.
The intercept plugin is registered with a static proxyPrefix string — no mutable array captured by reference. Plugin generation is a pure function of its inputs.
Default is true. For nuxt generate and static presets, a warning fires with actionable guidance.
GA defaults (PRIVACY_HEATMAP): anonymizes ip/language/hardware, passes through userAgent/screen/timezone. UA string is needed for Browser/OS/Device reports; hardware flag covers cross-site fingerprinting vectors (canvas/WebGL/plugins/fonts/high-entropy client hints) that GA doesn't need for standard reports.
hardware: true also strips high-entropy Client Hints (sec-ch-ua-arch, sec-ch-ua-model) which GA4 is migrating toward for device reporting.
src/normalize.ts— normalizes registry config entries to[input, scriptOptions?]tuple formsrc/first-party/proxy-configs.ts— all proxy configs withdomains[]+ privacy presetssrc/first-party/setup.ts— orchestration (setupFirstParty,finalizeFirstParty,applyAutoInject)src/first-party/intercept-plugin.ts— client-side__nuxtScriptswrapper (proxyUrl for non-same-origin URLs)src/first-party/types.ts— FirstPartyOptions, ProxyConfig, ProxyAutoInjectsrc/registry.ts— script metadata (labels, imports, bundling,proxy: falseopt-out);registryKey= proxy config lookup keysrc/registry-logos.ts— SVG logos extracted from registry for smaller diffssrc/runtime/server/proxy-handler.ts— server-side proxy with domain extraction + privacy transformssrc/runtime/server/utils/privacy.ts— privacy resolution, IP anonymization, UA normalization, payload strippingsrc/plugins/rewrite-ast.ts— AST URL rewriting + canvas fingerprinting neutralization (generic); SDK-specific patches inpostProcesssrc/plugins/transform.ts— build-time script download + URL rewriting, passespostProcessthroughsrc/module.ts— ModuleOptions, defaultsdocs/content/docs/1.guides/2.first-party.md— main docs page
proxy: false | { prefix?, privacy? }— module-level option (auto-inferred when any script has proxy capability)proxy.prefix— proxy endpoint path prefix (default:/_scripts/p)assets.prefix— bundled script asset path (default:/_scripts/assets)- Per-script
proxy: false— in flat config or scriptOptions to opt out individual scripts - Registry-level
proxy: false— in registry.ts capabilities for scripts that must never be proxied (fingerprinting requirements)