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.

  1. Choose region and mirror name From /health (or the home page table), take port and database. Example for region 14: host wss://relay.bitcraftsync.app:3014, database relay-mirror-bc14.
  2. 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; full TransactionUpdates 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.
    When several are offered, the relay prefers v2 over v1.
  3. Ask for uncompressed frames Use ?compression=None on the subscribe URL. The relay does not decompress inbound Brotli/Gzip payloads for you.
  4. 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

UseURL
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.

FieldMeaning
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.”

Client Relay frontend │ HTTP Upgrade + Sec-WebSocket-Protocol │ ─────────────────────────────────────► │ 101 Switching Protocols │ ◄───────────────────────────────────── │ │ Binary: InitialConnection { identity, token } ← wait for this │ ◄───────────────────────────────────── │ │ Binary: Subscribe { … } ← only then │ ─────────────────────────────────────► │ Binary: SubscribeApplied { … } │ ◄─────────────────────────────────────

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 toWhen
: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.