5. How relay-cache joins tables

relay-cache is a same-host process that subscribes to every regional frontend on loopback, keeps selected tables in memory, and serves enriched JSON/protobuf on https://relay.bitcraftsync.app/…. You do not need it to use the WebSocket relays — it is an optional read API that already performed the joins community tools usually want.

Ingest model

Per region, the cache opens one v2 WebSocket to ws://127.0.0.1:<port> and loads data in phases:

  1. Base query set One additive Subscribe with all primary tables (claims, buildings, inventories, players, crafts, storage logs, hexite resource filters, growth, …). Wait for SubscribeApplied.
  2. Hexite locations (additive) From the base dump, collect hexite resource_state entity ids, then subscribe SELECT * FROM location_state WHERE entity_id = … per id in a second query set. Wait for Applied again.
  3. Stream Mark the region ready and apply live TransactionUpdates into columnar stores.

Full location_state is enormous (~millions of rows per region); the cache only keeps non-overworld interiors (dimension != 1) plus the hexite PK follow-up.

Endpoint → tables

Each HTTP handler reads the in-memory stores and joins by entity id (and a few derived keys). Entity ids are JSON strings for JS safety; protobuf uses uint64.

Endpoint Primary tables Join / enrichment
GET /claim?name= claim_state, player_username_state, claim_tech_state, claim_tech_desc Name substring match; owner username; tier from learned techs.
GET /claim/<id> Above + claim_local_state, claim_tile_cost Supplies, treasury, tiles, upkeep, location, researched tech list.
…/inventory building_state, inventory_state, building_desc, building_nickname_state, location_state (interiors), dimension_network_state Shared storage only (no Town Banks). Group by dimension; attach entrance for interiors.
…/members claim_member_state, skills, hexcoins, player_state Roster + roles + skills + hexcoins + login timestamps. /citizens and /hexcoins are legacy projections.
…/citizens (alias of members) Deprecated — skills projection of /members.
…/hexcoins (alias of members) Deprecated — hexcoin projection of /members.
…/crafts progressive_action_state, passive_craft_state, crafting_recipe_desc, buildings, usernames Progress + recipe outputs; optional completed= filter.
GET /player?name= player_username_state, player_state Substring search; region; last login / last active / signed-in. Walkthrough with printable columns: Extract player data.
…/inventory Player bags via deployable_state_v2 / housing / rent links + inventory_state Pockets, toolbelt, wallet, bank, wagon, cache, boat, recovery, deployable — items aggregated per bag.
…/housing player_housing_state, housing desc, buildings, inventories, locations Resolve house entity; name from username/nickname + catalog; interior storages by dimension.
…/skills experience_state, skill_desc XP → level via vendored thresholds.
GET /deposits unowned claim_state (hexite name pattern), claim_local_state, resource_state, growth_state, hexite location_state Parse N/E from claim name; match resource at world x/z; respawn_at from growth end timestamp.
GET /storage-logs storage_log_state + building / claim / player username lookups Filter by storage, player, or item(+claim|player); enrich names.
GET /bitme/resolve?name= player_lowercase_username_state, user_state, user_region_state, region_connection_info, world_region_name_state, signed_in_player_state (global) Exact-match lowercase name → entity id, identity, home region, host/module, live sign-in. Region shards fill display name / sign-in fallback.
GET /bitme/session/<id> mobile_entity_state, stamina_state, active_buff_state, player_action_state, character_stats_state ⋈ character_stat_desc, resource_desc; claim via roads overlay; resource_health_state (tracked targets only) Bit-Me activity snapshot: position, claim, stamina+max, buffs, action lifecycle, target health, watched spawns (citric + yield chains). The GET itself registers the session (15 min TTL).
GET /bitme/session/<id>/resources roads region grid tile map (one u16 per odd-r tile: 10-bit dictionary index + origin flag + 3-bit direction for every resource_state row, paving flag for paved_tile_state, water flag from the terrain seed) + player tile from mobile_entity_state Packed 400×400 window anchored at the player (24-byte BMR1 header + 160k u16 LE tile words, player at relative 200,200). Merged resource + paving + water view; expand indices via the region resource-dictionary (separate resource/paving namespaces). The poll counts as session activity.
GET /bitme/region/<region>/resource-dictionary resource tile map dictionary ⋈ resource_desc 10-bit tile index → resource_id + name + harvestable flag + health/despawn/respawn gamedata, so clients can expand window payloads client-side. Refetch when a window header's dict_version changes.

Example: claim detail join

claim_state.entity_id = C │ ├─ claim_local_state[C] → supplies, treasury, num_tiles, x/z ├─ claim_tile_cost[num_tiles] → tile_cost, upkeep_cost, supplies_run_out ├─ claim_tech_state[C] → researching, learned[] │ └─ claim_tech_desc[id] → name, tier, requirements, … └─ player_username_state[owner_player_entity_id] → owner_player_username GET /claim/C → single enriched Claim JSON / protobuf

Example: hexite deposit join

claim_state where owner=0 and name ~= Hexite Deposit │ parse north/east from name ├─ claim_local_state[claim] → world x, z ├─ resource_state @ (x,z) with hexite resource_id │ └─ growth_state[resource_entity] → end_timestamp → respawn_at └─ location_state[resource_entity] (from 2nd subscribe) → coords check GET /deposits?region=14 → { deposits: [...], count }

HTTP API cheat sheet

Base URL: https://relay.bitcraftsync.app. Default JSON; send Accept: application/x-protobuf for protobuf (entity ids are uint64 there; JSON keeps them as strings).

MethodParamsNotes
GET /cache-health — ready + per-region ready. Internals (memory pressure, row counts) on loopback-only 127.0.0.1:8089/internal/stats.
GET /proto — List of .proto files; /proto/relay_cache.proto downloads one.
GET /claim?name= name (substring, case-insensitive) Array of claim summaries (tier, owner username, …).
GET /claim/<id> — Full enrichment (supplies, upkeep, techs, location).
…/inventory — Shared storage only; grouped by dimension.
…/members — Roster + roles + skills + hexcoins + last login.
…/citizens — Deprecated alias of /members (skills projection).
…/hexcoins — Deprecated alias of /members (hexcoin projection).
…/crafts completed=true|false (optional) Omit filter → in-progress and completed.
GET /player?name= name (≥2 chars recommended) Cross-region search.
GET /player/<id> — Plus /inventory, /housing, /skills, /crafts?completed=.
GET /deposits region=N (optional) Active omit respawn_at; depleted include it.
GET /storage-logs Exactly one mode:
storageId= or
playerId= or
itemId=+claimId= or
itemId=+playerId=
Optional: itemType=Item|Cargo, region=N, limit=N
Upstream retention ~15–16 days. Newest-first when limited.
GET /bitme/resolve?name= name (exact, case-insensitive) Player → entity id + identity + region + host/module + signed_in. Covers all 25 regions.
GET /bitme/session/<id> — (entity id in path) 1 Hz activity snapshot for the Bit-Me app: position, claim, stamina, buffs, actions, target health, watched spawns. Polling = session registration (15 min TTL).
GET /bitme/session/<id>/resources — (entity id in path) Packed 400×400 map centered on the player — binary application/octet-stream (BMR1, ~320 KB), not JSON. Merged resource + paving + water view (tile word bits: 10-bit index, origin, direction, paving, water). Poll only while the map screen is open.
GET /bitme/region/<region>/resource-dictionary — Tile-word index → resource_id + gamedata + harvestable flag. Fetch once per region; refetch when a window header's dict_version changes.

Bit-Me mobile app API

The Bit-Me companion app is the first JSON-only consumer: it never opens a SpacetimeDB socket. Four GET endpoints cover it — /bitme/resolve (name → ids/region), /bitme/session/<player_entity_id> (position, claim, stamina, buffs, action + target health, and watched spawns such as the Citric Giant berry bushes), /bitme/session/<player_entity_id>/resources (packed 400×400 resource map centered on the player — all resource types, served from the roads region's dense tile map), and /bitme/region/<region>/resource-dictionary (tile index → resource_id + gamedata, for expanding the window). Polling at ~1 Hz is the intended cadence and doubles as the session registration; countdowns are computed client-side from bundled gamedata.

SignalWhere it comes from
Player / region resolution Global exact-match chain (lowercase name → entity id → identity → region → host/module), plus live sign-in presence.
Current claim Player tile → roads overlay claim index (same data as /roads/region/:id/map).
Target health resource_health_state retained only for targets of polled sessions — bounded, no whole-table storage.
Citric / respawn events Live resource_state inserts whose id is a destroy-yield chain target or one of the three vendored Citric Giant bush ids, scoped to the player's claim / wilderness.
Resource map window Dense per-region tile map (one u16 word per odd-r tile; 10-bit dictionary index + origin flag + 3-bit direction + paving flag + water flag), stamped server-side for every resource_state and paved_tile_state row — multi-hex footprints land on every occupied tile; water is seeded once from terrain elevation.

Client walkthrough with the BMR1 resource-window format: Bit-Me app API & BMR1/BME1 maps. Full field-by-field reference with real payload examples: spacetimedb-bitcraft-mirror/crates/relay-cache/BITME-API.md in the relay repo.

Calling the public HTTP API

curl -s 'https://relay.bitcraftsync.app/claim?name=concordia' | jq .
curl -s 'https://relay.bitcraftsync.app/claim/1234567890/citizens' | jq .
curl -s 'https://relay.bitcraftsync.app/claim/1234567890/crafts?completed=false' | jq .
curl -s 'https://relay.bitcraftsync.app/deposits?region=14' | jq .
curl -s 'https://relay.bitcraftsync.app/storage-logs?storageId=1008806316593474517&limit=50' | jq .
curl -s https://relay.bitcraftsync.app/cache-health | jq .

# Bit-Me app endpoints (JSON polling; see BITME-API.md):
curl -s 'https://relay.bitcraftsync.app/bitme/resolve?name=whisper' | jq .
curl -s 'https://relay.bitcraftsync.app/bitme/session/504403158290646123' | jq .

# Bit-Me resource map window (binary BMR1, ~320 KB — NOT JSON):
curl -s 'https://relay.bitcraftsync.app/bitme/session/504403158290646123/resources' -o window.bmr1
# World-anchored variants (region derived from x/z; elevation = binary BME1,
# 134x134 packed super-hex terrain cells, ~140 KB):
curl -s 'https://relay.bitcraftsync.app/bitme/world/11170/13850/resources' -o pan.bmr1
curl -s 'https://relay.bitcraftsync.app/bitme/world/11170/13850/elevation' -o pan.bme1
# Tile words expand via the per-region dictionary (refetch on dict_version change):
curl -s 'https://relay.bitcraftsync.app/bitme/region/7/resource-dictionary' | jq .
curl -sH 'Accept: application/x-protobuf' \
  'https://relay.bitcraftsync.app/player/1297036692699996362/skills' \
  -o skills.pb

# Protobuf schemas:
curl -s https://relay.bitcraftsync.app/proto
curl -OJ https://relay.bitcraftsync.app/proto/relay_cache.proto