> ## Documentation Index
> Fetch the complete documentation index at: https://docs.traffical.io/llms.txt
> Use this file to discover all available pages before exploring further.

# SSR patterns

> Server-side rendering with SvelteKit, Next.js, and Remix — no flash of original content, no double-fetch on hydration.

Server-side rendering introduces a timing challenge: the server has to resolve parameters during the request lifecycle so the first paint is correct, but the client also needs the assignments for interactive behaviour. If you fetch the bundle twice — once on the server, once on the client — hydration can re-resolve to different values and cause a visible swap.

The pattern is always the same:

1. Fetch the bundle once on the server.
2. Pass it to the client via your framework's data-loading mechanism.
3. Initialize the client SDK with the server-provided bundle as `localConfig`.
4. The client uses the same bundle and resolves identically — no second fetch on hydration.

## SvelteKit

### Layout `load` (server)

```typescript theme={null}
// src/routes/+layout.server.ts
import { loadTrafficalBundle } from "@traffical/svelte/sveltekit";
import { TRAFFICAL_API_KEY } from "$env/static/private";

export async function load({ fetch }) {
  const { bundle } = await loadTrafficalBundle({
    orgId: "org_acme",
    projectId: "proj_marketplace",
    env: "production",
    apiKey: TRAFFICAL_API_KEY,
    fetch,    // SvelteKit's fetch handles caching
  });

  return { traffical: { bundle } };
}
```

`loadTrafficalBundle` respects standard HTTP caching, so subsequent SSR requests within the cache TTL benefit from SvelteKit's fetch cache.

### Root layout (server + client)

```svelte theme={null}
<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { TrafficalProvider } from "@traffical/svelte";
  let { data, children } = $props();
</script>

<TrafficalProvider
  config={{
    orgId: "org_acme",
    projectId: "proj_marketplace",
    env: "production",
    apiKey: import.meta.env.PUBLIC_TRAFFICAL_API_KEY,
    initialBundle: data.traffical.bundle,
  }}
>
  {@render children()}
</TrafficalProvider>
```

<Note>
  On the Svelte SDK the server-fetched bundle goes through `initialBundle` (not `localConfig`). `initialBundle` is what marks the provider ready during SSR and lets it resolve on the server so the first paint is correct. `localConfig` is the separate build-time/offline fallback bundle.
</Note>

### Page

```svelte theme={null}
<!-- src/routes/checkout/+page.svelte -->
<script lang="ts">
  import { useTraffical } from "@traffical/svelte";

  const { params, track } = useTraffical({
    defaults: {
      "checkout.cta_text": "Buy Now",
      "checkout.layout": "single-page",
    },
  });
</script>

<h1>{params["checkout.cta_text"]}</h1>
<button onclick={() => track("cta_click")}>Buy</button>
```

The server resolves with `data.traffical.bundle` and renders the correct variant. The client picks up the same bundle and resolves identically. No swap.

## Next.js (App Router / RSC)

The React SDK has no `@traffical/react/server` entry point. Fetch the config bundle directly from the SDK config endpoint — the same URL the client SDK calls — and pass it as `localConfig`:

```tsx theme={null}
// app/layout.tsx
import { TrafficalProvider } from "@traffical/react";

async function fetchBundle() {
  const res = await fetch(
    "https://sdk.traffical.io/v1/config/proj_marketplace?env=production",
    {
      headers: { Authorization: `Bearer ${process.env.TRAFFICAL_API_KEY!}` },
      next: { revalidate: 60 },   // cache via Next.js fetch integration
    },
  );
  return res.ok ? res.json() : null;
}

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const bundle = await fetchBundle();

  return (
    <html>
      <body>
        <TrafficalProvider
          config={{
            orgId: "org_acme",
            projectId: "proj_marketplace",
            env: "production",
            apiKey: process.env.NEXT_PUBLIC_TRAFFICAL_API_KEY!,
            localConfig: bundle,
          }}
        >
          {children}
        </TrafficalProvider>
      </body>
    </html>
  );
}
```

Wrapping the fetch in Next.js's `fetch` integration (`next: { revalidate }`) caches it, so subsequent renders within the cache window don't re-fetch.

## Next.js (Pages Router)

```tsx theme={null}
// pages/_app.tsx
import { TrafficalProvider } from "@traffical/react";

export default function App({ Component, pageProps }) {
  return (
    <TrafficalProvider
      config={{
        orgId: "org_acme",
        projectId: "proj_marketplace",
        env: "production",
        apiKey: process.env.NEXT_PUBLIC_TRAFFICAL_API_KEY!,
        localConfig: pageProps.trafficalBundle,
      }}
    >
      <Component {...pageProps} />
    </TrafficalProvider>
  );
}
```

```tsx theme={null}
// pages/checkout.tsx
export async function getServerSideProps() {
  const res = await fetch(
    "https://sdk.traffical.io/v1/config/proj_marketplace?env=production",
    { headers: { Authorization: `Bearer ${process.env.TRAFFICAL_API_KEY!}` } },
  );
  const bundle = res.ok ? await res.json() : null;
  return { props: { trafficalBundle: bundle } };
}
```

## Why this works

* **Same bundle, same hash, same answer.** Both server and client read the same bundle and run the same [SHA-256 v2 bucketing](/how-it-works#local-resolution). The same userId always resolves to the same allocation.
* **One network fetch.** The server fetches; the client receives the bundle inline. No second fetch on hydration.
* **First paint is correct.** Because the server resolves before rendering, the user sees the right variant immediately. No flash of original content.

## Pitfalls

* **Mismatched user IDs.** If the server reads `userId` from a session cookie but the client reads it from a different source, the two will resolve to different buckets and you'll see a swap on hydration. Make sure both sides use the same value.
* **Use the SDK key on both sides.** The same `traffical_sk_...` SDK key (scopes `sdk:read`+`sdk:write`) is browser-safe and is used both client-side in the `TrafficalProvider` and server-side when you fetch the bundle (the plain `fetch` on Next.js, `loadTrafficalBundle` on SvelteKit). There is no separate server secret for the SDK path — keep management-scoped keys out of all rendering code.
* **Stale bundle on long-lived pages.** The client refreshes the bundle every 60s by default. SPAs running for hours will pick up new policies automatically. For static pages, a hard reload is enough.

## Next steps

<CardGroup cols={2}>
  <Card title="SSR + hydration pattern" icon="server" href="/guides/canonical-experiments#ssr--client-hydration">
    Full end-to-end example.
  </Card>

  <Card title="React SDK" icon="react" href="/sdks/react">
    Provider, hooks, options.
  </Card>

  <Card title="Svelte SDK" icon="s" href="/sdks/svelte">
    Stores, context, runes.
  </Card>
</CardGroup>
