Skip to main content
Server-rendered apps should treat Fanfare as a browser SDK. Render the surrounding page on the server, then mount Fanfare inside a client component after hydration. The server can prepare product data, authenticated customer context, and checkout routes. The SDK should be initialized in the browser with a publishable key, and admitted customers should be handed to a trusted server or checkout boundary.

Integration Shape

SSR integration shape showing server route, SSR page, browser Fanfare boundary, handoff API, and checkout. Key rules:
  • Put FanfareProvider, ExperienceWidget, and useExperienceJourney in client-only code.
  • Use environment variables that are safe for the browser, such as NEXT_PUBLIC_FANFARE_PUBLISHABLE_KEY.
  • Keep grants out of URLs and third-party telemetry.
  • Let the server validate the handoff before protected checkout actions.

Next.js App Router

Create a small client provider.
"use client";

import { FanfareProvider } from "@fanfare-io/fanfare-sdk-react";
import "@fanfare-io/fanfare-sdk-react/styles";

export function FanfareClientProvider({ children }: { children: React.ReactNode }) {
  return (
    <FanfareProvider
      organizationId={process.env.NEXT_PUBLIC_FANFARE_ORG_ID!}
      publishableKey={process.env.NEXT_PUBLIC_FANFARE_PUBLISHABLE_KEY!}
    >
      {children}
    </FanfareProvider>
  );
}
Wrap only the part of the tree that needs Fanfare.
import { FanfareClientProvider } from "@/components/fanfare-client-provider";
import { LaunchExperience } from "@/components/launch-experience";

export default async function ProductPage() {
  const product = await getProduct();

  return (
    <main>
      <h1>{product.name}</h1>
      <FanfareClientProvider>
        <LaunchExperience experienceId={product.fanfareExperienceId} />
      </FanfareClientProvider>
    </main>
  );
}
Render the widget in a client component.
"use client";

import { ExperienceWidget } from "@fanfare-io/fanfare-sdk-react";

export function LaunchExperience({ experienceId }: { experienceId: string }) {
  return (
    <ExperienceWidget
      experienceId={experienceId}
      autoStart
      onGranted={async (admissionGrant) => {
        await fetch("/api/fanfare/admission", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ admissionGrant }),
        });

        window.location.assign("/checkout");
      }}
    />
  );
}
Use checkoutUrl instead of onGranted when simple browser navigation is enough.

Custom Client UI

For a custom SSR page, keep the server-rendered shell stable and branch on JourneyView only after the client component mounts.
"use client";

import { useExperienceJourney } from "@fanfare-io/fanfare-sdk-react";

export function CustomLaunch({ experienceId }: { experienceId: string }) {
  const { view, start, error } = useExperienceJourney(experienceId);

  if (error) return <ErrorPanel message={error} />;
  if (!view) return <LoadingPanel />;

  if (view.journeyStage === "ready") {
    return <button onClick={() => void start()}>Start</button>;
  }

  if (view.journeyStage === "routing") return <LoadingPanel />;
  if (view.journeyStage === "gated") return <GatePanel view={view} />;

  return <SequencePanel sequence={view.sequence} />;
}
Avoid rendering state-specific buttons from server assumptions. The current client view decides what is valid.

Remix And Nuxt

The same boundary applies in other SSR frameworks:
  • Load product and routing data on the server.
  • Mount Fanfare in browser-only code.
  • Import @fanfare-io/fanfare-sdk-react/styles once for React surfaces.
  • Use the core SDK or web components when React is not part of the client island.
For static or CMS-rendered islands, register the web component once:
<script type="module">
  import { registerWebComponents } from "@fanfare-io/fanfare-sdk-solid";

  registerWebComponents({
    organizationId: "org_123",
    publishableKey: "pk_live_123",
  });
</script>

<fanfare-experience-widget
  experience-id="exp_123"
  auto-start
  checkout-url="/checkout"
></fanfare-experience-widget>

Server Handoff

Your admission endpoint should accept the grant from your own page and prepare the next trusted step.
export async function POST(request: Request) {
  const { admissionGrant } = await request.json();

  await prepareCheckout({ admissionGrant });

  return Response.json({ ok: true });
}
Keep the endpoint focused on your application contract. Do not mirror private routing details into public responses.

Testing SSR Integrations

  • Confirm the page renders without accessing browser-only APIs on the server.
  • Confirm the client boundary hydrates and renders a public journey state.
  • Test admitted handoff through your own API route.
  • Repeat with a returning browser session.
  • Keep assertions based on visible states and app-owned side effects.
See Testing Your Integration for app-boundary test guidance.