---
title: "Server Rendering"
description: "What applies to vue-convex without Nuxt - nothing to do in a client-rendered app, and the hydration and serverAuth options for your own SSR setup."
canonical_url: "https://nuxt-convex.onmax.me/vue-guide/server-rendering"
---
# Server Rendering

> What applies to vue-convex without Nuxt - nothing to do in a client-rendered app, and the hydration and serverAuth options for your own SSR setup.

Most Vue apps that use `vue-convex` are client-rendered, and this page doesn't concern them: queries start `pending` and fill in over the WebSocket. If you use Nuxt, the module does everything below for you; read [SSR & Hydration](https://nuxt-convex.onmax.me/nuxt-module/ssr-and-hydration) instead.

This page is for apps that render on the server themselves, with `createSSRApp` and `renderToString`. It covers only the Convex part: the dev server, HTML template and `entry-client.ts` loading come from [Vite's SSR guide](https://vite.dev/guide/ssr), whose server puts the string `render()` returns where the template has `<!--app-html-->`.

## What runs where

<table>
<thead>
  <tr>
    <th>
      
    </th>
    
    <th>
      Server
    </th>
    
    <th>
      Browser
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Awaited <code>
        useConvexQuery
      </code>
      
       and <code>
        useConvexPaginatedQuery
      </code>
    </td>
    
    <td>
      Fetched once over HTTP
    </td>
    
    <td>
      Hydrated, then live over the WebSocket
    </td>
  </tr>
  
  <tr>
    <td>
      Queries you don't await
    </td>
    
    <td>
      Rendered as <code>
        pending
      </code>
    </td>
    
    <td>
      Loaded over the WebSocket
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        useConvexQueries
      </code>
    </td>
    
    <td>
      Every entry <code>
        undefined
      </code>
    </td>
    
    <td>
      Loaded over the WebSocket
    </td>
  </tr>
  
  <tr>
    <td>
      Mutations, actions, uploads
    </td>
    
    <td>
      Reject: no realtime client
    </td>
    
    <td>
      Run over the WebSocket
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        useConvexAuth({ fetchToken })
      </code>
    </td>
    
    <td>
      Ignored
    </td>
    
    <td>
      Authenticates the client
    </td>
  </tr>
</tbody>
</table>

Awaiting requires an async `setup()` inside `<Suspense>`, which `renderToString` waits for.

## Hand results to the client

The plugin's `hydration` option is the bridge. On the server it receives every settled query; on the client it gives them back while the app hydrates:

```ts [src/entry-server.ts]
import type { ConvexHydrationEntry } from 'vue-convex'
import { convexVue } from 'vue-convex'
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'
import App from './App.vue'

export async function render(url: string) {
  const entries: Record<string, ConvexHydrationEntry> = {}
  const app = createSSRApp(App).use(convexVue, {
    url: process.env.CONVEX_URL,
    hydration: {
      set: (key, entry) => { entries[key] = entry },
      get: () => undefined,
      isHydrating: () => false,
    },
  })

  const html = await renderToString(app)
  // Serialize safely: entries contain user data
  const state = JSON.stringify(entries).replace(/</g, '\\u003c')
  return `${html}<script>window.__CONVEX__=${state}</script>`
}
```

```ts [src/entry-client.ts]
import type { ConvexHydrationEntry } from 'vue-convex'
import { convexVue } from 'vue-convex'
import { createSSRApp } from 'vue'
import App from './App.vue'

const entries = (window as { __CONVEX__?: Record<string, ConvexHydrationEntry> }).__CONVEX__ ?? {}
let hydrating = true

createSSRApp(App, { onHydrated: () => (hydrating = false) })
  .use(convexVue, {
    url: import.meta.env.VITE_CONVEX_URL,
    hydration: {
      set: () => {},
      get: key => entries[key],
      isHydrating: () => hydrating,
    },
  })
  .mount('#app')
```

```vue [src/App.vue]
<script setup lang="ts">
import TaskList from './components/TaskList.vue'

defineProps<{ onHydrated?: () => void }>()
</script>

<template>
  <Suspense @resolve="onHydrated?.()">
    <TaskList />
  </Suspense>
</template>
```

`TaskList` awaits its query in `setup()`. With `vue-router`, put `<RouterView />` inside the `<Suspense>` instead.

```vue [src/components/TaskList.vue]
<script setup lang="ts">
import { useConvexQuery } from 'vue-convex'
import { api } from '../../convex/_generated/api'

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

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

Stop hydrating once the root `<Suspense>` resolves, not right after `mount()`: components below an async `setup()` are created later, and they need their entries too. After that, queries never read stale server results.

Entries are plain JSON. `bigint`, `ArrayBuffer` and `ConvexError` data survive the round trip.

## Render as the signed-in user

Pass `serverAuth` on the server to run awaited queries with the request's token. `getToken` is called at most once per app instance, and the token is never serialized:

```ts
createSSRApp(App).use(convexVue, {
  url: process.env.CONVEX_URL,
  serverAuth: { getToken: () => getConvexTokenFromRequest(request) },
  initialAuth: { isAuthenticated: Boolean(session) },
  hydration,
})
```

Pass the same `initialAuth` to the client plugin (serialize the boolean, never the token), so the auth components render the same slot on both sides. Queries marked `auth: 'none'` skip `getToken` and run anonymously.

Remember that responses rendered with a token contain that user's data: send them with `Cache-Control: private` and keep them out of shared caches.

## Related pages

- [`convexVue` options](https://nuxt-convex.onmax.me/api-reference/convex-vue)
- [Realtime and SSR](https://nuxt-convex.onmax.me/convex-patterns/realtime-and-ssr)


## Sitemap

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