SSR, Hydration, and Real-Time
Choose which stages of the query lifecycle each page needs.
SSR and real-time updates solve different parts of the same page lifecycle.
- SSR produces useful initial HTML.
- Hydration reuses that server result in Vue.
- A subscription keeps the result current after the page becomes interactive.
Choose the lifecycle
| Requirement | Options | Cost |
|---|---|---|
| Initial HTML and live updates | Defaults | SSR HTTP query plus browser subscription |
| Browser-only live data | server: false | Empty/loading server output, then browser subscription |
| Initial HTML snapshot | subscribe: false | SSR query with no live updates |
| Browser-only snapshot | server: false, subscribe: false | One browser HTTP query |
const livePage = await useConvexQuery(api.posts.list, {})
const privateBrowserData = await useConvexQuery(
api.notifications.list,
{},
{
server: false,
},
)
const reportSnapshot = await useConvexQuery(
api.reports.get,
{},
{
subscribe: false,
},
)Why two Convex executions appear
For the default lifecycle, the Convex dashboard may show an HTTP execution from SSR and a WebSocket execution for the browser subscription.
This is expected. The transports have different jobs. Convex can reuse query computation internally, but the module does not describe the two stages as one billable call.
Awaiting a query
useConvexQuery is async. Awaiting it integrates the first result with Nuxt navigation and suspense.
In subscribe mode, the browser waits for the first WebSocket result. The module default is 10 seconds. Configure convex.defaults.waitTimeoutMs; set it to 0 only when an indefinite wait is acceptable.
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 result.
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:
<template>
<NotificationSkeleton v-if="status === 'pending'" />
<NotificationList v-else-if="data" :items="data" />
<p v-else-if="error">Notifications are unavailable.</p>
</template>Avoid branching on browser-only globals during the initial render. That causes a hydration mismatch before the query lifecycle can help.