Skill 82 · Instrument Product Analytics
Subchapter 82.59
references/tanstack-start.mdMarkdown7 KBView on GitHub
AI agents: this is one page from PostHog’s docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt (opens in a new tab)
This tutorial shows how to integrate PostHog with a TanStack Start (opens in a new tab) app for both client-side and server-side analytics.
Install the required packages:
Terminal
npm install @posthog/react posthog-node@posthog/react - React package for our JS Web SDK (opens in a new tab) for client-side usageposthog-node - PostHog Node.js SDK (opens in a new tab) for server-side event captureIdentifying users is required. Call
posthog.identify('your-user-id')after login to link events to a known user. This is what connects frontend event captures, session replays (opens in a new tab), LLM traces (opens in a new tab), and error tracking (opens in a new tab) to the same person — and lets backend events link back too.Use a stable ID from your auth system when possible, not an email or display name. Send those as person properties instead. If your app has no other stable key, email works as a fallback if they are unique. Never a shared literal like
"anonymous"or"user", which pools many people onto one person and corrupts their data. When no ID is available at all, skip the identify and retain the anonymous distinct ID that’s automatically assigned.Call
posthog.reset()on logout, so the next person to use the browser doesn’t inherit the last one’s identity.See our guide on identifying users (opens in a new tab) for how to set this up.
If your app calls your own backend, tracing_headers adds X-POSTHOG-DISTINCT-ID and X-POSTHOG-SESSION-ID to matching fetch and XMLHttpRequest requests. This lets server-side SDKs link backend events, errors, and LLM traces back to frontend sessions and replays. Use hostnames only, without protocols or paths.
JavaScript
posthog.init('<ph_project_token>', {
api_host: 'https://us.i.posthog.com',
// Optional: send PostHog session/user context to your backend
tracing_headers: ['api.example.com'],
})This works in local development too, but match on the hostname alone: use 'localhost', not 'localhost:3000'. Ports are never part of a hostname, so a value with one in it never matches anything. localhost and 127.0.0.1 are also different hostnames — use whichever your app actually calls.
Tracing headers help you attribute events across front and backend consistently. When this isn’t available, use your server-side stable IDs to deduce the matching distinctId, and pass it in when capturing the event.
Wrap your app with PostHogProvider in your root route with your project token, host, and other options.
import CspAllowancesCallout from "../_snippets/csp-allowances-callout.mdx"
<CspAllowancesCallout />tsx file=src/routes/__root.tsx
// src/routes/__root.tsx
import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
import { PostHogProvider } from '@posthog/react'
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
],
}),
shellComponent: RootDocument,
})
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
<PostHogProvider
apiKey="<ph_project_token>"
options={{
api_host: 'https://us.i.posthog.com',
defaults: '2026-05-30',
capture_exceptions: true
}}
>
{children}
</PostHogProvider>
<Scripts />
</body>
</html>
)
}Once the provider is in place, PostHog automatically captures pageviews, sessions, and web vitals.
Use the usePostHog hook from @posthog/react in any component to capture custom events:
src/routes/checkout.tsx
import { usePostHog } from '@posthog/react'
function CheckoutButton({ orderId, total }: { orderId: string; total: number }) {
const posthog = usePostHog()
const handleClick = () => {
posthog.capture('checkout_started', {
order_id: orderId,
total: total,
})
}
return <button onClick={handleClick}>Checkout</button>
}Call posthog.identify() when a user logs in to link their events to a user ID:
TSX
import { usePostHog } from '@posthog/react'
function LoginForm() {
const posthog = usePostHog()
const handleLogin = async (userId: string, email: string) => {
// ... your login logic
posthog.identify(userId, {
email: email,
})
posthog.capture('user_logged_in')
}
}Call posthog.reset() on logout to clear the identified user.
Create a server-side PostHog client using posthog-node. Use a singleton pattern so you reuse the same client across requests:
src/utils/posthog-server.ts
// src/utils/posthog-server.ts
import { PostHog } from 'posthog-node'
let posthogClient: PostHog | null = null
export function getPostHogClient() {
if (!posthogClient) {
posthogClient = new PostHog(
'<ph_project_token>',
{
host: 'https://us.i.posthog.com',
flushAt: 1,
flushInterval: 0,
},
)
}
return posthogClient
}Use the server client in TanStack Start API routes to capture events server-side. Server-side capture is useful for tracking events that shouldn’t be spoofable from the client, like purchases or authentication:
src/routes/api/checkout.ts
// src/routes/api/checkout.ts
import { createFileRoute } from '@tanstack/react-router'
import { json } from '@tanstack/react-start'
import { getPostHogClient } from '../../utils/posthog-server'
export const Route = createFileRoute('/api/checkout')({
server: {
handlers: {
POST: async ({ request }) => {
const body = await request.json()
const posthog = getPostHogClient()
posthog.capture({
distinctId: body.userId,
event: 'item_purchased',
properties: {
item_id: body.itemId,
price: body.price,
source: 'api',
},
})
return json({ success: true })
},
},
},
})The server-side capture call requires a distinctId (the user identifier), an event name, and optional properties.
Installing the JS Web SDK and Node SDK means all of their functionality is available in your TanStack Start project. To learn more about this, have a look at our JS Web SDK docs (opens in a new tab) and Node SDK docs (opens in a new tab).
Ask PostHog AI
HelpfulCould be better