## Compatibility note This PR changes the generated TypeScript table/view handles for snake_case module accessors to the target-language TypeScript accessor casing, but keeps the old snake_case handles as deprecated aliases. Generated TS clients now expose the intended TypeScript accessor casing: - `ctx.db.databaseTree` - `tables.loggedOutPlayer` - `tables.myProgress` The old snake_case handles continue to work and are marked deprecated in the generated type surface: - `ctx.db.database_tree` still works as an alias for `ctx.db.databaseTree` - `tables.logged_out_player` still works as an alias for `tables.loggedOutPlayer` - `tables.my_progress` still works as an alias for `tables.myProgress` The database canonical names stay unchanged. For example, the generated TypeScript handle is camelCase, but the table metadata still has `name: 'logged_out_player'`. ## Remaining API breakage risk This should be non-breaking for normal generated TypeScript client usage, including: - direct table access through `conn.db.snake_case` - callback contexts like `ctx.db.snake_case` - exported query builders like `tables.snake_case` - type inference for deprecated aliases There are still a few edge cases where users may notice an API shape change: - Code that enumerates generated table handles with `Object.keys`, `Object.entries`, `for...in`, or similar will now see both the target-language handle and the deprecated snake_case alias. - Code with pathological table/view accessor collisions may not get every possible alias. The normal case is `logged_out_player` -> generated handle `loggedOutPlayer` plus deprecated alias `logged_out_player`. Pathological cases are shapes like: - `foo_bar` and `fooBar`: both want the generated TypeScript handle `fooBar`, so generated clients cannot provide two distinct `tables.fooBar` entries. - `foo_bar` and some other table/view whose target-language handle is already `foo_bar`: the deprecated `foo_bar` alias for `foo_bar` -> `fooBar` would shadow the other generated handle, so the generator skips that alias. - Code that compares the exact generated `index.ts` text, emitted declaration text, or public type names will see new helper/types such as `DbView`, `Tables`, and the alias metadata. TypeScript modules themselves are not expected to break from this unless they consume generated TypeScript client bindings or depend on exact generated client object keys. ## Terminology The casing proposal uses **canonical name** for the database/internal name. That is language-independent. It uses **accessor name** for the source/module/client-facing identifier that codegen derives language-specific handles from. This PR keeps database canonical names unchanged. It changes the generated TypeScript accessor handles to match TypeScript casing while retaining the old generated handles as deprecated aliases. ## Why The TypeScript code generator was using `table.accessor_name` and `view.accessor_name` directly as object keys in `tablesSchema`. That preserved the raw module accessor spelling instead of applying the target-language `Case::Camel` conversion for TypeScript handles. Per the casing policy, client codegen should use the server accessor name as its source and apply the target-language conversion. ## What changed - Convert generated TypeScript table and view handle keys with `Case::Camel`. - Generate deprecated snake_case aliases for table/view handles when the old generated handle differs from the target-language TypeScript handle. - Keep runtime table metadata and database canonical names unchanged. - Avoid duplicate runtime table definitions. - Add a type-only aliased schema so callback contexts infer deprecated aliases too. - Add regression coverage for TypeScript-cased handles and deprecated aliases. - Update checked-in generated TS bindings and references that change under this fix. Generated code in this repo that changes as a result: - `crates/bindings-typescript/test-app/src/module_bindings/index.ts` - `crates/bindings-typescript/test-react-router-app/src/module_bindings/index.ts` - `crates/bindings-typescript/test-solid-router/src/module_bindings/index.ts` - `crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts` - `demo/Blackholio/client-ts/src/module_bindings/index.ts` - `templates/hangman-react-ts/src/module_bindings/index.ts` - `templates/money-exchange-react-ts/src/module_bindings/index.ts` - `crates/codegen/tests/snapshots/codegen__codegen_typescript.snap` I also updated the corresponding client/test references to the generated TypeScript-cased handles. ## Verification - `cargo fmt --all --check` - `cargo test -p spacetimedb-codegen typescript` - `pnpm --dir crates/bindings-typescript test -- tests/client_query.test.ts tests/db_connection.test.ts` - `pnpm --dir crates/bindings-typescript exec vitest run --typecheck.enabled tests/client_query.test.ts tests/db_connection.test.ts` - `git diff --check` The explicit Vitest typecheck command still reports existing global typecheck errors from unrelated files loaded by the test project, including missing `spacetimesys@2.x` ambient modules and existing test type issues, but the touched alias tests report `Type Errors no errors` and the earlier generated-code `declare override` issue is gone. --------- Signed-off-by: Ryan <r.ekhoff@clockworklabs.io> Co-authored-by: clockwork-labs-bot <clockwork-labs-bot@users.noreply.github.com> Co-authored-by: Zeke Foppa <196249+bfops@users.noreply.github.com> Co-authored-by: Ryan <r.ekhoff@clockworklabs.io>
SpacetimeDB Module Library and SDK
Overview
This repository contains both the SpacetimeDB module library and the TypeScript SDK for SpacetimeDB. The SDK allows you to interact with the database server from a client and applies type information from your SpacetimeDB server module.
Installation
The SDK is an NPM package, thus you can use your package manager of choice like NPM or Yarn, for example:
npm add spacetimedb
You can use the package in the browser, using a bundler like vite/parcel/rsbuild, in server-side applications like NodeJS, Deno, Bun, NextJS, Remix, and in Cloudflare Workers.
NOTE: For usage in NodeJS 18-21, you need to install the
undicipackage as a peer dependency:npm add spacetimedb undici. Node 22 and later are supported out of the box.
Usage
In order to connect to a database you have to generate module bindings for your database.
import { DbConnection, tables } from './module_bindings';
const connection = DbConnection.builder()
.withUri('ws://localhost:3000')
.withDatabaseName('MODULE_NAME')
.onDisconnect(() => {
console.log('disconnected');
})
.onConnectError(() => {
console.log('client_error');
})
.onConnect((connection, identity, _token) => {
console.log(
'Connected to SpacetimeDB with identity:',
identity.toHexString()
);
connection.subscriptionBuilder().subscribe(tables.player);
})
.withToken('TOKEN')
.build();
If you need to disconnect the client:
connection.disconnect();
Typically, you will use the SDK with types generated from SpacetimeDB module. For example, given a table named Player you can subscribe to player updates like this:
connection.db.player.onInsert((ctx, player) => {
console.log(player);
});
Given a reducer called CreatePlayer you can call it using a call method:
connection.reducers.createPlayer();
React Usage
This module also includes React hooks to subscribe to tables under the spacetimedb/react subpath. The React integration is fully compatible with React StrictMode and handles the double-mount behavior correctly (only one WebSocket connection is created).
In order to use SpacetimeDB React hooks in your project, first add a SpacetimeDBProvider at the top of your component hierarchy:
const connectionBuilder = DbConnection.builder()
.withUri('ws://localhost:3000')
.withDatabaseName('MODULE_NAME')
.withLightMode(true)
.onDisconnect(() => {
console.log('disconnected');
})
.onConnectError(() => {
console.log('client_error');
})
.onConnect((conn, identity, _token) => {
console.log(
'Connected to SpacetimeDB with identity:',
identity.toHexString()
);
conn.subscriptionBuilder().subscribe(tables.player);
})
.withToken('TOKEN');
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<SpacetimeDBProvider connectionBuilder={connectionBuilder}>
<App />
</SpacetimeDBProvider>
</React.StrictMode>
);
One you add a SpacetimeDBProvider to your hierarchy, you can use SpacetimeDB React hooks in your render function:
function App() {
const conn = useSpacetimeDB<DbConnection>();
const { rows: messages } = useTable<DbConnection, Message>('message');
...
}
SolidJS Usage
This module also includes SolidJS primitives to subscribe to tables under the spacetimedb/solid subpath. The SolidJS integration uses Solid's fine-grained reactivity system (createSignal, createStore, createMemo, createComputed) for optimal rendering performance. Reactive updates are scoped to only the data that actually changed.
In order to use SpacetimeDB SolidJS primitives in your project, first add a SpacetimeDBProvider at the top of your component hierarchy:
import { SpacetimeDBProvider } from 'spacetimedb/solid';
import { DbConnection, tables } from './module_bindings';
const connectionBuilder = DbConnection.builder()
.withUri('ws://localhost:3000')
.withDatabaseName('MODULE_NAME')
.withLightMode(true)
.onDisconnect(() => {
console.log('disconnected');
})
.onConnectError(() => {
console.log('client_error');
})
.onConnect((conn, identity, _token) => {
console.log(
'Connected to SpacetimeDB with identity:',
identity.toHexString()
);
conn.subscriptionBuilder().subscribe(tables.player);
})
.withToken('TOKEN');
render(
() => (
<SpacetimeDBProvider connectionBuilder={connectionBuilder}>
<App />
</SpacetimeDBProvider>
),
document.getElementById('root')!
);
Once you add a SpacetimeDBProvider to your hierarchy, you can use the SpacetimeDB SolidJS primitives in your components:
import {
useSpacetimeDB,
useTable,
useReducer,
useProcedure,
} from 'spacetimedb/solid';
function App() {
// Access the connection state (identity, token, connection error, etc.)
const conn = useSpacetimeDB();
// Subscribe to a table — returns a reactive store of rows and an isReady accessor
const [rows, isReady] = useTable(() => tables.message);
// Subscribe to a filtered view
const [onlineUsers, onlineReady] = useTable(
() => tables.user.where(r => r.online.eq(true)),
{
onInsert: row => console.log('User came online:', row),
onDelete: row => console.log('User went offline:', row),
}
);
// Call a reducer — queues calls made before the connection is ready
const sendMessage = useReducer(reducers.sendMessage);
// Call a procedure — queues calls made before the connection is ready
const getResult = useProcedure(procedures.getSomeResult);
return (
<div>
<Show when={isReady()} fallback={<p>Loading...</p>}>
<p>{rows.length} messages</p>
<For each={rows}>{row => <div>{row.text}</div>}</For>
</Show>
<button onClick={() => sendMessage('hello')}>Send</button>
</div>
);
}
Key differences from the React API:
useTabletakes a getter function() => Query<TableDef>instead of a plain value, so the query can be reactive and update when signals change.useTablereturns[rows, isReady]whererowsis a Solid reactive store andisReadyis an accessor function() => boolean.- The
enabledcallback option is a getter() => booleaninstead of a plain boolean, allowing it to depend on reactive state. useReduceranduseProcedurequeue calls made before the connection is ready and flush them once connected.
Developer notes
To run the tests, do:
pnpm build && pnpm test