A type-safe, reactive SpacetimeDB client library for Svelte 5, built with runes. This library is based on the React useTable implementation provided by SpacetimeDB.
npm install spacetimedb-runes
This library requires:
svelte: ^5.0.0spacetimedb: ^1.8.0 (automatically installed)First, establish a connection to your SpacetimeDB instance and wrap your app with the SpacetimeDBContext component.
src/routes/+layout.svelte)<script lang="ts">
import { SpacetimeDB, SpacetimeDBContext } from 'spacetimedb-runes';
import { DbConnection } from '@module_bindings';
import type { Identity } from 'spacetimedb';
import { onMount } from 'svelte';
let connection = $state<DbConnection | null>(null);
onMount(() => {
const onConnect = (conn: DbConnection, identity: Identity, token: string) => {
SpacetimeDB.status.set('connected');
SpacetimeDB.authToken.current = token;
// Set up reducer callbacks
conn.reducers.onSendMessage(() => {
// Message sent callback
});
};
const onDisconnect = () => {
SpacetimeDB.status.set('disconnected');
};
const onConnectError = () => {
SpacetimeDB.status.set('error');
};
const connectionBuilder = DbConnection.builder()
.withUri('ws://localhost:3000')
.withModuleName('your-module-name')
.withToken(SpacetimeDB.authToken.current || undefined)
.onConnect(onConnect)
.onDisconnect(onDisconnect)
.onConnectError(onConnectError);
// Set status to 'connecting' before building the connection
// This helps TableQuery properly setup subscriptions when the connection becomes active
SpacetimeDB.status.set('connecting');
connection = connectionBuilder.build();
});
let status = SpacetimeDB.status;
let { children } = $props();
</script>
{#if $status === 'connecting'}
<div class="status-info">Connecting to SpacetimeDB...</div>
{/if}
{#if $status === 'disconnected'}
<div class="status-warning">
Disconnected from SpacetimeDB. Are you already connected from another tab?
</div>
{/if}
{#if $status === 'error'}
<div class="status-error">An error occurred while connecting to SpacetimeDB.</div>
{/if}
{#if connection && $status === 'connected'}
<SpacetimeDBContext {connection}>
{@render children()}
</SpacetimeDBContext>
{:else}
<p>Initializing connection...</p>
{/if}
<style>
.status-info,
.status-warning,
.status-error {
position: fixed;
top: 0;
left: 0;
width: 100%;
padding: 0.5rem;
text-align: center;
z-index: 1000;
}
.status-error {
background-color: tomato;
color: white;
}
.status-warning {
background-color: orange;
color: black;
}
.status-info {
background-color: deepskyblue;
color: white;
}
</style>
Use the TableQuery class to subscribe to table changes:
<script lang="ts">
import { TableQuery } from 'spacetimedb-runes';
const users = new TableQuery('User');
</script>
{#if users.state === 'loading'}
<p>Loading users...</p>
{:else}
<ul>
{#each users.rows as user}
<li>{user.name}</li>
{/each}
</ul>
{/if}
For a complete working example of spacetimedb-runes in action, check out this game project: spacetime-game (It's based on the Unreal tutorial for SpacetimeDB)
Here are some helpful SpacetimeDB CLI commands you can add to your package.json scripts:
{
"scripts": {
"generate": "spacetime generate --lang typescript --out-dir ./src/module_bindings --project-path ../spacetimedb",
"compile": "cd ../spacetimedb && spacetime build",
"publish": "spacetime publish --server local --project-path ../spacetimedb your-module-name"
}
}
generate - Generates TypeScript client bindings from your SpacetimeDB modulecompile - Builds your SpacetimeDB modulepublish - Publishes your module to a SpacetimeDB server (local or remote)Note: Adjust the paths (../spacetimedb, ./src/module_bindings) and module name (your-module-name) to match your project structure.
To get full type safety and autocomplete for your database schema, you need to augment the Register interface using TypeScript's module augmentation. This can be done in your src/app.d.ts file or any other .d.ts file that TypeScript picks up.
src/app.d.tsimport type { DbConnection } from '@module_bindings';
declare global {
namespace App {
// ... other App interfaces
}
}
// Augment the spacetimedb-runes Register interface
declare module 'spacetimedb-runes' {
interface Register {
connection: DbConnection;
}
}
export {};
Important: Replace DbConnection with your actual generated connection type from SpacetimeDB. The @module_bindings import should point to your SpacetimeDB generated client code.
@module_bindings AliasThe examples in this README use @module_bindings to import SpacetimeDB generated client code. To configure this alias in your SvelteKit project, add it to your svelte.config.js:
const config = {
kit: {
alias: {
'@module_bindings': 'src/module_bindings'
// ... other aliases
}
}
};
This allows you to import your SpacetimeDB generated types and connection from @module_bindings instead of using relative paths.
This enables full autocomplete for:
TableQueryThe library provides a callback-based API for building where clauses with full type safety:
<script lang="ts">
import { TableQuery } from 'spacetimedb-runes';
// Filter users where isActive = true
const activeUsers = new TableQuery('User', ({ where, eq }) => where(eq('isActive', true)));
// Complex queries with AND/OR
const complexQuery = new TableQuery('User', ({ where, eq, and, or }) =>
where(
and(eq('role', 'admin'), or(eq('department', 'Engineering'), eq('department', 'Product')))
)
);
</script>
The where clause callback receives these helper functions:
eq(column, value) - Equality check (column = value)and(...expressions) - Logical ANDor(...expressions) - Logical ORwhere(expression) - Wraps the final expressionType Safety: Column names are autocompleted based on your table schema, and values are type-checked against the column type.
TableQuery supports four lifecycle callbacks to react to database changes:
onInsertCalled when a new row is inserted that matches your query:
<script lang="ts">
const users = new TableQuery('User', undefined, {
onInsert: (row) => {
console.log('New user added:', row.name);
}
});
</script>
onUpdateCalled when a row that matches your query is updated:
<script lang="ts">
const users = new TableQuery('User', undefined, {
onUpdate: (oldRow, newRow) => {
console.log(`User ${oldRow.name} updated to ${newRow.name}`);
}
});
</script>
onDeleteCalled when a row that matches your query is deleted:
<script lang="ts">
const users = new TableQuery('User', undefined, {
onDelete: (row) => {
console.log('User deleted:', row.name);
}
});
</script>
onInitialSnapshotImportant: This callback runs only once when the subscription first receives the initial snapshot of existing data from the database.
Use this when you need to perform operations on existing data, since onInsert, onUpdate, and onDelete only fire for new changes that occur after the subscription is established.
<script lang="ts">
const users = new TableQuery('User', undefined, {
onInitialSnapshot: (rows) => {
console.log(`Loaded ${rows.length} existing users from database`);
// Example: Initialize local state based on existing data
const userMap = new Map(rows.map((u) => [u.id, u]));
},
onInsert: (row) => {
console.log('NEW user inserted:', row);
}
});
</script>
Lifecycle Flow:
TableQuery subscribes to the databaseonInitialSnapshot fires onceonInsert firesonUpdate firesonDelete firesThe where clause callbacks ensure type safety while building complex queries:
<script lang="ts">
import { TableQuery } from 'spacetimedb-runes';
const query = new TableQuery(
'Task',
({ where, eq, and }) => where(and(eq('status', 'active'), eq('assignedTo', currentUserId))),
{
onInitialSnapshot: (tasks) => {
console.log(`You have ${tasks.length} active tasks`);
},
onInsert: (task) => {
showNotification(`New task assigned: ${task.title}`);
},
onUpdate: (oldTask, newTask) => {
if (oldTask.status !== newTask.status) {
console.log(`Task ${newTask.title} status changed`);
}
}
}
);
</script>
If you need manual control over the lifecycle, use the destroy() method:
<script lang="ts">
import { TableQuery } from 'spacetimedb-runes';
import { onDestroy } from 'svelte';
const users = new TableQuery('User');
onDestroy(() => {
users.destroy();
});
</script>
Monitor connection status reactively:
<script lang="ts">
import { SpacetimeDB } from 'spacetimedb-runes';
let status = SpacetimeDB.status;
</script>
<div>
Status: {$status}
</div>
Status values: 'disconnected', 'connecting', 'connected', 'error'
SpacetimeDBStatic utilities for managing the SpacetimeDB connection:
SpacetimeDB.status - Writable store tracking connection statusSpacetimeDB.authToken - Persisted state for authentication tokenSpacetimeDB.getContext<Connection>() - Retrieve connection from Svelte contextSpacetimeDB.setContext(connection) - Set connection in Svelte contextTableQuery<TableName, RowType>Reactive class for subscribing to database tables:
Constructor:
new TableQuery(
tableName: TableName,
whereClause?: (helpers) => Expression,
callbacks?: UseQueryCallbacks<RowType>
)
Properties:
rows: readonly RowType[] - Reactive array of matching rowsstate: 'loading' | 'ready' - Current subscription stateMethods:
destroy(): void - Cleanup subscriptions and listenersUseQueryCallbacks<RowType>interface UseQueryCallbacks<RowType> {
onInsert?: (row: RowType) => void;
onDelete?: (row: RowType) => void;
onUpdate?: (oldRow: RowType, newRow: RowType) => void;
onInitialSnapshot?: (rows: readonly RowType[]) => void;
}
Built with Svelte 5 and SpacetimeDB.