Skip to content

Same-Origin Proxy

Standard sGTM deployments route data collection traffic to a subdomain: collect.yoursite.com or metrics.yoursite.com. The subdomain approach is effective and straightforward. The same-origin proxy approach routes that same traffic through yoursite.com/collect — the same domain and origin as your main website.

The difference matters for two reasons. First, some aggressive ad blockers block requests to known data collection subdomains even when the domain is first-party. Second, Safari’s cookie restrictions apply differently to cross-subdomain requests in some contexts. A same-origin request to /collect is treated identically to a request to your main application — no subdomain classification, no possible cross-site treatment.

This is an advanced configuration. The subdomain approach (first-party domain setup via CNAME) is sufficient for the vast majority of deployments. Consider the same-origin proxy only if you have measured significant blocking rates on your subdomain endpoint or have specific requirements around cookie behavior.

A reverse proxy intercepts requests to specific paths on your main domain and forwards them to your sGTM Cloud Run service. The browser makes a request to yoursite.com/collect/g/collect — the proxy rewrites this internally to your sGTM backend — and the response returns to the browser as if it came from yoursite.com.

Browser Proxy Layer sGTM Cloud Run
────────── ──────────── ────────────────
POST yoursite.com
/collect/g/collect
│ (appears as yoursite.com request)
├───────────────────────>│
│ │ Rewrite path:
│ │ /collect/g/collect
│ │ → /g/collect
│ │
│ │ Forward to sGTM backend
│ ├──────────────────────────>│
│ │ │ Process event
│ │ │ Set cookies
│ │<──────────────────────────│
│<───────────────────────│
│ (cookies set on
│ yoursite.com)

Cloudflare Workers provide the cleanest implementation for most sites that use Cloudflare as their DNS and CDN provider. The Worker runs at Cloudflare’s edge, adding minimal latency.

// Cloudflare Worker: sGTM Same-Origin Proxy
// Deploy via Cloudflare Dashboard → Workers → Create Service
const SGTM_BACKEND = 'https://your-cloud-run-url.a.run.app'; // your Cloud Run backend URL
const PROXY_PATH = '/measure'; // the path prefix you chose
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// Strip the proxy path prefix before forwarding
const upstreamPath = url.pathname.replace(PROXY_PATH, '') || '/';
const upstreamUrl = new URL(SGTM_BACKEND);
upstreamUrl.pathname = upstreamPath;
upstreamUrl.search = url.search;
// Build the forwarded request with critical headers
const reqInit = {
method: request.method,
headers: new Headers(request.headers),
body: request.method === 'GET' || request.method === 'HEAD'
? undefined
: request.body,
redirect: 'follow',
};
// CRITICAL: X-Forwarded-Host tells sGTM which domain to set cookies on.
// Cloudflare prevents direct Host header manipulation, so this is the workaround.
reqInit.headers.set('X-Forwarded-Host', url.host);
reqInit.headers.set('X-Forwarded-Proto', url.protocol.replace(':', ''));
reqInit.headers.delete('Host');
// Prevent caching of analytics requests
reqInit.headers.set('Cache-Control', 'no-store');
// Preserve the original client IP for geo/IP-based processing
const clientIp = request.headers.get('CF-Connecting-IP');
if (clientIp) {
reqInit.headers.set('X-Forwarded-For', clientIp);
}
const upstreamResp = await fetch(upstreamUrl.toString(), reqInit);
// Return the sGTM response — cookies set by sGTM now apply to yoursite.com
return new Response(upstreamResp.body, {
status: upstreamResp.status,
statusText: upstreamResp.statusText,
headers: upstreamResp.headers,
});
}
};

Route configuration in Cloudflare:

  1. Workers & Pages → your Worker → Triggers → Routes
  2. Add route: *.yoursite.com/measure/* → your Worker
  3. The Worker intercepts requests to yoursite.com/measure/* and proxies to sGTM

Update your GTM tag configuration to point to the same-origin path:

In your client-side GA4 configuration tag, set Server Container URL to https://yoursite.com/measure.

The three headers that make same-origin deployment work:

HeaderPurposeWhat happens without it
X-Forwarded-HostTells sGTM which domain to write cookies onsGTM sets cookies on the Cloud Run domain, not your domain
X-Forwarded-ProtoMaintains HTTPS context for secure cookiesCookies may not get the Secure flag
Cache-Control: no-storePrevents CDN/proxy from caching analytics responsesOne user’s response gets served to another user

Cloudflare (and most CDN providers) prevent direct Host header manipulation on Workers. The X-Forwarded-Host header is the standard workaround — sGTM reads it to determine the cookie domain.

The path you choose matters less than the subdomain, since filter lists rarely match on paths. But follow these guidelines:

PathNotes
/measure/Clean, not obviously tracking
/data/Generic, many legitimate uses
/api/v1/Blends with real API endpoints
/edge/Sounds like edge computing

Avoid: /collect/, /track/, /analytics/, /pixel/, /gtm/ — these are increasingly matched by sophisticated filter lists.

After deploying the Worker and updating your GTM configuration:

  1. Preview both your web and server containers
  2. In the sGTM preview, inspect incoming HTTP requests
  3. Look for a Set-Cookie header establishing the FPID cookie on your domain
  4. If the FPID cookie appears with the correct domain — your setup is working

If sGTM preview fails to load, check your Worker route configuration and ensure the X-Forwarded-Host header is being set correctly.

For self-hosted infrastructure running nginx, add a location block to your main site’s server configuration:

# nginx configuration for sGTM same-origin proxy
server {
listen 443 ssl;
server_name yoursite.com;
# ... your existing SSL and main application configuration ...
# sGTM proxy location
location /collect/ {
# Remove the /collect prefix before forwarding
rewrite ^/collect/(.*)$ /$1 break;
# Forward to your sGTM Cloud Run instance
proxy_pass https://your-cloud-run-url.run.app;
# Required proxy headers
proxy_set_header Host collect.yoursite.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSL verification for backend
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
# Timeouts appropriate for sGTM response times
proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;
# Pass cookies through unchanged
proxy_pass_header Set-Cookie;
proxy_cookie_path / /;
proxy_cookie_domain your-cloud-run-url.run.app yoursite.com;
}
}

Important: The proxy_cookie_domain directive rewrites the cookie’s Domain attribute from the Cloud Run domain to yoursite.com. Without this, cookies set by sGTM apply to the Cloud Run domain, not to your main site.

Alternatively, configure sGTM to set cookies with Domain=.yoursite.com explicitly in the cookie attributes — then the domain rewrite at the proxy level is unnecessary.

In vercel.json, add rewrites that forward sGTM paths to your Cloud Run backend:

{
"rewrites": [
{
"source": "/collect/:path*",
"destination": "https://collect.yoursite.com/:path*"
}
]
}

Vercel Edge Middleware gives more control for complex routing scenarios:

middleware.js
import { NextResponse } from 'next/server';
export function middleware(request) {
const { pathname } = request.nextUrl;
if (pathname.startsWith('/collect/')) {
const backendUrl = new URL(
pathname.replace('/collect/', '/'),
'https://collect.yoursite.com'
);
backendUrl.search = request.nextUrl.search;
return NextResponse.rewrite(backendUrl);
}
}
export const config = {
matcher: ['/collect/:path*'],
};
ApproachBlocking ResistanceCookie DomainComplexityLatency Added
Subdomain CNAME (collect.yoursite.com)Good.yoursite.com (with Domain=.)LowNone
Same-origin proxy (yoursite.com/collect)BetterAutomatic (same origin)Medium–High5–20ms
Subdomain + HttpOnly FPIDGood.yoursite.comLow-MediumNone

The subdomain approach with proper cookie configuration (leading dot domain, Max-Age set by server) achieves most of the same benefits as a same-origin proxy with far less operational complexity. The same-origin proxy is worth the additional complexity when:

  • You have measured 5%+ blocking rates on your subdomain endpoint
  • Your technical stack makes path-based routing straightforward (you already use Cloudflare Workers or a similar edge layer)
  • Your application already uses a path-based routing layer that can absorb the configuration

The same-origin proxy inherits your main domain’s SSL certificate. Your main domain’s certificate already covers yoursite.com — the /collect/ path is served under that certificate automatically. You do not need a separate certificate for sGTM traffic.

Your sGTM backend (Cloud Run or custom domain) still needs its own SSL certificate for the connection between the proxy and the backend. For Cloud Run, Google manages this automatically. For custom domains, the certificate you configured during sGTM setup remains in use for the backend connection.

Google provides a managed same-origin routing solution called Google Tag Gateway (previously known as the sGTM server path option in some documentation). It routes yoursite.com/gtag/ traffic to Google’s servers — without requiring you to set up Cloud Run — specifically for Google tags (GA4, Google Ads).

Google Tag Gateway limitations:

  • Only handles Google-owned tags (GA4, Google Ads, Floodlight)
  • Does not support custom templates or non-Google destinations
  • Does not provide the same data transformation capabilities as a full sGTM deployment
  • Managed by Google — you have less control over processing

For full sGTM deployments (Meta CAPI, TikTok, custom enrichment, Firestore), Google Tag Gateway is not a substitute. It is useful specifically for sites that only need Google Ads and GA4 via a same-origin path and want to avoid running their own Cloud Run infrastructure.

Not rewriting the path prefix. If your proxy forwards yoursite.com/collect/g/collect to sgtm.run.app/collect/g/collect instead of sgtm.run.app/g/collect, sGTM receives a path it does not recognize and returns 404. The path rewrite (removing the /collect prefix) is mandatory.

Breaking cookie domain attribution. When sGTM sets Set-Cookie: _ga=...; Domain=.yoursite.com, the proxy must pass this response header through unchanged. Some reverse proxy configurations strip Set-Cookie headers or rewrite the domain. Verify cookies are set correctly with browser DevTools after deploying.

Caching sGTM responses at the CDN layer. sGTM responses are unique per request (different events, different cookies being set). Caching them at the CDN serves one user’s response to another user. Set Cache-Control: no-store on sGTM paths or configure your CDN to bypass cache for the /collect/ path.

Forgetting to update the GTM Server Container URL. After setting up the proxy, update your client-side GA4 configuration tag’s Server Container URL to https://yoursite.com/collect. The browser needs to send requests to the new path, not the old subdomain.