Skip to content

Client Connection

SpacetimeDB: Client Connection ↗

Stdb.project(Module) produces a client-safe projection. It contains no server runtime; it is only the callable, route, table, and subscription shape needed by clients.

import * as Effect from "effect/Effect"
import * as Stdb from "effect-spacetimedb"
declare const Module: Stdb.AnyStdbModule
declare const uri: string
declare const databaseName: string
declare const token: string
const Example = Stdb.project(Module)
const HttpLive = Example.client.http.layer({ uri, databaseName, token })
const program = Effect.gen(function* () {
const client = yield* Example.client.http.Tag
yield* client.reducers.user_upsert({ userId: "u1", name: "Ada" })
}).pipe(Effect.provide(HttpLive))

Typed HTTP handlers are called with just their payload. Raw handlers take the payload and request options.

Typed post, put, and patch routes can be projected into a stock Effect HttpApi:

const api = Stdb.toHttpApi(Module)

This is useful when callers already use HttpApiClient.make(...) and want status-based declared-error decoding.

WebSocket sessions use generated native bindings through effect-spacetimedb/client.

import * as StdbClient from "effect-spacetimedb/client"
import * as Stdb from "effect-spacetimedb"
declare const Module: Stdb.AnyStdbModule
declare const DbConnection: StdbClient.GeneratedWsConnectionFactory<
typeof Module.spec,
unknown
>
declare const uri: string
declare const databaseName: string
declare const token: string
const Example = Stdb.project(Module)
const WsLive = Example.client.ws.layerGenerated({
DbConnection,
uri,
databaseName,
token,
})

layerGenerated and scopedGenerated are the generated-client paths. The standalone generated config helper is reserved for the package testing entrypoint.

Named tags let one process hold multiple sessions for the same module.

import * as StdbClient from "effect-spacetimedb/client"
import * as Stdb from "effect-spacetimedb"
declare const Module: Stdb.AnyStdbModule
declare const DbConnection: StdbClient.GeneratedWsConnectionFactory<
typeof Module.spec,
unknown
>
declare const uri: string
declare const databaseName: string
declare const token: string
const Example = Stdb.project(Module)
const MainSession = Example.client.ws.tag("main")
const MainLive = Example.client.ws.layerGenerated(
{ DbConnection, uri, databaseName, token },
{ name: "main" },
)

The native generated client still owns the actual WebSocket connection. effect-spacetimedb layers typed Effect services, scoped sessions, and projected HTTP clients over the module contract.

The handshake is the dividing line. A failure before the connection is established fails the scoped acquisition with WsConnectError; a failure after it is a connection loss, which invalidates the session and fails in-flight and subsequent calls with ConnectionLostError.

What happenedDuring acquisitionOn an established session
Socket error before the handshakeWsConnectError with the socket error as cause—
Socket error after the handshakeWsConnectError with the socket error as causeConnectionLostError whose raw is that error’s message
Close with no reported errorWsConnectError with cause: undefinedConnectionLostError with raw: "WebSocket connection disconnected"
Connect timeoutWsConnectError whose cause is WsConnectTimeoutError—
Generated client missing contract relationsGeneratedArtifactShapeError, naming the missing keys—

A socket error raised after the handshake never arrives as a connect error. The SDK records it, closes the socket, and reports it on the disconnect path, so cause is what distinguishes a connection that died with a reason from one that simply closed. The causeless case covers both a clean close and the close the SDK performs when an inbound frame fails to decompress.

In a browser, onerror hands the SDK an ErrorEvent rather than an Error. The SDK normalizes that to an Error carrying the event’s message, with the original event on .cause, so WsConnectError.cause is an Error on every runtime.

ConnectionLostError means the client never observed a terminal response for a call. The reducer or procedure may already have committed on the server. Treat the outcome as unknown, prefer idempotent mutations, and reconcile from the replacement session’s subscribed state before retrying anything that is not.

A bare scoped session surfaces terminal invalidation and stops there. Long-lived clients can opt into the package supervisor for bounded acquisition, jittered reconnect backoff, scoped per-session setup, and resubscription; a reconnect after a causeless close behaves exactly like one after a reported socket error.