Guide

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

Choose behavior with await useConvexQuery(...) plus server:

PatternSSR BehaviorClient NavigationUse Case
await useConvexQuery(...)Fetches data and renders with payloadBlocks until data resolvesDefault. Critical page content and SEO paths
await useConvexQuery(..., { server: false })Skips SSR fetchBlocks on client fetchClient-only/session-specific data

Choosing the Right Strategy

await useConvexQuery(...) (Default)

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

await useConvexQuery(..., { server: false })

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

When to Use What

Query vs Paginated Query

  • Is your data a list that grows over time? Use useConvexPaginatedQuery with Load More or infinite scroll.
  • Is it a fixed set of results? Use useConvexQuery.

Mutation vs Action

  • Does it only read/write the Convex database? Use a mutation.
  • Does it call external APIs, send emails, or do heavy computation? Use an action.

Loading Strategy

  • SEO-critical content? Use server: true (default).
  • User-specific data that shouldn't be in HTML? Use server: false.
  • Want deterministic data before render? Use await useConvexQuery.

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:

await 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

Convex Cache vs Pinia

For server-backed entities (posts, tasks, comments, memberships), Convex query state already acts as shared app state:

  1. Query results are cached by key.
  2. Subscribers are deduplicated across components.
  3. Live updates keep all consumers in sync.

You can still use Pinia for UI-only state (modals, local wizard steps, feature flags), but you typically do not need Pinia to mirror Convex database entities.

Next Steps

Fetching Data

Write your first query

Authentication

Add user login

SSR & Hydration

Deep dive into SSR patterns