Skip to main content

SSR, Hydration, and Real-Time

Choose whether a live query begins during SSR or only in the browser.

SSR and live updates solve different stages of one page lifecycle.

  • SSR produces useful initial HTML.
  • Hydration reuses that server result in Vue.
  • Browser observation keeps the result current after the page becomes interactive.

Choose where execution begins

RequirementOptionsCost
Initial HTML and live updatesDefaultSSR HTTP query plus browser observation
Browser-only live dataserver: falseEmpty or pending server output, then browser execution
ts
const livePage = useConvexQuery(api.posts.list)

const privateBrowserData = useConvexQuery(
  api.notifications.list,
  {},
  { server: false, auth: 'required' },
)

Why two Convex executions appear

For the default lifecycle, the Convex dashboard may show an HTTP execution from SSR and a WebSocket execution in the browser. The transports have different jobs. Convex can reuse query computation internally, but the module does not describe the two stages as one billable call.

Immediate state and optional await

useConvexQuery returns refs immediately. Its Nuxt return is also a native Promise when navigation or suspense should wait for initial settlement:

ts
const posts = useConvexQuery(api.posts.list)
const settled = await posts

The Promise resolves for a query error, skip, and server: false as well as success. The awaited value is not itself Promise-like; read its refs to determine the outcome.

Hydration is reuse, not a second store

Nuxt owns the payload that crosses the server/browser boundary. Convex owns live client query state. The composable connects them and exposes one Vue view.

Do not add a Pinia copy solely to carry the SSR result. That produces another state owner without improving the lifecycle.

Private data and SSR

SSR can render authenticated data when the request session is available. That is useful for a dashboard and inappropriate for some highly private or user-agent-specific data.

server: false controls rendering location. It is not authorization. The Convex function still enforces access.

Rendering browser-only data

For server: false, render a server-compatible pending state:

vue
<template>
  <NotificationSkeleton v-if="status === 'pending'" />
  <p v-else-if="error">Notifications are unavailable.</p>
  <NotificationList v-else-if="data !== undefined" :items="data" />
</template>

Avoid branching on browser-only globals during the initial render. That causes a hydration mismatch before the query lifecycle can help.