Codegen
SpacetimeDB remains the source of truth for native client generation. Build the
module, then run spacetime generate against the built bundle:
{ "scripts": { "build": "spacetime build --module-path .", "generate-client": "spacetime generate --lang typescript --js-path ./dist/bundle.js --out-dir ./src/generated --yes" }}The spacetime CLI has to match the spacetimedb package the module depends
on. spacetime build and spacetime generate read and write a
version-specific module-definition format, so a mismatched CLI either refuses
the module or emits bindings a host of the other version rejects. Move the CLI,
the package, and the server together.
This is the default path. In an ordinary strict TypeScript project, import the
generated DbConnection and pass it directly to the Effect client:
import * as Stdb from "effect-spacetimedb"import * as StdbClient from "effect-spacetimedb/client"
declare const Module: Stdb.AnyStdbModuledeclare const DbConnection: StdbClient.GeneratedWsConnectionFactory< typeof Module.spec, unknown>declare const uri: stringdeclare const databaseName: stringdeclare const token: string
const Example = Stdb.project(Module)
const WsLive = Example.client.ws.layerGenerated({ DbConnection, uri, databaseName, token,})Quarantined bindings for stricter projects
Section titled “Quarantined bindings for stricter projects”Some projects enable additional compiler settings that make third-party
generated source undesirable in their type graph. The published
effect-spacetimedb/codegen API preserves upstream runtime behavior while
quarantining that source. It accepts a prebuilt module bundle, runs the native
generator into staging, and emits only index.js plus a small erased
index.d.ts.
import * as NodeServices from "@effect/platform-node/NodeServices"import * as Effect from "effect/Effect"import { checkArtifact, generateArtifact,} from "effect-spacetimedb/codegen"
const target = { moduleBundlePath: "dist/bundle.js", stagingDir: ".tmp/effect-spacetimedb-codegen/staging", artifactDir: "src/generated-client", spacetimeCommand: ["spacetime"],}
const program = process.argv.includes("--check") ? checkArtifact(target) : generateArtifact(target)
await Effect.runPromise(program.pipe(Effect.provide(NodeServices.layer)))Run this script without --check to emit the artifact. Use --check in CI to
regenerate in staging and compare without rewriting the committed artifact.
Add .tmp/ to the consumer project’s .gitignore so generated staging files
cannot be committed accidentally.
The codegen export runs under plain Node 20 or newer. Install esbuild@^0.25 as
an optional peer and provide the platform layer at your executable boundary;
the example uses @effect/platform-node’s NodeServices.layer. The pipeline
asserts the exact SpaceTimeDB CLI version it was built against before
generating, and fails with CodegenCliVersionError on anything else.
The artifact declaration is deliberately erased. Restore type identity in one hand-written facade tied to the same module contract used by the server:
import { generatedConnection, type GeneratedConnectionOf,} from "effect-spacetimedb/client"import { DbConnection as GeneratedDbConnection } from "./generated-client/index.js"import { Module } from "./module"
export const DbConnection: GeneratedConnectionOf<typeof Module.spec> = generatedConnection(Module.spec, GeneratedDbConnection)Import the facade everywhere else. generatedConnection(...) is an identity at
runtime; the generated JavaScript still comes entirely from
spacetime generate, so upstream retains SDK and wire compatibility. Generated
sessions also validate that every public table, public event table, and public
view in the contract exists on the built connection and fail with
GeneratedArtifactShapeError, naming the missing keys, when the artifact is
stale.
Accessor Keys Are camelCase
Section titled “Accessor Keys Are camelCase”spacetime generate keys the generated db and tables accessors by the
camelCase spelling of each relation’s name, and emits a @deprecated
snake_case alias wherever the two spellings differ.
effect-spacetimedb resolves tables, event tables, and views alike by their
contract key — the key you wrote in addTables(...) or the views group.
Declared names are required to be camelCase-canonical, so the contract key is
the generated accessor key under either name policy, and the deprecated aliases
are never resolved. A client that exposes only the alias is treated as stale and
fails acquisition with GeneratedArtifactShapeError.
Only SQL and subscription text keeps the wire name. A module on the
snake_case policy still queries all_users while its rows arrive through the
allUsers accessor.
Never Format A Generated Artifact
Section titled “Never Format A Generated Artifact”Both the artifact pipeline’s --check mode and drift checks compare generated
files by content. A formatter run over src/generated or src/generated-client
rewrites bytes the generator did not produce and turns every subsequent check
red, with a diff that looks like real drift. Exclude the directory and its
contents from your formatter — a directory-only pattern does not stop a
pre-commit hook that passes explicit file paths:
{ "files": { "includes": ["**", "!src/generated-client", "!src/generated-client/**"] }}Not affiliated with SpacetimeDB (Clockwork Laboratories) or Effect (Effectful Technologies).