Integrations

Astro

Render request-time rows and hydrate an island for live database updates.

On this page

Render tasks in an Astro page, then keep the list live with a React island and @hulla/db-react. The server supplies the first rows; the island subscribes to later changes.

Complete the shared task setup first. This example uses Astro’s React integration. Other island frameworks use their matching database binding.

Use a compatible server adapter. For a Node standalone deployment:

js
// astro.config.mjs
import { defineConfig } from 'astro/config'
import node from '@astrojs/node'
import react from '@astrojs/react'

export default defineConfig({
  output: 'server',
  adapter: node({ mode: 'standalone' }),
  integrations: [react()],
  vite: { ssr: { external: ['@hulla/db'] } },
})

Install the framework’s @astrojs/node and @astrojs/react integrations alongside React and the database packages. The server-rendered page runs its frontmatter per request; static prerendered pages instead capture data at build time and must not contain a private user’s result.

Read the selected rows in frontmatter, then use client:load to enable browser observation without losing the server-rendered HTML:

astro
---
// src/pages/index.astro
import { db } from '../../db/_generated/server'
import { readTasks } from '../../db/task-data'
import { Tasks } from '../components/Tasks'

const initialTasks = await readTasks(db)
---

<html lang="en">
  <head>
    <title>Live tasks</title>
  </head>
  <body>
    <Tasks initialTasks={initialTasks} client:load />
  </body>
</html>

The React component shares the same hooks as other React applications:

tsx
// src/components/Tasks.tsx
import type { Output } from '@hulla/db'
import { useMutation, useQuery } from '@hulla/db-react'
import { create, list } from '../../db/_generated/client/tasks'

export function Tasks({ initialTasks }: { initialTasks: Output<typeof list> }) {
  const tasks = useQuery(list, { initialData: initialTasks })
  const add = useMutation(create)
  return (
    <>
      {tasks.status === 'error' && <p>Could not load tasks.</p>}
      {add.state.status === 'error' && <p>Could not save the task.</p>}
      <ul>
        {tasks.data?.map((task) => (
          <li key={task.id}>{task.title}</li>
        ))}
      </ul>
      <button
        onClick={() => {
          void add({ title: 'New task' })
        }}>
        Add task
      </button>
    </>
  )
}

Without a client hydration directive, the island remains HTML and never subscribes. client:only skips its server render; use client:load for this SSR handoff. Other hydration timings delay when live observation begins. Multiple React islands can observe the same generated query and share the database cache without sharing a React context.

Verify Astro.request.headers through your provider before a private read, or use a verified identity installed by application middleware in Astro.locals. Pass the resulting account key as initialAuthKey to the binding along with its rows. Do not prerender or publicly cache private pages. Query/mutation declarations still need their own checks; see the shared auth setup.

Astro actions and API endpoints can use direct db operations and transactions; their writes refresh live islands. The Astro page/frontmatter does not itself become live. Keep native assets with the server output, use a host permitting subprocesses, and deploy the persistent streaming service separately. See deployment requirements.