OneApp Docs
Guides

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

RecipePackages UsedUse Case
Recipe 1: SaaS Applicationauth, analytics, feature-flags, observabilityComplete SaaS app with authentication, tracking, and monitoring
Recipe 2: Enterprise Authauth, analytics, observabilityOrganization management, team roles, RBAC
Recipe 3: AI Chatbotai, auth, analytics, observability, db-prismaAI chatbot with safety, tracking, cost monitoring
Recipe 4: Analytics Workflowanalytics, feature-flags, observabilityFeature flag rollouts with analytics tracking
Recipe 5: Notification Systemdb-prisma, email, analytics, observabilityReal-time notifications with tracking and monitoring

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

Contributing recipes

Have a great recipe combining multiple packages? Submit a PR to add it to this cookbook!

Follow the recipe structure:

  1. Title — Clear, specific feature name
  2. What you'll build — Bulleted list of features
  3. Architecture — Simple text diagram showing package flow
  4. Implementation — Numbered steps with complete code
  5. "That's it!" — Confirmation at end of each recipe

All recipes must include proper error handling, analytics tracking, and monitoring.

On this page