Integrations
Full-stack framework recipes
Render a database snapshot on the server, then keep the browser view live.
On this page
Load the first rows on the server so the page can render immediately. Then subscribe in the browser to keep the view current as the database changes.
Choose your framework below for the files and code to connect those two steps.
| Framework | UI package | Recipe |
|---|---|---|
| Next.js App Router | @hulla/db-react | Server Components and client hooks |
| Nuxt 4 | @hulla/db-vue | Nitro routes and Vue components |
| SvelteKit with Svelte 5 | @hulla/db-svelte | Server loaders and reactive components |
| SolidStart 2 | @hulla/db-solid | Server functions and Solid resources |
| Astro with React | @hulla/db-react | Request-time pages and live islands |
Use the normal framework scaffold and install @hulla/db@0.1.0 plus the matching
UI package from npm. No separate Next, Nuxt, SvelteKit, SolidStart or Astro database package
is required. For Astro, install its normal integration for the chosen UI framework.
These recipes use the same public task table as the quickstart. Complete the database setup
and persistent browser service setup first. Put db/ and
db.config.ts at the project root, including when the framework uses src/ or
Nuxt’s app/ directory. Keep DATABASE_URL private.
// db/schema.ts
import { defineSchema, defineTable, v } from '@hulla/db/schema'
export default defineSchema({
tasks: defineTable({
title: v.string(),
completed: v.boolean().default(false),
}),
})Generate once to create the schema-bound constructors. Then add a database helper that both the server page and the browser query can reuse:
// db/task-data.ts
import type { ReadDatabase } from './_generated/types'
export const readTasks = (database: ReadDatabase) =>
database.select('tasks', ['id', 'title']).orderBy('title').limit(50)Declare the browser functions. Ordinary exported helpers are not endpoints:
// db/tasks.ts
import { z } from 'zod'
import { query, mutation } from './_generated/functions'
import { readTasks } from './task-data'
export const list = query((ctx) => readTasks(ctx.db))
export const create = mutation({
input: z.object({ title: z.string().min(1).max(200) }),
handler: (ctx, input) => ctx.db.insert('tasks', input),
})Regenerate the bindings, initialize storage explicitly, and start the persistent
service. The examples use your service’s public URL and cookie credentials. In
development that can be http://localhost:3001; configure the browser app’s exact
origin in service.origins. Production requires HTTPS. A reverse proxy can expose
the service alongside the application; otherwise configure cross-origin cookie
and CORS behavior deliberately. The service URL contains no database credentials.
Opening a recipe’s page renders the selected rows. Clicking Add task commits a
browser mutation, then the live query updates. A server route/action using
db.insert('tasks', ...) updates the same subscriptions. The server snapshot
itself is never a subscription, and awaiting a mutation confirms its commit
without waiting for every view to refresh.
The public example intentionally has no session checks. Before adapting it for private rows, authorize both the direct server read and every query/mutation declaration, using the same row/tenant filters. A browser component, hidden button or server-rendered login check cannot protect a public declaration.
Use your provider’s server verification method with the framework’s request
headers. Better Auth’s concrete call is auth.api.getSession({ headers }):
| Framework boundary | Request headers |
|---|---|
| Next server page/action | await headers() from next/headers |
| Nuxt Nitro handler | toWebRequest(event).headers from h3 |
| SvelteKit server loader/action | event.request.headers |
| SolidStart server function | getRequestEvent()?.request.headers from solid-js/web |
| Astro page/endpoint | Astro.request.headers / context.request.headers |
Reject missing sessions before direct db operations. Return only authorized
selected rows and an initialAuthKey built from the verified user/account/tenant.
Pass that key alongside initialData to the UI binding. Use the same key in the
browser provider listener’s setAuth(...); do not obtain it from an untrusted
query parameter. On sign-out use setAuth(null). Credentials still travel
separately, and the service independently verifies them on live delivery.
For example, change a binding’s options to
{ initialData: initialTasks, initialAuthKey: verifiedAccountKey }. The key does
not authorize anything; it prevents an SSR fallback from being reused for another
account. Protected responses must also avoid shared/CDN/framework caching. See
the complete Better Auth setup for storage, the config verifier
and a framework-independent session subscription. Put its browser subscription
in the startup hook shown by your framework recipe, not in each task component.
Use server routes/actions for application orchestration and direct database work.
Use the query/mutation declarations when the browser needs database calls and
reactive results. Share ordinary helpers taking ReadDatabase/WriteDatabase
when both paths need the same database logic. Group server writes with
db.transaction; keep external effects outside retryable declarations.
initialData is a stale, observer-local fallback. It renders without a network
request, is replaced by the first live result, and retires on input/account
changes. It never enters the authoritative shared cache. Rows in these recipes
contain strings so every framework can serialize them directly; codecs, Dates
and other representations must follow the framework’s serialization rules.
The UI bindings also support live pagination. Pass the cursor mapper to the paginated hook; for SSR, pass its flattened initial rows rather than treating a server cursor as a confirmed browser subscription.
Direct server database access requires Node/Bun, PostgreSQL connectivity and
permission to spawn the packaged native executables. Choose a compatible
framework adapter/preset; an edge isolate cannot run direct db calls. The
browser can be served from any suitable host, including a static host if it does
not need request-time server reads.
Generate during the build with Bun, package the native files for the target
platform, and run the persistent service where streaming responses are supported.
Bundled framework output must retain those native assets; externalizing a package
alone does not copy its executables. Share the server db across requests and
close it at application shutdown, not at the end of every request. See
deployment and packaging for assets, TLS and runtime requirements.