Nuxt reverse proxy

Before you start
  • If you use a self-hosted proxy, PostHog can't help troubleshoot. Use our managed reverse proxy if you want support.
  • Use domains matching your PostHog region: us.i.posthog.com for US, eu.i.posthog.com for EU.
  • Don't use obvious path names like /analytics, /tracking, /telemetry, or /posthog. Blockers will catch them. Use something unique to your app instead.

This guide shows you how to use Nuxt server routes as a reverse proxy for PostHog.

How it works

Nuxt server routes run on the server and can intercept requests before they reach the client. When a request matches your proxy path, the server route fetches the response from PostHog and returns it under your domain.

Here's the request flow:

  1. User triggers an event in your app
  2. Request goes to your domain (e.g., yourdomain.com/ph/e)
  3. Nuxt's server middleware intercepts requests matching your proxy path
  4. The server route fetches the response from PostHog's servers
  5. PostHog's response is returned to the user under your domain

This works because the proxy runs server-side, so the browser only sees requests to your domain. Ad blockers that filter by domain won't block these requests.

Prerequisites

This guide requires a Nuxt 3 project with server-side rendering enabled (the default).

Setup

  1. Create the server route

    Create a file at server/routes/ph/[...path].ts:

    export default defineEventHandler(async (event) => {
    const path = event.context.params?.path || ''
    const hostname = path.startsWith('static/')
    ? 'us-assets.i.posthog.com'
    : 'us.i.posthog.com'
    const targetUrl = `https://${hostname}/${path}`
    const headers = new Headers()
    headers.set('host', hostname)
    // Forward relevant headers
    const contentType = getHeader(event, 'content-type')
    if (contentType) {
    headers.set('content-type', contentType)
    }
    // Forward client IP for geolocation
    const clientIp = getHeader(event, 'x-forwarded-for') || getRequestIP(event)
    if (clientIp) {
    headers.set('x-forwarded-for', clientIp)
    }
    const body = event.method !== 'GET' && event.method !== 'HEAD'
    ? await readRawBody(event)
    : undefined
    const response = await fetch(targetUrl, {
    method: event.method,
    headers,
    body,
    })
    // Copy response headers
    const responseHeaders: Record<string, string> = {}
    response.headers.forEach((value, key) => {
    if (key.toLowerCase() !== 'content-encoding' && key.toLowerCase() !== 'content-length') {
    responseHeaders[key] = value
    }
    })
    setResponseHeaders(event, responseHeaders)
    setResponseStatus(event, response.status)
    return response.text()
    })

    The [...path] in the filename is a Nuxt catch-all route that matches any path after /ph/. This handles all PostHog endpoints like /ph/e, /ph/decide, and /ph/static/array.js.

    Here's what the code does:

    • Routes /static/* requests to PostHog's asset server and everything else to the main API
    • Sets the host header so PostHog can route the request correctly
    • Forwards the client's IP address for accurate geolocation
    • Strips content encoding headers to avoid issues with compressed responses
  2. Update your PostHog SDK

    In your Nuxt app, update your PostHog initialization to use your proxy path:

    // plugins/posthog.client.ts
    import { defineNuxtPlugin } from '#app'
    import posthog from 'posthog-js'
    export default defineNuxtPlugin(() => {
    const runtimeConfig = useRuntimeConfig()
    posthog.init(runtimeConfig.public.posthogKey, {
    api_host: '/ph',
    ui_host: 'https://us.posthog.com'
    })
    return {
    provide: {
    posthog
    }
    }
    })

    The api_host tells the SDK where to send events. Using a relative path ensures requests go to your domain. The ui_host must point to PostHog's actual domain so features like the toolbar link correctly.

  3. Deploy your changes

    Commit and push your changes. The server route will be active once deployed.

    In development, restart your dev server after creating the server route.

  4. Verify your setup

    Checkpoint

    Confirm events are flowing through your proxy:

    1. Open your browser's developer tools and go to the Network tab
    2. Navigate to your site or trigger an event
    3. Look for requests to your domain with your proxy path (e.g., yourdomain.com/ph)
    4. Verify the response status is 200 OK
    5. Check the PostHog app to confirm events appear in your activity feed

    If you see errors, check troubleshooting below.

Troubleshooting

Server route not matching

If requests to your proxy path return 404:

  1. Verify the file is at server/routes/ph/[...path].ts (not server/api/)
  2. Check the file extension is .ts not .js if you're using TypeScript
  3. Restart your dev server after creating the file

Static site generation

If you're using nuxt generate for static site generation, server routes won't be available. Use a CDN-level proxy like Cloudflare Workers or a platform-specific option like Netlify redirects or Vercel rewrites instead.

All users show same location

If geolocation data is wrong or all users appear in the same location, verify the x-forwarded-for header is being set correctly. If you're behind multiple proxies, you may need to adjust which header value is forwarded.

Community questions

Was this page useful?

Questions about this page? or post a community question.