Skip to content

Getting Started

effect-spacetimedb is an Effect-first authoring layer for SpaceTimeDB modules. You describe the module once, implement handlers with scoped Effect services, then build native module exports and generated clients from the same contract.

Terminal window
npm install effect-spacetimedb effect spacetimedb

The same module — a user table and a user_upsert reducer — in effect-spacetimedb and in the native SpaceTimeDB SDK:

import * as Effect from "effect/Effect"
import * as Schema from "effect/Schema"
import * as Stdb from "effect-spacetimedb"
import { build } from "effect-spacetimedb/server-compiler"
import "effect-spacetimedb/server-polyfills"
// Branded + validated id. In the native SDK this column is just `string`.
const UserId = Schema.String.pipe(
Schema.minLength(1),
Schema.maxLength(64),
Schema.brand("UserId"),
)
const user = Stdb.table("user", {
public: true,
columns: {
// The Effect schema IS the column's value type, so the brand and the
// length validation flow through to handlers and generated clients.
id: Stdb.string(UserId).primaryKey(),
name: Stdb.string(),
},
})
// camelCase names lower to snake_case on the wire (userUpsert -> user_upsert).
const Users = Stdb.StdbGroup.make("Users").add(
Stdb.StdbFn.reducer("userUpsert", {
// Params are validated by the same schema; the native SDK accepts any string.
params: Stdb.struct({
id: Stdb.string(UserId),
name: Stdb.string(),
}),
}),
)
const Module = Stdb.StdbModule.make("app").addTables(user).add(Users)
const { Db } = Module
const UsersLive = Stdb.StdbBuilder.group(Module, "Users", {
userUpsert: (args) =>
Effect.gen(function* () {
const db = yield* Db
yield* db.user.insert(args)
}),
})
export const compiled = build(Module, [UsersLive])
export const ModuleExports = compiled.exportGroup()
export default compiled.schema

The authoring flow is:

  1. Declare value types, tables, and endpoint groups.
  2. Assemble a module with StdbModule.make(...).addTables(...).add(...).
  3. Implement each group with StdbBuilder.group(...).
  4. Import server-polyfills once and build the native module export with build(...) from effect-spacetimedb/server-compiler.

Build modules with the native SpaceTimeDB CLI. Put the build and client generation commands behind package scripts:

{
"scripts": {
"build": "spacetime build --module-path .",
"generate-client": "spacetime generate --lang typescript --js-path ./dist/bundle.js --out-dir ./src/generated --yes"
}
}

Then run the scripts from the module package:

Terminal window
bun run build
bun run generate-client

A module entry statically imports the host-only compiler (build from effect-spacetimedb/server-compiler). To import that entry in Vitest without a real SpaceTimeDB host, alias the host syscalls to the shipped stub and inline the spacetimedb package:

vitest.config.ts
import { defineConfig } from "vitest/config"
import { spacetimeSysAlias } from "effect-spacetimedb/testing"
export default defineConfig({
test: {
server: { deps: { inline: ["spacetimedb"] } },
},
resolve: {
alias: { ...spacetimeSysAlias },
},
})

The stub keeps console and the deterministic backstops usable off-host, so importing or building a module entry in tests does not require a running host.