SpacetimeDB that stays in Effect.

Your whole realtime backend as one Effect contract — tables, reducers, HTTP, and clients, typed from the database to the UI.

  • Typed errors travel from reducer to client — recover with catchTag.
  • One Effect Schema defines tables, params, HTTP bodies, and domain types.
  • Handlers and clients compose with Effect.fn, Layers, and HttpApi.
contract.ts
import * as Effect from "effect/Effect"
import * as Schema from "effect/Schema"
import * as Stdb from "effect-spacetimedb"

const UserId = Schema.String.pipe(Schema.brand("App/UserId"))

const user = Stdb.table("user", {
  public: true,
  columns: {
    id: Stdb.string(UserId).primaryKey(),
    name: Stdb.string(),
  },
})

const AppErrors = Stdb.errors.namespace("App")({
  UserMissing: Stdb.error({ userId: Stdb.string(UserId) }),
})

const Users = Stdb.StdbGroup.make("Users").add(
  Stdb.StdbFn.procedure("user_get", {
    params: Stdb.struct({ userId: Stdb.string(UserId) }),
    returns: Stdb.option(user.row),
    errors: AppErrors,
  }),
)
One contract drives the server, the generated client, and your typed errors.
native sdk . effect-spacetimedb

Same call. Typed recovery.

Both call the same reducer and recover the same AppUserMissing failure. Native parses it out of a string; Effect SpacetimeDB hands you a typed error.

Without effect-spacetimedb native TS SDK
native-sdk.ts
import { SenderError } from "spacetimedb"
import { DbConnection } from "./module_bindings"

declare const conn: DbConnection
declare const userId: string

export async function ensureUser() {
  try {
    await conn.reducers.userRequire({ userId })
    return "ok" as const
  } catch (error) {
    if (error instanceof SenderError) {
      // untyped string - parse and match by hand
      const failure = JSON.parse(error.message) as {
        tag?: string
        error?: { userId?: string }
      }

      if (failure.tag === "AppUserMissing") {
        return `missing:${failure.error?.userId}` as const
      }
    }

    throw error
  }
}
With effect-spacetimedb Effect contract
effect-spacetimedb.ts
import * as Effect from "effect/Effect"
import * as Stdb from "effect-spacetimedb"
import { Module } from "./module"

declare const userId: string

const Example = Stdb.project(Module.spec)

export const ensureUser = Effect.gen(function* () {
  const client = yield* Example.client.http.Tag

  return yield* client.reducers.userRequire({ userId }).pipe(
    Effect.as("ok" as const),
    // typed tagged error - error.userId is a string
    Effect.catchTag("AppUserMissing", (error) =>
      Effect.succeed(`missing:${error.userId}` as const),
    ),
  )
})
effect-native value

What Effect brings to SpacetimeDB.

The parts you get because it is built on Effect — not a list of generic SpacetimeDB features.

Typed errors, end to end

Declare tagged errors once. Fail with them on the server; recover with catchTag on the client.

Error handling →

One Schema, everywhere

One branded Schema becomes your table columns, params, HTTP bodies, return values, and error fields.

Value type design →

Compose with Layers

Handlers are Effects: yield Db and Tx, use Effect.fn, and inject services through one runtime Layer.

Reducers →

Scoped subscriptions

Sessions share connection state, and tableGroup(keys).changes coalesces update bursts into cached snapshots.

Subscriptions →

Canonical Effect HTTP clients

toHttpApi projects your routes into a stock Effect HttpApi — call them from existing HttpApiClient code.

Client connection →

Generated typed clients

Generated bindings stay in sync with the contract, so every reducer, route, and subscription call is typed end to end.

Generate clients →
schema . clients . subscriptions

Stay in the same Effect vocabulary.

Effect Schema for data, services for dependencies, HttpApi for routes, streams for subscriptions — no new DSL to learn.

schema.ts
const UserId = Schema.String.pipe(Schema.brand("App/UserId"))
const UserName = Schema.String.pipe(Schema.minLength(1))

const user = Stdb.table("user", {
  public: true,
  columns: {
    id: Stdb.string(UserId).primaryKey(),
    name: Stdb.string(UserName),
  },
})

const CreateUser = Stdb.struct({
  userId: Stdb.string(UserId),
  name: Stdb.string(UserName),
})
client.ts
import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient"
import * as HttpClient from "effect/unstable/http/HttpClient"
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"

const Example = Stdb.project(Module.spec)
const api = Stdb.toHttpApi(Module.spec)

const httpClient = yield* HttpApiClient.make(api, {
  baseUrl: Stdb.httpApiBaseUrl({ uri, databaseName }),
  transformClient: HttpClient.mapRequest(
    HttpClientRequest.bearerToken(token),
  ),
})

yield* httpClient.Users.get({ path: { userId } })

const group = session.tableGroup(["user"] as const)
yield* group.changes.pipe(Stream.runForEach(renderUsers))
docs . llms.txt . source

Use the docs as your implementation contract.

Quickstart, guides, migration notes, and a generated llms.txt — plus the full source.