Zone Grouping
The Context
Zones register independently with zone-backend and appear in a flat list on GET /shards. Each zone has a name and a map but no mechanism to declare membership in a named region, cluster, or event. Clients receive all live zones undifferentiated.
The Problem Statement
Clients and operators cannot filter or route to a subset of zones that belong together. A set of zones serving the same world region, event, or logical cluster cannot be discovered as a unit without out-of-band coordination.
Describe how your proposal will work with code, pseudo-code, mock-ups, or diagrams
Add an optional tags column (string array) to the zones table:
ALTER TABLE zones ADD COLUMN tags TEXT[] NOT NULL DEFAULT '{}';Zone servers pass tags at registration and heartbeat:
PUT /shards/:id
{ "shard": { "tags": ["vketspring2026", "hall-a"] } }GET /shards accepts an optional tags query parameter. When provided, zone-backend filters to zones whose tag array intersects the requested set:
def list_fresh_zones(tags \\ []) do
stale_timestamp = DateTime.add(DateTime.utc_now(), -zone_freshness_time_in_seconds(), :second)
base = from z in Zone, where: z.last_put_at > ^stale_timestamp, preload: [:user]
if tags == [] do
Repo.all(base)
else
Repo.all(from z in base, where: fragment("? && ?", z.tags, ^tags))
end
endTags are free-form strings. Zone-backend applies no semantics — grouping, hierarchy, and naming conventions are left to the operator. A zone may carry multiple tags.
Tags are included in the GET /shards response alongside existing fields so clients can render group membership without a second request.
The Benefits
Tags mirror the pattern already used on SharedFile (same column type, same operator convention). No new infrastructure is required. Filtering is a single array-intersection query on an indexed column. Operators can define arbitrary groupings without schema changes.
The Downsides
Tags are strings with no validation or enumeration. A typo in a tag silently produces an empty filter result. No enforcement prevents tag collision across unrelated operators on a shared instance.
The Road Not Taken
A parent_zone_id foreign key enforces a strict hierarchy (exactly one parent per zone) but cannot express overlapping membership (a zone belonging to both a region and an event). A separate zone_groups join table normalises the many-to-many case but adds a join on every read path and complexity on writes. The tags array covers the common cases without either constraint.
The Infrequent Use Case
A zone that belongs to no group omits tags from its registration payload. Zone-backend stores an empty array. GET /shards without a tags filter returns it alongside grouped zones, preserving the current flat-list behaviour.
In Core and Done by Us
- Migration: add
tags TEXT[]tozones Uro.VSekai.Zone— addfield(:tags, {:array, :string}, default: [])Zone.changeset/2— casttagsUro.VSekai.list_fresh_zones/1— accept optionaltagsfilterUro.ZoneController.index/2— passtagsquery param to context functionZone.to_json_schema/1— includetagsin serialised response
Status
Status: Proposed
Decision Makers
- iFire
Further Reading
20260421-zone-registration-and-discovery.md— base registration design20260421-zone-asset-manifest.md— per-zone asset listingmultiplayer-fabric-zone-backend/lib/uro/v_sekai/zone.ex