PostHog
Client-side PostHog initialization with a first-party /ingest reverse proxy, wired per framework.
Requirements: A server runtime. The /ingest proxy runs on the server (Next.js rewrites, or a TanStack Start server route), so PostHog is unavailable with the cloudflare-static deployment.
@posthog/react is added to TanStack Start apps only.
What create-faster adds
The setup varies by stack. Events are sent to your own domain and proxied to PostHog Cloud (US), which keeps requests same-origin and avoids ad-blockers. PostHog is not initialized when the project token is unset. There is no server-side capture.
Next.js
Files added:
src/
└── instrumentation-client.ts # posthog.init through the /ingest proxyModified files:
next.config.ts- Adds the/ingestrewrites andskipTrailingSlashRedirectsrc/proxy.ts- Excludes/ingestfrom the matcher
Environment variables:
NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN- PostHog project token (scoped to app).
instrumentation-client.ts
Runs once on the client before hydration (Next.js 15.3+ convention):
import posthog from 'posthog-js';
if (process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN) {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN, {
api_host: '/ingest',
ui_host: 'https://us.posthog.com',
defaults: '2026-05-30',
capture_exceptions: true,
debug: process.env.NODE_ENV === 'development',
});
}next.config.ts
async rewrites() {
return [
{
source: '/ingest/static/:path*',
destination: 'https://us-assets.i.posthog.com/static/:path*',
},
{
source: '/ingest/array/:path*',
destination: 'https://us-assets.i.posthog.com/array/:path*',
},
{
source: '/ingest/:path*',
destination: 'https://us.i.posthog.com/:path*',
},
];
},
skipTrailingSlashRedirect: true,skipTrailingSlashRedirect is required: PostHog endpoints such as /e/ end with a slash, and Next.js would otherwise redirect them and drop events. For PostHog Cloud EU, replace us with eu in the destinations and ui_host.
TanStack Start
Files added:
src/
├── components/
│ └── analytics-provider.tsx # PostHogProvider, rendered by the root route
└── routes/
└── ingest/
└── $.ts # /ingest reverse proxy (server route)Modified files:
src/routes/__root.tsx- Wraps the page inAnalyticsProvider(inside theThemeProviderwhen next-themes is selected)
Environment variables:
VITE_POSTHOG_PROJECT_TOKEN- PostHog project token (scoped to app). Vite inlines it into client code at build time, hence theVITE_prefix.
analytics-provider.tsx
PostHogProvider calls posthog.init in an effect, so it never runs during server rendering. Page views are captured on client navigations through defaults: '2026-05-30':
<PostHogProvider
apiKey={POSTHOG_PROJECT_TOKEN}
options={{
api_host: '/ingest',
ui_host: 'https://us.posthog.com',
defaults: '2026-05-30',
capture_exceptions: true,
debug: import.meta.env.DEV,
}}
>
{children}
</PostHogProvider>routes/ingest/$.ts
One splat server route (ANY) proxies every /ingest/* request. The same file runs on Nitro and on Cloudflare Workers:
- strips the
/ingestprefix and keeps the rest of the path (trailing slash included) and the query string; - sends
/static/*and/array/*tous-assets.i.posthog.com, everything else tous.i.posthog.com; - forwards only an allowlist of request headers (
accept,accept-language,content-encoding,content-type,origin,referer,user-agent), so cookies, credentials and gateway headers never reach PostHog; - sets
x-forwarded-forfromcf-connecting-ipon Cloudflare, where it cannot be spoofed, and fromx-forwarded-for(leftmost entry, else the connection address) elsewhere, where the server usually sits behind a proxy or CDN; - streams the request body, rejects bodies over 64 MB with
413and does not follow redirects; - drops
content-encoding,content-length,set-cookie,strict-transport-securityandalt-svcfrom the response.
When evlog is selected, /ingest requests are logged like any other request. For PostHog Cloud EU, replace us with eu in the two hosts and in ui_host.
Turborepo mode:
Files are scoped to the app that selects PostHog (apps/{appName}/), and the env var lands in that app's .env.example, named after the app's stack.

