SpacetimeDB — Elixir Client

An Elixir client for SpacetimeDB using the v1.json.spacetimedb WebSocket subprotocol.

Features

Installation

def deps do
  [
    {:spacetimedb_ex, "~> 0.1"}
  ]
end

Quick start

{:ok, conn} = SpacetimeDB.start_link(
  host: "localhost",
  database: "my_module",
  handler: %{
    on_identity_token: fn token, _ ->
      IO.puts("Connected as #{token.identity}")
    end,
    on_transaction_update: fn update, _ ->
      Enum.each(update.tables, fn t ->
        IO.puts("#{t.table_name}: +#{length(t.inserts)} -#{length(t.deletes)}")
      end)
    end
  }
)

# Subscribe to a SQL query
SpacetimeDB.subscribe(conn, ["SELECT * FROM Player"])

# Call a reducer
{:ok, request_id} = SpacetimeDB.call_reducer(conn, "CreatePlayer", ["Alice"])

# One-off query (no subscription)
{:ok, msg_id} = SpacetimeDB.one_off_query(conn, "SELECT * FROM Player WHERE name = 'Alice'")

Handler behaviour

For production use, implement SpacetimeDB.Handler in a module:

defmodule MyApp.SpacetimeHandler do
  @behaviour SpacetimeDB.Handler

  @impl true
  def on_identity_token(%SpacetimeDB.Types.IdentityToken{} = token, _arg) do
    MyApp.Auth.store_token(token.token)
  end

  @impl true
  def on_initial_subscription(%SpacetimeDB.Types.InitialSubscription{} = sub, _arg) do
    Enum.each(sub.tables, &MyApp.Cache.seed_table/1)
  end

  @impl true
  def on_transaction_update(%SpacetimeDB.Types.TransactionUpdate{} = update, _arg) do
    Enum.each(update.tables, fn t ->
      MyApp.Cache.apply_diff(t.table_name, t.inserts, t.deletes)
    end)
  end

  @impl true
  def on_disconnect(reason, _arg) do
    MyApp.Metrics.record_disconnect(reason)
  end
end

Then connect with:

{:ok, conn} = SpacetimeDB.start_link(
  host: "prod.example.com",
  port: 443,
  tls: true,
  database: "game-prod",
  token: System.get_env("SPACETIMEDB_TOKEN"),
  handler: MyApp.SpacetimeHandler
)

Supervised usage

Add to your application's supervision tree:

defmodule MyApp.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      {SpacetimeDB,
       host: "localhost",
       database: "my_module",
       handler: MyApp.SpacetimeHandler,
       name: MyApp.SpacetimeDB}
    ]

    Supervisor.start_link(children, strategy: :one_for_one)
  end
end

Options

Option Type Default Description
:hostString.t() required SpacetimeDB host
:portnon_neg_integer()3000 Port
:tlsboolean()false Use TLS (wss://)
:databaseString.t() required Database name or identity hex
:tokenString.t() | nilnil Auth token (persisted on reconnect)
:handlermodule | {module, term} | map required Callback module, {module, arg} tuple, or map of anonymous functions
:reconnectboolean()true Auto-reconnect on disconnect
:reconnect_delay_msnon_neg_integer()500 Initial reconnect backoff delay
:max_reconnect_delay_msnon_neg_integer()30_000 Maximum reconnect backoff delay
:nameGenServer.name() Registered process name

Types

All decoded server messages are plain Elixir structs:

Struct Description
SpacetimeDB.Types.IdentityToken First message — identity + auth token
SpacetimeDB.Types.InitialSubscription Initial rows for a Subscribe call
SpacetimeDB.Types.SubscribeApplied Confirmation for SubscribeSingle / SubscribeMulti
SpacetimeDB.Types.UnsubscribeApplied Confirmation for Unsubscribe
SpacetimeDB.Types.SubscriptionError Server rejected a subscription
SpacetimeDB.Types.TransactionUpdate Row changes from a committed reducer call
SpacetimeDB.Types.OneOffQueryResponse Result of a one-off query
SpacetimeDB.Types.TableUpdate Per-table diff (inserts, deletes)
SpacetimeDB.Types.ReducerCallInfo Metadata in a TransactionUpdate
SpacetimeDB.Types.Timestamp Microseconds since Unix epoch

Architecture

SpacetimeDB (public API)
    │
    └── SpacetimeDB.Connection (GenServer)
            │  WebSocket frames via Mint.WebSocket
            │
            ├── SpacetimeDB.Protocol  encode/decode JSON
            ├── SpacetimeDB.Types     typed structs
            └── SpacetimeDB.Handler   callback behaviour

The Connection GenServer owns a single Mint.HTTP connection upgraded to WebSocket. Received frames are decoded by SpacetimeDB.Protocol.decode/1 and dispatched to the handler callbacks. On disconnect the process waits reconnect_delay_ms (doubling each attempt, capped at max_reconnect_delay_ms) before reconnecting.

Protocol

SpacetimeDB WebSocket URL format:

ws[s]://{host}:{port}/database/{name_or_identity}/subscribe

Subprotocol: v1.json.spacetimedb (text frames, JSON-encoded messages)

The library currently implements the JSON protocol only. BSATN binary protocol (v1.bsatn.spacetimedb) support is planned for a future release.

License

MIT