diff --git a/.gitignore b/.gitignore
index bead77e1d..50ae0b2e1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -97,6 +97,10 @@ Economy.Server/bin/
Economy.Server/obj/
Economy.Shared/bin/
Economy.Shared/obj/
+Aetheria.State/bin/
+Aetheria.State/obj/
+Aetheria.State.Smoke/bin/
+Aetheria.State.Smoke/obj/
# JetBrains Rider .idea folder
.idea/
diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc
index 168f00ca4..1c0d74e9a 100644
Binary files a/.voidbot/state/aetheria.cc and b/.voidbot/state/aetheria.cc differ
diff --git a/Aetheria.State.Smoke/Aetheria.State.Smoke.csproj b/Aetheria.State.Smoke/Aetheria.State.Smoke.csproj
new file mode 100644
index 000000000..ca788873d
--- /dev/null
+++ b/Aetheria.State.Smoke/Aetheria.State.Smoke.csproj
@@ -0,0 +1,13 @@
+
+
+ Exe
+ net10.0
+ enable
+ latest
+ enable
+
+
+
+
+
+
diff --git a/Aetheria.State.Smoke/Program.cs b/Aetheria.State.Smoke/Program.cs
new file mode 100644
index 000000000..d184ca608
--- /dev/null
+++ b/Aetheria.State.Smoke/Program.cs
@@ -0,0 +1,70 @@
+using Aetheria.State;
+using Aetheria.State.Documents;
+using Aetheria.State.Migration;
+using GameCult.Caching;
+
+var root = args.Length > 0 ? args[0] : Directory.GetCurrentDirectory();
+var statePath = AetheriaStatePaths.ResolveDefaultStatePath(root);
+var now = DateTimeOffset.UtcNow.ToString("O");
+var itemKey = new CultRecordKey("item:smoke-aether-drive");
+
+await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-state-smoke"))
+{
+ await node.PutWorldAsync(new AetheriaWorldState
+ {
+ Name = "Aetheria",
+ WorldId = "aetheria",
+ SchemaEpoch = 1,
+ CreatedAtUtc = now,
+ UpdatedAtUtc = now
+ });
+
+ await node.PutItemDefinitionAsync(
+ itemKey,
+ new AetheriaItemDefinition
+ {
+ Name = "Smoke Aether Drive",
+ Category = "ship-module",
+ LegacyId = "smoke:aether-drive",
+ Description = "Typed CultCache smoke document for the rebuild spine.",
+ Mass = 12.5,
+ Volume = 4.0,
+ Tags = ["smoke", "state-spine"]
+ });
+
+ await node.PutMigrationLedgerAsync(new AetheriaMigrationLedger
+ {
+ Source = LegacyMigrationBoundary.LegacyGameDataFile,
+ SourceFingerprint = "smoke",
+ LastMigrationAtUtc = now,
+ Counts =
+ [
+ new AetheriaMigrationCount
+ {
+ DocumentType = "aetheria.item_definition.v1",
+ Count = 1
+ }
+ ],
+ Notes = ["Smoke proves the new state owner can write, flush, reopen, and read without old JSON/Rethink authority."]
+ });
+
+ await node.FlushAsync();
+}
+
+await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aetheria-state-smoke-reopen"))
+{
+ var world = await reopened.GetWorldAsync();
+ var item = await reopened.GetItemDefinitionAsync(itemKey);
+
+ if (world?.WorldId != "aetheria")
+ {
+ throw new InvalidOperationException("World state did not survive flush/reopen.");
+ }
+
+ if (item?.Name != "Smoke Aether Drive")
+ {
+ throw new InvalidOperationException("Item definition did not survive flush/reopen.");
+ }
+}
+
+Console.WriteLine($"Aetheria typed state smoke passed: {statePath}");
diff --git a/Aetheria.State/Aetheria.State.csproj b/Aetheria.State/Aetheria.State.csproj
new file mode 100644
index 000000000..103d58879
--- /dev/null
+++ b/Aetheria.State/Aetheria.State.csproj
@@ -0,0 +1,16 @@
+
+
+ netstandard2.1
+ enable
+ latest
+ enable
+ Typed CultCache and CultMesh state spine for Aetheria.
+
+
+
+
+
+
+
+
+
diff --git a/Aetheria.State/AetheriaDocumentRegistry.cs b/Aetheria.State/AetheriaDocumentRegistry.cs
new file mode 100644
index 000000000..f6e9359e5
--- /dev/null
+++ b/Aetheria.State/AetheriaDocumentRegistry.cs
@@ -0,0 +1,45 @@
+using Aetheria.State.Documents;
+using GameCult.Caching;
+using GameCult.Networking;
+
+namespace Aetheria.State;
+
+public static class AetheriaDocumentRegistry
+{
+ public static CultDocumentRegistry CreateCultCacheRegistry()
+ {
+ var registry = new CultDocumentRegistry();
+ foreach (var documentType in DocumentTypes)
+ {
+ registry.GetRequired(documentType);
+ }
+
+ return registry;
+ }
+
+ public static CultNetDocumentRegistry CreateCultNetRegistry(CultDocumentRegistry? cacheRegistry = null)
+ {
+ var registry = cacheRegistry ?? CreateCultCacheRegistry();
+ return new CultNetDocumentRegistry(
+ registry,
+ new[]
+ {
+ CultNetDocumentBinding.ForDocument(registry),
+ CultNetDocumentBinding.ForDocument(registry),
+ CultNetDocumentBinding.ForDocument(registry),
+ CultNetDocumentBinding.ForDocument(registry),
+ CultNetDocumentBinding.ForDocument(registry),
+ CultNetDocumentBinding.ForDocument(registry)
+ });
+ }
+
+ public static IReadOnlyList DocumentTypes { get; } =
+ [
+ typeof(AetheriaWorldState),
+ typeof(AetheriaItemDefinition),
+ typeof(AetheriaCorporation),
+ typeof(AetheriaPlayerProfile),
+ typeof(AetheriaRuntimeSession),
+ typeof(AetheriaMigrationLedger)
+ ];
+}
diff --git a/Aetheria.State/AetheriaStateNode.cs b/Aetheria.State/AetheriaStateNode.cs
new file mode 100644
index 000000000..2e40ec927
--- /dev/null
+++ b/Aetheria.State/AetheriaStateNode.cs
@@ -0,0 +1,106 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Aetheria.State.Documents;
+using GameCult.Caching;
+using GameCult.Caching.MessagePack;
+using GameCult.Mesh;
+using GameCult.Networking;
+
+namespace Aetheria.State;
+
+public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
+{
+ private readonly CultMeshNode _node;
+
+ private AetheriaStateNode(string statePath, CultMeshNode node)
+ {
+ StatePath = statePath;
+ _node = node;
+ }
+
+ public string StatePath { get; }
+
+ public CultCache Cache => _node.Cache;
+
+ public CultNetDatabase Database => _node.Database;
+
+ public static async Task OpenAsync(
+ string statePath,
+ string runtimeId = "aetheria-local",
+ bool startServer = false)
+ {
+ if (string.IsNullOrWhiteSpace(statePath))
+ {
+ throw new ArgumentException("State path must be non-empty.", nameof(statePath));
+ }
+
+ Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(statePath)) ?? ".");
+
+ var cacheRegistry = AetheriaDocumentRegistry.CreateCultCacheRegistry();
+ var node = await CultMesh.CreateNodeAsync(
+ statePath,
+ new CultMeshNodeOptions
+ {
+ StartServer = startServer,
+ EnableDurableShardLogs = true,
+ CacheOptions = new CultCacheOpenOptions
+ {
+ Registry = cacheRegistry,
+ PullOnOpen = true,
+ StoreFlushOnDispose = true,
+ UseDirectoryStore = true
+ },
+ DatabaseOptions = new CultNetDatabaseOptions
+ {
+ RuntimeId = runtimeId,
+ DocumentRegistry = AetheriaDocumentRegistry.CreateCultNetRegistry(cacheRegistry)
+ }
+ }).ConfigureAwait(false);
+
+ return new AetheriaStateNode(statePath, node);
+ }
+
+ public Task> PutWorldAsync(AetheriaWorldState world)
+ {
+ return Database.PutAsync(new CultRecordKey("global:aetheria.world_state.v1"), world);
+ }
+
+ public Task GetWorldAsync()
+ {
+ return Database.GetAsync(new CultRecordKey("global:aetheria.world_state.v1"));
+ }
+
+ public Task> PutMigrationLedgerAsync(AetheriaMigrationLedger ledger)
+ {
+ return Database.PutAsync(new CultRecordKey("global:aetheria.migration_ledger.v1"), ledger);
+ }
+
+ public Task> PutItemDefinitionAsync(
+ CultRecordKey key,
+ AetheriaItemDefinition item)
+ {
+ return Database.PutAsync(key, item);
+ }
+
+ public Task GetItemDefinitionAsync(CultRecordKey key)
+ {
+ return Database.GetAsync(key);
+ }
+
+ public Task FlushAsync(bool soft = false)
+ {
+ return _node.FlushAsync(soft);
+ }
+
+ public void Dispose()
+ {
+ _node.Dispose();
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ await FlushAsync().ConfigureAwait(false);
+ Dispose();
+ }
+}
diff --git a/Aetheria.State/AetheriaStatePaths.cs b/Aetheria.State/AetheriaStatePaths.cs
new file mode 100644
index 000000000..2f5f96dea
--- /dev/null
+++ b/Aetheria.State/AetheriaStatePaths.cs
@@ -0,0 +1,16 @@
+using System.IO;
+
+namespace Aetheria.State;
+
+public static class AetheriaStatePaths
+{
+ public const string DefaultStateFileName = "aetheria-world.cc";
+
+ public static string ResolveDefaultStatePath(string? projectRoot = null)
+ {
+ var root = string.IsNullOrWhiteSpace(projectRoot)
+ ? Directory.GetCurrentDirectory()
+ : projectRoot!;
+ return Path.Combine(root, "GameData", DefaultStateFileName);
+ }
+}
diff --git a/Aetheria.State/Documents/AetheriaCorporation.cs b/Aetheria.State/Documents/AetheriaCorporation.cs
new file mode 100644
index 000000000..58b5b9d4a
--- /dev/null
+++ b/Aetheria.State/Documents/AetheriaCorporation.cs
@@ -0,0 +1,20 @@
+using GameCult.Caching;
+using MessagePack;
+
+namespace Aetheria.State.Documents;
+
+[CultDocument("aetheria.corporation", "aetheria.corporation.v1")]
+[MessagePackObject]
+public sealed class AetheriaCorporation
+{
+ [Key(0)]
+ [CultName]
+ public string Name { get; set; } = "";
+
+ [Key(1)]
+ [CultIndex("legacyId")]
+ public string LegacyId { get; set; } = "";
+
+ [Key(2)]
+ public string Description { get; set; } = "";
+}
diff --git a/Aetheria.State/Documents/AetheriaItemDefinition.cs b/Aetheria.State/Documents/AetheriaItemDefinition.cs
new file mode 100644
index 000000000..14d1dd8cb
--- /dev/null
+++ b/Aetheria.State/Documents/AetheriaItemDefinition.cs
@@ -0,0 +1,33 @@
+using GameCult.Caching;
+using MessagePack;
+
+namespace Aetheria.State.Documents;
+
+[CultDocument("aetheria.item_definition", "aetheria.item_definition.v1")]
+[MessagePackObject]
+public sealed class AetheriaItemDefinition
+{
+ [Key(0)]
+ [CultName]
+ public string Name { get; set; } = "";
+
+ [Key(1)]
+ [CultIndex("category")]
+ public string Category { get; set; } = "uncategorized";
+
+ [Key(2)]
+ [CultIndex("legacyId")]
+ public string LegacyId { get; set; } = "";
+
+ [Key(3)]
+ public string Description { get; set; } = "";
+
+ [Key(4)]
+ public double Mass { get; set; }
+
+ [Key(5)]
+ public double Volume { get; set; }
+
+ [Key(6)]
+ public string[] Tags { get; set; } = [];
+}
diff --git a/Aetheria.State/Documents/AetheriaMigrationLedger.cs b/Aetheria.State/Documents/AetheriaMigrationLedger.cs
new file mode 100644
index 000000000..9fa1a5973
--- /dev/null
+++ b/Aetheria.State/Documents/AetheriaMigrationLedger.cs
@@ -0,0 +1,39 @@
+using GameCult.Caching;
+using MessagePack;
+
+namespace Aetheria.State.Documents;
+
+[CultDocument("aetheria.migration_ledger", "aetheria.migration_ledger.v1")]
+[CultGlobal]
+[MessagePackObject]
+public sealed class AetheriaMigrationLedger
+{
+ [Key(0)]
+ [CultName]
+ public string Name { get; set; } = "Aetheria legacy migration ledger";
+
+ [Key(1)]
+ public string Source { get; set; } = "";
+
+ [Key(2)]
+ public string SourceFingerprint { get; set; } = "";
+
+ [Key(3)]
+ public string LastMigrationAtUtc { get; set; } = "";
+
+ [Key(4)]
+ public AetheriaMigrationCount[] Counts { get; set; } = [];
+
+ [Key(5)]
+ public string[] Notes { get; set; } = [];
+}
+
+[MessagePackObject]
+public sealed class AetheriaMigrationCount
+{
+ [Key(0)]
+ public string DocumentType { get; set; } = "";
+
+ [Key(1)]
+ public int Count { get; set; }
+}
diff --git a/Aetheria.State/Documents/AetheriaPlayerProfile.cs b/Aetheria.State/Documents/AetheriaPlayerProfile.cs
new file mode 100644
index 000000000..8dbf87908
--- /dev/null
+++ b/Aetheria.State/Documents/AetheriaPlayerProfile.cs
@@ -0,0 +1,25 @@
+using GameCult.Caching;
+using MessagePack;
+
+namespace Aetheria.State.Documents;
+
+[CultDocument("aetheria.player_profile", "aetheria.player_profile.v1")]
+[MessagePackObject]
+public sealed class AetheriaPlayerProfile
+{
+ [Key(0)]
+ [CultName]
+ public string Username { get; set; } = "";
+
+ [Key(1)]
+ [CultIndex("legacyId")]
+ public string LegacyId { get; set; } = "";
+
+ [Key(2)]
+ [CultIndex("email")]
+ public string Email { get; set; } = "";
+
+ [Key(3)]
+ [CultReference(typeof(AetheriaCorporation))]
+ public string CorporationKey { get; set; } = "";
+}
diff --git a/Aetheria.State/Documents/AetheriaRuntimeSession.cs b/Aetheria.State/Documents/AetheriaRuntimeSession.cs
new file mode 100644
index 000000000..8ad1a83f7
--- /dev/null
+++ b/Aetheria.State/Documents/AetheriaRuntimeSession.cs
@@ -0,0 +1,23 @@
+using GameCult.Caching;
+using MessagePack;
+
+namespace Aetheria.State.Documents;
+
+[CultDocument("aetheria.runtime_session", "aetheria.runtime_session.v1")]
+[MessagePackObject]
+public sealed class AetheriaRuntimeSession
+{
+ [Key(0)]
+ [CultName]
+ public string RuntimeId { get; set; } = "";
+
+ [Key(1)]
+ [CultIndex("role")]
+ public string Role { get; set; } = "local";
+
+ [Key(2)]
+ public string StartedAtUtc { get; set; } = "";
+
+ [Key(3)]
+ public string LastSeenAtUtc { get; set; } = "";
+}
diff --git a/Aetheria.State/Documents/AetheriaWorldState.cs b/Aetheria.State/Documents/AetheriaWorldState.cs
new file mode 100644
index 000000000..ed56ec046
--- /dev/null
+++ b/Aetheria.State/Documents/AetheriaWorldState.cs
@@ -0,0 +1,27 @@
+using GameCult.Caching;
+using MessagePack;
+
+namespace Aetheria.State.Documents;
+
+[CultDocument("aetheria.world_state", "aetheria.world_state.v1")]
+[CultGlobal]
+[MessagePackObject]
+public sealed class AetheriaWorldState
+{
+ [Key(0)]
+ [CultName]
+ public string Name { get; set; } = "Aetheria";
+
+ [Key(1)]
+ [CultIndex("worldId")]
+ public string WorldId { get; set; } = "aetheria";
+
+ [Key(2)]
+ public int SchemaEpoch { get; set; } = 1;
+
+ [Key(3)]
+ public string CreatedAtUtc { get; set; } = "";
+
+ [Key(4)]
+ public string UpdatedAtUtc { get; set; } = "";
+}
diff --git a/Aetheria.State/Migration/LegacyMigrationBoundary.cs b/Aetheria.State/Migration/LegacyMigrationBoundary.cs
new file mode 100644
index 000000000..2337bab00
--- /dev/null
+++ b/Aetheria.State/Migration/LegacyMigrationBoundary.cs
@@ -0,0 +1,18 @@
+namespace Aetheria.State.Migration;
+
+public static class LegacyMigrationBoundary
+{
+ public const string LegacyCacheNamespace = "Assets/Scripts/ServerShared/CultCache";
+ public const string LegacyRethinkNamespace = "Assets/Scripts/ServerShared/NIH/RethinkDb";
+ public const string LegacyGameDataFile = "GameData/AetherDB.msgpack";
+
+ public static string[] ForbiddenLiveAuthorities { get; } =
+ [
+ "DatabaseEntry",
+ "DatabaseCache",
+ "JsonKnownTypes",
+ "Newtonsoft.Json",
+ "RethinkDb.Driver",
+ "RethinkTableAttribute"
+ ];
+}
diff --git a/Aetheria.State/README.md b/Aetheria.State/README.md
new file mode 100644
index 000000000..ccf6d1ae0
--- /dev/null
+++ b/Aetheria.State/README.md
@@ -0,0 +1,21 @@
+# Aetheria.State
+
+`Aetheria.State` is the replacement owner for durable Aetheria game state.
+
+It uses modern `GameCult.Caching`, `GameCult.Caching.MessagePack`, and
+`GameCult.Mesh`. It does not reference the old `Assets/Scripts/ServerShared`
+tree, RethinkDB, JsonKnownTypes, or Newtonsoft.Json.
+
+The first live state file is `GameData/aetheria-world.cc`. Legacy files such as
+`GameData/AetherDB.msgpack` are migration inputs only.
+
+Current rebuild notes:
+
+- `MessagePack` 3.1.4 is pulled transitively through CultLib and is currently
+ flagged by NuGet advisory `GHSA-hv8m-jj95-wg3x`; update CultLib's
+ `GameCult.Caching.MessagePack` package ownership rather than adding a local
+ Aetheria override.
+- `CultRecordRef` should become the persisted reference shape once the
+ MessagePack source-generator path handles the GameCult formatter cleanly.
+ Until then, v1 profile documents store explicit record-key strings at the
+ persistence boundary.