Skip to main content
This page covers PostHog as a server-side analytics adapter: dual identity on track, identify mapped to PostHog’s alias API, and how that ties to session-backed anonymousAnalyticsId and login flows.

Why

PostHog gives you product analytics with flexible deployment options and a straightforward server SDK. If you need event tracking with provider-level host configurability while keeping the existing analytics abstraction, PostHog fits naturally into the pluggable tooling architecture.

How it works

Dual identity on track: Every track call receives AnalyticsIdentificationAttributesType: { anonymousId: string; userId?: string }. The adapter uses primary distinct id userId ?? anonymousId for capture. identify: The shared contract exposes identify({ anonymousId, userId }). The PostHog adapter maps this to posthog.alias({ distinctId: userId, alias: anonymousId }), linking the anonymous distinct id to the authenticated user id (see PostHog docs for alias semantics). When identify runs (server-only):
  1. After successful login (signInWithPassword, completeInvitedProfile, OAuth callback) via identifyUserInAnalytics.
  2. Legacy repair: middleware runs ensureAnonymousAnalyticsId then syncAnalyticsIdentityIfNeeded — if a new anonymous id was just created (wasCreated) and the user is already authenticated, identify runs on that request so pre-deploy sessions don’t stay unlinked.
No browser PostHog SDK is wired in this stack yet; all instrumentation is backend-only through tools.analytics and GraphQL trackFrontendEvent.

Setup

  1. Configure PostHog in apps/backend/src/configuration/production.ts (or staging.ts) with a server-side key:
    settings is optional in the shared type; if omitted, the SDK uses its defaults (you still need apiKey and trackingPolicy).
  2. Set env vars in your deployment:
    ANALYTICS_APP is required whenever analytics is enabled. The backend attaches it as the app property on every outbound track event so you can filter multiple Refract deployments in one PostHog project.
  3. Run verification:

Tracking policy behavior

trackingPolicy.allowedFields is top-level only in v1. Any field not listed is dropped. Fields with pii: true are masked before being sent to PostHog. Policy logs include field names only and never raw field values.

Frontend tracking endpoint

Frontend clients should send product analytics through the backend GraphQL mutation trackFrontendEvent instead of calling provider SDKs directly. This keeps validation, rate limiting, and policy enforcement centralized.
eventDomain is constrained, eventName must match a namespaced format (domain.action), and properties are key/value pairs with backend validation limits. The resolver resolves { anonymousId, userId? } from the session and request.

Future: browser SDK and one canonical id

When you add a browser (PostHog JS) or other client SDK, the intended pattern is to surface the same anonymousAnalyticsId the backend already stores in session — for example a first-party cookie set on responses that mirrors req.session.anonymousAnalyticsId — so the client initializes with the same distinct id, not a second generator. Choosing HttpOnly vs JS-readable and SameSite is a product/security decision for that phase.

Extending this system

  1. Adjust capture or alias behavior in apps/tools/analytics/posthog/src/index.ts.
  2. If you change the shared contract, update apps/shared/src/analytics/types.ts and apps/shared/src/analytics/schema.ts.
  3. Run make test module=tooling-analytics-posthog and make test module=backend path=apps/backend/src/tools/analytics/__tests__/loader.spec.ts.

What not to do

❌ Never import PostHog directly in a GraphQL resolver or React component. Use tools.analytics from the backend. ❌ Never use high-cardinality values as tags in telemetry; follow .cursor/rules/security.mdc for analytics properties. ❌ Never configure AnalyticsClientType.LOCAL or dev keys in production — use PostHog only in prod/staging configs.

File conventions

Gotchas

  • POSTHOG_API_KEY must be a project API key from server-side environment config, not a personal API key.
  • If trackingPolicy.allowedFields does not include a field, the field is removed before capture.
  • Local analytics can run without strict policy enforcement, so local logs may differ from production payloads.

What’s next?

  • Amplitude — another production analytics provider with the same interface.
  • Local — dev/test analytics stub.
  • Configuration — selecting tool implementations by environment.