Skip to main content

SignalR Hubs Overview

Titan uses SignalR WebSockets for real-time communication between clients and the server.

Hub Architecture

Available Hubs

HubEndpointDescription
AuthHub/authHubToken refresh, logout, profile
AccountHub/accountHubAccount info, characters, cosmetics
CharacterHub/characterHubCharacter progression, stats
InventoryHub/inventoryHubBag, equipment, items
TradeHub/tradeHubPeer-to-peer trading
SeasonHub/seasonHubSeasons, migrations
BaseTypeHub/baseTypeHubItem type registry

Connection Flow

Base Class: TitanHubBase

All authenticated hubs extend TitanHubBase, which provides:

User Identity

// Get authenticated user's ID from JWT
protected Guid GetUserId() => Guid.Parse(Context.UserIdentifier!);

Orleans Client Access

// Access Orleans cluster
protected IClusterClient ClusterClient => _clusterClient;

Ownership Verification

// Verify character belongs to caller
protected async Task VerifyCharacterOwnershipAsync(Guid characterId)
{
var characterIds = await GetOwnedCharacterIdsAsync();
if (!characterIds.Contains(characterId))
{
throw new HubException("Character does not belong to this account.");
}
}

Presence Tracking

On connect/disconnect, the base class:

  1. Registers/unregisters with PlayerPresenceGrain
  2. Logs sessions to SessionLogGrain (first/last connection only)

Authentication

All hubs (except AuthHub login methods) require JWT authentication:

[Authorize]
public class AccountHub : TitanHubBase
{
// All methods require valid JWT
}

Some methods require additional roles:

[Authorize(Roles = "Admin")]
public async Task<BaseType> Create(BaseType baseType)
{
// Admin-only operation
}

Input Validation

Hubs use HubValidationService with FluentValidation:

public async Task<CharacterSummary> CreateCharacter(
string seasonId,
string name,
CharacterRestrictions restrictions)
{
await _validation.ValidateIdAsync(seasonId, nameof(seasonId));
await _validation.ValidateNameAsync(name, nameof(name), 50);

// ... create character
}

Invalid input throws HubException which clients receive as errors.

Rate Limiting

The RateLimitHubFilter applies rate limits to hub methods:

Policy mapping is based on the hub endpoint path.

Error Handling

Hubs throw HubException for client-visible errors:

throw new HubException("Character does not belong to this account.");

Clients receive this as an error response. Internal exceptions are logged but not exposed.

Real-Time Updates

Hubs use SignalR groups for broadcasting updates:

// Join a group
await Groups.AddToGroupAsync(Context.ConnectionId, $"trade-{tradeId}");

// Broadcast to group
await Clients.Group($"trade-{tradeId}").SendAsync("TradeUpdate", data);

Clients subscribe to receive updates in real-time.

Client Connection Example

const connection = new signalR.HubConnectionBuilder()
.withUrl("https://api.example.com/accountHub", {
accessTokenFactory: () => accessToken
})
.withAutomaticReconnect()
.build();

// Handle reconnection
connection.onreconnected(connectionId => {
console.log('Reconnected:', connectionId);
});

// Start connection
await connection.start();

// Call hub method
const account = await connection.invoke("GetAccount");