Back to blog
Your FirstPart 3 of 9

Saturday, April 18, 2026

Your First Vault

cover

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: