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.
Install
Section titled “Install”npm install effect-spacetimedb effect spacetimedbpnpm add effect-spacetimedb effect spacetimedbbun add effect-spacetimedb effect spacetimedbMinimal Module
Section titled “Minimal Module”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.schemaimport { schema, table, t } from "spacetimedb/server"
const spacetime = schema({ user: table( { public: true }, { id: t.string().primaryKey(), name: t.string() }, ),})
export const user_upsert = spacetime.reducer( "user_upsert", { id: t.string(), name: t.string() }, (ctx, { id, name }) => { ctx.db.user.insert({ id, name }) },)
export default spacetimeThe authoring flow is:
- Declare value types, tables, and endpoint groups.
- Assemble a module with
StdbModule.make(...).addTables(...).add(...). - Implement each group with
StdbBuilder.group(...). - Import
server-polyfillsonce and build the native module export withbuild(...)fromeffect-spacetimedb/server-compiler.
Dev Loop
Section titled “Dev Loop”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:
bun run buildbun run generate-clientTesting your module off-host
Section titled “Testing your module off-host”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:
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.
Next Pages
Section titled “Next Pages”Not affiliated with SpacetimeDB (Clockwork Laboratories) or Effect (Effectful Technologies).