Implement PostHog reference architecture with best-practice project layout. Use when designing new PostHog integrations, reviewing project structure, or establishing architecture standards for...
Production-grade architecture for PostHog analytics in a web application. Covers file structure, event taxonomy design, SDK initialization layers, feature flag management, group analytics for B2B, and data pipeline integration.
posthog-js and posthog-node SDKspersonalApiKey option.āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Browser (posthog-js) ā
ā $pageview, $autocapture, custom events, identify ā
ā Feature flag evaluation, session recordings ā
āāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā HTTPS (direct or reverse proxy)
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā PostHog Cloud (us.i.posthog.com) ā
ā āāāāāāāāāāāā āāāāāāāāāāāā āāāāāāāāāāāāāāāāāāāāā ā
ā ā Events ā ā Feature ā ā Session Replay ā ā
ā ā Pipeline ā ā Flags ā ā & Recordings ā ā
ā āāāāāā¬āāāāāā āāāāāā¬āāāāāā āāāāāāāāāāāāāāāāāāāāā ā
ā ā ā ā
ā āāāāāā“āāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā Analytics: Trends, Funnels, Retention, Paths ā ā
ā ā HogQL (SQL), Dashboards, Cohorts ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā CDP: Destinations (Webhook, Slack, S3, etc.) ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā²
ā posthog-node (server events, local flag eval)
āāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Backend (API routes, webhooks, crons) ā
ā Server-side capture, group identify, flag eval ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Use Read to inspect the relevant configuration and implementation before proposing changes. Use Grep to locate initialization, capture, flag, and credential boundaries.
src/
āāā analytics/
ā āāā posthog.ts # Browser SDK init (singleton)
ā āāā posthog-server.ts # Server SDK init (singleton)
ā āāā events.ts # Typed event constants
ā āāā flags.ts # Feature flag key constants
ā āāā hooks/
ā āāā useFeatureFlag.ts # React hook for boolean flags
ā āāā useExperiment.ts # React hook for A/B variants
āāā app/
ā āāā providers.tsx # PostHogProvider wrapper
ā āāā layout.tsx # Root layout with provider
āāā lib/
āāā analytics.ts # High-level tracking functions
// analytics/events.ts
// Naming convention: object_action (noun_verb)
export const EVENTS = {
// User lifecycle (track conversion funnel)
USER_SIGNED_UP: 'user_signed_up',
USER_LOGGED_IN: 'user_logged_in',
USER_ONBOARDING_COMPLETED: 'user_onboarding_completed',
USER_INVITED_TEAMMATE: 'user_invited_teammate',
// Core product (track feature adoption)
FEATURE_USED: 'feature_used', // with feature_name property
ITEM_CREATED: 'item_created',
ITEM_UPDATED: 'item_updated',
ITEM_DELETED: 'item_deleted',
SEARCH_PERFORMED: 'search_performed',
EXPORT_COMPLETED: 'export_completed',
// Revenue (track MRR and churn)
SUBSCRIPTION_STARTED: 'subscription_started',
SUBSCRIPTION_UPGRADED: 'subscription_upgraded',
SUBSCRIPTION_DOWNGRADED: 'subscription_downgraded',
SUBSCRIPTION_CANCELED: 'subscription_canceled',
PAYMENT_COMPLETED: 'payment_completed',
// Engagement (track stickiness)
NOTIFICATION_CLICKED: 'notification_clicked',
FEEDBACK_SUBMITTED: 'feedback_submitted',
} as const;
// Standard property schema
interface BaseProps {
source?: 'web' | 'mobile' | 'api';
plan?: 'free' | 'pro' | 'enterprise';
}
// Type-safe capture
type EventMap = {
[EVENTS.USER_SIGNED_UP]: BaseProps & { method: 'email' | 'google' | 'github' };
[EVENTS.FEATURE_USED]: BaseProps & { feature_name: string; duration_ms?: number };
[EVENTS.SUBSCRIPTION_STARTED]: BaseProps & { plan: string; interval: 'monthly' | 'annual'; mrr: number };
};
// analytics/flags.ts
export const FLAGS = {
// Feature rollouts
NEW_DASHBOARD: 'new-dashboard-v2',
AI_SUMMARIZE: 'ai-summarize-beta',
BULK_EXPORT: 'bulk-export',
// Experiments
PRICING_PAGE: 'pricing-page-experiment',
ONBOARDING_FLOW: 'onboarding-flow-v3',
CHECKOUT_LAYOUT: 'checkout-layout-test',
} as const;
// Flag ā default value mapping (used when flags fail to load)
export const FLAG_DEFAULTS: Record<string, boolean | string> = {
[FLAGS.NEW_DASHBOARD]: false,
[FLAGS.AI_SUMMARIZE]: false,
[FLAGS.PRICING_PAGE]: 'control',
[FLAGS.ONBOARDING_FLOW]: 'control',
};
// lib/analytics.ts
import posthog from 'posthog-js';
import { getPostHogServer } from '../analytics/posthog-server';
import { EVENTS } from '../analytics/events';
// Client-side tracking
export function trackFeatureUsed(featureName: string, duration?: number) {
posthog.capture(EVENTS.FEATURE_USED, {
feature_name: featureName,
duration_ms: duration,
source: 'web',
});
}
export function trackSignup(method: 'email' | 'google' | 'github') {
posthog.capture(EVENTS.USER_SIGNED_UP, { method, source: 'web' });
}
export function identifyUser(userId: string, properties: {
email: string;
name: string;
plan: string;
companyId?: string;
companyName?: string;
}) {
posthog.identify(userId, {
email: properties.email,
name: properties.name,
plan: properties.plan,
});
if (properties.companyId) {
posthog.group('company', properties.companyId, {
name: properties.companyName,
plan: properties.plan,
});
}
}
// Server-side tracking
export function trackServerEvent(
userId: string,
event: string,
properties?: Record<string, any>
) {
const ph = getPostHogServer();
ph.capture({
distinctId: userId,
event,
properties: { ...properties, source: 'api' },
});
}
// PostHog ā External Systems via CDP Destinations
//
// PostHog Cloud Data Pipeline:
// 1. Events captured ā PostHog stores in ClickHouse
// 2. CDP Destinations fire webhooks to your endpoints
// 3. HogQL queries available for custom analysis
//
// Common destination patterns:
// - PostHog ā Webhook ā Your API ā CRM sync
// - PostHog ā S3 export ā Data warehouse
// - PostHog ā Slack ā Team notifications
// - PostHog ā Webhook ā Billing system (revenue events)
// Server route to receive PostHog CDP webhooks
export async function handlePostHogWebhook(event: string, payload: any) {
switch (event) {
case EVENTS.SUBSCRIPTION_STARTED:
await syncToStripe(payload);
break;
case EVENTS.USER_SIGNED_UP:
await syncToCRM(payload);
await notifySlack(payload);
break;
}
}
| Issue | Cause | Solution |
|---|---|---|
| Events not appearing | SDK not initialized | Verify posthog.init() runs before capture |
| Flag always returns default | Flags not loaded | Use posthog.onFeatureFlags() callback |
| Identity fragmentation | Inconsistent distinct_id |
Use same user ID from auth system everywhere |
| Group analytics empty | posthog.group() not called |
Call group() before capture |
| Server events lost | No flush() in serverless |
Always await posthog.shutdown() |
object_action naming conventionFor a multi-service B2B product, place browser and server SDK adapters behind one typed analytics contract, define identity and group ownership, isolate environment tokens, centralize safe defaults, and document failure behavior. Return a component map plus decision and rollback records, not generic sample code.
See official PostHog references for current authority and verification boundaries.