3. Discover the schema

Each frontend serves the upstream schema as SATS-JSON over plain HTTPS on the same port as its WebSocket. These bytes are the ones the relay cached at startup and used to codegen the running mirror — so they always match the data you can subscribe to. Prefer the data explorer if you just want to click through tables in the browser.

  1. Build the URL from port + mirror database https://relay.bitcraftsync.app:<port>/v1/database/<mirror>/schema?version=9
  2. GET it Expect Content-Type: application/json, roughly ~580 KB for a BitCraft module, Cache-Control: public, max-age=60.
  3. Use table names from the response Public tables appear under the schema’s table list. Mirror-only reducers (relay_apply_*, _relay_meta) are not in this document — it is the upstream schema.
  4. Feed codegen Point spacetime generate / your SDK generator at this JSON (or a saved copy) so client bindings match the mirror.

curl

curl -fsSL \
  "https://relay.bitcraftsync.app:3014/v1/database/relay-mirror-bc14/schema?version=9" \
  -o schema-bc14.json

# Quick peek at public table names (jq):
jq -r '.. | objects | select(has("name") and has("columns"))? | .name' \
  schema-bc14.json | sort -u | head

Python

import json
import urllib.request

URL = (
    "https://relay.bitcraftsync.app:3014"
    "/v1/database/relay-mirror-bc14/schema?version=9"
)

with urllib.request.urlopen(URL, timeout=60) as resp:
    raw = resp.read()
    print(resp.status, resp.headers.get("Content-Type"), f"{len(raw)} bytes")

schema = json.loads(raw)
# Shape is RawModuleDef v9 SATS-JSON. Walk tables for names:
tables = []

def walk(obj):
    if isinstance(obj, dict):
        if "name" in obj and "columns" in obj:
            tables.append(obj["name"])
        for v in obj.values():
            walk(v)
    elif isinstance(obj, list):
        for v in obj:
            walk(v)

walk(schema)
print(f"{len(tables)} tables, e.g. {tables[:5]}")

Rust

use anyhow::{bail, Result};

#[tokio::main]
async fn main() -> Result<()> {
    let url = concat!(
        "https://relay.bitcraftsync.app:3014",
        "/v1/database/relay-mirror-bc14/schema?version=9",
    );
    let bytes = reqwest::get(url).await?.error_for_status()?.bytes().await?;
    println!("downloaded {} bytes", bytes.len());

    // Prefer the same parser the relay uses (SATS-JSON → module def)
    // if you depend on relay-protocol; otherwise serde_json::Value is fine
    // for listing table names during exploration.
    let v: serde_json::Value = serde_json::from_slice(&bytes)?;
    if !v.is_object() {
        bail!("expected JSON object");
    }
    println!("schema JSON root keys: {:?}",
        v.as_object().unwrap().keys().collect::<Vec<_>>());
    Ok(())
}

Generate client bindings

Once you have the schema (or can reach the module), generate SDK bindings the usual SpacetimeDB way. Against a reachable module:

# Example — adjust flags to your spacetime CLI version:
spacetime generate --lang rust \
  --out-dir ./module_bindings \
  --module-name relay-mirror-bc14 \
  -s https://relay.bitcraftsync.app:3014

spacetime generate --lang python \
  --out-dir ./module_bindings \
  --module-name relay-mirror-bc14 \
  -s https://relay.bitcraftsync.app:3014

If generate expects a file, pass the downloaded schema-bc14.json. Table SQL names in subscribe strings must match the schema (e.g. claim_state, not the Rust struct name).