Skip to content

Database

@delightstack/database is a type-safe data layer for Cloudflare Durable Objects with built-in full-text & vector search, runtime validation, automatic migrations, and a reactive Svelte 5 client.

  • Declarative schema — fluent TypeScript API; field types and validators inferred at compile time.
  • Full-text & vector search — built-in Orama; mark fields .searchable().
  • Automatic migrations — new columns are added when you change the schema; no migration files.
  • Zod validation — every create()/update() is validated at runtime.
  • Transactions — batch operations atomically.
  • Incremental syncsync() returns only changes since a timestamp for client mirroring.
  • Reactive clientDatabaseClient gives reactive entity state, live search, IndexedDB caching, and optimistic updates, all off-thread in a SharedWorker.
Terminal window
pnpm add @delightstack/database
ImportUse
@delightstack/databaseDatabase — the schema builder
@delightstack/database/workerDatabaseServer — the Durable Object class
@delightstack/database/servercreateDatabaseHandle, SqlServer (raw SQL)
@delightstack/database/clientDatabaseClient — reactive Svelte 5 client
import { Database } from '@delightstack/database';
const postsTable = Database.table('post', (schema) => ({
id: schema.primaryKey(),
title: schema.string().searchable(),
body: schema.string().searchable(),
author_id: schema.foreignKey({ type: 'string', table: 'user', column: 'id', on_delete: 'CASCADE' }),
tags: schema.array(schema.string()).searchable().optional(),
published: schema.boolean().default(false),
created_at: schema.string().datetime(),
}));

Root-level scalars become real SQLite columns; nested objects/arrays are serialized into a JSON catch-all column and transparently rehydrated on read.

Subclass DatabaseServer with your tables and register it in wrangler.toml (see Architecture):

import { DatabaseServer } from '@delightstack/database/worker';
export class OrgDatabaseServer extends DatabaseServer {
tables = [postsTable /* … */];
}
const db = platform.env.DB.get(platform.env.DB.idFromName(org_id));
await db.create('post', { title: 'Hello', body: '', author_id: 'u_1' });
const results = await db.search('post', { term: 'hello' }); // full-text
const recent = await db.list('post', { limit: 20 });

For declarative CRUD routes, mount createDatabaseHandle() in your SvelteKit handle.

<script>
import { DatabaseClient } from '@delightstack/database/client';
const db = new DatabaseClient({ tables, db_name: `app:${org_id}`, fetch });
const posts = db.entity('post'); // reactive $state
</script>
{#each posts.list as post}
<article>{post.title}</article>
{/each}

Every entity carries its table’s form wiring (entity.form.field / entity.form.schema). Hand the entity to the components Form and spread the field props — values, validation, saving, and submit state are all handled:

<script>
const post = $derived(db.entity('post', post_id)); // omit the id to create
</script>
<Form entity={post} onsaved={() => goto('/posts')}>
<Input {...post.form.field.title} />
<Button type="submit">Save</Button>
</Form>

See Working with Forms for the full pattern.