Files
clockwork-labs-bot 84cfe5a920 Fix TypeScript table handle casing (#5286)
## 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>
2026-06-26 13:01:35 +00:00
..
2026-02-16 02:21:52 +00:00
2026-06-22 04:08:56 +00:00

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 undici package 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:

  • useTable takes a getter function () => Query<TableDef> instead of a plain value, so the query can be reactive and update when signals change.
  • useTable returns [rows, isReady] where rows is a Solid reactive store and isReady is an accessor function () => boolean.
  • The enabled callback option is a getter () => boolean instead of a plain boolean, allowing it to depend on reactive state.
  • useReducer and useProcedure queue calls made before the connection is ready and flush them once connected.

Developer notes

To run the tests, do:

pnpm build && pnpm test