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
| Requirement | Options | Cost |
|---|---|---|
| Initial HTML and live updates | Default | SSR HTTP query plus browser observation |
| Browser-only live data | server: false | Idle server output, then browser execution |
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 the first result:
const posts = useConvexQuery(api.posts.list)
const settled = await postsThe 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.
The SSR payload is reused only while the page hydrates. After a client navigation, a query goes live instead of reusing an old payload.
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, SSR renders status: 'idle' with pending: false, as Nuxt useAsyncData does. Hydration keeps idle. The query becomes pending when the browser starts it after hydration. Render one skeleton for both states:
<template>
<NotificationSkeleton v-if="status === 'idle' || 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.