Configuration

Altruist uses YAML configuration files to define server behavior. No builder pattern, no fluent API — just a clean config.yml that drives everything.

Config File Loading

On startup, AppConfigLoader loads configuration in this order (later sources override earlier ones):

  1. config.yml (required)
  2. config.{environment}.yml (optional, e.g., config.Development.yml; see Environment)
  3. Environment variables (ALTRUIST__... and those of your own config roots, see Environment Variables)
  4. Command-line arguments

Note:

Config files are loaded from AppContext.BaseDirectory — typically your build output directory. Make sure config.yml is copied to the output.

Full Configuration Reference

Server & HTTP

altruist:
  server:
    http:
      host: "0.0.0.0"    # Bind address
      port: 8080          # HTTP port
      path: "/"           # Base path for HTTP controllers
      errors: { format: simple }   # {error, message, field?} for every API failure (default: none)
      hardening: { ... }           # forwarded headers, security headers, body limits (all off by default)
      rate-limit: { ... }          # named HTTP rate-limit policies

See HTTP API for errors, hardening and rate-limit.

Transport

Enable any combination of transports. Each has its own enabled flag:

altruist:
  server:
    transport:
      codec:
        provider: json       # json or messagepack (shared across all transports)
      websocket:
        enabled: true        # Enable WebSocket
        path: /ws            # WebSocket endpoint path
      tcp:
        enabled: true        # Enable TCP
        port: 13000          # TCP listen port
      udp:
        enabled: false       # Disable UDP
        port: 13001          # UDP listen port

Persistence

altruist:
  persistence:
    cache:
      provider: inmemory     # In-memory cache (default)
    database:
      provider: postgres     # Database provider
      host: localhost
      port: 5432
      username: myuser
      password: mypassword
      database: mydb

Security

altruist:
  security:
    mode: "jwt"              # Enables JWT authentication
    key: ""                  # signing key; Production refuses a missing, default or short one

Passwords, access tokens, refresh tokens, cookies and tickets have their own keys under altruist:security — see Authentication, Accounts & Sessions and Connection Tickets. Failure lockouts (lockout) and the captcha (captcha): Lockout & Captcha. Email delivery is altruist:email: Email.

Game Engine

altruist:
  environment:
    mode: "3D"               # 2D or 3D mode

  game:
    engine:
      diagnostics: true      # Enable engine diagnostics logging
      framerateHz: 30        # Engine tick rate
      unit: "hz"             # Cycle unit
      gravity: { x: 0, y: -9.81 }

    worlds:
      partitioner: { width: 256, height: 256, depth: 256 }
      items:
        - index: 0
          id: "main-world"
          size: { x: 1000, y: 1000, z: 1000 }
          gravity: { x: 0, y: -9.81, z: 0 }
          position: { x: 0, y: 0, z: 0 }
          data-path: "Resources/world.json"  # Optional world data file

Server Node & Fleet

Capacity, drain and the fleet of servers (see Capacity, Drain & Health and Fleet & Scaling):

altruist:
  server:
    capacity:
      max-load: 0             # load budget; 0 = unlimited
      world-load: 0           # load per persistent world
    drain:
      timeout: 30             # seconds running work may take when the server stops
      force-grace: 5
      on-shutdown: true
      token: ""               # POST /altruist/server/drain from outside loopback
    fleet:
      redis: ""               # shared backplane; empty = a fleet of one (in-memory)
      cluster: "default"
      region: ""
      heartbeat: 2
      internal-address: ""    # default: machine address + HTTP port; "{ip}:8000" allowed
      public-address: ""      # "{node-id}" placeholder
      node-param: "node"
      secret: ""              # relay signature key (default: altruist:security:key)

  game:
    engine:
      step-workers: 1         # cores for room / unit steps: a number or auto

Environment Variables

Any config value can be overridden with an environment variable named after its path: upper-case, __ for : and _ for -:

# altruist:server:http:port
ALTRUIST__SERVER__HTTP__PORT=9090

# altruist:persistence:database:max-pool-size
ALTRUIST__PERSISTENCE__DATABASE__MAX_POOL_SIZE=80

This works for your own sections too: every top-level section of config.yml / config.{environment}.yml is a root. Keep secrets out of the files and set them in the environment:

myapp:
  public-base-url: "http://localhost:5173"
  email:
    provider: log
MYAPP__PUBLIC_BASE_URL=https://play.example.com   # myapp:public-base-url
MYAPP__EMAIL__API_KEY=re_...                      # myapp:email:api-key (not in the file at all)

A _ stays a _ where the YAML already has that key with an underscore. For a root that has no section in the files, list it:

altruist:
  config:
    env-roots: [secrets]      # SECRETS__API_KEY -> secrets:api-key

AppConfigLoader.EnvironmentVariableName("myapp:email:api-key") returns the variable name (MYAPP__EMAIL__API_KEY), e.g. for error messages.

Placeholders

A value in the YAML files may read an environment variable — useful when your deploys already set variables under other names:

altruist:
  security:
    captcha:
      secret-key: "${MYGAME__TURNSTILE__SECRET_KEY}"                 # empty when unset
  email:
    provider: "${MYGAME__EMAIL__PROVIDER:-log}"                      # default when unset or empty
    from: "${MYGAME__EMAIL__FROM:-My Game <no-reply@mygame.gg>}"

$${ writes a literal ${. Only values from the YAML files are expanded, and a variable for the key itself (ALTRUIST__SECURITY__CAPTCHA__SECRET_KEY) still wins over the expanded value.

Environment

AltruistEnvironment.Name is the hosting environment: DOTNET_ENVIRONMENT, else ASPNETCORE_ENVIRONMENT, else Production. It picks config.{environment}.yml and decides which safety checks are fatal (the JWT key guard). Use it instead of reading the variables yourself:

if (AltruistEnvironment.IsDevelopment)
    await seeder.SeedTestAccountsAsync();

if (AltruistEnvironment.IsProduction && string.IsNullOrEmpty(settings.ApiKey))
    throw new InvalidOperationException("Refusing to start: no API key.");

Binding Config to Classes

Use [ConfigurationProperties] to automatically bind a YAML section to a class:

[ConfigurationProperties("myapp:smtp")]
public class SmtpOptions
{
    public string Host { get; set; } = "";
    public int Port { get; set; } = 587;
    public bool EnableSsl { get; set; } = true;
    public string From { get; set; } = "";
}
myapp:
  smtp:
    host: "smtp.example.com"
    port: 587
    enableSsl: true
    from: "noreply@example.com"

The bound class is automatically registered as a singleton in DI and can be injected anywhere.

Conditional Service Registration

Services can be conditionally registered based on config values using [ConditionalOnConfig]:

// Only registered when transport mode is "websocket"
[Service(typeof(ITransport))]
[ConditionalOnConfig("altruist:server:transport:mode", havingValue: "websocket")]
public class WebSocketTransport : ITransport { ... }

// Only registered when the transport section exists
[Service(typeof(IConnectionManager))]
[ConditionalOnConfig("altruist:server:transport")]
public class ConnectionManager : IConnectionManager { ... }

Injecting Config Values

Individual config values can be injected into constructors using [AppConfigValue]:

public class MyService
{
    public MyService(
        [AppConfigValue("altruist:server:http:port", "8080")] string port,
        [AppConfigValue("altruist:server:transport:timeout", "10")] int timeout)
    {
        // port and timeout are resolved from config with fallback defaults
    }
}