Integrations

Nuxt

Load rows through Nitro and keep Vue components live after hydration.

On this page

Load tasks from a Nitro route with Nuxt’s useFetch, then subscribe with @hulla/db-vue to keep the list current in the browser.

Complete the shared task setup first. The example uses Nuxt 4’s app/ layout and Vue 3.5.

Set database.client in db.config.ts before generating the application:

TypeScript
// Inside defineConfig({ ... })
database: {
  server: { url: process.env.DATABASE_URL! },
  client: { url: process.env.PUBLIC_DATABASE_SERVICE_URL!, credentials: 'include' },
},

Generation copies only these public client settings into browser bindings. Importing callables requires no setup call and opens no network connection. Use a root-relative URL such as /db when your application proxies the service on the same origin. The URL is selected at generation time; regenerate when changing environments.

For authenticated views, call generated setAuth from the provider’s browser-only session listener. Never put a request’s identity in a shared server module. Component cleanup releases its subscriptions; it does not dispose the shared application runtime.

The route returns the same selected rows as the browser declaration. A public route is appropriate for this public example:

TypeScript
// server/api/tasks.get.ts
import { db } from '../../db/_generated/server'
import { readTasks } from '../../db/task-data'

export default defineEventHandler(() => readTasks(db))

Use Nuxt’s SSR-aware fetch to transfer its result to the page without doing a second page-loader request during hydration:

vue
<!-- app/pages/index.vue -->
<script setup lang="ts">
const { data, error } = await useFetch('/api/tasks')
</script>

<template>
  <p v-if="error">Could not load the initial tasks.</p>
  <Tasks v-else-if="data" :initial-tasks="data" />
</template>

Nuxt discovers the component below from app/components/. Its live query still fetches after mount: the page payload is a display fallback, not a confirmed live result.

vue
<!-- app/components/Tasks.vue -->
<script setup lang="ts">
import type { Output } from '@hulla/db'
import { useMutation, useQuery } from '@hulla/db-vue'
import { create, list } from '../../db/_generated/client/tasks'

const props = defineProps<{ initialTasks: Output<typeof list> }>()
const tasks = useQuery(list, { initialData: props.initialTasks })
const add = useMutation(create)
const addTask = () => {
  void add({ title: 'New task' })
}
</script>

<template>
  <p v-if="tasks.status === 'error'">Could not load tasks.</p>
  <p v-if="add.state.value.status === 'error'">Could not save the task.</p>
  <ul>
    <li v-for="task in tasks.data" :key="task.id">{{ task.title }}</li>
  </ul>
  <button @click="addTask">Add task</button>
</template>

Vue unwraps the query ref in templates; script code uses tasks.value. Mutation state is add.state.value in script. For queries with input, pass a getter such as () => ({ completed: filter.value }) so the binding tracks reactive changes.

Authorize private Nitro routes with your provider before direct reads. Obtain request headers through toWebRequest(event).headers from h3; Nuxt forwards request cookies for relative server-side useFetch. Return a verified account key with the rows and pass it as initialAuthKey to the binding. Declarations still require their own guards and filters. Avoid cached Nitro handlers or shared route rules for private results. See the shared auth setup.

Use ordinary Nitro routes for server orchestration and db.transaction for atomic groups of direct writes. A browser mutation or direct server write updates the live query independently of Nuxt’s refreshNuxtData. The latter refreshes Nuxt loader data when you need it, rather than managing the database subscription.

Choose a Node-compatible Nitro preset, retain the external database package and its native executables, and keep the persistent service available for streaming. See deployment requirements; an edge preset cannot execute the direct server database.