Dashboard
Altruist's dashboard is the built-in runtime UI for debugging both your live world and the Altruist framework itself while the server is running. It is useful for games because it can render worlds, terrain, objects, players, sessions, cache, and vault data, but it is also a general observability surface for understanding how Altruist is configured, what the engine is doing, and how runtime systems are behaving.
When it is enabled, Altruist serves the web UI at /altruist/dashboard and exposes the backing APIs under /dashboard/v1/*.
Note:
Use the dashboard as a framework debugger, not only as a game scene viewer. The world view helps inspect spatial/runtime state, while the summary, config, sessions, cache, and vault surfaces show how the Altruist host is wired and what data the framework is managing.
For the world view specifically, the dashboard renders three kinds of data:
- terrain heightfields
- regular world objects such as props, NPCs, and players
- optional gizmos and live object-position updates
What You Can Debug
The dashboard is meant to answer both world-level and framework-level questions:
- Is the Altruist host running with the expected config and enabled modules?
- Which sessions are connected, and what runtime state is attached to them?
- What is currently stored in cache or vault-backed persistence?
- Which worlds exist, how are they partitioned, and which objects are spawned?
- Are terrain heightfields, colliders, gizmos, and live object updates reaching the dashboard backend?
- Are framework systems such as synchronization, visibility, sessions, persistence, and dashboard streaming producing the data your client expects?
What You Need
The dashboard world scene is not automatic from config alone. For it to show useful data, all of these must be true:
- Your app references the
Altruist.Dashboardpackage or project. altruist:dashboard:enabledistrue.- A 3D world exists in
IGameWorldOrganizer3D. - Objects are actually spawned into that world.
- If you want terrain wireframe, you must spawn
Terrainworld objects that containHeightfieldData. - WebSocket transport is enabled so the live dashboard portal can stream updates.
Note:
ITerrainProvider by itself is not enough for rendering the dashboard terrain. It helps movement and spatial queries, but the dashboard scene only sees terrain that exists as actual Terrain world objects inside the world snapshot.
Turn It On
Add the dashboard package or project reference:
<ItemGroup>
<ProjectReference Include="..\..\libs\Altruist\Altruist\Gaming\Main\Gaming.csproj" />
<ProjectReference Include="..\..\libs\Altruist\Altruist\Postgres\Postgres.csproj" />
<ProjectReference Include="..\..\libs\Altruist\Altruist\Dashboard\Dashboard.csproj" />
</ItemGroup>
Then enable the dashboard and WebSocket transport:
altruist:
environment:
mode: "3D"
dashboard:
enabled: true
token: ${DASHBOARD_TOKEN} # required outside Development, see "Securing The Dashboard"
server:
http:
host: "0.0.0.0"
port: "8080"
path: "/"
transport:
websocket:
enabled: true
path: /ws
Once the app starts, the stock UI is served at:
http://localhost:8080/altruist/dashboard
In the Development environment (ASPNETCORE_ENVIRONMENT=Development) that works straight away from the same machine. Anywhere else you must protect it first, see below.
Securing The Dashboard
The dashboard is an admin surface: it runs SQL against your vaults, edits caches and live config, disconnects players and can invoke any gate as any client id. Every dashboard path (/altruist/dashboard, /dashboard/v1/*, /ws/dashboard) therefore goes through a mandatory access check. Pick one of two ways in:
| Config key | Meaning |
|---|---|
altruist:dashboard:token | Shared secret, at least 16 characters. Compared in constant time. |
altruist:dashboard:policy | Name of an ASP.NET Core authorization policy your app registers. The signed-in user (default authentication scheme) must satisfy it. |
altruist:dashboard:allow-loopback-without-token | Default true. In Development only, with neither of the above, loopback requests are let in. Set false to require credentials there too. |
altruist:
dashboard:
enabled: true
token: ${DASHBOARD_TOKEN} # >= 16 chars, keep it out of source control
# or: policy: ops
With a token, scripts and tools send it as a header, either X-Altruist-Dashboard-Token: <token> or Authorization: Bearer <token>:
curl -H "X-Altruist-Dashboard-Token: $DASHBOARD_TOKEN" http://localhost:8080/dashboard/v1/summary
In a browser, open the UI once with the token in the URL:
http://localhost:8080/altruist/dashboard?dashboard_token=<token>
The server swaps it for an HttpOnly, SameSite=Strict session cookie (altruist_dashboard) and reloads the page without the token. The cookie is derived from the token, so changing the token logs everyone out.
With a policy, register it in your app and point the dashboard at it:
services.AddAuthorization(o => o.AddPolicy("ops", p => p.RequireRole("ops")));
altruist:
dashboard:
enabled: true
policy: ops
If both are configured, either one grants access.
Status codes: 401 means no credentials were sent, 403 means the wrong credentials (or, in Development without a token, a non-loopback client), and 503 means the dashboard is closed.
Note:
If the dashboard is enabled outside Development with neither a token nor a policy, the server still starts and serves your game, but every dashboard request answers 503 and startup logs a critical DASHBOARD CLOSED error. Nothing is exposed until you configure access. A token shorter than 16 characters counts as not configured.
Note:
Development loopback access ignores requests that carry X-Forwarded-For, Forwarded or X-Real-IP, so a reverse proxy on the same machine does not make remote clients look local.
What The APIs Reveal
/dashboard/v1/summarylists everyaltruist*config key, but returns***for secret values: keys containingpassword,secret,token,key(for examplesigning-keyorapi-key),credentialorconnection-string, and values that look like connection strings with a password.- The cache editor (
PUT/DELETE /dashboard/v1/cache/entry) only accepts a type that is currently in the cache or a registered vault model, named by its exact full or assembly-qualified name. Any other type name gets400. - The API lab's HTTP calls always go to this server's own bound address. It never uses the request's
Hostheader. - When
altruist:dashboard:enabledis nottrue, none of the dashboard controllers are mapped, sessions included. The world viewer endpoints and the/ws/dashboardportal in the gaming package also need theAltruist.Dashboardassembly to be referenced.
What The Dashboard Backend Exposes
The current dashboard server surface comes from the Altruist.Dashboard package and the gaming dashboard controllers:
/altruist/dashboard- embedded dashboard SPA/dashboard/v1/summary- summary and config data/dashboard/v1/worlds- world list/dashboard/v1/worlds/{worldIndex}/objects- full world snapshot/dashboard/v1/worlds/{worldIndex}/objects/stream- NDJSON partition/object stream/dashboard/v1/worlds/{worldIndex}/gizmos- current gizmo snapshot/dashboard/v1/sessions- session management/dashboard/v1/cache- cache inspection/dashboard/v1/vaults- vault inspection
For live world updates, Altruist also hosts a dashboard portal:
[Portal("dashboard")]
public sealed class DashboardPortal : Portal
{
[Cycle]
public async Task UpdateDashboard()
{
// sends DashboardWorldObjectStatePacket
}
}
With the common WebSocket transport path of /ws, the dashboard world stream is exposed at:
ws://localhost:8080/ws/dashboard
Note:
The current embedded dashboard UI source still hardcodes ws://localhost:8000/ws/dashboard for the world scene client. If your host, port, or transport path differs, the HTTP dashboard can still load, but live world updates need the dashboard UI client adjusted to your runtime URL.
How World Rendering Works
There are two different data flows:
1. Full snapshot
WorldDashboardController builds the initial scene from:
- world partitions
WorldObjectDto- collider DTOs
HeightfieldDto- gizmo snapshot
This is where terrain comes from. Heightfields are serialized out of collider data during snapshot generation.
2. Live delta stream
DashboardPortal sends DashboardWorldObjectStatePacket every second for changed object positions and gizmo changes.
The important limitation is that live packets are intentionally minimal:
- only changed object positions are streamed
- terrain is not resent in the live packet
- full collider and heightfield shape comes from the HTTP snapshot, not the websocket delta
That means the dashboard scene always boots from the snapshot first, then patches movement on top.
What You Must Implement For The World To Show
1. A 3D world
The dashboard world controller depends on IGameWorldOrganizer3D. If your app is not running in 3D mode with real worlds, there is nothing for it to inspect.
altruist:
environment:
mode: "3D"
game:
worlds:
partitioner: { width: 25600, height: 25600, depth: 25600 }
items:
- index: 0
id: "main"
size: { x: 3300000, y: 1900000, z: 50000 }
2. Real spawned objects
Players, NPCs, monsters, props, and other world objects appear automatically if they are actually inside the world.
var world = _worldOrganizer.GetWorld(0);
await world.SpawnDynamicObject(playerPrefab);
await world.SpawnDynamicObject(monsterPrefab);
await world.SpawnStaticObject(treeObject);
You do not need a special dashboard-only player type. A player becomes visible in the dashboard simply by being a normal spawned IWorldObject3D.
Note:
For runtime physics bodies, the snapshot controller prefers the live body transform over the original stored transform, so the dashboard reflects the actual body position and rotation.
3. A terrain provider for gameplay
If physics is disabled and your game uses heightmaps, implement ITerrainProvider so movement, grounding, and spatial queries work correctly:
[Service(typeof(ITerrainProvider))]
public sealed class MyTerrainProvider : ITerrainProvider
{
public bool IsWalkable(float x, float y, float z)
=> true;
public float GetHeight(float x, float z)
=> 0f;
public Vector3 GetNormal(float x, float z)
=> Vector3.UnitY;
}
This is required for gameplay-side terrain behavior, but not yet sufficient for dashboard rendering.
4. Terrain world objects for dashboard rendering
To actually see a terrain wireframe in the dashboard scene, you must also spawn Terrain world objects containing HeightfieldData.
This is the usual bootstrap pattern:
private static async Task SpawnTerrainForDashboard(
IGameWorldOrganizer3D worldOrganizer,
ITerrainCatalog terrainCatalog,
ILogger logger)
{
var world = worldOrganizer.GetWorld(0);
if (world == null)
return;
var terrains = terrainCatalog.GetTerrains();
foreach (var terrain in terrains)
{
var transform = Transform3D.Identity.WithPosition(
Position3D.From(new Vector3(terrain.BaseX, 0f, terrain.BaseZ)));
await world.SpawnStaticObject(
new Terrain(
transform,
terrain.Heightfield,
zoneId: terrain.MapName,
archetype: $"terrain.{terrain.MapName}"));
}
}
That startup step is the difference between:
- movement working on your terrain
- the dashboard being able to draw that terrain
5. A terrain catalog or heightfield source
Expose an extra catalog service on top of ITerrainProvider when you want the same loaded heightmaps to power both gameplay and dashboard rendering:
[Service(typeof(ITerrainProvider))]
[Service(typeof(ITerrainCatalog))]
public sealed class GameTerrainProvider : ITerrainProvider, ITerrainCatalog
{
public IReadOnlyList<TerrainDefinition> GetTerrains()
=> _heightmaps.Value
.Select(heightmap => new TerrainDefinition(
heightmap.MapName,
heightmap.BaseX,
heightmap.BaseZ,
heightmap.WorldWidth,
heightmap.WorldDepth,
heightmap.Heightfield))
.ToList();
}
public interface ITerrainCatalog
{
IReadOnlyList<TerrainDefinition> GetTerrains();
}
public sealed record TerrainDefinition(
string MapName,
float BaseX,
float BaseZ,
float WorldWidth,
float WorldDepth,
HeightfieldData Heightfield);
That split is a clean production pattern:
ITerrainProvideranswers gameplay queries- your own catalog service exposes the heightfields you want to bootstrap into the dashboard scene
6. Optional gizmos
If you want debug circles, markers, zones, path previews, or AI overlays, use IDashboardGizmoRegistry.
The world snapshot includes the current gizmo snapshot, and the live portal streams gizmo additions, updates, and removals.
This is ideal for:
- spawn areas
- aggro radii
- patrol paths
- navigation targets
- trigger zones
The Packet Contract
The live world packet currently contains:
public sealed class DashboardWorldObjectStatePacket
{
public uint MessageCode { get; set; }
public int WorldIndex { get; set; }
public DateTime TimestampUtc { get; set; }
public DashboardPartitionStateDto[] Partitions { get; set; }
public DashboardGizmo[] Gizmos { get; set; }
public string[] RemovedGizmoIds { get; set; }
}
Each streamed object only includes:
idarchetypeposition
So if you need richer live inspector data in the future, that has to be added to the dashboard contract intentionally.
Recommended Integration Checklist
- Reference
Altruist.Dashboard. - Enable
altruist:dashboard:enabledand setaltruist:dashboard:tokenoraltruist:dashboard:policy(only optional for local Development). - Enable WebSocket transport.
- Make sure a 3D world exists and is populated.
- Implement
ITerrainProviderif your game uses heightmaps. - Expose a terrain catalog if you want dashboard terrain rendering.
- Spawn
Terrainobjects at startup for dashboard snapshot rendering. - Spawn players and objects normally into the world.
- Optionally register gizmos for debug overlays.
If all of these are in place, the dashboard can show:
- terrain wireframe
- static props
- dynamic objects
- player positions
- live movement deltas
- debug gizmos