Starting Point
After the authentication step, users can prove who they are.
Now they need a place where their actual game data can live:
- display name
- level
- gold
- last known position
That is what Vault gives us.
This post adds a durable player profile using the real IVault<T> API.
1. Turn On Persistence
Update config.yml so the project has a database and a cache:
altruist:
server:
http:
host: "0.0.0.0"
port: 8080
path: "/"
transport:
codec:
provider: json
websocket:
enabled: true
path: /game
security:
mode: "jwt"
key: "your-base64-secret-key"
persistence:
database:
provider: postgres
host: localhost
port: 5432
username: postgres
password: postgres
database: spacegame
cache:
provider: inmemory
The cache matters too. Even before autosave, a lot of Altruist systems expect fast cache-backed runtime access.
2. Define a Real Vault Model
Create Persistence/PlayerProfileVault.cs:
using Altruist;
using Altruist.UORM;
namespace SpaceGame.Persistence;
[Vault("player_profiles")]
[VaultUniqueKey(nameof(Username))]
public sealed class PlayerProfileVault : VaultModel
{
[VaultColumn("username")]
public string Username { get; set; } = "";
[VaultColumn("display_name")]
public string DisplayName { get; set; } = "";
[VaultColumn("level")]
public int Level { get; set; } = 1;
[VaultColumn("gold")]
public int Gold { get; set; } = 100;
[VaultColumn("last_x")]
public float LastX { get; set; }
[VaultColumn("last_y")]
public float LastY { get; set; }
}
This already gives you a table-backed model with:
- inherited
StorageId - inherited timestamps and versioning
- one unique username per player
3. Create a Profile Service Around IVault<T>
Create Services/PlayerProfileService.cs:
using Altruist;
using SpaceGame.Persistence;
namespace SpaceGame.Services;
[Service]
public sealed class PlayerProfileService
{
private readonly IVault<PlayerProfileVault> _profiles;
public PlayerProfileService(IVault<PlayerProfileVault> profiles)
{
_profiles = profiles;
}
public async Task<PlayerProfileVault> LoadOrCreateAsync(
string profileId,
string username,
string displayName)
{
var existing = await _profiles
.Where(x => x.StorageId == profileId)
.FirstOrDefaultAsync();
if (existing != null)
return existing;
var created = new PlayerProfileVault
{
StorageId = profileId,
Username = username,
DisplayName = displayName,
Level = 1,
Gold = 100,
LastX = 0,
LastY = 0,
};
await _profiles.SaveAsync(created);
return created;
}
public async Task SavePositionAsync(string profileId, float x, float y)
{
var profile = await _profiles
.Where(p => p.StorageId == profileId)
.FirstOrDefaultAsync();
if (profile == null)
return;
profile.LastX = x;
profile.LastY = y;
await _profiles.SaveAsync(profile);
}
public Task<List<PlayerProfileVault>> GetTopPlayersAsync()
{
return _profiles
.OrderByDescending(p => p.Level)
.Take(10)
.ToListAsync();
}
}
That is your first durable application service:
IVault<T>handles the query and save API- your service decides the game rules around creation and updates
4. Use the Profile When a Player Joins
Now wire it into Portals/GamePortal.cs.
Add the dependency:
private readonly PlayerProfileService _profiles;
public GamePortal(
IGameSessionService gameSessionService,
IAltruistRouter router,
PlayerProfileService profiles,
ILoggerFactory loggerFactory)
: base(gameSessionService, router)
{
_profiles = profiles;
_logger = loggerFactory.CreateLogger<GamePortal>();
}
Then use it inside OnJoinGameReceived(...):
var profileId = message.Name.ToLowerInvariant();
var profile = await _profiles.LoadOrCreateAsync(
profileId,
message.Name,
message.Name);
var session = _gameSessionService.CreateSession(
clientId,
DateTime.UtcNow.AddHours(2));
session.SetContext(clientId, profile);
And when the player leaves:
var session = _gameSessionService.GetSession(clientId);
var profile = session?.GetContext<PlayerProfileVault>(clientId);
if (profile != null)
{
await _profiles.SavePositionAsync(profile.StorageId, profile.LastX, profile.LastY);
}
That means the game session stays your short-lived runtime layer, while Vault becomes the durable profile layer underneath it.
5. Why This Matters
Until now, the project only knew players as live connections.
Vault changes that:
- reconnecting users can load durable data
- the server can keep rankings and progression
- later systems can store profiles without inventing their own DB access layer
This is also the exact foundation we need before we start batching hot-path changes.
Next Up
Now that profiles are durable, we can stop writing to the database on every tiny update:
Continue the Series