Skip to content

ROWS

ROWS is a single-binary Rust reimplementation of the OWS (Open World Server) game backend. It consolidates the five .NET microservices (PublicAPI, InstanceManagement, CharacterPersistence, GlobalData, Management) into one unified service with both REST and gRPC interfaces.

  • REST API (Axum) — player-facing endpoints: login, registration, character CRUD, zone connections
  • gRPC (Tonic) — internal service-to-service communication and UE5 dedicated server coordination
  • RabbitMQ — async job queue for instance lifecycle events (spin-up, shutdown, health checks)
  • PostgreSQL — persistent storage for accounts, characters, abilities, world state
  • ValKey — session cache and ephemeral game state
  • Agones — game server fleet management for UE5 dedicated server instances
GroupPath PrefixDescription
Auth/api/users/*Login, register, session management
Characters/api/characters/*Character CRUD, class selection
Zones/api/zones/*Zone listing, server-to-connect-to
Instance/api/instance/*Server instance lifecycle
Global/api/global/*Key/value world state
Health/health, /readyLiveness and readiness probes
gRPCPort 4322 (multiplexed)Internal service mesh

ROWS deploys as a single Docker image (ghcr.io/kbve/rows). ArgoCD syncs the manifest from apps/kube/rows/manifest/rows-deployment.yaml with auto-prune and self-heal enabled.

The container exposes:

  • Port 4322 — REST + gRPC multiplexed (Axum + Tonic on a single port)

Each game + environment is a separate ROWS deployment pinned to its own tenant via env. The config is loaded and validated once at boot (RowsConfig::from_env):

EnvPurposeNotes
OWS_API_KEYTenant customer_guid (UUID)Required for beta/release; dev falls back to an ephemeral guid with a warning
OWS_TENANT_SLUGHuman label (e.g. chuckrpg-dev)Surfaced in logs, DeploymentInfo, and the Prometheus service label (rows-<slug>)
OWS_ENVdev | beta | releaseDrives the required-env guards
AGONES_FLEETPer-tenant Agones fleetRequired for beta/release; the GameServer watcher filters by agones.dev/fleet=<fleet>
AGONES_NAMESPACEAgones namespaceRequired for beta/release
SUPABASE_JWT_SECRET / SUPABASE_SERVICE_KEY_HASHPlayer + trusted-server authOptional; ROWS boots in legacy mode without them

GET /api/System/DeploymentInfo echoes the active tenant_slug, environment, and customer_guid for verification.


ROWS has three distinct identities. They are different axes — do not conflate them.

IdentityWhat it isSourceHow ROWS reads it
Tenant (customer_guid)A game + environment — e.g. chuckrpg-dev, chuckrpg-beta, xy-release. One fixed value per deployment.OWS_API_KEY env (per-tenant secret)X-CustomerGUID header, enforced equal to the process tenant
Player (userguid)A person.Supabase UUID (sub)Supabase JWT, or a session GUID minted from it
Trusted serverThe UE dedicated server / Iris instances.Service keyx-service-key header, argon2-verified against SUPABASE_SERVICE_KEY_HASH

The tenant is not the player. customer_guid partitions every table (users, characters, zones, world servers, maps). Within one tenant, all players and all UE instances share that customer_guid so they inhabit the same world — multiple Iris instances are just rows in worldservers / mapinstances under that tenant. The player is identified by their Supabase UUID, which becomes the OWS userguid.

Multi-tenant by deployment, single-tenant by process. ROWS runs one process per tenant (game + environment); each deployment pins its customer_guid from OWS_API_KEY at boot. The Postgres DB is shared across tenants and isolated only by the customerguid column, so the require_customer_guid middleware rejects (403) any request whose X-CustomerGUID differs from the process tenant — a client cannot reach another game’s rows by swapping the header. gRPC, WebSocket, MQ consumers, and background jobs always use the process tenant directly.

Player login. The client signs in via Supabase (GoTrue) and posts the JWT to ExternalLoginAndCreateSession. ROWS validates it (HS256, SUPABASE_JWT_SECRET), keys the OWS user on the Supabase sub, and returns a UserSessionGUID. The UE client glue lives in the ROWSupabase module of the KBVESupabase plugin.

Gates.

  • World IP (GetServerToConnectTo / JoinMap) requires a confirmed login — a Supabase bearer JWT or a live session GUID — and verifies the caller owns the requested character before returning a server address.
  • Server-write endpoints (position/stats/logout, zone status, launcher register, spin-up/down) require a valid service key; player credentials do not pass. The dedicated server loads the key from OWS_SERVICE_KEY only on IsRunningDedicatedServer(), so it never enters a client build, and talks to ROWS in-cluster over the ClusterIP — the key never crosses the public internet.

SUPABASE_SERVICE_KEY_HASH (rows) and the plaintext OWS_SERVICE_KEY (fleet) both derive from the one kilobase secret supabase-service-key (hash + key). The GoTrue customer_guid JWT claim is intentionally unused — tenant identity comes from the process config (OWS_API_KEY), never from the token.


Procedural World — Seed-Based Multi-Zone Architecture

Section titled “Procedural World — Seed-Based Multi-Zone Architecture”

Each game zone is a procedurally generated region driven by a deterministic seed. The same seed always produces identical terrain, foliage, and spawn layouts. This enables:

  • One map, many worlds — a single Lvl_Procedural map generates different zones from different seeds
  • Elastic scaling — any server pod can host any zone, just give it a seed
  • Crash recovery — replace a dead server, give it the same seed, world is identical
  • Discovery — new zones are just new seeds, no additional map packaging required

The maps table gains a seed column:

ALTER TABLE ows.maps ADD COLUMN seed BIGINT DEFAULT 0;
-- Example zones
INSERT INTO maps (customerguid, mapname, zonename, seed) VALUES
('83d88046-...', 'Lvl_Procedural', 'Grassland', 0xA1B2C3D4),
('83d88046-...', 'Lvl_Procedural', 'Arctic', 0xDEAD0002),
('83d88046-...', 'Lvl_Procedural', 'Desert', 0xCAFE0003),
('83d88046-...', 'Lvl_Procedural', 'Marshlands', 0xBEEF0004),
('83d88046-...', 'Lvl_PvPArena', 'PvPArena', 0); -- static map, no seed
1. Player → GetServerToConnectTo(zoneName: "Arctic")
2. ROWS → Agones allocate → server starts on Entry
3. ROWS → GetZoneAssignment returns:
{
"assigned": true,
"mapName": "Lvl_Procedural",
"zoneName": "Arctic",
"seed": 3735552002,
"zoneInstanceId": 42
}
4. Server → ServerTravel("/Game/Procedural/Lvl_Procedural?listen?seed=3735552002")
5. Lvl_Procedural BeginPlay → reads seed → PCG generates Arctic biome
6. Server reports ready → players connect to deterministic world

PCG (Procedural Content Generation) Framework:

LayerDriven ByExamples
Terrain heightmapSeed + biome typeMountain ranges, valleys, coastlines
Biome selectionSeed bits [0:7]Grassland, Arctic, Desert, Marshland, Tropic
Foliage placementSeed + density rulesTrees, bushes, grass patches
Resource nodesSeed + spawn tablesOre veins, herb clusters, fishing spots
Mob spawnsSeed + difficulty curveCreature type, patrol routes, density
LandmarksFixed coordinatesTowns, dungeons, POIs (handcrafted, placed at deterministic positions)
WeatherSeed + time-of-dayRain cycles, fog density, wind patterns

Hybrid approach:

  • Handcrafted landmarks are pre-built actors placed at coordinates derived from the seed
  • Procedural fill generates the terrain and natural environment between landmarks
  • Biome blending uses seed-derived Voronoi regions for smooth transitions

GameMode seed consumption:

// In Lvl_Procedural's GameMode BeginPlay
FString SeedStr = UGameplayStatics::ParseOption(OptionsString, TEXT("seed"));
int64 Seed = FCString::Atoi64(*SeedStr);
FRandomStream WorldRNG(Seed);
// Feed to PCG subsystem
UPCGSubsystem* PCG = GetWorld()->GetSubsystem<UPCGSubsystem>();
PCG->SetGlobalSeed(Seed);

Players move between zones via zone portals or world map travel:

Player enters Arctic→Desert portal
→ Client calls SetSelectedCharacterAndGetUserSession
→ Client calls GetServerToConnectTo(zoneName: "Desert")
→ ROWS finds/allocates Desert server (seed: 0xCAFE0003)
→ Client disconnects from Arctic, connects to Desert
→ Character position saved on Arctic, loaded on Desert

Seeds can be:

  • Static — admin-created zones with curated seeds (main world regions)
  • Dynamic — player-triggered instanced content (dungeons, events) with random seeds
  • Seasonal — time-limited zones that rotate seeds periodically
GET /api/System/SeedRegistry
{
"zones": [
{ "zoneName": "Grassland", "seed": 2712847316, "type": "static", "biome": "grassland" },
{ "zoneName": "Arctic", "seed": 3735552002, "type": "static", "biome": "arctic" },
{ "zoneName": "Dungeon_42", "seed": 8827361024, "type": "dynamic", "biome": "cave", "expires": "2026-04-01T00:00:00Z" }
]
}
PhaseScopeDependencies
Phase 1Add seed column to maps table, include in GetZoneAssignment responsedbmate migration
Phase 2Lvl_Procedural map with basic PCG — terrain heightmap from seedUE5 PCG framework
Phase 3Biome system — seed bits select biome, PCG generates appropriate flora/faunaBiome data tables
Phase 4Handcrafted landmarks — POI actors placed at seed-derived coordinatesLevel design
Phase 5Zone portals — client-side travel between seed-based zonesOWS travel flow
Phase 6Dynamic instances — player-triggered dungeons with random seedsInstance lifecycle
Phase 7Seed registry API — admin tools for zone managementDashboard integration