-
{paste.title}
+
+ {paste.title}
+
{paste.content.substring(0, 100)}
{paste.content.length > 100 ? "..." : ""}
@@ -244,53 +273,53 @@ export default function PastesRoute({ loaderData, params }: Route.ComponentProps
)}
{/* Create Paste Form */}
-
+
);
}
-
diff --git a/apps/react-router/saas-template/app/routes/paste.$pasteId.tsx b/apps/react-router/saas-template/app/routes/paste.$pasteId.tsx
index 6c78e67e..3e970b2b 100644
--- a/apps/react-router/saas-template/app/routes/paste.$pasteId.tsx
+++ b/apps/react-router/saas-template/app/routes/paste.$pasteId.tsx
@@ -7,7 +7,7 @@ import { prisma } from "~/utils/database.server";
export async function loader({ params, context, request }: Route.LoaderArgs) {
const { pasteId } = params;
-
+
if (!pasteId) {
throw new Response("Paste ID required", { status: 400 });
}
@@ -24,16 +24,16 @@ export async function loader({ params, context, request }: Route.LoaderArgs) {
// If paste is public, allow access
if (paste.isPublic) {
await prisma.paste.update({
- where: { id: pasteId },
data: { viewCount: { increment: 1 } },
+ where: { id: pasteId },
});
return data({
+ pageTitle: paste.title,
paste: {
...paste,
viewCount: paste.viewCount + 1,
},
- pageTitle: paste.title,
});
}
@@ -49,7 +49,10 @@ export async function loader({ params, context, request }: Route.LoaderArgs) {
throw new Response("Paste not found", { status: 404 });
}
- const userAccount = await retrieveUserAccountWithMembershipsFromDatabaseBySupabaseUserId(supabaseUser.id);
+ const userAccount =
+ await retrieveUserAccountWithMembershipsFromDatabaseBySupabaseUserId(
+ supabaseUser.id,
+ );
if (!userAccount) {
throw new Response("Paste not found", { status: 404 });
@@ -57,7 +60,7 @@ export async function loader({ params, context, request }: Route.LoaderArgs) {
// Check if user is a member of the paste's organization
const hasAccess = userAccount.memberships.some(
- (membership) => membership.organization.id === paste.organizationId
+ (membership) => membership.organization.id === paste.organizationId,
);
if (!hasAccess) {
@@ -66,17 +69,17 @@ export async function loader({ params, context, request }: Route.LoaderArgs) {
// User has access
await prisma.paste.update({
- where: { id: pasteId },
data: { viewCount: { increment: 1 } },
+ where: { id: pasteId },
});
return data(
{
+ pageTitle: paste.title,
paste: {
...paste,
viewCount: paste.viewCount + 1,
},
- pageTitle: paste.title,
},
{ headers },
);
@@ -100,4 +103,3 @@ export default function ViewPasteRoute({ loaderData }: Route.ComponentProps) {
);
}
-
diff --git a/apps/react-router/saas-template/app/routes/pricing.tsx b/apps/react-router/saas-template/app/routes/pricing.tsx
index 5db207bf..97f34527 100644
--- a/apps/react-router/saas-template/app/routes/pricing.tsx
+++ b/apps/react-router/saas-template/app/routes/pricing.tsx
@@ -1,5 +1,6 @@
+import { usePostHog } from "@posthog/react";
import { IconCheck } from "@tabler/icons-react";
-import { useState } from "react";
+import { useEffect, useRef, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { href, Link } from "react-router";
@@ -39,6 +40,16 @@ export default function PricingRoute() {
const { t } = useTranslation("billing", { keyPrefix: "pricing" });
const { t: tPage } = useTranslation("billing", { keyPrefix: "pricingPage" });
const [billingPeriod, setBillingPeriod] = useState("annual");
+ const posthog = usePostHog();
+ const hasTrackedPageView = useRef(false);
+
+ // Track pricing page view (only once on initial mount)
+ useEffect(() => {
+ if (posthog && !hasTrackedPageView.current) {
+ posthog.capture("pricing_page_viewed");
+ hasTrackedPageView.current = true;
+ }
+ }, [posthog]);
const getFeatures = (key: string): string[] =>
t(`plans.${key}.features`, "", { returnObjects: true }) as string[];
diff --git a/apps/react-router/saas-template/package-lock.json b/apps/react-router/saas-template/package-lock.json
index 323d88ee..7c951f26 100644
--- a/apps/react-router/saas-template/package-lock.json
+++ b/apps/react-router/saas-template/package-lock.json
@@ -18,6 +18,7 @@
"@oslojs/crypto": "1.0.1",
"@oslojs/encoding": "1.1.0",
"@paralleldrive/cuid2": "3.0.4",
+ "@posthog/react": "^1.7.0",
"@prisma/adapter-pg": "7.1.0",
"@prisma/client": "7.1.0",
"@radix-ui/react-visually-hidden": "1.2.4",
@@ -43,6 +44,8 @@
"input-otp": "1.4.2",
"isbot": "5.1.32",
"motion": "12.23.26",
+ "posthog-js": "^1.333.0",
+ "posthog-node": "^5.21.2",
"pretty-cache-header": "1.0.0",
"ramda": "0.32.0",
"react": "19.2.3",
@@ -1326,7 +1329,7 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -1336,7 +1339,7 @@
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -1370,7 +1373,7 @@
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.5"
@@ -1518,7 +1521,7 @@
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
@@ -2946,6 +2949,252 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@opentelemetry/api": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
+ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/api-logs": {
+ "version": "0.208.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz",
+ "integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/core": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
+ "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-logs-otlp-http": {
+ "version": "0.208.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.208.0.tgz",
+ "integrity": "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.208.0",
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/otlp-exporter-base": "0.208.0",
+ "@opentelemetry/otlp-transformer": "0.208.0",
+ "@opentelemetry/sdk-logs": "0.208.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-exporter-base": {
+ "version": "0.208.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz",
+ "integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/otlp-transformer": "0.208.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-transformer": {
+ "version": "0.208.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz",
+ "integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.208.0",
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/resources": "2.2.0",
+ "@opentelemetry/sdk-logs": "0.208.0",
+ "@opentelemetry/sdk-metrics": "2.2.0",
+ "@opentelemetry/sdk-trace-base": "2.2.0",
+ "protobufjs": "^7.3.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
+ "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/resources": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz",
+ "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.5.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz",
+ "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-logs": {
+ "version": "0.208.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz",
+ "integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.208.0",
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/resources": "2.2.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.4.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
+ "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-metrics": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz",
+ "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/resources": "2.2.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.9.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
+ "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-base": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz",
+ "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/resources": "2.2.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
+ "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.39.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.39.0.tgz",
+ "integrity": "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@oslojs/asn1": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@oslojs/asn1/-/asn1-1.0.0.tgz",
@@ -3007,6 +3256,37 @@
"node": ">=18"
}
},
+ "node_modules/@posthog/core": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.13.0.tgz",
+ "integrity": "sha512-knjncrk7qRmssFRbGzBl1Tunt21GRpe0Wv+uVelyL0Rh7PdQUsgguulzXFTps8hA6wPwTU4kq85qnbAJ3eH6Wg==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.6"
+ }
+ },
+ "node_modules/@posthog/react": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@posthog/react/-/react-1.7.0.tgz",
+ "integrity": "sha512-pM7GL7z/rKjiIwosbRiQA3buhLI6vUo+wg+T/ZrVZC7O5bVU07TfgNZTcuOj8E9dx7vDbfNrc1kjDN7PKMM8ug==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": ">=16.8.0",
+ "posthog-js": ">=1.257.2",
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@posthog/types": {
+ "version": "1.333.0",
+ "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.333.0.tgz",
+ "integrity": "sha512-9Wg/2ez+EZh6NmtOjhtYSkBHz/yIq8WMS0QSIizUoggh35hHVg4BTMXl3rz/tPearJNKU/8oRjEyuZ0OYTEDOA==",
+ "license": "MIT"
+ },
"node_modules/@prisma/adapter-pg": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.1.0.tgz",
@@ -3103,6 +3383,20 @@
"url": "https://dotenvx.com"
}
},
+ "node_modules/@prisma/config/node_modules/magicast": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
+ "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/parser": "^7.25.4",
+ "@babel/types": "^7.25.4",
+ "source-map-js": "^1.2.0"
+ }
+ },
"node_modules/@prisma/config/node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -3254,6 +3548,70 @@
"react-dom": "^18.0.0 || ^19.0.0"
}
},
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
@@ -6341,6 +6699,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@types/whatwg-mimetype": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
@@ -7535,6 +7900,17 @@
"toggle-selection": "^1.0.6"
}
},
+ "node_modules/core-js": {
+ "version": "3.48.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
+ "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
"node_modules/cosmiconfig": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
@@ -7602,7 +7978,6 @@
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
@@ -8099,6 +8474,15 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
+ "node_modules/dompurify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
+ "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
@@ -8600,6 +8984,12 @@
"fxparser": "src/cli/cli.js"
}
},
+ "node_modules/fflate": {
+ "version": "0.4.8",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
+ "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
+ "license": "MIT"
+ },
"node_modules/figures": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@@ -9711,7 +10101,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "devOptional": true,
"license": "ISC"
},
"node_modules/istanbul-lib-coverage": {
@@ -10621,7 +11010,6 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
- "devOptional": true,
"license": "Apache-2.0"
},
"node_modules/longest": {
@@ -11493,7 +11881,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -11798,6 +12185,58 @@
"node": ">=0.10.0"
}
},
+ "node_modules/posthog-js": {
+ "version": "1.333.0",
+ "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.333.0.tgz",
+ "integrity": "sha512-c7vquERMedjuGE2GnaDDJW/V1BIMMQG7BlYKrH0z8O7fc3WpEsQ/IyQ+9aD9+DLxlDCFpzrwgoxVDWi9K37mdA==",
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/api-logs": "^0.208.0",
+ "@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
+ "@opentelemetry/resources": "^2.2.0",
+ "@opentelemetry/sdk-logs": "^0.208.0",
+ "@posthog/core": "1.13.0",
+ "@posthog/types": "1.333.0",
+ "core-js": "^3.38.1",
+ "dompurify": "^3.3.1",
+ "fflate": "^0.4.8",
+ "preact": "^10.28.0",
+ "query-selector-shadow-dom": "^1.0.1",
+ "web-vitals": "^4.2.4"
+ }
+ },
+ "node_modules/posthog-node": {
+ "version": "5.21.2",
+ "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.21.2.tgz",
+ "integrity": "sha512-Jehlu0KguL1LLyUczCt86OtA5INmeStK3zcgbv1BSyMcNxs0HP3GQogBrYhwhqHsk6JopiFFVpJyZEoXOUMhGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@posthog/core": "1.10.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/posthog-node/node_modules/@posthog/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-Xk3JQ+cdychsvftrV3G9ZrN9W329lbyFW0pGJXFGKFQf8qr4upw2SgNg9BVorjSrfhoXZRnJGt/uNF4nGFBL5A==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.6"
+ }
+ },
+ "node_modules/preact": {
+ "version": "10.28.2",
+ "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz",
+ "integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/preact"
+ }
+ },
"node_modules/prettier": {
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
@@ -11921,6 +12360,30 @@
"devOptional": true,
"license": "ISC"
},
+ "node_modules/protobufjs": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
+ "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/node": ">=13.7.0",
+ "long": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -11966,6 +12429,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/query-selector-shadow-dom": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz",
+ "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==",
+ "license": "MIT"
+ },
"node_modules/ramda": {
"version": "0.32.0",
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.32.0.tgz",
@@ -12902,7 +13371,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
@@ -12915,7 +13383,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -13091,7 +13558,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "devOptional": true,
+ "dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -14634,6 +15101,12 @@
"defaults": "^1.0.3"
}
},
+ "node_modules/web-vitals": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
+ "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
+ "license": "Apache-2.0"
+ },
"node_modules/whatwg-mimetype": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
@@ -14648,7 +15121,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "devOptional": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
diff --git a/apps/react-router/saas-template/package.json b/apps/react-router/saas-template/package.json
index 6f664047..81a68472 100644
--- a/apps/react-router/saas-template/package.json
+++ b/apps/react-router/saas-template/package.json
@@ -1,40 +1,4 @@
{
- "name": "saas-template",
- "private": true,
- "type": "module",
- "scripts": {
- "build": "react-router build",
- "check": "biome check --write .",
- "dev": "react-router dev",
- "dev:mocks": "cross-env MOCKS=true npm run dev",
- "lint": "biome ci --css-parse-tailwind-directives=true .",
- "prepare": "husky",
- "prisma:deploy": "npx prisma migrate deploy && npx prisma generate",
- "prisma:migrate": "npx prisma migrate dev --name",
- "prisma:push": "npx prisma db push && npx prisma generate",
- "prisma:reset-dev": "npm run prisma:wipe && npm run prisma:seed && npm run dev",
- "prisma:seed": "tsx ./prisma/seed.ts",
- "prisma:setup": "prisma generate && prisma migrate deploy && prisma db push",
- "prisma:studio": "npx prisma studio",
- "prisma:wipe": "npx prisma migrate reset --force && npx prisma db push",
- "quickstart": "cp .env.example .env && npm run prisma:setup && npm run prisma:seed && npm run dev:mocks",
- "start": "react-router-serve ./build/server/index.js",
- "start:mocks": "cross-env MOCKS=true npm run start",
- "stripe:resend-events": "xargs -n1 stripe events resend < stripe-events.txt",
- "test": "vitest --reporter=verbose --run",
- "test:e2e": "npx playwright test",
- "test:e2e:ui": "npx playwright test --ui",
- "test:watch": "vitest --reporter=verbose",
- "typecheck": "npm run typegen && tsc",
- "typegen": "npm run typegen:router && npm run typegen:prisma",
- "typegen:prisma": "prisma generate",
- "typegen:router": "react-router typegen"
- },
- "lint-staged": {
- "*.{js,jsx,ts,tsx,json,css,md}": [
- "biome check --write --unsafe"
- ]
- },
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
@@ -53,6 +17,7 @@
"@oslojs/crypto": "1.0.1",
"@oslojs/encoding": "1.1.0",
"@paralleldrive/cuid2": "3.0.4",
+ "@posthog/react": "^1.7.0",
"@prisma/adapter-pg": "7.1.0",
"@prisma/client": "7.1.0",
"@radix-ui/react-visually-hidden": "1.2.4",
@@ -78,6 +43,8 @@
"input-otp": "1.4.2",
"isbot": "5.1.32",
"motion": "12.23.26",
+ "posthog-js": "^1.333.0",
+ "posthog-node": "^5.21.2",
"pretty-cache-header": "1.0.0",
"ramda": "0.32.0",
"react": "19.2.3",
@@ -131,5 +98,41 @@
"vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.15"
},
- "packageManager": "npm@11.7.0"
-}
\ No newline at end of file
+ "lint-staged": {
+ "*.{js,jsx,ts,tsx,json,css,md}": [
+ "biome check --write --unsafe"
+ ]
+ },
+ "name": "saas-template",
+ "packageManager": "npm@11.7.0",
+ "private": true,
+ "scripts": {
+ "build": "react-router build",
+ "check": "biome check --write .",
+ "dev": "react-router dev",
+ "dev:mocks": "cross-env MOCKS=true npm run dev",
+ "lint": "biome ci --css-parse-tailwind-directives=true .",
+ "prepare": "husky",
+ "prisma:deploy": "npx prisma migrate deploy && npx prisma generate",
+ "prisma:migrate": "npx prisma migrate dev --name",
+ "prisma:push": "npx prisma db push && npx prisma generate",
+ "prisma:reset-dev": "npm run prisma:wipe && npm run prisma:seed && npm run dev",
+ "prisma:seed": "tsx ./prisma/seed.ts",
+ "prisma:setup": "prisma generate && prisma migrate deploy && prisma db push",
+ "prisma:studio": "npx prisma studio",
+ "prisma:wipe": "npx prisma migrate reset --force && npx prisma db push",
+ "quickstart": "cp .env.example .env && npm run prisma:setup && npm run prisma:seed && npm run dev:mocks",
+ "start": "react-router-serve ./build/server/index.js",
+ "start:mocks": "cross-env MOCKS=true npm run start",
+ "stripe:resend-events": "xargs -n1 stripe events resend < stripe-events.txt",
+ "test": "vitest --reporter=verbose --run",
+ "test:e2e": "npx playwright test",
+ "test:e2e:ui": "npx playwright test --ui",
+ "test:watch": "vitest --reporter=verbose",
+ "typecheck": "npm run typegen && tsc",
+ "typegen": "npm run typegen:router && npm run typegen:prisma",
+ "typegen:prisma": "prisma generate",
+ "typegen:router": "react-router typegen"
+ },
+ "type": "module"
+}
diff --git a/apps/react-router/saas-template/posthog-setup-report.md b/apps/react-router/saas-template/posthog-setup-report.md
new file mode 100644
index 00000000..6d613d3a
--- /dev/null
+++ b/apps/react-router/saas-template/posthog-setup-report.md
@@ -0,0 +1,81 @@
+# PostHog Post-Wizard Report
+
+The wizard has completed a deep integration of PostHog analytics into your React Router 7 SaaS template. This integration provides comprehensive event tracking across the entire user journey, from signup through subscription management. Both client-side and server-side tracking have been implemented using best practices for React Router v7 Framework mode.
+
+## Integration Summary
+
+### Core Setup
+- **Client-side SDK**: Initialized in `app/entry.client.tsx` with `PostHogProvider` wrapper
+- **Server-side SDK**: PostHog Node middleware created in `app/lib/posthog-middleware.server.ts`
+- **Error Boundary**: Added exception capture in `app/root.tsx`
+- **SSR Configuration**: Updated `vite.config.ts` with PostHog packages in `ssr.noExternal`
+- **Environment Variables**: Added `VITE_PUBLIC_POSTHOG_KEY` and `VITE_PUBLIC_POSTHOG_HOST` to `.env`
+
+### Tracing Headers
+The client-side SDK is configured with `__add_tracing_headers` to automatically pass session and distinct IDs to server-side requests, ensuring seamless user journey tracking across client and server.
+
+## Events Implemented
+
+| Event Name | Description | File Path |
+|------------|-------------|-----------|
+| `user_signed_up` | User successfully completed registration and account was created | `app/routes/_user-authentication+/_anonymous-routes+/auth.callback.ts` |
+| `user_logged_in` | Existing user successfully logged in via email OTP or OAuth | `app/routes/_user-authentication+/_anonymous-routes+/auth.callback.ts` |
+| `organization_created` | User created a new organization during onboarding or from organizations page | `app/features/onboarding/organization/onboarding-organization-action.server.ts`, `app/features/organizations/create-organization/create-organization-action.server.ts` |
+| `pricing_page_viewed` | User viewed the pricing page - top of conversion funnel | `app/routes/pricing.tsx` |
+| `checkout_session_started` | User initiated a checkout session to subscribe to a plan | `app/features/billing/billing-action.server.ts` |
+| `checkout_completed` | Stripe webhook - checkout session was completed successfully | `app/features/billing/stripe-event-handlers.server.ts` |
+| `subscription_created` | Stripe webhook - new subscription was created | `app/features/billing/stripe-event-handlers.server.ts` |
+| `subscription_updated` | Stripe webhook - subscription was updated (plan change, renewal, etc) | `app/features/billing/stripe-event-handlers.server.ts` |
+| `subscription_cancelled` | User initiated subscription cancellation | `app/features/billing/billing-action.server.ts` |
+| `subscription_resumed` | User resumed a subscription that was set to cancel at period end | `app/features/billing/billing-action.server.ts` |
+| `subscription_plan_switched` | User initiated a plan switch to upgrade or downgrade their subscription | `app/features/billing/billing-action.server.ts` |
+| `subscription_deleted` | Stripe webhook - subscription was cancelled/deleted | `app/features/billing/stripe-event-handlers.server.ts` |
+| `contact_sales_form_submitted` | User submitted the enterprise contact sales form | `app/features/billing/contact-sales/contact-sales-action.server.ts` |
+| `invite_link_accepted` | User accepted an organization invite link and joined the organization | `app/routes/_user-authentication+/_anonymous-routes+/auth.callback.ts` |
+
+## Next Steps
+
+### Create Your Dashboard
+
+Visit your PostHog project and create a new dashboard called **"Analytics Basics"** with these recommended insights:
+
+1. **Signup to Subscription Funnel**
+ - Funnel: `user_signed_up` → `organization_created` → `pricing_page_viewed` → `checkout_session_started` → `checkout_completed`
+
+2. **Weekly Active Users**
+ - Trend: Unique users with any event, grouped by week
+
+3. **Subscription Conversion Rate**
+ - Funnel: `pricing_page_viewed` → `checkout_completed`
+
+4. **Churn Analysis**
+ - Trend: `subscription_cancelled` and `subscription_deleted` events over time
+
+5. **Enterprise Lead Pipeline**
+ - Trend: `contact_sales_form_submitted` events over time
+
+### PostHog Dashboard Links
+
+- **PostHog Project**: https://us.i.posthog.com
+- **Create New Dashboard**: https://us.i.posthog.com/dashboard/new
+- **Event Definitions**: https://us.i.posthog.com/data-management/events
+
+### Agent Skill
+
+We've left an agent skill folder in your project at `.claude/skills/react-react-router-7-framework/`. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog.
+
+## Files Modified
+
+- `app/entry.client.tsx` - PostHog client initialization
+- `app/root.tsx` - Middleware registration and error boundary
+- `app/lib/posthog-middleware.server.ts` - Server-side PostHog middleware (new file)
+- `vite.config.ts` - SSR configuration for PostHog packages
+- `.env` - PostHog environment variables
+- `.env.example` - PostHog environment variable documentation
+- `app/routes/pricing.tsx` - Pricing page view tracking
+- `app/routes/_user-authentication+/_anonymous-routes+/auth.callback.ts` - Auth events
+- `app/features/onboarding/organization/onboarding-organization-action.server.ts` - Onboarding events
+- `app/features/organizations/create-organization/create-organization-action.server.ts` - Org creation events
+- `app/features/billing/billing-action.server.ts` - Billing action events
+- `app/features/billing/contact-sales/contact-sales-action.server.ts` - Contact sales events
+- `app/features/billing/stripe-event-handlers.server.ts` - Stripe webhook events
diff --git a/apps/react-router/saas-template/vite.config.ts b/apps/react-router/saas-template/vite.config.ts
index 711d1885..30fb6dfe 100644
--- a/apps/react-router/saas-template/vite.config.ts
+++ b/apps/react-router/saas-template/vite.config.ts
@@ -48,6 +48,9 @@ const rootConfig = defineConfig({
sudoFilesPlugin,
],
server: { port: 3000 },
+ ssr: {
+ noExternal: ["posthog-js", "@posthog/react"],
+ },
});
const testConfig = defineVitestConfig({