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

# SvelteKit Server Rendering

> Server-side render Bijection data in SvelteKit with bijectionLoad and bijectionLoadPaginated, transport hooks, authenticated SSR fetches, and server helpers.

This page builds on the core
[Queries, Mutations & Actions](/client/svelte/reactivity) API. Make sure
`setupBijection()` is in your root layout before using these features — see
[Overview](/client/svelte/overview#setup).

Import from `bijection-svelte/sveltekit` for SvelteKit-specific features: SSR
transport with live upgrade, and a server-side HTTP client helper.

<Info>
  **Why bother with SSR on a realtime backend?**

  The client will open a WebSocket and get live updates anyway — so is SSR worth
  it? Almost always yes: it's faster for time-to-data on first page load. See
  [Why server-side rendering with Bijection?](/client/svelte/why-server-rendering)
  for the full comparison.
</Info>

## SSR with bijectionLoad / bijectionLoadPaginated (recommended)

`bijectionLoad()` and `bijectionLoadPaginated()` fetch data on the server and
automatically upgrade to live subscriptions on the client. No manual
`initialData` wiring needed. Use `bijectionLoad()` for regular queries and
`bijectionLoadPaginated()` for paginated queries.

### Setup

Add `initBijection()` and the transport hooks to `hooks.ts` (universal hooks — runs
on both server and client). `initBijection()` creates the `BijectionClient` singleton
early so the transport decoder can upgrade SSR data to live subscriptions.
`setupBijection()` in your root layout automatically reuses this singleton.

If you only use `bijectionLoad()`, you only need the `BijectionLoadResult` transport.
Add `BijectionLoadPaginatedResult` when using `bijectionLoadPaginated()`.

```ts src/hooks.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import {
  initBijection,
  encodeBijectionLoad,
  decodeBijectionLoad,
  encodeBijectionLoadPaginated,
  decodeBijectionLoadPaginated,
} from "bijection-svelte/sveltekit";
import { PUBLIC_BIJECTION_URL } from "$env/static/public";

initBijection(PUBLIC_BIJECTION_URL);

export const transport = {
  BijectionLoadResult: {
    encode: encodeBijectionLoad,
    decode: decodeBijectionLoad,
  },
  // Only needed if you use bijectionLoadPaginated()
  BijectionLoadPaginatedResult: {
    encode: encodeBijectionLoadPaginated,
    decode: decodeBijectionLoadPaginated,
  },
};
```

### Usage with bijectionLoad

```ts src/routes/+page.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { bijectionLoad } from "bijection-svelte/sveltekit";
import { api } from "$bijection/_generated/api";

export const load = async () => ({
  tasks: await bijectionLoad(api.tasks.get, {}),
});
```

```svelte src/routes/+page.svelte theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
<script lang="ts">
  let { data } = $props();
  const tasks = $derived(data.tasks);
</script>

{#if tasks.isLoading}
  Loading...
{:else if tasks.error}
  Error: {tasks.error.message}
{:else}
  <ul>
    {#each tasks.data as task}
      <li>{task.text}</li>
    {/each}
  </ul>
{/if}
```

The result has the same shape as `useQuery()` — `.data`, `.isLoading`, `.error`,
`.isStale` — and is reactive. On first load, data arrives via SSR (no loading
flash). After hydration, a live WebSocket subscription takes over automatically.

### Usage with bijectionLoadPaginated

`bijectionLoadPaginated()` works the same way but for paginated queries. It fetches
the first page on the server and upgrades to a live paginated subscription on
the client — with `loadMore()` support for incremental loading.

```ts src/routes/+page.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { bijectionLoadPaginated } from "bijection-svelte/sveltekit";
import { api } from "$bijection/_generated/api";

export const load = async () => ({
  messages: await bijectionLoadPaginated(
    api.messages.paginatedList,
    { searchWords: [] },
    { initialNumItems: 10 },
  ),
});
```

```svelte src/routes/+page.svelte theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
<script lang="ts">
  let { data } = $props();
  const messages = $derived(data.messages);
</script>

{#if messages.isLoading}
  Loading...
{:else if messages.error}
  Error: {messages.error.message}
{:else}
  <ul>
    {#each messages.results as message}
      <li>{message.author}: {message.body}</li>
    {/each}
  </ul>
  {#if messages.status === "CanLoadMore"}
    <button onclick={() => messages.loadMore(10)}>Load more</button>
  {/if}
{/if}
```

The result has the same shape as `usePaginatedQuery()` — `.results`, `.status`,
`.isLoading`, `.error`, `.loadMore()` — and is reactive. On first load, the
first page arrives via SSR (no loading flash). After hydration, a live WebSocket
subscription takes over and `loadMore()` becomes functional.

### Authenticated fetches

For authenticated SSR fetches, use `withServerBijectionToken` in your server hook.
This stores the auth token per-request via `AsyncLocalStorage`, so `bijectionLoad`
and `createBijectionHttpClient` pick it up automatically — no `{ token }` option
needed.

```ts src/hooks.server.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import type { Handle } from "@sveltejs/kit";
import { withServerBijectionToken } from "bijection-svelte/sveltekit/server";

export const handle: Handle = async ({ event, resolve }) => {
  const token = await getAuthToken(event.cookies); // your auth provider
  event.locals.token = token;
  return withServerBijectionToken(token, () => resolve(event));
};
```

Then use `bijectionLoad` in **any** load function — `+page.ts` or
`+page.server.ts`:

```ts src/routes/+page.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
// Universal — works for both SSR and client-side navigation
import { bijectionLoad } from "bijection-svelte/sveltekit";
import { api } from "$bijection/_generated/api";

export const load = async () => ({
  tasks: await bijectionLoad(api.tasks.get, {}),
});
```

The explicit `{ token }` option still works as a manual override:

```ts src/routes/+page.server.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
// Explicit token (escape hatch)
export const load = async ({ locals }) => ({
  tasks: await bijectionLoad(api.tasks.get, {}, { token: locals.token }),
});
```

### Skipping queries

Pass `'skip'` as args to avoid fetching — useful for auth-gated queries that
should not run when the user is unauthenticated:

```ts src/routes/+page.server.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
// Skip when unauthenticated
export const load = async ({ locals }) => ({
  user: await bijectionLoad(api.users.get, locals.token ? {} : "skip"),
});
```

When skipped, `bijectionLoad` returns
`{ data: undefined, isLoading: false, error: undefined, isStale: false }`
without making any request. `bijectionLoadPaginated` returns
`{ results: [], status: 'Exhausted', isLoading: false, error: undefined, loadMore: () => false }`.

### Choosing between `+page.ts` and `+page.server.ts`

`bijectionLoad` works in both universal (`+page.ts`) and server-only
(`+page.server.ts`) load functions. The difference is what happens during
**client-side navigation** (after the first SSR page load):

* **`+page.ts` (universal):** On client-side navigation, `bijectionLoad` runs in
  the browser and queries Bijection directly — no server roundtrip. Auth is handled
  implicitly via the already-authenticated `BijectionClient` singleton (configured
  by `setupAuth()` in your root layout).
* **`+page.server.ts` (server-only):** On client-side navigation, SvelteKit
  fetches from your server, which then queries Bijection — adding an extra network
  hop. Auth is always explicit (server-side via `withServerBijectionToken` or
  `locals.token`).

Both produce identical SSR on first page load. Use `+page.ts` for best
navigation performance. Use `+page.server.ts` if you need access to server-only
data (e.g. `locals`, cookies) or prefer explicit auth handling.

## SSR with initialData (manual alternative)

If you prefer server-only load functions (`+page.server.ts`) or need more
control, you can use the `initialData` option on `useQuery()` and
`usePaginatedQuery()` directly.

```ts src/routes/+page.server.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { BijectionHttpClient } from "bijection/browser";
import type { PageServerLoad } from "./$types.js";
import { PUBLIC_BIJECTION_URL } from "$env/static/public";
import { api } from "../bijection/_generated/api.js";

export const load = (async () => {
  const client = new BijectionHttpClient(PUBLIC_BIJECTION_URL!);
  return {
    messages: await client.query(api.messages.list, { searchWords: [] }),
  };
}) satisfies PageServerLoad;
```

```svelte src/routes/+page.svelte theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
<script lang="ts">
  import type { PageData } from "./$types.js";
  let { data }: { data: PageData } = $props();

  import { useQuery } from "bijection-svelte";
  import { api } from "../bijection/_generated/api.js";

  const messages = useQuery(
    api.messages.list,
    () => ({ searchWords: [] }),
    () => ({ initialData: data.messages }),
  );
</script>
```

Combining `initialData` with `keepPreviousData: true` (or never changing the
query arguments) should be enough to avoid ever seeing a loading state.

<Note>
  **When to use this over bijectionLoad**

  Use `initialData` when building a library that needs to support Svelte-only,
  SvelteKit SPA, and SvelteKit SSR without requiring the transport hook setup.
</Note>

## Server helpers

These are server-only helpers (`hooks.server.ts`, `+page.server.ts`, form
actions, endpoints) for authenticating SSR fetches and running one-off calls
from the server. For one-off calls from the **client**, use
[`getBijectionClient()`](/client/svelte/reactivity#one-time-calls) instead.

### withServerBijectionToken (recommended)

Import from `bijection-svelte/sveltekit/server`. Wraps your SvelteKit `resolve()`
call to store the auth token per-request via `AsyncLocalStorage`. Both
`bijectionLoad` and `createBijectionHttpClient` automatically read it during SSR.

```ts src/hooks.server.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import type { Handle } from "@sveltejs/kit";
import { withServerBijectionToken } from "bijection-svelte/sveltekit/server";

export const handle: Handle = async ({ event, resolve }) => {
  const token = await getAuthToken(event.cookies);
  event.locals.token = token; // still available for direct use
  return withServerBijectionToken(token, () => resolve(event));
};
```

```ts src/app.d.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
declare global {
  namespace App {
    interface Locals {
      token: string | undefined;
    }
  }
}
```

With this setup, `bijectionLoad()` and `createBijectionHttpClient()` automatically
authenticate during SSR — no `{ token }` option needed in load functions.

### Setting up `locals.token` (without withServerBijectionToken)

If you prefer not to use `withServerBijectionToken`, you can still extract the
token and pass it explicitly:

```ts src/hooks.server.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import type { Handle } from "@sveltejs/kit";

export const handle: Handle = async ({ event, resolve }) => {
  event.locals.token = await getAuthToken(event.cookies);
  return resolve(event);
};
```

Then pass `{ token: locals.token }` to `bijectionLoad` or `createBijectionHttpClient`
in each load function.

### createBijectionHttpClient

For server-only code (`+page.server.ts`, form actions, API routes), use
`createBijectionHttpClient()`:

```ts src/routes/+page.server.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
// With withServerBijectionToken (no args needed)
import { createBijectionHttpClient } from "bijection-svelte/sveltekit";
import { api } from "$bijection/_generated/api";

export const load = async () => {
  const client = createBijectionHttpClient();
  const tasks = await client.query(api.tasks.get, {});
  return { tasks };
};
```

Explicit token still works as an override:

```ts src/routes/+page.server.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
// Explicit token (escape hatch)
export const load = async ({ locals }) => {
  const client = createBijectionHttpClient({ token: locals.token });
  const tasks = await client.query(api.tasks.get, {});
  return { tasks };
};
```

The `url` option falls back to the URL set by `initBijection()`.

## Deploying

See [Deploy Your Frontend](/production/hosting/hosting) and
[`bijection deploy`](/cli/reference/deploy) for detailed instructions on
deploying your app and Bijection functions to production. For the biggest SSR
performance win, co-locate your framework server in the same region as Bijection —
see
[Co-locate your server with Bijection](/client/svelte/why-server-rendering#co-locate-your-server-with-bijection).

## API reference

Functions and types exported from `bijection-svelte/sveltekit`:

| Export | Kind | Description |
| - | - | - |
| `initBijection(url, options?)` | Function | Create the `BijectionClient` singleton early. Only needed for [bijectionLoad SSR setup](#setup). |
| `getBijectionUrl()` | Function | Retrieve the deployment URL set by `initBijection()` or `setupBijection()`. |
| `closeBijection()` | Function | Close the app-scoped client and clear the singleton (also exported from `bijection-svelte`). |
| `bijectionLoad(query, args, options?)` | Function | Fetch data server-side, upgrade to live subscription on client. |
| `encodeBijectionLoad` | Function | Transport encoder — use in `hooks.ts` (see [Setup](#setup)). |
| `decodeBijectionLoad` | Function | Transport decoder — use in `hooks.ts` (see [Setup](#setup)). |
| `bijectionLoadPaginated(query, args, options)` | Function | Fetch first page server-side, upgrade to live paginated subscription on client. |
| `encodeBijectionLoadPaginated` | Function | Paginated transport encoder — use in `hooks.ts`. |
| `decodeBijectionLoadPaginated` | Function | Paginated transport decoder — use in `hooks.ts`. |
| `createBijectionHttpClient(options?)` | Function | Create a `BijectionHttpClient` for server-side use. |
| `CreateBijectionHttpClientOptions` | Type | Options for `createBijectionHttpClient`: `url`, `token`, `options`. |

The server-only helper `withServerBijectionToken` is imported from
`bijection-svelte/sveltekit/server`.
