Category: database
Source: https://github.com/playstructs/structs-pg
Last Updated: 2026-05-29
Description: Authoritative structural catalog of the Structs Guild Stack PostgreSQL schema. Mirrors playstructs/structs-pg as deployed by Sqitch.
For query patterns, grid gotchas, and agent-ready SQL examples, see knowledge/infrastructure/database-schema.md.
| Property | Value |
|---|---|
| Name | structs |
| Engine | PostgreSQL 17 + TimescaleDB |
| Management | Sqitch (sqitch schema) |
| Repository | https://github.com/playstructs/structs-pg |
| Ingestion | structs-sync-state (polls chain RPC; writes structs.* and sync_state.*) |
Chain events are not ingested by structsd. The structs-sync-state service owns block-by-block indexing.
| Schema | Purpose |
|---|---|
structs |
Core game state (~50 tables) |
sync_state |
Chain indexer state: sync cursor, block log, raw chain data, handler errors |
cache |
Four read-only compatibility views over sync_state.raw_* (legacy webapp shim) |
view |
Computed views — entity projections, permissions, work queue, leaderboards |
signer |
Transaction Signing Agent (TSA) queue — roles, accounts, pending txs |
sqitch |
Migration tracking |
| Role | Access | Used By |
|---|---|---|
structs |
Owner / superuser | Administration, Sqitch migrations |
structs_indexer |
Read/write on structs.*, sync_state.*; SELECT on cache.* views |
structs-sync-state, structs-grass |
structs_webapp |
Read/write on most structs.*; full on signer.*; SELECT on cache.* views |
Webapp, TSA |
structs_crawler |
Read-only on select structs.* tables |
Crawler |
For agent queries, connect as structs_indexer via the GRASS container (see .cursor/skills/structs-guild-stack/SKILL.md).
structs.grass_categoryActivity category enum on structs.planet_activity.category.
| Value | Notes |
|---|---|
block |
New block committed (emitted as a consensus heartbeat on subject consensus) |
guild_consensus |
Guild state changes |
guild_meta |
Off-chain guild metadata updates |
guild_membership |
Membership changes |
raid_status |
Raid status transitions (initiated, ongoing, shieldsVulnerable, … — no completed) |
shield_change |
Planetary shield value changed (planetary_shield / planetary_shield_old) |
block_raid_start |
Raid vulnerability clock (blockStartRaid) armed |
fleet_arrive |
Fleet arrived at planet |
fleet_advance |
Defined but not emitted — movement surfaces as fleet_depart + fleet_arrive |
fleet_depart |
Fleet departed |
struct_attack |
Combat attack (per-shot detail; stubbed when the payload exceeds ~7995 bytes — stub:true, no detail, pull planet_activity for shot detail) |
struct_defense_add |
Defense assignment added |
struct_defense_remove |
Defense assignment removed |
struct_status |
Struct status change (online / offline / destroyed) |
struct_move |
Struct moved between slots / ambits |
struct_block_build_start |
Build PoW started |
struct_block_ore_mine_start |
Mining PoW started |
struct_block_ore_refine_start |
Refining PoW started |
struct_health |
Struct health changed (health / health_old) |
player_consensus |
Player state changes (including UGC username / pfp updates) |
player_meta |
Off-chain player metadata updates |
Live enum ends at player_meta — there are no player_address / player_address_pending grass_category values (those are tables, not categories).
Every planet_activity INSERT fires pg_notify('grass', …) on subject structs.planet.{planet_id}.{player_id} (literal noPlayer when owner unresolved; top-level player_id in payload). Subscribe with a trailing wildcard: structs.planet.{planet_id}.*. The struct_attack stub guard (and the same guard on player_consensus / guild_consensus / guild_meta) replaces an over-8000-byte payload with a { "stub": true } envelope. Grid attribute changes (structs.grid.{object}) and ledger actions (structs.inventory.…) stream on their own subjects with their own category values.
| Enum | Schema | Values (summary) |
|---|---|---|
object_type |
structs |
guild, player, planet, reactor, substation, struct, allocation, infusion, address, fleet, provider, agreement |
signer_tx_status |
structs |
pending, claimed, broadcast, error |
signer_tx_module |
structs |
structs, bank, staking, auth, authz |
signer_tx_type |
structs |
100+ command types (see signer.tx.command) |
UGC-related signer_tx_type values: player-update-name, player-update-pfp, guild-update-name, guild-update-pfp, planet-update-name, substation-update-name, substation-update-pfp.
structs Schema| Group | Tables |
|---|---|
| Core entities | player, planet, fleet, struct, struct_type, guild, guild_meta, reactor, substation, allocation, infusion, provider, agreement |
| Attributes & grid | grid, struct_attribute, planet_attribute, struct_defender |
| Permissions | permission, permission_guild_rank |
| Planet activity | planet_activity, planet_activity_sequence, planet_raid |
| Player addresses | player_address, player_address_activity, player_address_meta, player_address_pending, player_address_activation_code, player_object |
| Onboarding / pending | player_pending, player_internal_pending, player_external_pending, guild_membership_application |
| Integrations | player_discord |
| Economy / ledger | ledger, defusion |
| Stats (TimescaleDB) | stat_ore, stat_fuel, stat_capacity, stat_load, stat_structs_load, stat_power, stat_connection_count, stat_connection_capacity, stat_struct_health, stat_struct_status |
| Config / moderation | setting, banned_word, address_tag, current_block |
Dropped tables (migrated 2026-05-25): player_meta → columns on player; planet_meta → name on planet.
structs.player| Column | Type | Notes |
|---|---|---|
id |
varchar PK | 1-{index} |
index |
integer | Numeric portion of ID |
guild_id |
varchar | Guild membership (0-{index}) |
guild_rank |
bigint | Rank within guild (1 = highest; 101 = default on join) |
substation_id |
varchar | Connected substation (4-{index}) |
planet_id |
varchar | Home planet (2-{index}) |
fleet_id |
varchar | Fleet (9-{index}) |
primary_address |
varchar | Cosmos address |
creator |
varchar | Creating address |
username |
varchar | Chain UGC display name |
pfp |
varchar | Chain UGC profile picture URI |
pfp_client_render_attributes |
varchar | Chain-sourced pfp render hints — a compacted JSON string, mirroring username/pfp. (The JSONB sibling is player_pending.pfp_cr_attributes, a different table and column.) |
created_at / updated_at |
timestamptz | Row timestamps |
player_meta was dropped as a table; username / pfp live on player. Off-chain meta updates still surface as grass_category player_meta. Sync-state writes UGC from MsgPlayerUpdateName / MsgPlayerUpdatePfp or signup proxy fields.
structs.planet| Column | Type | Notes |
|---|---|---|
id |
varchar PK | 2-{index} |
owner |
varchar | Player ID |
max_ore |
integer | Maximum ore capacity |
space_slots / air_slots / land_slots / water_slots |
integer | Slot counts per ambit |
status |
varchar | active, complete |
name |
text | Chain UGC display name |
map |
jsonb | Planet map data |
creator |
varchar | Creating address |
location_list_start / location_list_end |
varchar | Location list bounds |
created_at / updated_at |
timestamptz | Row timestamps |
planet_meta was dropped; name lives directly on this table.
There is no seized_ore column on structs.planet — seized ore lives on planet_raid. Related: planet_attribute (key-value, e.g. planetaryShield), and planet_raid (planet_id, fleet_id, status, updated_at, seized_ore). Note planet_raid’s primary key is planet_id alone, so it holds only the latest raid per planet, not raid history — derive history from ledger rows with action = 'seized'.
structs.guild| Column | Type | Notes |
|---|---|---|
id |
varchar PK | 0-{index} |
index |
integer | Numeric portion of ID |
name |
varchar | Chain UGC display name |
pfp |
varchar | Chain UGC profile picture URI |
entry_rank |
bigint | Default rank for new members |
endpoint |
varchar | Guild API endpoint |
join_infusion_minimum |
numeric | Generated from join_infusion_minimum_p |
join_infusion_minimum_p |
numeric | Raw infusion minimum |
primary_reactor_id |
varchar | Guild reactor |
entry_substation_id |
varchar | Default substation for new members |
creator / owner |
varchar | Addresses |
created_at / updated_at |
timestamptz | Row timestamps |
structs.guild_metaOff-chain guild configuration — not chain UGC name/pfp (those are on structs.guild).
| Column | Type | Notes |
|---|---|---|
id |
varchar PK | Guild ID (mirrors structs.guild.id) |
name |
varchar | Legacy display name (prefer structs.guild.name for on-chain UGC) |
description |
text | Guild description |
tag |
varchar | Short guild tag |
logo |
varchar | Logo URI |
socials |
jsonb | Social links |
denom |
jsonb | Guild token denomination map |
services |
jsonb | Guild API / GRASS / webapp endpoints |
domain |
varchar | Guild domain |
website |
varchar | Guild website |
base_energy |
numeric | Base energy allocation |
this_infrastructure |
boolean | Whether this guild stack hosts this guild’s infra |
status |
varchar | Meta status |
created_at / updated_at |
timestamptz | Row timestamps |
pfp was removed from this table (2026-05-25); chain UGC lives on structs.guild.
structs.struct| Column | Type | Notes |
|---|---|---|
id |
varchar PK | 5-{index} |
index |
integer | Numeric portion of ID |
type |
integer | FK to struct_type.id (1–22) |
owner |
varchar | Player ID |
creator |
varchar | Creating address |
location_type |
varchar | fleet or planet |
location_id |
varchar | Fleet or planet ID |
operating_ambit |
varchar | space, air, land, water |
slot |
integer | Position within ambit (0–3) |
is_destroyed |
boolean | Destruction state (default false) |
destroyed_block |
bigint | Block height when destroyed |
created_at / updated_at |
timestamptz | Row timestamps |
Related: struct_attribute (health, status, protectedStructIndex, …), struct_defender (defending_struct_id PK → protected_struct_id). Full balance data in struct_type (~60 columns — weapons, defense, charges, difficulties).
structs.fleet| Column | Type | Notes |
|---|---|---|
id |
varchar PK | 9-{index} |
owner |
varchar | Player ID |
status |
varchar | onStation, away — camelCase, not snake_case. Querying = 'on_station' returns zero rows, which for a raid check fails open (every target looks raidable). |
location_type |
varchar | planet |
location_id |
varchar | Planet ID where fleet is located |
command_struct |
varchar | Command Ship struct ID |
space_slots / air_slots / land_slots / water_slots |
integer | Available slots per ambit |
map |
jsonb | Fleet map data |
location_list_forward / location_list_backward |
varchar | Movement list bounds |
created_at / updated_at |
timestamptz | Row timestamps |
structs.gridKey-value store for resource attributes. No dedicated ore column — filter by attribute_type.
| Column | Type | Notes |
|---|---|---|
id |
varchar PK | Encoded attribute_type-object_type-object_index |
object_id |
varchar | Player, planet, struct, etc. ID |
object_type |
varchar | Entity type prefix |
object_index |
integer | Entity index |
attribute_type |
varchar | Grid labels only: ore, fuel, capacity, load, structsLoad, power, connectionCapacity, connectionCount, allocationPointerStart, allocationPointerEnd, proxyNonce, lastAction, nonce, ready, checkpointBlock. No alpha — Alpha Matter is a ledger denom, not a grid attribute. |
val |
numeric | Attribute value (single column — no value_p/value pair on grid) |
updated_at |
timestamptz | Last update |
structs.planet_activityTimescaleDB hypertable — planet-level event log (GRASS source).
| Column | Type | Notes |
|---|---|---|
time |
timestamptz | Event time (hypertable partition key) |
seq |
integer | Monotonic sequence per planet (high-water mark for polling) |
planet_id |
varchar | Planet where event occurred |
category |
grass_category |
Event type (see enum above) |
detail |
jsonb | Event-specific payload |
block_height |
bigint | Block height when event occurred (populated by sync-state) |
| Category | Description |
|---|---|
block |
New block committed |
raid_status |
Raid status (initiated / ongoing / shieldsVulnerable / … — not “completed”) |
shield_change |
Planetary shield changed |
block_raid_start |
Raid vulnerability clock armed |
fleet_arrive |
Fleet arrived at planet |
fleet_advance |
Never emitted — use fleet_depart + fleet_arrive |
fleet_depart |
Fleet departed |
struct_attack |
Combat attack |
struct_defense_add |
Defense assignment added |
struct_defense_remove |
Defense assignment removed |
struct_status |
Struct status change |
struct_move |
Struct moved |
struct_block_build_start |
Build PoW started |
struct_block_ore_mine_start |
Mining PoW started |
struct_block_ore_refine_start |
Refining PoW started |
struct_health |
Struct health changed |
guild_consensus |
Guild state changes |
guild_meta |
Off-chain guild metadata |
guild_membership |
Membership changes |
player_consensus |
Player changes (including UGC) |
player_meta |
Off-chain player metadata |
structs tables (summary)| Table | Key columns | Purpose |
|---|---|---|
permission |
object_id, player_id, val |
Per-player permission bitmask on objects |
permission_guild_rank |
object_id, guild_id, permission, rank |
Guild-rank permission grants (PK: object + guild + permission bit) |
reactor |
id, guild_id, validator, owner |
Validator-linked energy production |
substation |
id, owner, name, pfp |
Power distribution; chain UGC on name / pfp |
allocation |
id, source_id, destination_id, controller |
Energy routing |
infusion |
destination_id, address, fuel, power, commission |
Composite PK (destination_id, address) |
provider |
id, rate_amount, rate_denom, access_policy |
Energy marketplace listings |
agreement |
id, provider/consumer refs, capacity, duration |
Active purchase contracts |
ledger |
time, address, amount, action, direction, denom |
Financial transaction log (hypertable) |
defusion |
validator_address, delegator_address, amount, completes_at |
In-flight reactor unbonding |
setting |
name PK, value |
Live tunables (REACTOR_RATIO, PLANET_STARTING_ORE, …) |
banned_word |
word PK |
UGC name validation seed data |
address_tag |
address, label, entry |
Labelled address records |
current_block |
height, status, lag_blocks, tip_height |
Chain tip mirror (sync-state writes) |
player_pending |
primary_address PK, username, pfp, … |
Signup queue |
player_address |
address PK, player_id, guild_id, status |
Address ↔ player mapping |
Most stat_* hypertables use time, object_type, object_index, value, block_height — but several omit object_type (e.g. stat_structs_load, stat_connection_*, stat_struct_*). Check \d before joining.
sync_state SchemaWritten by structs-sync-state. Operator and debugging tables.
| Table | Purpose | Key columns |
|---|---|---|
sync_cursor |
Per-chain ingest pointer | chain_id PK, last_height, status, lag_blocks, tip_height, last_block_hash, last_block_time |
block_log |
One row per ingested block | chain_id, height, block_hash, num_txs, num_events, num_handler_errors |
handler_error_log |
Per-event handler failures | chain_id, height, composite_key, error, severity, resolved_at |
raw_blocks |
Raw block mirror | chain_id, height, block_time |
raw_tx_results |
Raw tx mirror | height, tx_index, tx_hash |
raw_events |
Raw event mirror | height, tx_index, event_index, event_type |
raw_attributes |
Raw attribute mirror | height, tx_index, event_index, key, composite_key, value |
verification_report |
Output of sync-state verify runs |
run_id, scope, status, expected, actual |
genesis_log |
Genesis import audit trail | Genesis-state snapshots |
unknown_event_log |
Unrecognized event types | Debugging unhandled chain events |
Monitor indexer health:
SELECT chain_id, last_height, status, lag_blocks, tip_height
FROM sync_state.sync_cursor;
cache SchemaRead-only compatibility layer over sync_state.raw_*. cache consists of four views.
| View | Source | Notes |
|---|---|---|
cache.blocks |
sync_state.raw_blocks |
rowid = height |
cache.tx_results |
sync_state.raw_tx_results |
tx_result is permanently NULL (protobuf not captured) |
cache.events |
sync_state.raw_events |
Surrogate rowid from height + tx_index + event_index |
cache.attributes |
sync_state.raw_attributes |
event_id matches cache.events.rowid |
New code should query sync_state.raw_* directly. cache.* exists for webapp backward compatibility.
view SchemaComputed projections — prefer these over hand-joining raw tables where available.
| View | Purpose |
|---|---|
view.player |
Player + grid attributes + inline username / pfp |
view.planet |
Planet + attributes + defense struct counts + name |
view.guild |
Guild + off-chain meta + on-chain name / pfp |
view.struct |
Struct joined with full struct_type balance data |
view.substation |
Substation + grid attributes + UGC fields |
view.reactor |
Reactor + grid attributes |
view.grid |
Expanded grid with object metadata |
view.permission_player / view.permission_address |
Permission projections — one boolean column per permission bit (perm_play, perm_admin, …, perm_hash_raid) |
view.work |
Active PoW tasks (object_id, player_id, category, block_start, difficulty_target) |
view.guild_bank |
Per-guild Central Bank position |
view.leaderboard_guild |
Guild scoreboard |
view.leaderboard_player |
Player scoreboard |
view.permission_player / view.permission_addressProject structs.permission joined with permission_guild_rank, keyed by player_id and address respectively. Each exposes one boolean column per permission bit (perm_play, perm_admin, perm_update, …, perm_hash_build/perm_hash_mine/perm_hash_refine/perm_hash_raid) alongside object_id, object_type, and updated_at.
PermAll = 33554431 (25 bits, 0..24 set; bit 24 = PermGuildUGCUpdate = 16777216).
signer.UPDATE_PENDING_ACCOUNT defaults to PermAll so newly provisioned signer addresses receive every permission bit.
signer SchemaTSA (Transaction Signing Agent) infrastructure. Services insert into signer.tx with status = 'pending'; TSA claims, signs, and broadcasts.
| Table | Key columns | Notes |
|---|---|---|
signer.role |
id, player_id, guild_id, status |
Status: stub, generating, pending, ready |
signer.account |
id, role_id, address, status |
Status: stub, generating, pending, available, signing |
signer.tx |
id, object_id, module, command, args (jsonb), status |
Status: pending, claimed, broadcast, error |
12 PL/pgSQL signer.tx_* functions queue UGC updates:
PermUpdate (4) on the target objectPermGuildUGCUpdate (16777216) on the target owner’s guildBoth groups broadcast the same chain messages (MsgPlayerUpdateName, MsgGuildUpdatePfp, etc.). Guild moderation is the same update message gated by moderator permissions; the chain emits ugc_moderated when actor ≠ owner.
sqitch SchemaSqitch migration registry. Do not modify manually.
knowledge/infrastructure/database-schema.md — Agent query guide (grid patterns, planet_activity polling, energy commerce)knowledge/infrastructure/guild-stack.md — Architecture and service topology.cursor/skills/structs-guild-stack/SKILL.md — Local deployment and common queriesknowledge/mechanics/permissions.md — 25-bit permission flag reference