diff --git a/apps/web/package.json b/apps/web/package.json
index 7b7b1ab4..515a02ed 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -20,6 +20,7 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^3",
+ "@microsoft/clarity": "^1.0.2",
"@monaco-editor/react": "^4.7.0",
"@peculiar/x509": "^2.0.0",
"@radix-ui/react-accordion": "^1.2.12",
diff --git a/apps/web/src/components/clarity-analytics.tsx b/apps/web/src/components/clarity-analytics.tsx
new file mode 100644
index 00000000..f223230e
--- /dev/null
+++ b/apps/web/src/components/clarity-analytics.tsx
@@ -0,0 +1,20 @@
+"use client"
+
+import { useEffect } from "react"
+
+// Microsoft Clarity heatmaps + session replays. No-op unless
+// NEXT_PUBLIC_CLARITY_PROJECT_ID is set (so it stays off in local dev),
+// matching how Vercel Analytics only reports in production.
+// Data lives in clarity.microsoft.com — nothing touches our backend.
+let initialized = false // ponytail: module guard so StrictMode/remounts can't double-inject the script
+
+export function ClarityAnalytics() {
+ useEffect(() => {
+ const projectId = process.env.NEXT_PUBLIC_CLARITY_PROJECT_ID
+ if (!projectId || initialized) return
+ initialized = true
+ import("@microsoft/clarity").then((m) => m.default.init(projectId))
+ }, [])
+
+ return null
+}
diff --git a/apps/web/src/components/client-shell.tsx b/apps/web/src/components/client-shell.tsx
index f24ba9fb..0a0e9b02 100644
--- a/apps/web/src/components/client-shell.tsx
+++ b/apps/web/src/components/client-shell.tsx
@@ -20,6 +20,10 @@ const ClientSpeedInsights = dynamic(
() => import('@vercel/speed-insights/next').then((m) => m.SpeedInsights),
{ ssr: false }
)
+const ClientClarity = dynamic(
+ () => import('@/components/clarity-analytics').then((m) => m.ClarityAnalytics),
+ { ssr: false }
+)
const ClientToaster = dynamic(
() => import('@/components/branded-toaster').then((m) => m.BrandedToaster),
{ ssr: false }
@@ -47,6 +51,9 @@ export function ClientShell({ children }: Props) {
+
+
+