Convex
Contents
Convex runs your backend as short-lived isolates, so the usual posthog-node shutdown flow doesn't apply. This doc covers what does: capturing events and flags from Convex functions, matching frontend and backend events to the same person, tracing LLM calls, and syncing Convex tables into PostHog.
| You want to | Use |
|---|---|
| Capture events and evaluate feature flags from inside Convex functions | @posthog/convex component |
| Stitch frontend and backend events into one user timeline | Frontend + backend stitching |
Trace LLM calls or @convex-dev/agent runs | AI observability |
| Query your Convex tables alongside PostHog event data | Convex data warehouse source |
| Forward Convex logs to PostHog Logs | Convex dashboard log streams (no code) |
| Forward Convex exceptions to PostHog Error Tracking | Convex dashboard exception reporting (no code) |


Get your PostHog project token
Every integration on this page needs the same two values:
- Project token - Starts with
phc_. Found in PostHog under Settings > Project > General, labeled Project token. - Host -
https://us.i.posthog.comfor PostHog US Cloud,https://eu.i.posthog.comfor EU Cloud, or your own URL for self-hosted PostHog.
Capture events and flags from your Convex code
The @posthog/convex component captures events, identifies users, and forwards exceptions from mutations and actions. Local feature flags also work in queries.
Requires Convex 1.39 or newer.
1. Install the component
Register it in convex/convex.config.ts and forward your credentials from the app down to the component:
Environment variables:
| Env var | Required | What it does |
|---|---|---|
POSTHOG_PROJECT_TOKEN | Yes | Your project token (phc_). Sends events and evaluates flags remotely. |
POSTHOG_HOST | No | Defaults to https://us.i.posthog.com. Use https://eu.i.posthog.com for EU Cloud or your self-hosted URL. |
POSTHOG_PERSONAL_API_KEY | No | A secret API key (phs_, recommended) or personal API key (phx_). Setting it enables local flag evaluation. |
POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS | No | Cron interval for refreshing flag definitions. Defaults to 60. Raise it on free-tier dev deployments to cut function-call usage. |
2. Set your PostHog credentials
For local feature flag evaluation (covered in step 5), also set a secret API key:
Local evaluation kicks in on the next cron tick — no redeploy needed.
3. Initialize the client
Create convex/posthog.ts. Other backend functions import the posthog instance from here. Credentials live on the component, so the constructor just needs the component reference:
4. Capture events and identify users
capture and identify work in mutations and actions. They schedule the PostHog API call via ctx.scheduler.runAfter, so they return immediately without blocking the caller.
Use the same Convex user ID as the distinctId everywhere so PostHog can stitch events from every source onto one person. See stitching frontend and backend below for the frontend setup.
5. Evaluate feature flags
@posthog/convex supports two flag evaluation paths:
- Local (
getFeatureFlag,isFeatureEnabled,getFeatureFlagPayload,getAllFlags) runs against flag definitions cached on your Convex deployment. Works in queries, mutations, and actions. No per-call network request, and a query re-runs automatically when cached definitions refresh. RequiresPOSTHOG_PERSONAL_API_KEY. Use this when you can. - Remote (
evaluateFlag,evaluateFlagPayload,evaluateAllFlags) hits PostHog's/flagsendpoint on every call. Action-only. Handles every flag, including the ones local can't, and needs no personal API key.
Local evaluation runs automatically once POSTHOG_PERSONAL_API_KEY is set. The component runs its own cron that refreshes flag definitions every minute, so you don't write your own. Tune the cadence with POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS. If you call a local-eval method without the key configured, the client throws and points you at the remote evaluate* methods.
Read a flag from a query:
A React client subscribed to this query re-renders the next time the cron refreshes flag definitions, up to the configured interval (one minute by default).
To force a refresh between cron ticks (for example, right after creating a flag in development), call posthog.reloadFeatureFlags(ctx) from an action.
If you're running an experiment against a locally-evaluated flag, fire an exposure event yourself from a mutation or action, since local eval can't schedule capture from inside a query:
A handful of flag types can't be resolved locally (experience continuity flags, static cohorts, flags whose targeting depends on person properties you don't pass in). For those, getFeatureFlag returns undefined. Call evaluateFlag from an action to hit /flags directly. See the full list of limitations.
6. Capture exceptions with custom properties
If you want every uncaught exception forwarded to PostHog automatically without wrapping each call site, configure Convex's first-party PostHog Error Tracking destination in the Convex dashboard. Use captureException when you want to attach custom properties at a specific call site:
Stitch frontend and backend events together
PostHog stitches frontend and backend events onto the same person when both sides use the same distinctId string.
Install posthog-js and @posthog/react in your frontend. The example below uses React, but any JavaScript framework PostHog supports works the same way: call posthog.identify() with the same string the backend uses as distinctId.
Initialize PostHog at the root of your app, the same way you would in any React project:
Then, right after a user signs in via Convex Auth (or Clerk, Auth0, etc.), call identify on the frontend with the same Convex user ID your backend uses. The currentUser query referenced below is whatever Convex query exposes the authenticated user to your client (the Convex Auth docs cover defining it):
Mount <AuthSync /> once near the root of your app, inside the <PostHogProvider> and below your Convex <Authenticated> boundary if you have one. The me?._id guard skips the identify call until authentication completes.
Matching IDs connect frontend and backend events for that person. Session links need an explicit session ID. Convex log streams and dashboard exception reporting use separate ID rules, described below.
Remember you'll also need to configure the environment variables on the frontend to point to your PostHog instance.
Pass session IDs to backend functions
Query, mutation, and action calls from the Convex client do not give your handler an HTTP request. Pass the browser IDs as function arguments when you need them. Read the session ID when you call the function, since it can change while the page is open:
Use your existing browser PostHog instance and Convex mutation. Add optional string validators to that mutation, and resolve the authenticated person on the server:
Add this capture to your existing authorized profile update. This example uses the authenticated tokenIdentifier. If your app uses its own user ID, resolve that ID on the server and use the same string with posthog.identify() in the browser. The component's optional identify callback also takes priority over a supplied distinctId.
Browser IDs are analytics hints. Use Convex authentication and authorization to control access to data. Missing or invalid session IDs stay absent; the server does not create browser sessions.
@posthog/convex copies event properties into the scheduled capture call. Mutation scheduling commits with the mutation: if the mutation throws, its scheduled capture is rolled back too. Queries cannot schedule captures. Scheduled application functions do not inherit authentication or ambient context. Pass the selected IDs as arguments to an internal scheduled function when it needs them, and keep authorization checks at the public entry point.
Application spans and logs
@posthog/convex does not install an OpenTelemetry provider or attach IDs to function spans. For application-created spans, use sessionId and posthogDistinctId as span attributes. Keep explicit application attributes when you add these defaults:
Apply the IDs to each span that needs them. If a Node action already uses posthog-node and an application-owned OpenTelemetry provider, the Node span processor can apply the IDs inside withContext(). Keep those Node imports in a file with "use node". The full Node SDK cannot be bundled into queries or mutations. The default Convex runtime supports AsyncLocalStorage, but its values do not cross ctx.runQuery, ctx.runMutation, or ctx.runAction calls. Pass IDs explicitly across these boundaries. Await action work and exporter flushes before the action returns.
Convex's PostHog log stream sends function metadata and console output in the OTLP log body. IDs you print in console output remain body content; this does not establish canonical log attributes or trace links. The dashboard's PostHog exception destination uses the authenticated tokenIdentifier, or the deployment name when unauthenticated. It does not document a browser session ID. Use the component's captureException() with an explicit distinctId and $session_id in additionalProperties when you need that session link on a specific exception.
Trace LLM calls and Convex Agent
If your app calls LLM providers from Convex actions, or uses @convex-dev/agent, forward the traces to PostHog AI observability to capture generations, traces, spans, token counts, latency, and cost per model call.
Setup uses @posthog/ai and OpenTelemetry rather than the @posthog/convex component. The full installation steps live on the Convex AI observability installation page, covering both vanilla Vercel AI SDK or OpenAI calls and experimental_telemetry on @convex-dev/agent.
Sync Convex tables into PostHog
If you want to query Convex data alongside event data, use the Convex data warehouse source to stream your tables into PostHog's warehouse. You can then query them with SQL, join them to event data, and build insights over the union.
Requires a Convex Professional plan for Convex's streaming export API.











