ECS
An Entity Component System for game state, simulations, and any fleet of objects with typed, queryable state: schema-validated components on entities, derived archetypes, parent-child hierarchies, queries with field-level filters, shared per-world resources, and systems driven by a tick coordinator. Every example on this page was executed against a live server.
When to reach for it: game worlds, agent or physics simulations, device fleets - anywhere many objects share typed state shapes that you query by shape ("all entities with Position and Velocity") and mutate with optimistic locking.
When not to: plain JSON storage keyed by name belongs to KV Store; distributed locks and compare-and-swap over a shared value belong to Distributed State.
Concepts
- Index - a namespace isolating one ECS world, passed as
-x/--indexon almost every command. Everything below except schemas lives inside an index; two indexes never see each other's entities. - Schemas are global and mandatory - a component type must have a JSON Schema before any entity can carry it. Global (no index) and versioned.
- Entity - an ID plus components (JSON documents of schema-defined
types), tags, an optional parent, and a
versionthat increments on every mutation (optimistic locking). - Archetype - the set of component types on an entity, identified by a hash. Derived automatically; you never create or name one.
- Resource - shared singleton JSON per index (world config, clocks), not attached to any entity, with versioned updates.
- Systems and the coordinator - external workers matched to entities by a stored query and driven by a tick loop; they have their own page.
Schemas
Schema commands take no index. Component data is validated against the
latest schema version at write time - verified by relaxing a required
field in a v2 and watching a previously rejected write succeed:
snug ecs schema create --type Position --version 1 \
--schema '{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"}},"required":["x","y"]}'
snug ecs schema list
snug ecs schema get --type Position # latest; add --version 1 for a specific one
snug ecs schema delete --type Position --version 1 --forceWriting a component type that has no schema fails with
404 schema_not_found; data that violates the schema fails with
400 schema_validation_failed and the validator's message
("y" is a required property).
Entities and components
snug ecs entity create -x game1 --id player1 \
--components '{"Position":{"x":10,"y":20},"Health":{"current":100,"max":100}}' \
--tags player,active
snug ecs component set -x game1 --entity player1 --type Velocity --data '{"x":1,"y":0}'
snug ecs component get -x game1 --entity player1 --type Velocity
snug ecs component remove -x game1 --entity player1 --type Velocity
snug ecs entity get -x game1 --id player1
snug ecs entity tags -x game1 --id npc1 --tags hostile,elite
snug ecs entity delete -x game1 --id player1 --forceCreating an ID that already exists in the index fails with
409 entity_already_exists. entity tags replaces the whole tag set
with the supplied list - it does not append.
Batch operations create from a JSON array and delete by query, not by ID
list. The CLI's batch-delete exposes only --has-all; the HTTP endpoint
accepts a full query object:
snug ecs entity batch-create -x game1 --entities \
'[{"entity_id":"crate1","components":{"Position":{"x":0,"y":0}}},{"entity_id":"crate2","components":{"Position":{"x":1,"y":1}},"tags":["loot"]}]'
snug ecs entity batch-delete -x game1 --has-all Position --limit 100 --forceQueries
The CLI matches on component presence, with --count and cursor
pagination (pass the returned cursor back via --cursor):
snug ecs query -x game1 --has-all Position,Velocity --limit 10
snug ecs query -x game1 --has-all Position --has-none Velocity --countThe HTTP endpoint adds field-level filters
(eq|ne|lt|lte|gt|gte|between|in|contains), plus select and order_by
that the CLI does not surface. The query lives under a where key whose
four arrays are all required, even when empty:
curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"where":{"has_all":["Health"],"has_any":[],"has_none":[],"filters":[{"component":"Health","field":"current","op":"lt","value":50}]}}' \
http://localhost:4000/api/v1/ecs/game1/query{
"status": 200,
"msg": "OK",
"data": {
"entities": [
{
"entity_id": "npc2",
"components": {
"Health": { "current": 30, "max": 30 },
"Position": { "x": 50, "y": 8 }
},
"tags": ["npc"],
"parent": null,
"archetype_hash": "92ba98566c2e1f6a",
"version": 1
}
],
"total": 1,
"cursor": null,
"has_more": false
}
}Archetypes
Read-only views over the component-set groupings the server derives:
snug ecs archetype list -x game1
snug ecs archetype get -x game1 --hash 92ba98566c2e1f6aThe hash is a pure function of the component set - verified by adding a component (hash changed) and removing it again (original hash returned).
Hierarchy
Single parent, many children. Set the parent at create time with
--parent, or later:
snug ecs hierarchy set-parent -x game1 --id sword1 --parent player1
snug ecs hierarchy children -x game1 --id player1
snug ecs hierarchy remove-parent -x game1 --id sword1Setting a new parent replaces the old one; a parent chain that loops back
is rejected with 422 circular_reference.
Resources
snug ecs resource create -x game1 --name GameConfig --data '{"gravity":9.8,"max_players":8}'
snug ecs resource get -x game1 --name GameConfig
snug ecs resource update -x game1 --name GameConfig --data '{"gravity":10.0,"max_players":8}' --expected-version 1
snug ecs resource delete -x game1 --name GameConfig --force--expected-version is optimistic locking: a stale version fails with
409 version_conflict (expected 1, found 2) and leaves the resource
untouched. Systems can request resources in their tick payloads via
--include-resources - see Systems and the coordinator.
Change subscriptions
Subscription records declare what to watch - one entity, one component type everywhere, or a query:
snug ecs subscription create -x game1 --subscription-type entity --entity-id player1
snug ecs subscription create -x game1 --subscription-type component --component-type Position
snug ecs subscription create -x game1 --subscription-type query \
--query '{"has_all":["Position"],"has_any":[],"has_none":[],"filters":[]}'
snug ecs subscription delete -x game1 --subscription-id <sub-id>Notifications are delivered over the WebSocket endpoint
GET /api/v1/ecs/{index}/subscribe, authenticated with a Bearer header or
a ?token= query parameter - verified: the handshake upgrades (101) with a
token and is rejected (401) without. The CLI manages only the records; the
message protocol is in the
Subscriptions API reference.
Limits and configuration
Defaults: 64 components per entity, 64 KiB per component payload, batches
of 100, query pages of 100 (clamped at 1000), and a 100 ms default tick -
all tunable via the ECS_* variables in the
ECS CONFIG reference.
Reference
- API reference, split by area: Schemas, Entities, Components, Queries, Archetypes, Hierarchy, Resources, Systems, Coordinator, Subscriptions
- Related: KV Store for plain key-value data, Distributed State for locks and CAS, FSM for strict state-transition workflows