PostHog Analytics Architecture
This document outlines the analytics architecture using PostHog across HappyClient's applications, covering two distinct tracking domains with separate projects.
Overview
HappyClient uses two separate PostHog projects to track different audiences:
| Project | Audience | Purpose |
|---|---|---|
| A: HappyClient Users | Our users | Track journey from website to activation |
| B: Video Flow End-Users | Our users' users | Track video testimonial flow completion |
This separation ensures:
- Clear data ownership boundaries
- Different privacy considerations per audience
- Focused analytics for each use case
- Independent dashboard and funnel management
Architecture Diagram
┌─────────────────────────────────────────────────────────────────────────┐
│ PostHog Project A: HappyClient Users │
│ (website + onboarding + library) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Website │ │ Hello │ │ Library │ │
│ │ thehappyclient │ │ (hello.*) │ │ (library.*) │ │
│ │ .com │ │ │ │ │ │
│ └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │ │
│ │ cookie_domain: ".thehappyclient.com" │ │
│ └──────────────────────┼──────────────────────┘ │
│ │ │
│ ┌─────────────▼─────────────┐ │
│ │ Unified distinct_id │ │
│ │ Cross-subdomain cookie │ │
│ └─────────────┬─────────────┘ │
│ │ │
│ ┌─────────────▼─────────────┐ │
│ │ posthog.identify() │ │
│ │ on Clerk login/signup │ │
│ └───────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ PostHog Project B: Video Flow End-Users │
│ (branded-video-flow) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Branded Video Flow │ │
│ │ flow.thehappyclient.com │ │
│ │ │ │
│ │ Events: flow_page_visited, video_recorded, submit_success, etc │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Library (Server-side) │ │
│ │ Queries Project B for analytics dashboards │ │
│ │ │ │
│ │ Uses: POSTHOG_FLOW_ANALYTICS_API_KEY, _PROJECT_ID (server-only)│ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Project A: HappyClient User Analytics
Purpose
Track HappyClient's own users across their complete journey:
- First website visit (anonymous)
- CTA click → Onboarding
- Onboarding steps completion
- Signup/login
- Product activation and usage
Applications
| App | Subdomain | Codebase | Status |
|---|---|---|---|
| Website | thehappyclient.com | separate | To be implemented |
| Onboarding | hello.thehappyclient.com | apps/hello | Needs cross-domain config |
| Library | library.thehappyclient.com | apps/library | To be implemented |
Environment Variables
# All apps use the same credentials
NEXT_PUBLIC_POSTHOG_KEY=phc_<happyclient_users_project>
NEXT_PUBLIC_POSTHOG_HOST=https://eu.posthog.com
Initialization Pattern
All apps must use identical initialization for cross-subdomain tracking:
// Shared initialization config
import posthog from "posthog-js";
export function initPostHog(key: string, host: string) {
if (typeof window === "undefined") return null;
if (!posthog.__loaded) {
posthog.init(key, {
api_host: host,
persistence: "localStorage+cookie",
cookie_domain: ".thehappyclient.com", // Leading dot required!
capture_pageview: true,
capture_pageleave: true,
// Optional: Session replay
disable_session_recording: false,
session_recording: {
maskAllInputs: true,
maskInputFn: (text, element) => {
// Mask sensitive fields
if (element?.type === "password") return "***";
return text;
},
},
});
}
return posthog;
}
Identity Resolution
PostHog identity is resolved when users authenticate via Clerk:
// After Clerk login/signup
import { useUser } from "@clerk/nextjs";
import { usePostHog } from "posthog-js/react";
export function useIdentifyUser() {
const { user, isLoaded } = useUser();
const posthog = usePostHog();
useEffect(() => {
if (!isLoaded || !user || !posthog) return;
posthog.identify(user.id, {
email: user.emailAddresses[0]?.emailAddress,
name: user.fullName,
org_id: user.organizationMemberships?.[0]?.organization.id,
created_at: user.createdAt,
});
}, [user, isLoaded, posthog]);
}
Identity flow:
- User visits website → Anonymous
distinct_idcreated - User navigates to hello.* → Same
distinct_id(cross-subdomain cookie) - User completes onboarding → Same
distinct_id - User signs up via Clerk →
posthog.identify(clerkUserId)merges IDs - User uses library → Full timeline from website visit available
Event Naming Convention
Events follow the pattern: {app}_{action}
// Website events
posthog.capture("website_page_view", { page: "/pricing" });
posthog.capture("website_cta_click", { cta: "start_free_trial" });
posthog.capture("website_demo_requested");
// Onboarding events
posthog.capture("onboarding_started");
posthog.capture("onboarding_step_completed", { step: "upload_video", step_index: 2 });
posthog.capture("onboarding_finished");
// Library events
posthog.capture("library_login");
posthog.capture("library_project_created", { project_id: "..." });
posthog.capture("library_flow_published", { flow_id: "..." });
Default Event Properties
Register default properties on initialization:
// In each app's PostHog provider
posthog.register({
app: "library", // or "onboarding" or "website"
env: process.env.NODE_ENV === "production" ? "production" : "development",
version: process.env.NEXT_PUBLIC_APP_VERSION || "unknown",
});
Library App Implementation
The library app needs a new client-side PostHog provider:
// apps/library/components/providers/PostHogProvider.tsx
"use client";
import { useEffect, useMemo } from "react";
import posthog from "posthog-js";
import { PostHogProvider as PHProvider } from "posthog-js/react";
import { useUser } from "@clerk/nextjs";
import { getLibraryPublicEnvironment } from "@/lib/env";
export function PostHogProvider({ children }: { children: React.ReactNode }) {
const { user, isLoaded } = useUser();
const client = useMemo(() => {
if (typeof window === "undefined") return null;
const env = getLibraryPublicEnvironment();
const key = env.NEXT_PUBLIC_POSTHOG_KEY;
const host = env.NEXT_PUBLIC_POSTHOG_HOST;
if (!key || !host) return null;
if (!posthog.__loaded) {
posthog.init(key, {
api_host: host,
persistence: "localStorage+cookie",
cookie_domain: ".thehappyclient.com",
capture_pageview: true,
capture_pageleave: true,
});
// Register default properties
posthog.register({
app: "library",
env: env.ENVIRONMENT === "cloud" ? "production" : "development",
});
}
return posthog;
}, []);
// Identify user when Clerk auth is ready
useEffect(() => {
if (!isLoaded || !user || !client) return;
client.identify(user.id, {
email: user.emailAddresses[0]?.emailAddress,
name: user.fullName,
org_id: user.organizationMemberships?.[0]?.organization.id,
});
}, [user, isLoaded, client]);
if (!client) return <>{children}</>;
return <PHProvider client={client}>{children}</PHProvider>;
}
Hello App Updates
Update existing PostHog config for cross-subdomain cookies:
// apps/hello/app/_providers/client-providers.tsx
// Update the posthog.init call to include:
posthog.init(key, {
api_host: host,
persistence: "localStorage+cookie",
cookie_domain: ".thehappyclient.com", // ADD THIS
capture_pageview: true,
capture_pageleave: true,
});
// Register default properties
posthog.register({
app: "onboarding",
env: "production",
});
Project B: Video Flow End-User Analytics
Purpose
Track end-users (customers' customers) as they complete video testimonial flows. This is a separate audience from HappyClient users.
PostHog Project
- Project ID: 74547 (Production)
- Host: https://eu.posthog.com
Client-Side Tracking
Implemented in apps/branded-video-flow via the @repo/app-video-flow package.
Key files:
packages/app-video-flow/src/web/analytics/posthog.ts— Type-safe event definitionspackages/app-video-flow/src/web/components/providers/PostHogProvider.tsx— React providerapps/branded-video-flow/app/_providers/client-providers.tsx— App initialization
Event Types
All events are type-safe via PostHogEventMap:
// From packages/app-video-flow/src/web/analytics/posthog.ts
export type PostHogEventMap = {
flow_page_visited: PostHogEvents.FlowPageVisited;
flow_start: PostHogEvents.FlowStart;
step_change: PostHogEvents.FlowStepChange;
session_created: PostHogEvents.SessionCreated;
reward_selected: PostHogEvents.RewardSelected;
video_recorded: PostHogEvents.VideoRecorded;
video_skipped: PostHogEvents.VideoSkipped;
upload_start: PostHogEvents.UploadStart;
upload_success: PostHogEvents.UploadSuccess;
upload_error: PostHogEvents.UploadError;
form_submitted: PostHogEvents.FormSubmitted;
submit_start: PostHogEvents.SubmitStart;
submit_success: PostHogEvents.SubmitSuccess;
submit_error: PostHogEvents.SubmitError;
// ... video playback events
};
Server-Side Querying
The library app queries Project B's data for analytics dashboards:
Environment variables (server-only):
# apps/library/.env
# Note: POSTHOG_FLOW_ANALYTICS_* prefix clarifies these are for QUERYING Project B,
# not for client-side tracking (which uses NEXT_PUBLIC_POSTHOG_*)
POSTHOG_FLOW_ANALYTICS_API_KEY=phk_... # Personal API Key with query:read scope
POSTHOG_FLOW_ANALYTICS_PROJECT_ID=74547
POSTHOG_FLOW_ANALYTICS_HOST=https://eu.posthog.com
Key files:
apps/library/lib/services/posthog-client.ts— HogQL query executionapps/library/lib/services/posthog-analytics.ts— Analytics serviceapps/library/lib/services/funnel-calculator.ts— Funnel metrics calculation
Example query:
// From apps/library/lib/services/posthog-client.ts
export async function queryPostHogEvents(
flowId: string,
excludeAdmins: boolean = false
): Promise<PostHogEvent[]> {
const client = getPostHogClient();
if (!client.isConfigured()) {
throw new Error("PostHog not configured");
}
const query = new HogQLQueryBuilder()
.select(["event", "timestamp", "person_id", "properties"])
.from("events")
.where(`properties.flow_id = '${flowId}'`)
.where(`timestamp >= now() - INTERVAL 30 DAY`)
.orderBy("timestamp", "ASC")
.build();
const response = await client.executeQuery(query);
return PostHogEventParser.parseEvents(response.results);
}
Flow Analytics Dashboard
The library displays flow analytics by querying Project B:
// apps/library/components/admin/FlowAnalyticsDashboard.tsx
import { getFlowAnalyticsData } from "@/lib/services/posthog-analytics";
export async function FlowAnalyticsDashboard({ flowId }: { flowId: string }) {
const analytics = await getFlowAnalyticsData(flowId);
if (!analytics.isPostHogConfigured) {
return <PostHogNotConfiguredMessage />;
}
return (
<div>
<FunnelChart data={analytics.funnel} />
<VisitorsTrend data={analytics.dailyVisitors} />
</div>
);
}
Dashboards & Investigable Questions (Project A)
Two PostHog dashboards in the app project (ID 117783) visualize the custom events instrumented across the hello and library apps. Together they cover the full user lifecycle from onboarding entry to post-publish activation.
Dashboard 1: Onboarding Funnel & Engagement
URL: https://eu.posthog.com/project/117783/dashboard/502991
Tags: onboarding, analytics
| Insight | Type | What It Answers |
|---|---|---|
| Onboarding Funnel | Funnel | Where do users drop off during onboarding? Shows conversion from Started → Objective → Company → Exit Path → Published. Identifies the highest-drop-off step. |
| Onboarding Volume | Bold Number | How many users are starting onboarding? Total onboarding_started count over the last 30 days — the top-of-funnel input metric. |
| Publish Conversion | Bold Number | How many flows actually get published? Total onboarding_published count — the bottom-of-funnel output metric. Compare with volume for overall conversion rate. |
| Objective Distribution | Pie | What are users trying to achieve? Breakdown of onboarding_objective_selected by objective_id — shows which use cases (testimonials, case studies, etc.) drive adoption. |
| AI Feature Usage | Bar | Are users engaging with AI features? Breakdown of onboarding_ai_action by type (generate_questions, regenerate, translate, generate_image, generate_more). Measures AI feature-market fit. |
| Auth Wall Funnel | Funnel | How many users does the auth gate lose? Publish Initiated → Auth Shown → Published. Measures authentication friction for unauthenticated users. |
| Publish Outcomes | Line | How reliable is publishing? Side-by-side onboarding_published vs onboarding_publish_failed over time. Spikes in failures signal infrastructure issues. |
| Language Adoption | Line | How popular is multi-language support? onboarding_language_added count broken down by language. Shows which languages are most requested and whether multi-language is a growth driver. |
Key questions this dashboard answers:
- What is our onboarding conversion rate? — Compare Onboarding Volume to Publish Conversion.
- Where is the biggest drop-off? — Read the Onboarding Funnel step-by-step conversion rates.
- Is the auth gate killing conversion? — Check the Auth Wall Funnel for the Initiated → Published conversion rate.
- Which objectives should we prioritize? — Look at Objective Distribution to see what users actually want.
- Is AI worth the investment? — AI Feature Usage shows whether users engage with AI features and which ones they prefer.
- Are publishes reliable? — Publish Outcomes line chart shows failure trends over time.
- Should we invest more in i18n? — Language Adoption reveals demand for non-English flows.
Dashboard 2: Post-Publish Activation
URL: https://eu.posthog.com/project/117783/dashboard/502992
Tags: library, analytics
| Insight | Type | What It Answers |
|---|---|---|
| Activation Funnel | Funnel | Do published flows actually get used? Published → URL Copied → First Video Received. Measures the full activation loop. |
| Flows Published | Bold Number | How many flows went live this period? Total onboarding_published count from the library perspective — input to activation. |
| Share Actions | Line | Are users sharing their flows? library_flow_url_copied + library_try_yourself_clicked over time. Share actions are the first signal of distribution intent. |
| First Video Received | Bold Number | How many flows reached the activation milestone? Count of library_first_video_received — the 0→1 video transition that means a flow is truly activated. |
| Video Views | Line | Are users engaging with received videos? library_video_viewed per day — measures whether users actually watch the video responses they receive. |
| CSV Exports | Line | Are users extracting data? library_csv_exported per day — a power-user engagement signal indicating deeper product adoption. |
Key questions this dashboard answers:
- What percentage of published flows get shared? — Activation Funnel: Published → URL Copied conversion.
- What percentage of shared flows receive a video response? — Activation Funnel: URL Copied → First Video Received conversion.
- Is the "aha moment" happening? — First Video Received count. This is the key activation metric — a flow that never receives a video is a dead flow.
- Are users coming back to view responses? — Video Views trend. Sustained views indicate retention and engagement.
- Are power users emerging? — CSV Exports trend. Export behavior signals users who are deeply engaged and may be candidates for upsell.
- Is distribution growing? — Share Actions trend over time. Rising share actions mean organic growth potential.
Combining Both Dashboards
The two dashboards form a complete picture of the user lifecycle:
Onboarding Funnel & Engagement Post-Publish Activation
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ │ │ │
│ Start → Objective → │ │ Published → URL Copied → │
│ Company → Exit Path → │ ────────► │ First Video Received │
│ Published │ (handoff) │ │
│ │ │ + Video Views, CSV Exports │
│ + AI Usage, Auth Funnel, │ │ │
│ Objectives, Languages │ │ │
└─────────────────────────────┘ └─────────────────────────────┘
Cross-dashboard questions:
| Question | Where to Look |
|---|---|
| What is our end-to-end conversion from start to activation? | Onboarding Volume ÷ First Video Received |
| Are users who use AI features more likely to activate? | Filter Onboarding Funnel by users who fired onboarding_ai_action, compare activation rates |
| Which objectives lead to highest activation? | Cross-reference Objective Distribution with Activation Funnel (requires PostHog cohort analysis) |
| Is the auth wall impacting activation, not just publish? | Compare Auth Wall Funnel drop-off with Activation Funnel rates for auth vs pre-auth users |
What These Dashboards Don't Cover
| Gap | Why | Alternative |
|---|---|---|
| Per-step time spent | No duration tracking — use PostHog session replay | Session replay + $pageview timestamps |
| Return visits / multi-session onboarding | onboarding_draft_resumed exists but no dashboard insight yet | Create ad-hoc insight from onboarding_draft_resumed |
| Asset upload patterns | onboarding_asset_uploaded is tracked but not dashboarded | Create ad-hoc insight breaking down by asset_type and source |
| Per-organization activation | Events carry organization_id but dashboards show aggregate | Filter any insight by organization_id property |
| Website → Onboarding attribution | Website tracking not yet implemented (Project A) | Pending website PostHog integration |
| Video flow completion rates | Belongs to Project B (end-user analytics) | See Project B dashboards |
Environment Variable Summary
Project A: HappyClient User Analytics
| Variable | Apps | Purpose |
|---|---|---|
NEXT_PUBLIC_POSTHOG_KEY | website, hello, library | Client-side tracking key |
NEXT_PUBLIC_POSTHOG_HOST | website, hello, library | PostHog API host |
Project B: Video Flow Analytics
| Variable | Apps | Purpose |
|---|---|---|
NEXT_PUBLIC_POSTHOG_KEY | branded-video-flow | Client-side tracking key |
NEXT_PUBLIC_POSTHOG_HOST | branded-video-flow | PostHog API host |
POSTHOG_FLOW_ANALYTICS_API_KEY | library (server) | Personal API key for querying |
POSTHOG_FLOW_ANALYTICS_PROJECT_ID | library (server) | Project ID for queries |
POSTHOG_FLOW_ANALYTICS_HOST | library (server) | API host for queries |
Best Practices
1. Never Mix Projects
Each app should only send events to its designated project. Never configure library to send client events to Project B.
2. Use Type-Safe Events
For Project B, use the typed captureEvent function:
import { captureEvent } from "@repo/app-video-flow/web";
captureEvent("flow_start", {
flow_id: flowId,
session_id: sessionId,
});
3. Handle Missing Configuration Gracefully
if (!posthogClient) {
console.warn("PostHog not configured");
return <>{children}</>;
}
4. Don't Track on Kiosk/Preview Routes
The BVF PostHogProvider automatically disables tracking on kiosk routes and when disableTracking={true}.
5. Respect User Privacy
- Mask sensitive inputs in session recordings
- Don't capture PII in event properties
- Respect browser DNT settings where appropriate
Related Documents
prds/posthog-unified-analytics-prd.md— Requirements and acceptance criteriadocs/architecture/posthog-event-tracking.md— Event instrumentation details, user journey mapping, and type-safe event moduledocs/posthog-flow-analytics-overview.md— Video flow analytics detailsdocs/posthog-customer-analytics-workflow.md— Customer analytics workflows