First-party routing uses bundling or SDK endpoint configuration.
- 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 uses npm mode, with no loader to download or rewrite.
Google Analytics keeps its visitor-specific loader remote.
The module provides a collection endpoint for its initial gtag configuration through transport_url.
Per-script proxy: false and static output disable that endpoint.
TikTok Pixel keeps its loader and collection requests direct to preserve visitor IDs and vendor cookies. It has no collection proxy or proxy anonymization. Google Tag Manager also keeps its visitor-specific loader remote and has no collection proxy.
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.
Google Analytics uses its configured collection endpoint instead of AST rewriting or native network interception.
By default proxy paths embed the verbatim third-party hostname (/_scripts/p/us.i.posthog.com/e/), which leaks self-hosted/internal domains and is trivially classified by ad-blockers. scripts.proxy.alias replaces the hostname segment with an alias:
true— auto-generate a short deterministic hash per domain (sha256(domain).slice(0,8))Record<domain, alias>— explicit aliases; unlisted domains stay verbatim
The pure logic lives in proxy-alias.ts (aliasForDomain, buildDomainAliasMap, invertAliasMap, aliasProxyValue). The module builds a domain → alias map from every proxied domain (those in domainPrivacy) and threads it through every point that emits a proxy path:
- Build-time rewrites (
transform.ts):to: ${proxyPrefix}/${alias ?? domain} - Auto-inject (
applyAutoInject):aliasProxyValuerewrites the host segment of the computed endpoint - Runtime intercept (
intercept.ts): embeds the alias map;proxyUrlmapsparsed.host → alias - Partytown (
generatePartytownResolveUrl): embeds the alias map; worker requests mapurl.host → alias - Server handler (
proxy-handler.ts): the invertedaliasToDomainmap resolves the alias segment back to the real domain before allowlist matching and forwarding (verbatim hostnames still resolve, so aliasing is non-breaking) - Devtools (
useScript.tsnetwork matcher):aliasToDomainis exposed in devtools config so aliased requests still attribute to their script
Wildcard domains (*) are never aliased — they have no literal path form to rewrite and only exist for runtime allowlist matching.
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, X, Snap, Reddit, LinkedIn |
PRIVACY_HEATMAP |
ip, language, hardware | GA, Clarity, Hotjar |
PRIVACY_IP_ONLY |
ip only | PostHog, Plausible, Umami, Rybbit, Databuddy, Ahrefs, Fathom, Vercel, Matomo, Carbon Ads, Lemon Squeezy, Intercom, Gravatar, YouTube, Vimeo, Calendly |
Note: Cloudflare Web Analytics, TikTok, GTM, Segment, Crisp, Mixpanel, Bing UET, SpeedCurve, Pulse, and Statable have no proxy capability, so no privacy transforms are applied.
| Config Key | Registry Scripts | Privacy | Mechanism |
|---|---|---|---|
googleAnalytics |
googleAnalytics | PRIVACY_HEATMAP |
Path B, remote loader |
googleAdsense |
googleAdsense | PRIVACY_HEATMAP |
Path A, shared GA proxy config |
metaPixel |
metaPixel | PRIVACY_FULL |
Path A |
tiktokPixel |
tiktokPixel | n/a | Direct loader and collection |
xPixel |
xPixel | PRIVACY_FULL |
Path A |
snapchatPixel |
snapchatPixel | PRIVACY_FULL |
Path A |
redditPixel |
redditPixel | PRIVACY_FULL |
Path A |
linkedinInsight |
linkedinInsight | PRIVACY_FULL |
Path A |
ahrefsAnalytics |
ahrefsAnalytics | PRIVACY_IP_ONLY |
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 | n/a | Bundled script, direct collection |
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 |
calendly |
calendly | PRIVACY_IP_ONLY |
Path A |
googleTagManager |
googleTagManager | n/a | Direct loader and runtime scripts |
segment |
segment | n/a | Bundle only |
crisp |
crisp | n/a | Bundle only |
speedcurve |
speedcurve | n/a | No proxy (ID-parameterized CDN URL) |
| 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.
Bundled SDKs call __nuxtScripts wrappers after AST rewriting.
Google Analytics uses its configured collection endpoint.
Native application requests retain their original URLs.
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)