This module uses a hybrid SSR + WebSocket approach that gives you the best of both worlds:
Browser Request
|
v
+------------------+
| Nuxt Server | 1. SSR: Fetch data via HTTP
| (HTTP) | - Fast initial render
+------------------+ - SEO-friendly HTML
|
v
+------------------+
| HTML Response | 2. Hydration: Client receives
| with Data | pre-fetched data (no flash!)
+------------------+
|
v
+------------------+
| Convex Client | 3. Real-time: WebSocket starts
| (WebSocket) | - Live updates from any client
+------------------+ - Automatic sync
| Traditional SPA | This Module |
|---|---|
| Blank page → API call → Content | Content immediately in HTML |
| Loading spinner on navigation | Instant page transitions |
| Manual refetching after mutations | Automatic real-time sync |
| Complex state management | Reactive data binding |
When a user requests a page, Nuxt runs your queries on the server via HTTP:
// This runs on the server during SSR
const { data: posts } = await useConvexQuery(api.posts.list, {})
The server:
The browser receives pre-rendered HTML with data already embedded. Vue hydrates the page without any loading flash because the data is already there.
After hydration, the Convex client opens a WebSocket connection and subscribes to your queries. From this point on:
The server and lazy options control when and how data loads:
| Options | SSR Behavior | Client Navigation | Use Case |
|---|---|---|---|
server: false, lazy: false | No fetch, renders pending state | Blocks navigation until data loads | Default. Client-only data that shouldn't be in SSR HTML (e.g., session data). |
server: false, lazy: true | No fetch, renders pending state | Instant navigation, shows loading state | Non-critical, client-only content. |
server: true, lazy: false | Fetches data, blocks render | Blocks navigation until data loads | Critical content that must be visible immediately. SEO-critical data. |
server: true, lazy: true | Fetches data, blocks render | Instant navigation, shows loading state | Best of both worlds. SSR for initial load, fast client navigation. |
server: false, lazy: false (Default)
const { data } = await useConvexQuery(api.session.get, {})
server: false, lazy: true
const { data, pending } = useConvexQuery(api.session.get, {}, { lazy: true })
server: true, lazy: false
const { data } = await useConvexQuery(api.posts.get, { id }, { server: true })
server: true, lazy: true (Recommended for SEO content)
const { data, pending } = await useConvexQuery(api.posts.get, { id }, { server: true, lazy: true })
Auth state is pre-populated during SSR for zero loading flash:
1. Browser Request
Cookie: session_token=xxx
|
v
2. SSR Plugin (server)
- Read session cookie
- Exchange for JWT via Better Auth
- Fetch user data
- Store in state for hydration
|
v
3. HTML Response
- Contains pre-populated auth state
- User sees authenticated UI immediately
|
v
4. Client Hydration
- Receives auth state (no flash!)
- Initializes ConvexClient with token
- Starts WebSocket subscription
Users see authenticated content immediately without a flash of unauthenticated state.
Understanding the full lifecycle helps debug issues:
useConvexQuery(api.posts.list, {})
|
v
[SSR Phase]
- Runs on Nuxt server
- HTTP request to Convex
- Data embedded in HTML
|
v
[Hydration]
- Client receives data
- Vue hydrates (no fetch!)
- status: 'success'
|
v
[Subscription]
- WebSocket connection opens
- Query subscribes for updates
- Real-time sync begins
|
v
[Live Updates]
- Any client mutates data
- Convex pushes update
- Your UI updates automatically
In your Convex dashboard, you may see queries called twice:
Q posts:list (HTTP API) <- SSR
Q posts:list (WebSocket) <- Client subscription
This is expected. The HTTP call is for SSR (one-time), and the WebSocket is for real-time updates. Convex caches efficiently, so the WebSocket call returns instantly with cached data.
To avoid this duplication:
subscribe: false for data that doesn't need real-time updatesserver: false for client-only data