First Real-Time Page
Render a Convex query during SSR and continue it as a live browser subscription.
Build a small public todo list. The page will render through Nuxt SSR, then stay live through Convex.
Define the schema
import { defineSchema, defineTable } from 'convex/server'
import { v } from 'convex/values'
export default defineSchema({
todos: defineTable({
text: v.string(),
completed: v.boolean(),
createdAt: v.number(),
}).index('by_created', ['createdAt']),
})Add the query
import { query } from './_generated/server'
export const list = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query('todos').withIndex('by_created').order('desc').take(50)
},
})Convex codegen adds api.todos.list to the generated API.
Render the page
<script setup lang="ts">
import { api } from '#convex/api'
const { data: todos, status, error } = await useConvexQuery(api.todos.list, {})
</script>
<template>
<main>
<h1>Todos</h1>
<p v-if="status === 'pending'">Loading todos…</p>
<p v-else-if="error">Could not load todos.</p>
<p v-else-if="todos?.length === 0">No todos yet.</p>
<ul v-else>
<li v-for="todo in todos" :key="todo._id">
{{ todo.text }}
</li>
</ul>
</main>
</template>Start Nuxt in another terminal:
pnpm devVerify SSR
Open the page and inspect its HTML response or disable JavaScript temporarily. Existing todos should appear in the initial HTML.
The query lifecycle is:
SSR HTTP query → Nuxt payload → hydration → WebSocket subscriptionVerify live updates
Open the Convex dashboard data view and insert a todo with:
{
"text": "Live from Convex",
"completed": false,
"createdAt": 1
}The open page updates without a refresh. No Pinia store or manual refetch is involved.
The page is read-only. Add an application write in Add a mutation.