---
title: "Server & SSR"
description: "One-shot Convex calls in Nitro, useAsyncQuery for SSR data the Nuxt way, and hydration-safe preloading — a complete port of convex/nextjs."
canonical_url: "https://nuxt-convex-module.dev/guide/server-and-ssr"
---
# Server & SSR

> One-shot Convex calls in Nitro, useAsyncQuery for SSR data the Nuxt way, and hydration-safe preloading — a complete port of convex/nextjs.

These server utilities mirror `convex/nextjs`. They are auto-imported in Nitro server code — server routes, API handlers, and server middleware. In app-side code (pages, components, `useAsyncData`), even when it runs during SSR, import them explicitly from `nuxt-convex-module/server`. All of them read the deployment URL from `runtimeConfig.public.convex.url` — set at build time from `convex.url`, `NUXT_PUBLIC_CONVEX_URL` or the `CONVEX_URL` that `convex dev` writes — or from `NUXT_PUBLIC_CONVEX_URL` where the built app runs.

## One-shot calls

```ts [server/api/messages.get.ts]
import { api } from '#convex/api'

export default defineEventHandler(async () => {
  return await fetchQuery(api.messages.list, {})
})
```

`fetchQuery`, `fetchMutation`, and `fetchAction` run a single Convex call over HTTP (no subscription). They (and `preloadQuery`) accept a trailing options object: `token` — a JWT-encoded OpenID Connect token, the manual path to authenticated calls without Better Auth — plus `url` to override the deployment URL and `skipConvexDeploymentUrlCheck` for self-hosted deployments:

```ts
await fetchMutation(api.messages.send, { body }, { token })
```

Full signatures live in the [API reference](https://nuxt-convex-module.dev/api-reference/reference/server).

## `useAsyncQuery` — SSR data the Nuxt way

<callout icon="i-lucide-sparkles">

Only here — no upstream counterpart. It is `useAsyncData` over a Convex query: the server renders the data, the client keeps it live.

</callout>

`useAsyncQuery` is the one-stop composable for pages that should render with data on the server *and* stay live on the client. It models Nuxt's `useAsyncData`: during SSR the query runs over HTTP and the result is embedded in the Nuxt payload; after hydration the composable upgrades to the WebSocket subscription — no loading flash, no client refetch.

<playground-async-query>



</playground-async-query>

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

const { data: tasks, status, error } = useAsyncQuery(api.tasks.list, { completed: false })
</script>

<template>
  <div v-if="status === 'pending'">Loading…</div>
  <div v-else-if="error">{{ error.message }}</div>
  <TaskList v-else :tasks="tasks" />
</template>
```

It returns Nuxt's familiar `{ data, error, status, refresh }` refs and is awaitable (`await useAsyncQuery(...)` blocks like `useAsyncData`). Compared to `useQuery` — which renders `undefined` during SSR and fetches on the client — `useAsyncQuery` puts real data in the server-rendered HTML. Compared to `preloadQuery` + `usePreloadedQuery`, there's no server route or prop-threading.

Options (all optional, third argument):

- `server` (default `true`) — fetch during SSR; `false` fetches on the client only.
- `lazy` (default `false`) — don't block client-side navigation on the initial fetch.
- `live` (default `true`) — upgrade to the live subscription on the client. With `live: false` you get SSR + hydration *without* a WebSocket — `refresh()` is then the way to get fresh data. With `live: true`, `refresh()` is mainly for retrying after an error; the server pushes updates anyway.
- `key` — the payload/dedup key; defaults to the query name + initial args.
- `token` — a JWT (or a function resolving one) to authenticate the SSR fetch. With the [Better Auth](https://nuxt-convex-module.dev/components/better-auth) integration enabled, the request's prefetched token is used automatically.

Pass `'skip'` as args (or a reactive getter returning it) to pause the query. Reactive args are honored by the live subscription; the payload entry is keyed on the initial args and only matters for first paint.

`refresh({ dedupe? })` re-runs the fetch through `useAsyncData`, with the same `'cancel'` / `'defer'` semantics. `status` is `'idle'` (a lazy query not yet triggered), `'pending'`, `'success'` or `'error'`. Errors from the live subscription land in `error` with `status` set to `'error'` — they are never thrown, unlike `useQuery`.

`useAsyncQuery` is Nuxt-only (it needs `#app`). It is auto-imported; import it explicitly, or its `AsyncQueryReturn` / `AsyncQueryOptions` / `AsyncQueryStatus` / `AsyncQueryData` types, from `nuxt-convex-module/app` — or from `#imports`, where the types are registered too.

For a paginated query, [`useAsyncPaginatedQuery`](https://nuxt-convex-module.dev/guide/pagination#useasyncpaginatedquery-ssr-first-page) does the same for the first page and hands over to `usePaginatedQuery` on the client.

## Preloading for SSR

Preload on the server, hydrate on the client without a flash or a refetch. The `Preloaded` payload is a plain JSON-serializable object designed to cross the server/client boundary — return it from a server route (or `useAsyncData`), then hand it to the consuming component, for example as a prop.

The demo below is rendered from data fetched **on the server**: a Nitro route runs `preloadQuery` (and a one-shot `fetchQuery` for the count), the component picks the payload up with `useFetch`, and `usePreloadedQuery` hydrates it without a flash — then keeps it live over the WebSocket.

<playground-preload>



</playground-preload>

```ts [server/api/messages.get.ts]
import { api } from '#convex/api'

// preloadQuery and fetchQuery are auto-imported in Nitro server code
export default defineEventHandler(async () => {
  const [preloaded, count] = await Promise.all([
    preloadQuery(api.messages.list, {}), // Preloaded payload for the client
    fetchQuery(api.messages.count, {}), // plain one-shot value
  ])
  return { preloaded, count }
})
```

```vue [components/Messages.vue]
<script setup lang="ts">
import type { api } from '#convex/api'
import type { Preloaded } from 'nuxt-convex-module/client'

const props = defineProps<{
  preloaded: Preloaded<typeof api.messages.list>
}>()

const messages = usePreloadedQuery(props.preloaded)
</script>
```

`usePreloadedQuery` renders the preloaded value identically on server and initial client, then takes over as a live subscription — so there's no hydration mismatch. It returns a computed ref: templates unwrap it, script code reads `messages.value`. The payload argument also accepts a ref, computed, or getter — a fresh payload re-subscribes with the new query and args.

`preloadedQueryResult(preloaded)` extracts just the value when you don't need the live handoff. `fetchQuery` is a one-shot HTTP read with no subscription — that's why the count in the demo stays frozen at its render-time value.

## Authenticated server calls

With the [Better Auth](https://nuxt-convex-module.dev/components/better-auth) integration installed, `convexAuth(event)` returns a request-scoped, authenticated helper service (`getToken`, `isAuthenticated`, `fetchAuthQuery`/`fetchAuthMutation`/`fetchAuthAction`, `preloadAuthQuery`, and `handler` — the Better Auth route proxy). It's also auto-imported under its upstream-style name, `convexBetterAuthNuxt`:

```ts [server/api/me.get.ts]
import { api } from '#convex/api'

export default defineEventHandler(async (event) => {
  const { fetchAuthQuery, isAuthenticated } = convexAuth(event)
  if (!await isAuthenticated()) {
    throw createError({ statusCode: 401 })
  }
  return await fetchAuthQuery(api.auth.getAuthUser)
})
```

An optional second argument takes Better Auth's `GetTokenOptions` (e.g. `jwtCache`) plus a `convexSiteUrl` override — it defaults to `runtimeConfig.convex.siteUrl`, then `runtimeConfig.public.convex.siteUrl`, then the `NUXT_PUBLIC_CONVEX_SITE_URL` process variable. See the [API reference](https://nuxt-convex-module.dev/api-reference/reference/better-auth/server) for the full service shape.

<warning>

Authenticated SSR responses embed per-user secrets in the HTML: the server plugin serializes the signed-in user's Convex JWT into the Nuxt payload (`window.__NUXT__`) so the client can take over without a refetch — mirroring upstream's `initialToken` handoff — and `preloadAuthQuery` data rides the same payload. Never let a CDN, reverse proxy, or any shared cache store these pages keyed only on the URL: one user's HTML (token included) would be served to another. The module sets `Cache-Control: private, no-store` on every response where it prefetched a token — don't override it, and keep such routes out of ISR, SWR and prerendering. Details under [auth tokens in SSR payloads](https://nuxt-convex-module.dev/getting-started/security#auth-tokens-in-ssr-payloads).

</warning>


## Sitemap

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