Guild Stack Architecture

Purpose: AI-readable reference for the Structs Guild Stack — a Docker Compose application that provides local PostgreSQL access to indexed game state, GRASS real-time events, a webapp, and transaction signing. This is an advanced/optional upgrade for agents who need sub-second query performance.

Repository: https://github.com/playstructs/docker-structs-guild


Overview

The Guild Stack runs a full Structs guild node locally via Docker Compose. A single docker compose up -d brings up everything needed to participate in the network with fast indexed data access. The key benefit for agents is PostgreSQL: every game state query that takes 1-60 seconds via CLI completes in under 1 second via PG.

Three compose variants exist:

Variant File Purpose
Guild node compose.yaml Standard deployment: chain node, sync-state indexer, PG, GRASS/NATS, webapp, Struct Control SPA, TSA, crawler
Reactor node compose-reactor.yaml Validator node (adds reactor bootstrap; no sync-state/webapp)
Guild + Discord compose-discord.yaml Discord-oriented topology: structsd-indexer-config + structs-proxy + structs-discord; no structs-sync-state / structs-control (not “standard + bot”)

Network identifiers (.env): NETWORK_CHAIN_ID=structstestnet-111, NETWORK_VERSION=116b (v0.20.0).


Why PostgreSQL Matters

The performance difference between CLI queries and PG is not incremental — it determines what operations are physically possible.

The Combat Math

One blockchain block is ~6 seconds. Charge is a single per-player bar (shared across all your structs), regenerating 1 per block. Fast-fire primaries (Command Ship, Pursuit Fighter, Tank, Starfighter) cost 3 charge — roughly 3 blocks (~18s) between shots if you fire nothing else; heavier hulls cost 5. In active combat, the decision loop is:

  1. Query enemy state (who’s alive, what defenses)
  2. Pick optimal attacker + target + weapon
  3. Submit attack transaction
  4. Wait for the player’s charge bar to refill to the next action’s cost (~18-30s)
  5. Repeat

With CLI: Steps 1-2 take 5-10 seconds. Your 6-second combat cycle becomes 15 seconds. You fire at half rate, and state may be stale by the time you act.

With PG: Steps 1-2 take < 1 second. Your combat cycle matches the chain’s block time. Targeting decisions use current-block state.

What PG Enables

Capability CLI PG
Real-time threat detection Impossible (query > block time) Poll every 6s
Defense-aware combat targeting Minutes to gather, stale by execution Full matrix in <1s
Automated raid target selection 2-5 min to scout Score all planets in <1s
Fleet composition analysis Fetch all structs, parse, cross-reference Single JOIN, instant
Multi-step combat sequences 5-10s gaps between shots Re-query between shots in <1s
Galaxy-wide intelligence 30-60s for player-all, often times out Arbitrary filters and JOINs in <1s

Service Catalog

Service Ports Purpose
structsd 26656, 26657, 1317 Blockchain node (CometBFT + Cosmos SDK). Chain only — does not write to PG.
structs-pg-init One-shot: database creation, role setup, initial Sqitch deploy. Exits on success.
structs-pg 5432 PostgreSQL 17 + TimescaleDB. Central data store.
structs-pg-auto-migrate Re-runs sqitch deploy on a loop for schema updates.
structs-sync-state Chain event indexer. Polls structsd RPC, writes structs.* and sync_state.*. Do not scale (writer lock).
structs-nats 4222, 8222, 1443 NATS message broker (cluster: GRASS). WebSocket on 1443, monitoring on 8222.
structs-grass Event bridge: PG NOTIFY 'grass' → NATS subjects.
structs-webapp 8080, 443 Guild webapp (PHP/Symfony). Guild API at /api/. Bind-mounts WEBAPP_SOURCE.
structs-control 8081 Struct Control SPA — browser admin/operator console. Dev mode proxies the webapp; bind-mounts CONTROL_SOURCE.
structs-tsa Transaction Signing Agent. Manages signing account pool.
structs-crawler Supplementary guild metadata crawler.

Two services mount source trees from sibling checkouts on the host, not from their images: structs-webapp needs WEBAPP_SOURCE (default ../structs-webapp/src) and structs-control needs CONTROL_SOURCE (default ../structs-control). Start either without that checkout and the container crash-loops — structs-control fails immediately on a missing /app/package.json. Neither is needed for PG query work.

structs-pg starts structs-pg-init automatically through a service_completed_successfully dependency, but structs-pg-auto-migrate has no such link. If you start only the query services, schema migrations are applied once at init and then never again; start structs-pg-auto-migrate explicitly to keep pulling STRUCTS_PG_BRANCH updates.

The PostgreSQL schema (structs.*, sync_state.*, cache.* views, signer.*, view.* — see database-schema.md) is owned by playstructs/structs-pg and applied with Sqitch. Pin the schema branch with STRUCTS_PG_BRANCH in .env.

The Guild Stack does not include an MCP server. Agents connect to the game through the MCP server embedded in structs-desktop; the Guild Stack provides the PostgreSQL data store and event bridge that back it. See TOOLS.md for the MCP interface.


Data Flow

Blockchain Network (P2P :26656)
    |
    v
structsd (CometBFT + Cosmos SDK)
    |
    +---> REST API (:1317) ---> External queries
    +---> RPC (:26657) ---> Transaction submission
    |
    +---> RPC poll
            |
            v
      structs-sync-state
            |
            +---> structs.* (game state tables)
            +---> sync_state.* (ingest cursor, raw blocks/events)
            |
            v
      structs-pg (:5432)
            |
            +---------------------+
            |                     |
            v                     v
      PG NOTIFY 'grass'     Direct queries
            |               (webapp, tsa, crawler, agents)
            |                     |
            |                     v
            |               structs-webapp (:8080) <--- structs-control SPA (:8081)
            v
      structs-grass
            |
            v
      structs-nats (:4222 / :1443 WebSocket)
            |
            +---> Internal services
            +---> Browser/agent WebSocket clients

Write path: Transactions go through structsd RPC (:26657) or via the TSA signer schema (services insert rows into signer.tx, TSA signs and broadcasts).

Read path (fast): PostgreSQL queries via any service with PG access.

Read path (slow): CLI queries via structsd query (hits the node’s local state store).

Real-time path: sync-state writes game state → PG triggers fire NOTIFY 'grass'structs-grass → NATS → WebSocket clients. Block height is tracked in sync_state.sync_cursor and structs.current_block.


Sync-State Indexer

structs-sync-state is the sole chain-to-PG ingester:

PG game state is empty or stale until sync-state is running and caught up. The default agent profile must include structs-sync-state alongside structsd and structs-pg.

Derived rows and historical gaps

Some structs.planet_activity rows have no corresponding chain event — sync-state derives them from state changes it observes. That means a fix to the indexer changes only future rows; already-indexed history keeps whatever shape it was written with. Three cases currently matter to anyone querying the timeline:

Row / field Derived from Gap in older data
raid_status detail.seized_ore The EventRaid payload, which already carries it Key absent entirely on rows indexed before the fix
struct_defense_remove The struct_defender_clear handler (no chain event exists) Category never written, so removals are missing while additions are present
shield_change, block_raid_start planet_attribute writes for planetaryShield / blockStartRaid Not emitted before v0.18.0

Upstream ships one-time repair scripts under sync-state/sql/ (repair-raid-status-seized-ore.sql, repair-refine-ledger-dupes.sql, repair-infusion-ledger-dupes.sql) that backfill or clean the affected rows. These are operator tasks — they write to the database and are not something an agent should run as part of gameplay. See database-schema.md for the query-side consequences.


GRASS Bridge

The structs-grass service bridges PostgreSQL change notifications to NATS:

structs-grass declares no dependency on structs-nats, so starting it by name does not bring NATS up. Launch both if you want events to reach subscribers.

This is the same system documented in the structs-streaming skill. The guild stack runs it locally rather than connecting to a remote GRASS endpoint.


Node Upgrades (Cosmovisor)

The structsd Docker image runs under cosmovisor. On-chain x/upgrade plans swap the binary in-place without restarting the container. The image bakes in the upgrade binaries keyed to their scheduled heights. Roll out a new structs/structsd image before the next on-chain upgrade height. See docker-structsd README for operator details.


Startup Timing

Phase Duration Notes
PG init + healthy 10-30 seconds structs-pg-init deploys schema, then exits
Initial chain sync Hours (first run) Syncing from genesis. 48-hour health check start period on structsd.
Sync-state catch-up Hours (first run) Indexes blocks after chain is reachable; sync_state.sync_cursor.status shows catching_up
Warm start catch-up 1-5 minutes Chain and indexer resume from checkpoint
PG-dependent services After PG healthy sync-state, GRASS, TSA, crawler, webapp start once PG is ready

The initial sync is the main cost. After that, warm starts are fast.


Platform Notes


See Also