Skip to main content

First real-time page

Render a Convex query on the server and keep it live in the browser.

Build a public todo list. Nuxt renders it on the server. Convex then keeps it live in the browser.

Keep better-convex convex dev running from the installation page. It uploads each Convex file when you save it.

Define the schema

convex/schema.ts
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

convex/todos.ts
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)
  },
})

When you save, Convex generates api.todos.list.

Render the page

app/pages/index.vue
<script setup lang="ts">
import { api } from '#convex/api'

const { data: todos, status, error } = 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>

useConvexQuery returns refs. data is undefined until the first result arrives. status is 'pending', 'success', 'error', or 'idle'.

Start Nuxt in a second terminal:

bash
pnpm exec nuxt dev --dotenv .env.local

Open http://localhost:3000. The page shows "No todos yet."

See the live update

  1. Open the Convex dashboard and go to Data, then todos.
  2. Click Add documents and insert:

    json
    { "text": "Live from Convex", "completed": false, "createdAt": 1 }
  3. Look at the open page. The todo appears without a reload.

See the server rendering

Reload the page and view its source (View Page Source in the browser). The todo text is in the HTML. Nuxt ran the query on the server before it sent the page.

The same ref then subscribes over a WebSocket, so later changes arrive live. SSR, hydration, and real-time explains this in detail.

Next, add a mutation so the page can write data.