> ## 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

> Integrate Fanfare in server-rendered applications with a browser-only SDK boundary.

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

<img src="https://mintcdn.com/fanfare/9lBxxAA0GJkGRgw-/images/guides/ssr-integration-shape.webp?fit=max&auto=format&n=9lBxxAA0GJkGRgw-&q=85&s=995118c222f70edd000e6b5c2ef48988" alt="SSR integration shape showing server route, SSR page, browser Fanfare boundary, handoff API, and checkout." width="1774" height="887" data-path="images/guides/ssr-integration-shape.webp" />

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.

```tsx theme={null}
"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.

```tsx theme={null}
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.

```tsx theme={null}
"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.

```tsx theme={null}
"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:

```html theme={null}
<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.

```ts theme={null}
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](/getting-started/testing) for app-boundary test guidance.
