Integrations
SvelteKit
Combine server loaders with reactive Svelte queries and mutations.
On this page
Load the first tasks in a server load function, then subscribe with @hulla/db-svelte to update the page when rows change.
Complete the shared task setup first. The example uses SvelteKit’s client init hook and Svelte 5 runes, with db/ at the project root.
Set database.client in db.config.ts before generating the application:
// 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.
A +page.server.ts module stays on the server. A universal +page.ts can also run
in the browser and must not import the generated server database:
// src/routes/+page.server.ts
import { db } from '../../db/_generated/server'
import { readTasks } from '../../db/task-data'
export const load = async () => ({ initialTasks: await readTasks(db) })Forward the loaded page data to a component:
<!-- src/routes/+page.svelte -->
<script lang="ts">
import Tasks from '$lib/Tasks.svelte'
import type { PageData } from './$types'
let { data }: { data: PageData } = $props()
</script>
<Tasks initialTasks={data.initialTasks} />The live component displays those rows during SSR and subscribes after mount:
<!-- src/lib/Tasks.svelte -->
<script lang="ts">
import type { Output } from '@hulla/db'
import { createMutation, createQuery } from '@hulla/db-svelte'
import { create, list } from '../../db/_generated/client/tasks'
let { initialTasks }: { initialTasks: Output<typeof list> } = $props()
const tasks = createQuery(list, { initialData: initialTasks })
const add = createMutation(create)
const addTask = () => { void add({ title: 'New task' }) }
</script>
{#if tasks.status === 'error'}<p>Could not load tasks.</p>{/if}
{#if add.state.status === 'error'}<p>Could not save the task.</p>{/if}
<ul>{#each tasks.data ?? [] as task (task.id)}<li>{task.title}</li>{/each}</ul>
<button onclick={addTask}>Add task</button>Read tasks.data, tasks.status and add.state reactively. Destructuring the
getters into ordinary variables loses updates. Queries with input accept a
getter, such as () => ({ completed: filter }), to read rune state.
SvelteKit form actions and server endpoints can use direct db operations.
Authenticate event.request.headers, enforce ownership and group writes with
db.transaction when rollback must apply to the whole operation. Server-loader
reruns and SvelteKit invalidation are separate from the live query’s updates.
For protected SSR data, return a verified account key with the rows and pass it
as initialAuthKey to createQuery; update the same key from the browser auth
listener. Guard query/mutation declarations independently. Disable prerendering
and shared response caching for private request-time pages. See the
shared authentication boundary.
Deploy using a compatible Node/Bun adapter and include the database package’s native assets. Browser-only/static deployments can still use live declarations, but cannot execute these direct server loaders. See deployment.