# SpacetimeDB — Elixir Client
An Elixir client for [SpacetimeDB](https://spacetimedb.com) using the
`v1.json.spacetimedb` WebSocket subprotocol.
## Features
- Full WebSocket lifecycle management via [Mint.WebSocket](https://github.com/elixir-mint/mint_websocket)
- All client→server messages: `Subscribe`, `SubscribeSingle`, `SubscribeMulti`,
`Unsubscribe`, `CallReducer`, `OneOffQuery`
- All server→client messages decoded into typed structs
- Automatic reconnection with exponential backoff
- Handler behaviour for clean separation of connection and business logic
- Works as a supervised child in OTP applications
## Installation
```elixir
def deps do
[
{:spacetimedb_ex, "~> 0.1"}
]
end
```
## Quick start
```elixir
{: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:
```elixir
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:
```elixir
{: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:
```elixir
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 |
|--------|------|---------|-------------|
| `:host` | `String.t()` | required | SpacetimeDB host |
| `:port` | `non_neg_integer()` | `3000` | Port |
| `:tls` | `boolean()` | `false` | Use TLS (`wss://`) |
| `:database` | `String.t()` | required | Database name or identity hex |
| `:token` | `String.t() \| nil` | `nil` | Auth token (persisted on reconnect) |
| `:handler` | `module \| {module, term} \| map` | required | Callback module, `{module, arg}` tuple, or map of anonymous functions |
| `:reconnect` | `boolean()` | `true` | Auto-reconnect on disconnect |
| `:reconnect_delay_ms` | `non_neg_integer()` | `500` | Initial reconnect backoff delay |
| `:max_reconnect_delay_ms` | `non_neg_integer()` | `30_000` | Maximum reconnect backoff delay |
| `:name` | `GenServer.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