Getting Started

Core Concepts

Understand how Nuxt and Convex work together with SSR and real-time updates.

The Hybrid Model

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

Why This Matters

Traditional SPAThis Module
Blank page → API call → ContentContent immediately in HTML
Loading spinner on navigationInstant page transitions
Manual refetching after mutationsAutomatic real-time sync
Complex state managementReactive data binding

The Data Flow

1. Server-Side Rendering (SSR)

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:

  • Fetches data from Convex via HTTP
  • Renders the complete HTML with data
  • Embeds the data in the response for hydration

2. Client Hydration

The browser receives pre-rendered HTML with data already embedded. Vue hydrates the page without any loading flash because the data is already there.

3. Real-time Subscription

After hydration, the Convex client opens a WebSocket connection and subscribes to your queries. From this point on:

  • Changes from any client sync automatically
  • No manual refetching needed
  • UI stays in sync with the database

Loading Strategy Matrix

The server and lazy options control when and how data loads:

OptionsSSR BehaviorClient NavigationUse Case
server: false, lazy: falseNo fetch, renders pending stateBlocks navigation until data loadsDefault. Client-only data that shouldn't be in SSR HTML (e.g., session data).
server: false, lazy: trueNo fetch, renders pending stateInstant navigation, shows loading stateNon-critical, client-only content.
server: true, lazy: falseFetches data, blocks renderBlocks navigation until data loadsCritical content that must be visible immediately. SEO-critical data.
server: true, lazy: trueFetches data, blocks renderInstant navigation, shows loading stateBest of both worlds. SSR for initial load, fast client navigation.

Choosing the Right Strategy

server: false, lazy: false (Default)

const { data } = await useConvexQuery(api.session.get, {})
  • No SSR fetch
  • Client navigation blocked until data loads
  • Use for: Client-only data, session info, user-specific data

server: false, lazy: true

const { data, pending } = useConvexQuery(api.session.get, {}, { lazy: true })
  • No SSR fetch (avoids hydration mismatch for user-specific data)
  • Client navigation is instant (shows loading state)
  • Use for: Non-critical, client-only content

server: true, lazy: false

const { data } = await useConvexQuery(api.posts.get, { id }, { server: true })
  • SSR waits for data before sending HTML
  • Client navigation waits for data before showing page
  • Use for: Main page content, SEO-critical data

server: true, lazy: true (Recommended for SEO content)

const { data, pending } = await useConvexQuery(api.posts.get, { id }, { server: true, lazy: true })
  • SSR waits for data before sending HTML
  • Client navigation is instant (shows loading state)
  • Use for: Most content pages where you want fast navigation with SSR

Authentication Flow

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.

Query Lifecycle

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

Duplicate Queries in Dashboard

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:

  • Use subscribe: false for data that doesn't need real-time updates
  • Use server: false for client-only data

Next Steps

Fetching Data

Write your first query

Authentication

Add user login

SSR & Hydration

Deep dive into SSR patterns