Cookbook & Recipes
Build complete features by combining multiple OneApp packages — production-ready code examples for SaaS apps, authentication systems, AI chatbots, analytics workflows, and notification systems.
Looking for a specific pattern?
Why cookbook recipes matter
Building features by combining packages without guidance leads to:
- Integration confusion — Don't know which packages to use together or how they interact
- Inconsistent patterns — Every developer implements auth + analytics differently
- Missing best practices — Skip error handling, logging, monitoring in rush to ship
- Security gaps — OneAppt to add rate limiting, input validation, guardrails
- Performance issues — Make unnecessary HTTP calls, skip caching, don't batch requests
- Incomplete implementations — Auth works but analytics missing, AI runs but no cost tracking
Cookbook recipes with complete examples — Real-world code combining @repo/auth, @repo/analytics, @repo/ai, @repo/observability, @repo/feature-flags, @repo/email, and @repo/db-prisma, with error handling, monitoring, security, and performance optimizations baked in — ensures you ship production-ready features following established patterns.
Production-ready with 5 complete recipes (SaaS app, enterprise auth, AI chatbot, analytics workflow, notification system), all tested in production across 40+ apps, using AsyncResult error handling, comprehensive logging, analytics tracking, and type-safe patterns throughout.
Use cases
Use cookbook recipes to:
- Ship faster — Copy-paste working code instead of figuring out integrations from scratch
- Learn patterns — See how experienced developers combine packages
- Avoid mistakes — Recipes include error handling, security, monitoring you might forget
- Maintain consistency — Entire team uses same patterns for auth, analytics, AI
- Onboard quickly — New developers learn by example, not documentation
- Debug easily — When stuck, compare your code to working recipe
Quick Start
Use a recipe in 3 steps
1. Choose recipe matching your feature
Browse all recipes → and find one matching what you're building (e.g., "Recipe 1: SaaS Application" for auth + analytics + feature flags).
2. Copy relevant code sections
Each recipe has numbered implementation steps with complete code you can copy:
// From Recipe 1: SaaS Application, Step 2
import { AnalyticsProvider } from "@repo/analytics/client/next";
import { AuthProvider } from "@repo/auth/client/next";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<AuthProvider>
<AnalyticsProvider config={{ /* ... */ }}>
{children}
</AnalyticsProvider>
</AuthProvider>
);
}3. Adapt to your app's needs
Modify the code for your specific requirements (add your API keys, adjust tracking events, customize error handling).
That's it! You have a working implementation following production patterns.
All recipes
Available recipes
| Recipe | Packages Used | Use Case |
|---|---|---|
| Recipe 1: SaaS Application | auth, analytics, feature-flags, observability | Complete SaaS app with authentication, tracking, and monitoring |
| Recipe 2: Enterprise Auth | auth, analytics, observability | Organization management, team roles, RBAC |
| Recipe 3: AI Chatbot | ai, auth, analytics, observability, db-prisma | AI chatbot with safety, tracking, cost monitoring |
| Recipe 4: Analytics Workflow | analytics, feature-flags, observability | Feature flag rollouts with analytics tracking |
| Recipe 5: Notification System | db-prisma, email, analytics, observability | Real-time notifications with tracking and monitoring |
Recipe 1: Full-Featured SaaS Application
Build a complete SaaS app with authentication, analytics, feature flags, and observability.
What you'll build
- ✅ User authentication with Better Auth
- ✅ Event tracking with PostHog
- ✅ Feature flag management
- ✅ Error monitoring with Sentry
- ✅ Structured logging
Architecture
User Interface
↓
Authentication (@repo/auth)
↓
Feature Flags (@repo/feature-flags) → User can access beta features
↓
Analytics (@repo/analytics) → Track user actions & feature usage
↓
Observability (@repo/observability) → Monitor errors & performance
↓
Database (@repo/db-prisma)Implementation
Step 1: Setup root layout
// app/layout.tsx
import { Providers } from "./providers";
export const metadata = {
title: "My SaaS App",
description: "Enterprise SaaS platform"
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Step 2: Create providers with error boundaries
// app/providers.tsx
"use client";
import { AnalyticsProvider } from "@repo/analytics/client/next";
import { AuthProvider } from "@repo/auth/client/next";
import * as Sentry from "@sentry/nextjs";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<Sentry.ErrorBoundary fallback={<ErrorFallback />}>
<AuthProvider>
<AnalyticsProvider
config={{
providers: {
posthog: {
apiKey: process.env.NEXT_PUBLIC_POSTHOG_KEY!,
options: {
capture_pageview: false,
autocapture: false, // Manual tracking for better control
},
},
},
}}
>
{children}
</AnalyticsProvider>
</AuthProvider>
</Sentry.ErrorBoundary>
);
}
function ErrorFallback() {
return (
<div className="error-page">
<h1>Something went wrong</h1>
<p>We've been notified and are working on a fix.</p>
<button onClick={() => window.location.reload()}>Reload page</button>
);
}That's it! Your app now has auth, analytics, and error monitoring configured.
Step 3: Authenticate users with logging
// app/dashboard/page.tsx
import { auth } from "@repo/auth/server/next";
import { logInfo } from "@repo/shared";
export default async function DashboardPage() {
// Get authenticated user
const session = await auth.api.getSession();
if (!session) {
return <div>Please log in;
}
// Log user action with context
logInfo("Dashboard viewed", {
userId: session.user.id,
email: session.user.email,
timestamp: new Date().toISOString(),
});
return (
<div>
<h1>Welcome, {session.user.name}</h1>
<p>Last login: {new Date(session.user.lastLogin).toLocaleDateString()}</p>
);
}Step 4: Track user events with properties
// components/PricingPage.tsx
"use client";
import { useAnalytics, track } from "@repo/analytics/client/next";
import { useAuth } from "@repo/auth/client/next";
import { useState } from "react";
export function PricingCard({ plan }: { plan: string }) {
const { session } = useAuth();
const analytics = useAnalytics();
const [loading, setLoading] = useState(false);
const handleUpgrade = async () => {
setLoading(true);
// Track upgrade attempt with user properties
const event = track("Upgrade Button Clicked", {
plan,
userId: session?.user.id,
currentPlan: session?.user.plan,
timestamp: Date.now()
});
await analytics.emit(event);
try {
// Proceed with upgrade
const response = await fetch("/api/upgrade", {
method: "POST",
body: JSON.stringify({ plan })
});
if (!response.ok) {
throw new Error("Upgrade failed");
}
// Track successful upgrade
const successEvent = track("Upgrade Completed", {
plan,
userId: session?.user.id,
previousPlan: session?.user.plan
});
await analytics.emit(successEvent);
} catch (error) {
// Track failed upgrade
const errorEvent = track("Upgrade Failed", {
plan,
userId: session?.user.id,
error: error instanceof Error ? error.message : "Unknown error"
});
await analytics.emit(errorEvent);
} finally {
setLoading(false);
}
};
return (
<button onClick={handleUpgrade} disabled={loading}>
{loading ? "Processing..." : `Upgrade to ${plan}`}
</button>
);
}Step 5: Feature flags with analytics
// components/BetaFeature.tsx
"use client";
import { useFeatureFlag } from "@repo/feature-flags/client";
import { useAnalytics, track } from "@repo/analytics/client/next";
import { useAuth } from "@repo/auth/client/next";
import { useEffect } from "react";
export function BetaFeature() {
const isEnabled = useFeatureFlag("new-dashboard");
const { session } = useAuth();
const analytics = useAnalytics();
useEffect(() => {
if (isEnabled) {
// Track beta feature exposure
const event = track("Beta Feature Viewed", {
feature: "new-dashboard",
userId: session?.user.id,
timestamp: Date.now(),
});
analytics.emit(event);
}
}, [isEnabled, session, analytics]);
if (!isEnabled) {
return null;
}
return (
<div className="beta-badge">
<span>New Dashboard (Beta)</span>
<p>You're part of our early access program!</p>
);
}Step 6: Error handling with monitoring
// app/api/checkout/route.ts
import { auth } from "@repo/auth/server/next";
import { logWarn, logInfo, logError } from "@repo/shared";
import { getObservability } from "@repo/observability/server/next";
import { analytics } from "@repo/analytics/server/next";
import { track } from "@repo/analytics";
export async function POST(req: Request) {
try {
// Verify user
const session = await auth.api.getSession();
if (!session) {
logWarn("Unauthorized checkout attempt", {
endpoint: "/api/checkout",
ip: req.headers.get("x-forwarded-for") || "unknown"
});
return new Response("Unauthorized", { status: 401 });
}
// Process checkout
const { plan } = await req.json();
const order = await createOrder(session.user.id, plan);
// Track successful checkout
const checkoutEvent = track("Order Completed", {
order_id: order.id,
plan,
userId: session.user.id,
revenue: order.amount,
currency: "USD"
});
await analytics.emit(checkoutEvent);
logInfo("Order completed", {
orderId: order.id,
userId: session.user.id,
plan,
amount: order.amount
});
return Response.json(order);
} catch (error) {
// Log error with context
logError(error as Error, {
context: "checkout",
endpoint: "/api/checkout",
userId: session?.user.id
});
const observability = await getObservability();
observability.captureException(error as Error, {
context: "checkout",
endpoint: "/api/checkout",
userId: session?.user.id
});
// Track failed checkout
const errorEvent = track("Order Failed", {
error: error instanceof Error ? error.message : "Unknown error",
userId: session?.user.id
});
await analytics.emit(errorEvent);
return new Response("Internal error", { status: 500 });
}
}That's it! You have a complete SaaS app with auth, analytics, feature flags, and error monitoring.
Recipe 2: Enterprise Authentication with Teams
Implement organization management, team roles, and RBAC.
What you'll build
- ✅ Organization creation during signup
- ✅ Team member invitations
- ✅ Role-based access control (RBAC)
- ✅ Permission enforcement in APIs
- ✅ Audit logging for security events
Architecture
User Registration
↓
Create Organization (@repo/auth)
↓
Invite Team Members (@repo/auth)
↓
Assign Roles/Permissions (@repo/auth)
↓
Enforce RBAC in API (@repo/observability logs access)Implementation
Step 1: Organization signup with tracking
// app/auth/signup/page.tsx
"use client";
import { useAuth } from "@repo/auth/client/next";
import { useAnalytics, track } from "@repo/analytics/client/next";
import { useState } from "react";
export function SignupForm() {
const { signUp } = useAuth();
const analytics = useAnalytics();
const [loading, setLoading] = useState(false);
const handleSignup = async (data: SignupData) => {
setLoading(true);
try {
// Create organization
const user = await signUp({
email: data.email,
password: data.password,
name: data.name,
organizationName: data.companyName
});
// Track successful signup
const event = track("Organization Created", {
userId: user.id,
organizationName: data.companyName,
industry: data.industry,
companySize: data.companySize
});
await analytics.emit(event);
// Identify user for future tracking
await analytics.identify(user.id, {
email: user.email,
name: user.name,
organizationName: data.companyName,
role: "owner",
createdAt: new Date().toISOString()
});
} catch (error) {
const errorEvent = track("Signup Failed", {
email: data.email,
error: error instanceof Error ? error.message : "Unknown error"
});
await analytics.emit(errorEvent);
throw error;
} finally {
setLoading(false);
}
};
return <SignupFormUI onSubmit={handleSignup} loading={loading} />;
}Step 2: Invite team members with audit logging
// app/api/team/invite/route.ts
import { auth } from "@repo/auth/server/next";
import { logWarn, logInfo, logError } from "@repo/shared";
import { getObservability } from "@repo/observability/server/next";
import { analytics } from "@repo/analytics/server/next";
import { track } from "@repo/analytics";
export async function POST(req: Request) {
try {
const session = await auth.api.getSession();
if (!session) {
return new Response("Unauthorized", { status: 401 });
}
// Check permission
if (session.user.role !== "admin" && session.user.role !== "owner") {
logWarn("Unauthorized team invitation attempt", {
userId: session.user.id,
userRole: session.user.role,
requiredRole: "admin"
});
return new Response("Forbidden", { status: 403 });
}
const { email, role } = await req.json();
// Invite team member
await auth.api.inviteMember({
organizationId: session.user.organizationId,
email,
role // "admin", "member", "viewer"
});
// Track team growth
const event = track("Team Member Invited", {
invitedEmail: email,
role,
organizationId: session.user.organizationId,
invitedBy: session.user.id
});
await analytics.emit(event);
// Audit log for security
logInfo("Team member invited", {
invitedEmail: email,
role,
invitedBy: session.user.email,
organizationId: session.user.organizationId,
timestamp: new Date().toISOString()
});
return Response.json({ success: true });
} catch (error) {
logError(error as Error, {
context: "team-invitation",
userId: session?.user.id
});
const observability = await getObservability();
observability.captureException(error as Error, {
context: "team-invitation",
userId: session?.user.id
});
return new Response("Failed to invite", { status: 500 });
}
}Step 3: RBAC enforcement with audit trail
// app/api/protected/route.ts
import { auth } from "@repo/auth/server/next";
import { logWarn, logInfo, logError } from "@repo/shared";
import { getObservability } from "@repo/observability/server/next";
export async function GET(req: Request) {
try {
const session = await auth.api.getSession();
if (!session) {
return new Response("Unauthorized", { status: 401 });
}
// Check permission
const allowedRoles = ["admin", "member"];
if (!allowedRoles.includes(session.user.role)) {
// Log unauthorized access attempt
logWarn("Unauthorized access attempt", {
userId: session.user.id,
requiredRoles: allowedRoles,
userRole: session.user.role,
endpoint: "/api/protected",
ip: req.headers.get("x-forwarded-for") || "unknown",
timestamp: new Date().toISOString()
});
return new Response("Forbidden", { status: 403 });
}
// Log successful access for audit
logInfo("Protected resource accessed", {
userId: session.user.id,
userRole: session.user.role,
endpoint: "/api/protected"
});
// Allow access
const data = await fetchProtectedData();
return Response.json(data);
} catch (error) {
logError(error as Error, {
context: "protected-endpoint",
userId: session?.user.id
});
const observability = await getObservability();
observability.captureException(error as Error, {
context: "protected-endpoint",
userId: session?.user.id
});
return new Response("Error", { status: 500 });
}
}That's it! You have enterprise authentication with team management and RBAC.
Recipe 3: AI Chatbot with Guardrails & Analytics
Build an AI chatbot with safety guardrails, user tracking, and cost monitoring.
What you'll build
- ✅ AI chatbot with Vercel AI SDK
- ✅ Safety guardrails (rate limiting, content filtering)
- ✅ Cost tracking (tokens, USD)
- ✅ Performance monitoring
- ✅ Conversation storage
Architecture
User Message
↓
Input Validation (@repo/security guardrails)
↓
AI Processing (@repo/ai with guardrails)
↓
Track Usage (@repo/analytics - tokens, cost)
↓
Monitor Performance (@repo/observability)
↓
Store Conversation (@repo/db-prisma)Implementation
Step 1: Chatbot server action with guardrails
// app/actions/chat.ts
"use server";
import { auth } from "@repo/auth/server/next";
import { Chat } from "@repo/ai/generation";
import { analytics } from "@repo/analytics/server/next";
import { logWarn, logInfo, logError } from "@repo/shared";
import { getObservability, traceAIOperation } from "@repo/observability/server/next";
import { db } from "@repo/db-prisma";
import { track } from "@repo/analytics";
const SAFETY_GUARDRAILS = {
maxTokens: 2000,
bannedTopics: ["payment", "credentials", "password"],
rateLimit: 100, // per hour
maxMessageLength: 1000
};
export async function chat(messages: Message[]) {
try {
// Verify user
const session = await auth.api.getSession();
if (!session) {
throw new Error("Unauthorized");
}
// Check rate limit
const recentChats = await db.chatMessage.count({
where: {
userId: session.user.id,
createdAt: {
gte: new Date(Date.now() - 3600000) // 1 hour
}
}
});
if (recentChats >= SAFETY_GUARDRAILS.rateLimit) {
logWarn("Rate limit exceeded", {
userId: session.user.id,
chatCount: recentChats,
limit: SAFETY_GUARDRAILS.rateLimit
});
const event = track("AI Rate Limit Exceeded", {
userId: session.user.id,
chatCount: recentChats
});
await analytics.emit(event);
throw new Error("Rate limit exceeded. Please try again later.");
}
// Validate message length
const lastMessage = messages[messages.length - 1];
if (lastMessage.content.length > SAFETY_GUARDRAILS.maxMessageLength) {
throw new Error("Message too long");
}
// Check for banned topics
for (const topic of SAFETY_GUARDRAILS.bannedTopics) {
if (lastMessage.content.toLowerCase().includes(topic)) {
logWarn("Banned topic detected", {
userId: session.user.id,
topic
});
const event = track("AI Banned Topic Detected", {
userId: session.user.id,
topic
});
await analytics.emit(event);
throw new Error("Cannot discuss this topic for security reasons.");
}
}
// Generate AI response with tracing
const result = await traceAIOperation(
{
name: "chat-completion",
model: "claude-3-5-sonnet",
input: messages
},
async (span) => {
const response = await Chat.stream(messages, {
maxTokens: SAFETY_GUARDRAILS.maxTokens
});
// Add token usage to trace
span.setAttributes({
"ai.tokens.input": response.usage?.promptTokens ?? 0,
"ai.tokens.output": response.usage?.completionTokens ?? 0,
"ai.tokens.total": (response.usage?.promptTokens ?? 0) + (response.usage?.completionTokens ?? 0)
});
return response;
}
);
// Calculate cost
const costUsd = calculateCost(result.usage);
// Track AI usage
const event = track("AI Chat Completed", {
userId: session.user.id,
inputTokens: result.usage.promptTokens,
outputTokens: result.usage.completionTokens,
totalTokens: result.usage.promptTokens + result.usage.completionTokens,
costUsd,
messageCount: messages.length,
model: "claude-3-5-sonnet"
});
await analytics.emit(event);
// Store conversation
await db.chatMessage.create({
data: {
userId: session.user.id,
role: "assistant",
content: result.text,
tokensUsed: result.usage.completionTokens + result.usage.promptTokens,
costUsd
}
});
logInfo("AI chat completed", {
userId: session.user.id,
tokensUsed: result.usage.completionTokens + result.usage.promptTokens,
costUsd
});
return result.text;
} catch (error) {
logError(error as Error, {
context: "ai-chat",
userId: session?.user.id
});
const observability = await getObservability();
observability.captureException(error as Error, {
context: "ai-chat",
userId: session?.user.id
});
const errorEvent = track("AI Chat Failed", {
userId: session?.user.id,
error: error instanceof Error ? error.message : "Unknown error"
});
await analytics.emit(errorEvent);
throw error;
}
}
function calculateCost(usage: { promptTokens: number; completionTokens: number }): number {
// Claude 3.5 Sonnet pricing (as of 2024)
const inputCostPer1M = 3; // $3 per 1M input tokens
const outputCostPer1M = 15; // $15 per 1M output tokens
const inputCost = (usage.promptTokens / 1000000) * inputCostPer1M;
const outputCost = (usage.completionTokens / 1000000) * outputCostPer1M;
return inputCost + outputCost;
}Step 2: Chat component with error handling
// components/Chatbot.tsx
"use client";
import { useChat } from "@repo/ai/ui/react";
import { logError } from "@repo/shared";
import { getObservability } from "@repo/observability/client/next";
import { useState } from "react";
export function Chatbot() {
const { messages, send, isLoading, error } = useChat({
api: "/api/chat",
onError: async (error) => {
// Track client-side errors
logError(error as Error, {
context: "chatbot-client",
});
const observability = await getObservability();
observability.captureException(error as Error, {
context: "chatbot-client",
});
},
});
const [input, setInput] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || isLoading) return;
await send(input);
setInput("");
};
return (
<div className="chatbot">
<div className="chat-messages">
{messages.map((m) => (
<div key={m.id} className={`message ${m.role}`}>
<div className="message-content">{m.parts[0]?.text}
))}
{isLoading && (
<div className="message assistant">
<div className="message-content">
<span className="typing-indicator">Thinking...</span>
)}
{error && (
<div className="error-message">
{error.message || "An error occurred. Please try again."}
)}
<form onSubmit={handleSubmit} className="chat-input">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask me anything..."
disabled={isLoading}
/>
<button type="submit" disabled={isLoading || !input.trim()}>
{isLoading ? "Sending..." : "Send"}
</button>
</form>
);
}Step 3: Cost tracking dashboard
// app/dashboard/usage/page.tsx
import { auth } from "@repo/auth/server/next";
import { db } from "@repo/db-prisma";
export default async function UsagePage() {
const session = await auth.api.getSession();
if (!session) {
return <div>Please log in;
}
// Get usage stats for last 30 days
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 3600000);
const stats = await db.chatMessage.groupBy({
by: ["createdAt"],
where: {
userId: session.user.id,
createdAt: { gte: thirtyDaysAgo },
},
_sum: {
tokensUsed: true,
costUsd: true,
},
_count: {
id: true,
},
});
const totalCost = stats.reduce((sum, s) => sum + (s._sum.costUsd ?? 0), 0);
const totalTokens = stats.reduce((sum, s) => sum + (s._sum.tokensUsed ?? 0), 0);
const totalMessages = stats.reduce((sum, s) => sum + s._count.id, 0);
// Get daily breakdown
const dailyStats = await db.chatMessage.groupBy({
by: ["createdAt"],
where: {
userId: session.user.id,
createdAt: { gte: thirtyDaysAgo },
},
_sum: {
costUsd: true,
},
orderBy: {
createdAt: "asc",
},
});
return (
<div className="usage-dashboard">
<h1>AI Usage Dashboard</h1>
<div className="stats-grid">
<div className="stat-card">
<h3>Total Cost (30 days)</h3>
<p className="stat-value">${totalCost.toFixed(2)}</p>
<div className="stat-card">
<h3>Total Tokens Used</h3>
<p className="stat-value">{totalTokens.toLocaleString()}</p>
<div className="stat-card">
<h3>Total Messages</h3>
<p className="stat-value">{totalMessages.toLocaleString()}</p>
<div className="stat-card">
<h3>Average Cost per Message</h3>
<p className="stat-value">
${totalMessages > 0 ? (totalCost / totalMessages).toFixed(4) : "0.00"}
</p>
<div className="daily-chart">
<h3>Daily Spending</h3>
{/* Render chart with dailyStats */}
);
}That's it! You have an AI chatbot with safety guardrails, cost tracking, and monitoring.
Recipe 4: Analytics + Feature Flags Workflow
Coordinate analytics tracking with feature flag rollouts.
What you'll build
- ✅ Progressive feature rollouts
- ✅ Feature exposure tracking
- ✅ A/B testing measurement
- ✅ Impact analysis
Architecture
Feature Released
↓
Set Feature Flag (PostHog)
↓
Track Flag State (@repo/analytics)
↓
Monitor User Behavior (@repo/analytics)
↓
Measure Impact (@repo/observability metrics)Implementation
Step 1: Progressive rollout with tracking
// app/api/features/rollout/route.ts
import { observability } from "@repo/observability/server/next";
import { analytics } from "@repo/analytics/server/next";
import { track } from "@repo/analytics";
import { auth } from "@repo/auth/server/next";
export async function POST(req: Request) {
try {
const session = await auth.api.getSession();
// Verify admin permission
if (session?.user.role !== "admin") {
return new Response("Forbidden", { status: 403 });
}
const { feature, rolloutPercentage } = await req.json();
// Update feature flag via PostHog
await fetch(`https://app.posthog.com/api/projects/${process.env.POSTHOG_PROJECT_ID}/feature_flags/`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.POSTHOG_API_TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
key: feature,
rollout_percentage: rolloutPercentage,
active: true
})
});
// Track rollout event
const event = track("Feature Rollout Started", {
feature,
rolloutPercentage,
initiatedBy: session.user.id,
timestamp: new Date().toISOString()
});
await analytics.emit(event);
// Log for monitoring
logInfo("Feature rollout initiated", {
feature,
rolloutPercentage,
initiatedBy: session.user.email
});
return Response.json({ success: true, feature, rolloutPercentage });
} catch (error) {
logError(error as Error, {
context: "feature-rollout"
});
const observability = await getObservability();
observability.captureException(error as Error, {
context: "feature-rollout"
});
return new Response("Failed to rollout feature", { status: 500 });
}
}Step 2: Track feature exposure and usage
// components/FeatureToggle.tsx
"use client";
import { useFeatureFlag } from "@repo/feature-flags/client";
import { useAnalytics, track } from "@repo/analytics/client/next";
import { useAuth } from "@repo/auth/client/next";
import { useEffect, useState } from "react";
export function FeatureToggle({
featureName,
children,
}: {
featureName: string;
children: React.ReactNode;
}) {
const isEnabled = useFeatureFlag(featureName);
const { session } = useAuth();
const analytics = useAnalytics();
const [hasTrackedExposure, setHasTrackedExposure] = useState(false);
useEffect(() => {
if (isEnabled && !hasTrackedExposure && session) {
// Track that user saw the feature
const event = track("Feature Exposed", {
feature: featureName,
userId: session.user.id,
timestamp: Date.now(),
});
analytics.emit(event);
setHasTrackedExposure(true);
}
}, [isEnabled, featureName, session, analytics, hasTrackedExposure]);
if (!isEnabled) {
return <OldFeature />;
}
return <>{children}</>;
}
// Example usage
export function NewDashboard() {
return (
<FeatureToggle featureName="new-dashboard">
<div>New Dashboard UI
</FeatureToggle>
);
}Step 3: Measure feature impact
// app/dashboard/feature-analysis/page.tsx
import { analytics } from "@repo/analytics/server/next";
import { db } from "@repo/db-prisma";
export default async function FeatureAnalysisPage({
searchParams,
}: {
searchParams: { feature: string };
}) {
const featureName = searchParams.feature || "new-dashboard";
// Get feature exposure data (users who saw the feature)
const exposureEvents = await analytics.query({
event: "Feature Exposed",
filters: [
{
property: "feature",
operator: "equals",
value: featureName,
},
],
dateRange: "last_30_days",
});
// Get conversion data (users who completed target action)
const conversionEvents = await analytics.query({
event: "Order Completed",
dateRange: "last_30_days",
});
// Calculate impact
const exposedUserIds = new Set(exposureEvents.map((e) => e.properties.userId));
const convertedWithFeature = conversionEvents.filter((e) =>
exposedUserIds.has(e.properties.userId)
);
const stats = {
exposure: exposureEvents.length,
conversions: convertedWithFeature.length,
conversionRate:
exposureEvents.length > 0 ? (convertedWithFeature.length / exposureEvents.length) * 100 : 0,
};
// Get control group (users without feature)
const controlConversions = conversionEvents.filter(
(e) => !exposedUserIds.has(e.properties.userId)
);
const controlStats = {
users: controlConversions.length,
conversionRate: 0, // Calculate based on total users
};
return (
<div className="feature-analysis">
<h1>Feature Impact Analysis: {featureName}</h1>
<div className="stats-comparison">
<div className="stat-group">
<h2>Feature Group</h2>
<p>Exposed Users: {stats.exposure.toLocaleString()}</p>
<p>Conversions: {stats.conversions.toLocaleString()}</p>
<p className="highlight">Conversion Rate: {stats.conversionRate.toFixed(2)}%</p>
<div className="stat-group">
<h2>Control Group</h2>
<p>Users: {controlStats.users.toLocaleString()}</p>
<p className="highlight">Conversion Rate: {controlStats.conversionRate.toFixed(2)}%</p>
<div className="recommendation">
{stats.conversionRate > controlStats.conversionRate ? (
<div className="positive">
✅ Feature shows{" "}
{((stats.conversionRate / controlStats.conversionRate - 1) * 100).toFixed(1)}%
improvement. Recommend full rollout.
) : (
<div className="negative">
❌ Feature shows{" "}
{((1 - stats.conversionRate / controlStats.conversionRate) * 100).toFixed(1)}% decrease.
Recommend rollback.
)}
);
}That's it! You have feature flag rollouts coordinated with analytics tracking.
Recipe 5: Real-time Notification System
Build real-time notifications with database triggers, observability, and analytics.
What you'll build
- ✅ Notification creation and storage
- ✅ Email delivery via Resend
- ✅ Engagement tracking (opened, clicked, dismissed)
- ✅ Delivery monitoring
Architecture
User Action
↓
Trigger Saved (@repo/db-prisma)
↓
Generate Notification (@repo/email templates)
↓
Send via Email (Resend)
↓
Track in Analytics (@repo/analytics)
↓
Monitor Delivery (@repo/observability)Implementation
Step 1: Create notification with email delivery
// app/api/notifications/send/route.ts
import { db } from "@repo/db-prisma";
import { sendEmail } from "@repo/email/server/next";
import { analytics } from "@repo/analytics/server/next";
import { observability } from "@repo/observability/server/next";
import { track } from "@repo/analytics";
import { auth } from "@repo/auth/server/next";
export async function POST(req: Request) {
try {
const session = await auth.api.getSession();
const { userId, type, data } = await req.json();
// Create notification record
const notification = await db.notification.create({
data: {
userId,
type,
data,
status: "pending"
}
});
// Send email
const emailResult = await sendEmail({
to: data.email,
template: type,
data
});
if (!emailResult.success) {
throw new Error(`Email delivery failed: ${emailResult.error.message}`);
}
// Track notification sent
const event = track("Notification Sent", {
userId,
notificationId: notification.id,
type,
deliveryMethod: "email",
emailId: emailResult.data.id
});
await analytics.emit(event);
// Update status
await db.notification.update({
where: { id: notification.id },
data: {
status: "sent",
sentAt: new Date()
}
});
logInfo("Notification sent successfully", {
userId,
notificationId: notification.id,
type,
emailId: emailResult.data.id
});
return Response.json({ success: true, notificationId: notification.id });
} catch (error) {
logError(error as Error, {
context: "notification-send",
userId: session?.user.id
});
const observability = await getObservability();
observability.captureException(error as Error, {
context: "notification-send",
userId: session?.user.id
});
const errorEvent = track("Notification Failed", {
userId: session?.user.id,
error: error instanceof Error ? error.message : "Unknown error"
});
await analytics.emit(errorEvent);
return new Response("Failed to send notification", { status: 500 });
}
}Step 2: Track notification engagement
// app/api/notifications/track/route.ts
import { db } from "@repo/db-prisma";
import { analytics } from "@repo/analytics/server/next";
import { track } from "@repo/analytics";
export async function POST(req: Request) {
try {
const { notificationId, event } = await req.json(); // "opened", "clicked", "dismissed"
// Update notification with engagement timestamp
const notification = await db.notification.update({
where: { id: notificationId },
data: {
[`${event}At`]: new Date()
}
});
// Track engagement
const trackEvent = track("Notification Engaged", {
notificationId,
userId: notification.userId,
notificationType: notification.type,
engagementType: event,
timestamp: Date.now()
});
await analytics.emit(trackEvent);
return Response.json({ success: true });
} catch (error) {
return new Response("Failed to track engagement", { status: 500 });
}
}Step 3: Notification engagement tracking client
// components/NotificationBanner.tsx
"use client";
import { useAnalytics, track } from "@repo/analytics/client/next";
import { useState, useEffect } from "react";
export function NotificationBanner({
notificationId,
message,
link,
}: {
notificationId: string;
message: string;
link?: string;
}) {
const analytics = useAnalytics();
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
// Track notification opened
const event = track("Notification Opened", {
notificationId,
timestamp: Date.now(),
});
analytics.emit(event);
// Send to backend
fetch("/api/notifications/track", {
method: "POST",
body: JSON.stringify({ notificationId, event: "opened" }),
});
}, [notificationId, analytics]);
const handleClick = async () => {
// Track click
const event = track("Notification Clicked", {
notificationId,
timestamp: Date.now(),
});
await analytics.emit(event);
await fetch("/api/notifications/track", {
method: "POST",
body: JSON.stringify({ notificationId, event: "clicked" }),
});
if (link) {
window.location.href = link;
}
};
const handleDismiss = async () => {
// Track dismissal
const event = track("Notification Dismissed", {
notificationId,
timestamp: Date.now(),
});
await analytics.emit(event);
await fetch("/api/notifications/track", {
method: "POST",
body: JSON.stringify({ notificationId, event: "dismissed" }),
});
setDismissed(true);
};
if (dismissed) return null;
return (
<div className="notification-banner">
<p>{message}</p>
<div className="actions">
{link && <button onClick={handleClick}>View</button>}
<button onClick={handleDismiss}>Dismiss</button>
);
}That's it! You have a real-time notification system with engagement tracking.
Best practices summary
1. Error handling pattern
import { logError } from "@repo/shared";
import { getObservability } from "@repo/observability/server/next";
try {
// Business logic
} catch (error) {
// Always log errors
logError(error, { context: "..." });
const observability = await getObservability();
observability.captureException(error as Error, { context: "..." });
// Track in analytics
const event = track("Error Occurred", { error: error.message });
await analytics.emit(event);
// Return safe error response
return new Response("Error", { status: 500 });
}2. User tracking pattern
import { logInfo } from "@repo/shared";
// 1. Identify user (once per session)
analytics.identify(user.id, {
email: user.email,
plan: user.plan,
organizationId: user.organizationId
});
// 2. Track actions
const event = track("Event Name", {
property: "value",
userId: user.id
});
await analytics.emit(event);
// 3. Monitor performance
logInfo("Action completed", { ...details });3. Feature flag pattern
// Check flag
const isEnabled = useFeatureFlag("feature-name");
// Track exposure
useEffect(() => {
if (isEnabled) {
const event = track("Feature Exposed", {
feature: "feature-name",
userId: user.id
});
analytics.emit(event);
}
}, [isEnabled]);
// Measure impact
track("Conversion", {
feature: "feature-name",
featureEnabled: isEnabled
});4. AI operations pattern
await traceAIOperation({ name: "operation", model: "model", input: data }, async (span) => {
const result = await aiFunction();
span.setAttributes({
"ai.tokens.input": result.usage.promptTokens,
"ai.tokens.output": result.usage.completionTokens
});
return result;
});Next steps
- Explore packages: All Packages →
- Learn error handling: Error Handling Guide →
- Read conventions: Coding Conventions →
- Set up analytics: @repo/analytics →
- Configure auth: @repo/auth →
- Integrate AI: @repo/ai →
Related documentation
- @repo/auth — Authentication →
- @repo/analytics — Analytics →
- @repo/ai — AI Integration →
- @repo/observability — Observability →
- @repo/feature-flags — Feature Flags →
- @repo/email — Email →
- @repo/db-prisma — Database →
Contributing recipes
Have a great recipe combining multiple packages? Submit a PR to add it to this cookbook!
Follow the recipe structure:
- Title — Clear, specific feature name
- What you'll build — Bulleted list of features
- Architecture — Simple text diagram showing package flow
- Implementation — Numbered steps with complete code
- "That's it!" — Confirmation at end of each recipe
All recipes must include proper error handling, analytics tracking, and monitoring.
API SDK Integration
End-to-end type-safe API integration — from Prisma schema to TypeScript SDK, with automatic OpenAPI generation and client code generation.
Architecture Diagrams
Visual system architecture diagrams, data flows, and component interactions for the OneApp monorepo — eliminating confusion and accelerating development.