-
{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/utils/env.server.ts b/apps/react-router/saas-template/app/utils/env.server.ts
index cf9dda2d..a2bcd2c8 100644
--- a/apps/react-router/saas-template/app/utils/env.server.ts
+++ b/apps/react-router/saas-template/app/utils/env.server.ts
@@ -16,6 +16,8 @@ const schema = z.object({
STRIPE_WEBHOOK_SECRET: z.string(),
SUPABASE_PROJECT_ID: z.string(),
SUPABASE_SERVICE_ROLE_KEY: z.string(),
+ VITE_PUBLIC_POSTHOG_HOST: z.url(),
+ VITE_PUBLIC_POSTHOG_KEY: z.string(),
VITE_SUPABASE_ANON_KEY: z.string(),
VITE_SUPABASE_URL: z.url(),
});
diff --git a/apps/react-router/saas-template/package-lock.json b/apps/react-router/saas-template/package-lock.json
index 323d88ee..2f42bfad 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.5.2",
"@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.325.0",
+ "posthog-node": "^5.21.0",
"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.4.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz",
+ "integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.4.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.4.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz",
+ "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==",
+ "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.38.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz",
+ "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==",
+ "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.9.1",
+ "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.9.1.tgz",
+ "integrity": "sha512-kRb1ch2dhQjsAapZmu6V66551IF2LnCbc1rnrQqnR7ArooVyJN9KOPXre16AJ3ObJz2eTfuP7x25BMyS2Y5Exw==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.6"
+ }
+ },
+ "node_modules/@posthog/react": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/@posthog/react/-/react-1.5.2.tgz",
+ "integrity": "sha512-KHdXbV1yba7Y2l8BVmwXlySWxqKVLNQ5ZiVvWOf7r3Eo7GIFxCM4CaNK/z83kKWn8KTskmKy7AGF6Hl6INWK3g==",
+ "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.325.0",
+ "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.325.0.tgz",
+ "integrity": "sha512-cBOZvVwpL8VHnNmkOVNxVsbwuf0/Fbxl32SjveQGapvkDojzFbpuj9wQPzrzF/wtCukkPjlPEDKAG/f1c8oPVQ==",
+ "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.47.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz",
+ "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==",
+ "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,49 @@
"node": ">=0.10.0"
}
},
+ "node_modules/posthog-js": {
+ "version": "1.325.0",
+ "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.325.0.tgz",
+ "integrity": "sha512-u4A/QPtI742X4lnXmdr9fl00jwg22mUIs3SrbHg7i8Ju5uBa4CThV/2n+4XSXyre3LygOASshFxhzT3UMpcQPQ==",
+ "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.9.1",
+ "@posthog/types": "1.325.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.0",
+ "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.21.0.tgz",
+ "integrity": "sha512-M7v/+Zyz/z3ZDC4u896K2Lb/pLbPA1Czo6Tp/WeQ1vuBsJtJajqWO3vRev3BHFTP92nao5YCrU0aIM+Flwbv1A==",
+ "license": "MIT",
+ "dependencies": {
+ "@posthog/core": "1.9.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "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 +12351,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 +12420,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 +13362,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 +13374,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 +13549,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 +15092,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 +15112,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..a02dd955 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.5.2",
"@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.325.0",
+ "posthog-node": "^5.21.0",
"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..2343381c
--- /dev/null
+++ b/apps/react-router/saas-template/posthog-setup-report.md
@@ -0,0 +1,86 @@
+# PostHog Post-Wizard Report
+
+The wizard has completed a deep integration of PostHog into your React Router 7 Framework application. This integration includes:
+
+- **Client-side SDK initialization** in `entry.client.tsx` with the PostHogProvider wrapper
+- **Server-side middleware** for capturing events with user/session context correlation
+- **Error boundary integration** for automatic exception capture
+- **Event tracking** across 13 key business events including user authentication, billing, and organization management
+
+## Configuration
+
+Environment variables have been configured in `.env`:
+- `VITE_PUBLIC_POSTHOG_KEY` - Your PostHog project API key
+- `VITE_PUBLIC_POSTHOG_HOST` - PostHog host URL (https://us.i.posthog.com)
+
+## Events Implemented
+
+| Event Name | Description | File |
+|------------|-------------|------|
+| `user_signed_up` | User completed registration (email OTP or Google OAuth) | `app/features/user-authentication/registration/register-action.server.ts` |
+| `user_logged_in` | User successfully logged in | `app/features/user-authentication/login/login-action.server.ts` |
+| `user_logged_out` | User logged out of the application | `app/routes/_user-authentication+/logout.ts` |
+| `organization_created` | User created a new organization | `app/features/organizations/create-organization/create-organization-action.server.ts` |
+| `invite_link_accepted` | User accepted an organization invite link | `app/features/organizations/accept-invite-link/accept-invite-link-action.server.ts` |
+| `contact_sales_submitted` | User submitted the contact sales form | `app/features/billing/contact-sales/contact-sales-action.server.ts` |
+| `checkout_completed` | Stripe checkout session completed successfully | `app/features/billing/stripe-event-handlers.server.ts` |
+| `subscription_created` | New subscription was created via Stripe webhook | `app/features/billing/stripe-event-handlers.server.ts` |
+| `subscription_cancelled` | Subscription was cancelled (churn event) | `app/features/billing/stripe-event-handlers.server.ts` |
+| `subscription_updated` | Subscription was modified | `app/features/billing/stripe-event-handlers.server.ts` |
+| `user_account_updated` | User updated their account settings | `app/features/user-accounts/settings/account/account-settings-action.server.ts` |
+| `user_account_deleted` | User deleted their account (churn event) | `app/features/user-accounts/settings/account/account-settings-action.server.ts` |
+| `onboarding_user_completed` | User completed the user onboarding step | `app/features/onboarding/user-account/onboarding-user-account-action.server.ts` |
+
+## Files Modified/Created
+
+### New Files
+- `app/lib/posthog-middleware.server.ts` - Server-side PostHog middleware for session/user context
+
+### Modified Files
+- `app/entry.client.tsx` - PostHog client initialization and provider wrapper
+- `app/root.tsx` - Error boundary with exception capture, middleware registration
+- `vite.config.ts` - SSR configuration for PostHog packages
+- `app/utils/env.server.ts` - PostHog environment variable schema
+- `.env` - PostHog environment variables
+- `.env.example` - PostHog environment variable templates
+
+## Next Steps
+
+### Create Your Dashboard
+
+To get the most out of your PostHog integration, create a dashboard in PostHog with the following suggested insights:
+
+1. **Signup to Checkout Funnel** - Track conversion from `user_signed_up` → `checkout_completed`
+2. **User Retention** - Monitor `user_logged_in` events over time
+3. **Churn Analysis** - Track `subscription_cancelled` and `user_account_deleted` events
+4. **Organization Growth** - Monitor `organization_created` and `invite_link_accepted` events
+5. **Revenue Events** - Track `checkout_completed` with amount properties
+
+Visit your [PostHog Dashboard](https://us.i.posthog.com) to create insights based on these events.
+
+### Recommended Funnel Insights
+
+1. **Signup Funnel**: `user_signed_up` → `onboarding_user_completed` → `organization_created`
+2. **Conversion Funnel**: `user_signed_up` → `checkout_completed`
+3. **Engagement Funnel**: `user_logged_in` → `organization_created` → `invite_link_accepted`
+
+### 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.
+
+## Technical Details
+
+### Client-Side Tracking
+- Uses `posthog-js` and `@posthog/react` packages
+- Initialized with tracing headers for server correlation
+- Available via `usePostHog()` hook in any component
+
+### Server-Side Tracking
+- Uses `posthog-node` package
+- Middleware extracts session/distinct IDs from headers
+- Events are automatically correlated with client sessions
+- Proper shutdown handling for each request
+
+### Error Tracking
+- Automatic exception capture in the root ErrorBoundary
+- Uses `posthog.captureException()` for error reporting
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({