1. Connect over wss://
Point any SpacetimeDB-compatible client at a public relay frontend. The host is the TLS endpoint; the database name is the mirror module, not the upstream BitCraft database.
-
Choose region and mirror name
From /health (or the home page table), take
portanddatabase. Example for region 14: hostwss://relay.bitcraftsync.app:3014, databaserelay-mirror-bc14. -
Offer a subprotocol
The frontend negotiates whichever you list in
Sec-WebSocket-Protocol(and echoes the chosen one):v2.bsatn.spacetimedb— default for current SDKs (binary BSATN).v1.bsatn.spacetimedb— binary v1; fullTransactionUpdates are synthesised for subscribers.v1.json.spacetimedb— text JSON frames (same v1 message shapes). Handy for scripts without a BSATN codec; see the Python examples on the Subscribe page.
-
Ask for uncompressed frames
Use
?compression=Noneon the subscribe URL. The relay does not decompress inbound Brotli/Gzip payloads for you. - Wait for the first server message The WebSocket upgrade alone is not enough. SpacetimeDB then sends a handshake payload that carries your connection identity (details below). Only after that should you subscribe.
Addresses
| Use | URL |
|---|---|
| SDK host | wss://relay.bitcraftsync.app:3014 |
| Full subscribe path | wss://relay.bitcraftsync.app:3014/v1/database/relay-mirror-bc14/subscribe?compression=None |
| Handshake header | Sec-WebSocket-Protocol: v2.bsatn.spacetimedb (or v1.bsatn.spacetimedb / v1.json.spacetimedb) |
What InitialConnection is
After the TCP/TLS WebSocket handshake succeeds, the first
application message from the server is the connection greeting.
On the v2 wire (v2.bsatn.spacetimedb) that
message is named InitialConnection. On v1
(v1.bsatn.spacetimedb / v1.json.spacetimedb) the
same step is called IdentityToken — same role, different
enum tag.
| Field | Meaning |
|---|---|
identity |
The Identity this connection was issued. Used for auth on reducer calls (not needed for read-only subscribe). |
token |
Opaque bearer string. Pass it on later connects if you want
the same identity back (SDK with_token /
saved auth token). |
Frame shape (v2): one WebSocket binary message =
0x00 compression tag (None) + BSATN-encoded
ServerMessage::InitialConnection { identity, token }.
Do not send Subscribe until you have received (and, if
raw, decoded) this message. Official SDKs wait for you: their
on_connect / connected callback fires only after the
greeting is processed — that callback is “after
InitialConnection.”
Python
Official Python SDK + codegen when you want typed bindings long-term.
Prefer v1.json (websockets + text frames) when you
want copy-paste without a BSATN codec — that path is what the
Subscribe and
Players runnable examples use.
# pip install spacetimedb-sdk
# Generate bindings from the relay schema first (see "Discover schema"):
# spacetime generate --lang python \
# --out-dir module_bindings \
# --module-name relay_mirror_bc14 \
# # or feed the downloaded schema JSON to your codegen pipeline
import asyncio
from spacetimedb_sdk.spacetimedb_async_client import SpacetimeDBAsyncClient
import module_bindings
HOST = "relay.bitcraftsync.app:3014" # host:port only
DATABASE = "relay-mirror-bc14"
SSL = True
client = SpacetimeDBAsyncClient(module_bindings)
def on_connect(_client, identity, token):
# Fires after InitialConnection has been received and decoded by the SDK.
# Safe place to start Subscribe calls (see the Subscribe tutorial page).
print(f"connected identity={identity} token_len={len(token or '')}")
# Persist token; on next run pass it as auth_token=... to reconnect as same identity.
async def main():
# auth_token=None → anonymous identity issued by local stdb
await client.run(
None,
f"https://{HOST}",
DATABASE,
on_connect,
[], # subscribe in a later step — or pass initial SQL here
ssl_enabled=SSL,
)
asyncio.run(main())
# Illustrative only — v2 frames are BSATN, not JSON.
# Prefer the SDK unless you already have a BSATN codec.
import asyncio
import websockets
SUBPROTOCOL = "v2.bsatn.spacetimedb"
URL = (
"wss://relay.bitcraftsync.app:3014"
"/v1/database/relay-mirror-bc14/subscribe"
"?compression=None"
)
async def main():
async with websockets.connect(
URL,
subprotocols=[SUBPROTOCOL],
max_size=None, # BitCraft tables can be huge
) as ws:
# --- InitialConnection (required before any Subscribe) ---
# First binary frame: u8 compression (0=None) + BSATN ServerMessage.
# Tag 0x00 of the ServerMessage enum is InitialConnection { identity, token }.
frame = await ws.recv()
assert isinstance(frame, (bytes, bytearray))
assert frame[0] == 0, "expected compression=None"
print(f"InitialConnection frame: {len(frame)} bytes")
# bsatn_decode(frame[1:]) → InitialConnection; then you may Subscribe.
await asyncio.Future() # keep alive
asyncio.run(main())
Rust
Official spacetimedb-sdk after spacetime generate is the
usual path. Below: SDK builder, then the same wire shape
relay-cache uses internally (useful when you subscribe with
raw SQL strings and no generated module).
// After: spacetime generate --lang rust -o module_bindings …
use module_bindings::DbConnection;
use spacetimedb_sdk::DbContext;
fn main() {
let conn = DbConnection::builder()
.with_uri("wss://relay.bitcraftsync.app:3014")
.with_database_name("relay-mirror-bc14")
.on_connect(|_ctx, identity, token| {
// Runs after InitialConnection — safe to Subscribe from here.
println!("connected {identity} token_len={}", token.len());
})
.on_connect_error(|_ctx, err| eprintln!("connect error: {err}"))
.on_disconnect(|_ctx, err| {
if let Some(e) = err {
eprintln!("disconnected: {e}");
}
})
.build()
.expect("connect");
// Drive the SDK event loop however your app does (tick / block_on).
let _ = conn;
}
use anyhow::{anyhow, Result};
use futures_util::StreamExt;
use http::header::SEC_WEBSOCKET_PROTOCOL;
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
use tokio_tungstenite::tungstenite::Message;
const SUBPROTOCOL: &str = "v2.bsatn.spacetimedb";
#[tokio::main]
async fn main() -> Result<()> {
let url = concat!(
"wss://relay.bitcraftsync.app:3014",
"/v1/database/relay-mirror-bc14/subscribe",
"?compression=None",
);
let mut req = url.into_client_request()?;
req.headers_mut()
.insert(SEC_WEBSOCKET_PROTOCOL, SUBPROTOCOL.parse()?);
let (mut ws, _) = tokio_tungstenite::connect_async(req).await?;
let Some(Ok(Message::Binary(data))) = ws.next().await else {
return Err(anyhow!("expected binary InitialConnection"));
};
assert_eq!(data[0], 0, "compression tag");
// bsatn::from_slice::<ServerMessage>(&data[1..]) → InitialConnection
println!("handshake {} bytes", data.len());
Ok(())
}
TypeScript
Use the current spacetimedb npm package (the older
@clockworklabs/spacetimedb-sdk is deprecated). Generate
bindings, then point withUri at the relay.
// npm i spacetimedb
// spacetime generate --lang typescript --out-dir src/module_bindings \
// --module-name relay-mirror-bc14 -s https://relay.bitcraftsync.app:3014
import { DbConnection } from "./module_bindings";
const conn = DbConnection.builder()
.withUri("wss://relay.bitcraftsync.app:3014")
.withDatabaseName("relay-mirror-bc14")
.onConnect((conn, identity, token) => {
// Fires after InitialConnection — start Subscribe here (see Subscribe page).
console.log("connected", identity.toHexString(), "token_len", token.length);
// localStorage.setItem("stdb_token", token); // then .withToken(...) on next launch
})
.onConnectError((_ctx, err) => console.error("connect error", err))
.onDisconnect((_ctx, err) => console.log("disconnect", err))
.build();
Global vs regional
| Connect to | When |
|---|---|
:3000 / relay-mirror-bc-global |
Shared reference / catalog-style data that is not per-world (item/recipe/desc tables as mirrored globally). Not where live players and claims for a world live. |
:3000+N / relay-mirror-bcN |
World state for region N — players, claims,
inventories, hexite, storage logs. Use this for almost all
gameplay tooling. |
HTTP /player, /claim, … |
Already fans out across all regional cache shards. No need to pick a port for cross-region name search. |