Remix reverse proxy
Contents
- 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.comfor US,eu.i.posthog.comfor 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 Remix resource routes as a reverse proxy for PostHog.
How it works
Remix resource routes are server-side endpoints that can handle any HTTP method. When a request matches your proxy path, the route fetches the response from PostHog and returns it under your domain.
Here's the request flow:
- User triggers an event in your app
- Request goes to your domain (e.g.,
yourdomain.com/ph/e) - Remix's resource route intercepts requests matching your proxy path
- The route fetches the response from PostHog's servers
- PostHog's response is returned to the user under your domain
This works because the route runs server-side, so the browser only sees requests to your domain. Ad blockers that filter by domain won't block these requests.
Why splat routes? The $ in the filename creates a splat route that matches any path after your prefix. This lets a single route handle all PostHog endpoints like /ph/e, /ph/decide, and /ph/static/array.js.
Prerequisites
This guide requires a Remix project with a server runtime (Node.js, Deno, or Cloudflare Workers).
Setup
- 1
Create the resource route
Create a file at
app/routes/ph.$.tsx:Here's what the code does:
- Routes
/static/*requests to PostHog's asset server and everything else to the main API - Sets the
hostheader so PostHog can route the request correctly - Removes
accept-encodingfrom the request andcontent-encodingfrom the response to avoid issues with compressed content - Uses
arrayBuffer()to properly handle binary content like fonts or images - Exports both
loader(for GET requests) andaction(for POST/PUT/etc.) to handle all HTTP methods
The
@ts-ignorecomment is needed because TypeScript's fetch types don't include theduplexoption yet, but it's required for streaming request bodies in Node.js. - Routes
- 2
Update your PostHog SDK
In your application code, update your PostHog initialization:
The
api_hosttells the SDK where to send events. Using a relative path ensures requests go to your domain. Theui_hostmust point to PostHog's actual domain so features like the toolbar link correctly. - 3
Deploy your changes
Commit and push your changes. The resource route will be active once deployed.
In development, restart your dev server after creating the route file.
Verify your setup
CheckpointConfirm events are flowing through your proxy:
- Open your browser's developer tools and go to the Network tab
- Navigate to your site or trigger an event
- Look for requests to your domain with your proxy path (e.g.,
yourdomain.com/ph) - Verify the response status is
200 OK - Check the PostHog app to confirm events appear in your activity feed
If you see errors, check troubleshooting below.
Troubleshooting
Route not matching requests
If requests to your proxy path return 404:
- Verify your file is at
app/routes/ph.$.tsx(the$is important) - Check the file extension matches your project (
.tsxfor TypeScript,.jsxfor JavaScript) - Restart your dev server after creating the file
Config file appears truncated
If PostHog's config.js file loads partially or appears cut off, verify you're deleting both content-encoding and content-length headers from the response. The original content-length reflects the compressed size, which causes browsers to stop reading early when the content is decompressed.
Content decoding errors
If you see ERR_CONTENT_DECODING_FAILED errors, the proxy is forwarding compressed responses incorrectly. The code handles this by:
- Deleting
accept-encodingfrom the request so PostHog sends uncompressed data - Deleting
content-encodingandcontent-lengthfrom the response - Reading the response as
arrayBufferto handle binary content properly
TypeScript error on duplex property
The duplex: 'half' property shows a TypeScript error because it's a newer fetch API feature not yet in TypeScript's types. The @ts-ignore comment suppresses this. The code works correctly at runtime.