---
title: "SSR & Hydration"
description: "How awaited Convex queries render on the server, travel in the Nuxt payload, and hand off to the WebSocket, and how ssr, server and auth change that."
canonical_url: "https://nuxt-convex.onmax.me/nuxt-module/ssr-and-hydration"
---
# SSR & Hydration

> How awaited Convex queries render on the server, travel in the Nuxt payload, and hand off to the WebSocket, and how ssr, server and auth change that.

A query renders on the server when you `await` it. The result goes to the browser in the Nuxt payload, the client hydrates from it without fetching again, and then the WebSocket subscription keeps it live.

```vue [app/pages/index.vue]
<script setup lang="ts">
import { api } from '#convex/api'

const { data: tasks, status } = await useConvexQuery(api.tasks.list)
</script>

<template>
  <ul v-if="status === 'success'">
    <li v-for="task in tasks" :key="task._id">
      {{ task.title }}
    </li>
  </ul>
</template>
```

## What happens on a request

<steps level="3">

### The server runs the query once

`await` runs the query over a `ConvexHttpClient`. Identical queries in the same request, same function and deep-equal args, share one fetch. When the request has a user, the query runs [as that user](https://nuxt-convex.onmax.me/nuxt-module/authenticated-ssr).

### The result goes into the payload

The settled result, value or error, is stored in `nuxtApp.payload.data` under `$convex:<function>|<args>`, for example `$convex:tasks:list|{}`. Values are encoded with `convexToJson`, so `bigint` and `ArrayBuffer` survive, and a `ConvexError` keeps its `data`.

### The client hydrates from the payload

While Nuxt hydrates, each query reads its entry, so the first client render matches the server HTML and makes no HTTP request. The entries are deleted once hydration finishes (`app:suspense:resolve`), so later navigations never reuse stale SSR data.

### The WebSocket takes over

The client subscribes over the WebSocket and replaces the value whenever Convex pushes an update. From here on the query behaves the same as one that started on the client.

</steps>

Awaiting never rejects. A failed query resolves with `status` `'error'` and `error` set, and is hydrated as an error, so the client renders the same error state. See [Error Handling](https://nuxt-convex.onmax.me/nuxt-module/error-handling).

## Without `await`

A query you don't await is not fetched on the server. It renders as `pending` in the HTML, then loads over the WebSocket after hydration. That's the right choice for data below the fold, or anything you don't need in the first paint.

```ts
// Rendered on the server
const { data: project } = await useConvexQuery(api.projects.get, { id })
// Loaded in the browser
const { data: activity } = useConvexQuery(api.activity.recent, { projectId: id })
```

Each `await` waits for its query before the next line runs, so awaited queries in a row fetch one after another. Start independent queries together:

```ts
const [{ data: project }, { data: members }] = await Promise.all([
  useConvexQuery(api.projects.get, { id }),
  useConvexQuery(api.projects.members, { id }),
])
```

## Turning SSR off

Three levels, from broad to narrow:

<table>
<thead>
  <tr>
    <th>
      Setting
    </th>
    
    <th>
      Scope
    </th>
    
    <th>
      Effect
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        ssr: false
      </code>
      
       in <code>
        nuxt.config.ts
      </code>
    </td>
    
    <td>
      Whole app
    </td>
    
    <td>
      Nuxt renders nothing on the server, so no query does either
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        convex: { ssr: false }
      </code>
    </td>
    
    <td>
      Every Convex query
    </td>
    
    <td>
      Awaited queries resolve immediately on the server as <code>
        pending
      </code>
      
       and load in the browser
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        { server: false }
      </code>
      
       on a query
    </td>
    
    <td>
      One query
    </td>
    
    <td>
      Same, for that query only. <code>
        { server: true }
      </code>
      
       opts one query back in when <code>
        convex.ssr
      </code>
      
       is <code>
        false
      </code>
    </td>
  </tr>
</tbody>
</table>

The module option is called `ssr` because it plays the role of Nuxt's `ssr` for Convex queries. The per-query option is called `server`, like the `server` option of `useFetch` and `useAsyncData`.

The `convex.server` option from older releases still works as an alias of `convex.ssr` and logs a deprecation warning.

## Auth and SSR

The per-query `auth` option decides how a query treats the user, on the server and in the browser:

<table>
<thead>
  <tr>
    <th>
      <code>
        auth
      </code>
    </th>
    
    <th>
      On the server
    </th>
    
    <th>
      In the browser
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        'optional'
      </code>
      
       (default)
    </td>
    
    <td>
      Runs with the request's token if there is one, otherwise anonymously
    </td>
    
    <td>
      Runs with whatever auth the client has
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'required'
      </code>
    </td>
    
    <td>
      Runs only when the request is authenticated, otherwise renders <code>
        idle
      </code>
    </td>
    
    <td>
      Stays <code>
        idle
      </code>
      
       until the user is signed in
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'none'
      </code>
    </td>
    
    <td>
      Runs anonymously, without resolving the request's token
    </td>
    
    <td>
      Runs with the client's auth (the result shouldn't depend on it)
    </td>
  </tr>
</tbody>
</table>

Use `'none'` for public data on pages that also have a signed-in user. Those queries don't wait for the token, and you document that the result is the same for everyone.

`'required'` matters on the client too: without it, a private query would run anonymously before the token arrives. When SSR rendered a signed-in user, the module also turns on the client's `expectAuth`, so no query runs anonymously in the gap between hydration and the first token.

## Components and pagination

`<ConvexQuery>` and `<ConvexPaginatedQuery>` await for you on the server, unless their `options.server` is `false`, and render their `error` slot when the query fails. On the client they render without waiting, unless you pass `suspense`.

```vue
<ConvexQuery :query="api.tasks.list">
  <template #default="{ data }">
    <TaskList :tasks="data" />
  </template>
  <template #loading>
    Loading…
  </template>
</ConvexQuery>
```

`await useConvexPaginatedQuery(...)` fetches the first page on the server and hydrates it. The client subscribes to that page and loads the next ones with `loadMore()`.

## `useConvexQueries` is client only

`useConvexQueries` isn't awaitable and never runs on the server: every entry is `undefined` in the server render and fills in after hydration. When the data must be in the HTML, await one `useConvexQuery` per query, or write one Convex query that returns everything.

## Mutations and actions

`useConvexMutation` and `useConvexAction` need the WebSocket client, which only exists in the browser. Call them from event handlers. To write to Convex during a request, use the [server helpers](https://nuxt-convex.onmax.me/nuxt-module/server-routes).

## Caching

Each request runs its awaited queries fresh. Pages rendered with a user's token are sent with `Cache-Control: private, no-cache` and `Vary: Cookie`. Read [Authenticated SSR](https://nuxt-convex.onmax.me/nuxt-module/authenticated-ssr#caching) before you add `cache` or `swr` route rules.

## Next steps

- [Authenticated SSR](https://nuxt-convex.onmax.me/nuxt-module/authenticated-ssr) to render signed-in pages
- [Error Handling](https://nuxt-convex.onmax.me/nuxt-module/error-handling) for the `error` and `status` refs
- [`useConvexQuery`](https://nuxt-convex.onmax.me/api-reference/use-convex-query) for the full signature


## Sitemap

See the full [sitemap](https://nuxt-convex.onmax.me/sitemap.md) for all pages.
