Documentation Index
Fetch the complete documentation index at: https://docs.fanfare.io/llms.txt
Use this file to discover all available pages before exploring further.
SSR Integration Guide
Learn how to integrate Fanfare into server-side rendered applications using Next.js, Remix, or Nuxt.
Overview
This guide covers integrating Fanfare into applications that use server-side rendering (SSR). The key challenge with SSR is that the SDK runs in the browser, but your pages may render on the server first.
What you’ll learn:
- Handling client/server rendering boundaries
- Hydration-safe SDK initialization
- Server-side consumer identification
- Session management across requests
Complexity: Intermediate
Time to complete: 45 minutes
Prerequisites
- A Fanfare account with API credentials
- A Next.js 14+, Remix, or Nuxt application
- Understanding of SSR concepts and hydration
Key Concepts
Client-Only SDK
The Fanfare SDK is a browser-only library. It uses:
localStorage for session persistence
BroadcastChannel for cross-tab sync
- Browser fingerprinting for device identification
This means SDK initialization must happen client-side after hydration.
Server-Side Consumer Identification
For SSR apps, you may want to identify consumers server-side using:
- JWT tokens from your authentication provider
- External authentication exchange
Next.js Integration
Step 1: Create a Client Provider
Create a client-only provider component:
// src/components/FanfareClientProvider.tsx
"use client";
import { FanfareProvider, type FanfareProviderProps } from "@waitify-io/fanfare-sdk-react";
import { type ReactNode } from "react";
interface FanfareClientProviderProps extends Omit<FanfareProviderProps, "children"> {
children: ReactNode;
}
export function FanfareClientProvider({ children, ...props }: FanfareClientProviderProps) {
return <FanfareProvider {...props}>{children}</FanfareProvider>;
}
Step 2: Add Provider to Layout
Add the provider to your root layout:
// src/app/layout.tsx
import { FanfareClientProvider } from "@/components/FanfareClientProvider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<FanfareClientProvider
organizationId={process.env.NEXT_PUBLIC_FANFARE_ORG_ID!}
publishableKey={process.env.NEXT_PUBLIC_FANFARE_PUBLISHABLE_KEY!}
loadingComponent={<LoadingSpinner />}
>
{children}
</FanfareClientProvider>
</body>
</html>
);
}
function LoadingSpinner() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="border-primary h-8 w-8 animate-spin rounded-full border-4 border-t-transparent" />
</div>
);
}
Step 3: Create a Client Experience Component
Experience components must be client components:
// src/components/ExperienceClient.tsx
"use client";
import { useExperienceJourney, useFanfareAuth } from "@waitify-io/fanfare-sdk-react";
import { useEffect } from "react";
interface ExperienceClientProps {
experienceId: string;
productName: string;
}
export function ExperienceClient({ experienceId, productName }: ExperienceClientProps) {
const { isAuthenticated, guest } = useFanfareAuth();
const { journey, state, status, start } = useExperienceJourney(experienceId);
// Initialize auth
useEffect(() => {
if (!isAuthenticated) {
guest();
}
}, [isAuthenticated, guest]);
// Start journey
useEffect(() => {
if (isAuthenticated && status === "idle") {
start();
}
}, [isAuthenticated, status, start]);
return (
<div className="experience-widget">
<h2>{productName}</h2>
<ExperienceUI status={status} journey={journey} state={state} />
</div>
);
}
Step 4: Use in Server Component Page
Fetch product data server-side, render experience client-side:
// src/app/products/[slug]/page.tsx
import { ExperienceClient } from "@/components/ExperienceClient";
import { getProduct } from "@/lib/products";
interface ProductPageProps {
params: { slug: string };
}
export default async function ProductPage({ params }: ProductPageProps) {
// Server-side data fetching
const product = await getProduct(params.slug);
if (!product) {
notFound();
}
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Client-side experience widget */}
{product.experienceId && <ExperienceClient experienceId={product.experienceId} productName={product.name} />}
</main>
);
}
// Generate static params for popular products
export async function generateStaticParams() {
const products = await getPopularProducts();
return products.map((p) => ({ slug: p.slug }));
}
Remix Integration
Step 1: Client-Only Provider
Create a client-only module:
// app/components/fanfare.client.tsx
import { FanfareProvider } from "@waitify-io/fanfare-sdk-react";
import type { ReactNode } from "react";
interface FanfareWrapperProps {
organizationId: string;
publishableKey: string;
children: ReactNode;
}
export function FanfareWrapper({ organizationId, publishableKey, children }: FanfareWrapperProps) {
return (
<FanfareProvider organizationId={organizationId} publishableKey={publishableKey}>
{children}
</FanfareProvider>
);
}
Step 2: Lazy Load in Root
Use ClientOnly pattern or lazy loading:
// app/root.tsx
import { Links, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from "@remix-run/react";
import { lazy, Suspense } from "react";
const FanfareWrapper = lazy(() => import("./components/fanfare.client").then((m) => ({ default: m.FanfareWrapper })));
export async function loader() {
return {
fanfareOrgId: process.env.FANFARE_ORG_ID,
fanfareKey: process.env.FANFARE_PUBLISHABLE_KEY,
};
}
export default function App() {
const { fanfareOrgId, fanfareKey } = useLoaderData<typeof loader>();
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Suspense fallback={<LoadingState />}>
{typeof window !== "undefined" && fanfareOrgId && fanfareKey ? (
<FanfareWrapper organizationId={fanfareOrgId} publishableKey={fanfareKey}>
<Outlet />
</FanfareWrapper>
) : (
<Outlet />
)}
</Suspense>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
Step 3: Use in Route Component
// app/routes/products.$slug.tsx
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { ExperienceWidget } from "~/components/ExperienceWidget.client";
export async function loader({ params }: LoaderFunctionArgs) {
const product = await getProduct(params.slug);
if (!product) throw new Response("Not Found", { status: 404 });
return json({ product });
}
export default function ProductRoute() {
const { product } = useLoaderData<typeof loader>();
return (
<main>
<h1>{product.name}</h1>
{/* Renders only on client */}
{typeof window !== "undefined" && product.experienceId && (
<ExperienceWidget experienceId={product.experienceId} />
)}
</main>
);
}
Server-Side Consumer Linking
When users are logged into your platform, link their identity to Fanfare:
Step 1: Create Exchange Endpoint
// app/api/fanfare/link/route.ts (Next.js)
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
export async function POST() {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Call Fanfare's external authorize endpoint
const response = await fetch("https://api.fanfare.io/auth/external/authorize", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Organization-Id": process.env.FANFARE_ORG_ID!,
"X-Secret-Key": process.env.FANFARE_SECRET_KEY!,
},
body: JSON.stringify({
provider: "your-platform",
issuer: "https://your-domain.com",
subject: session.user.id,
claims: {
email: session.user.email,
name: session.user.name,
},
}),
});
if (!response.ok) {
return NextResponse.json({ error: "Failed to link" }, { status: 500 });
}
const { exchangeCode, expiresAt } = await response.json();
return NextResponse.json({ exchangeCode, expiresAt });
}
Step 2: Exchange Code on Client
// src/hooks/useFanfareIdentity.ts
"use client";
import { useFanfare, useFanfareAuth } from "@waitify-io/fanfare-sdk-react";
import { useEffect } from "react";
export function useFanfareIdentity(isLoggedIn: boolean) {
const fanfare = useFanfare();
const { isAuthenticated, isGuest, session } = useFanfareAuth();
useEffect(() => {
async function linkIdentity() {
// Only link if user is logged in but Fanfare session is guest
if (!isLoggedIn || !isGuest) return;
try {
// Get exchange code from your backend
const res = await fetch("/api/fanfare/link", { method: "POST" });
if (!res.ok) return;
const { exchangeCode } = await res.json();
// Exchange for authenticated session
await fanfare.auth.exchangeExternal({ exchangeCode });
} catch (error) {
console.error("Failed to link identity:", error);
}
}
linkIdentity();
}, [isLoggedIn, isGuest, fanfare]);
return { session, isLinked: isAuthenticated && !isGuest };
}
Handling Hydration Mismatches
Prevent hydration errors by conditionally rendering client content:
// src/components/ClientOnly.tsx
"use client";
import { useEffect, useState, type ReactNode } from "react";
interface ClientOnlyProps {
children: ReactNode;
fallback?: ReactNode;
}
export function ClientOnly({ children, fallback = null }: ClientOnlyProps) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return <>{fallback}</>;
}
return <>{children}</>;
}
// Usage
function ProductPage() {
return (
<div>
<h1>Product Name</h1>
<ClientOnly fallback={<QueuePlaceholder />}>
<ExperienceWidget experienceId="exp_xxx" />
</ClientOnly>
</div>
);
}
Session Persistence Across Routes
The SDK automatically persists sessions in localStorage. For SSR apps:
- Initial page load: SDK initializes, restores session from storage
- Client navigation: Session persists, no re-initialization needed
- Full page reload: Session restored from storage on hydration
// The FanfareProvider handles this automatically
<FanfareProvider
autoRestore={true} // Restore session on mount (default)
autoResume={true} // Resume polling/watching (default)
>
Environment Variables
Configure environment variables for SSR:
# .env.local (Next.js)
NEXT_PUBLIC_FANFARE_ORG_ID=org_xxx
NEXT_PUBLIC_FANFARE_PUBLISHABLE_KEY=pk_live_xxx
FANFARE_SECRET_KEY=sk_live_xxx # Server-only, for external auth
# .env (Remix)
FANFARE_ORG_ID=org_xxx
FANFARE_PUBLISHABLE_KEY=pk_live_xxx
FANFARE_SECRET_KEY=sk_live_xxx
Testing SSR Integration
Verify Client-Only Rendering
- Disable JavaScript in your browser
- Load the page - experience widget should show loading/placeholder
- Enable JavaScript - widget should hydrate and function
Check Hydration
// Add hydration debugging
useEffect(() => {
console.log("Fanfare SDK hydrated", { isAuthenticated, session });
}, [isAuthenticated, session]);
Test Server-Side Identity Linking
- Log into your platform
- Check that exchange code is generated
- Verify consumer ID matches across sessions
Troubleshooting
Hydration Mismatch Errors
- Ensure SDK components are client-only
- Use
ClientOnly wrapper or "use client" directive
- Check for conditional rendering based on
typeof window
Session Not Persisting
- Verify
localStorage is available
- Check for privacy mode/incognito restrictions
- Ensure same origin for all pages
Identity Not Linking
- Verify secret key permissions
- Check exchange code hasn’t expired (60 seconds)
- Confirm user is logged into your platform
What’s Next