User Evacuation Process

Technical documentation for the server move (user evacuation) system.

Overview

The server move feature allows users to migrate their Honse Farm identity from one federated server to another. The process is entirely client-driven — the user's plugin orchestrates all steps. The source server is never contacted, which means moves can happen even when the source server is offline or decommissioned.

Three roles exist in a move:

  • Target server — the server the user is moving to. Handles registration, identity verification, and local reference migration.
  • Remote servers — third-party federation servers that hold references to the moving user (pairs, group memberships). They receive a notification after the move and update their records.
  • Source server — the server the user is leaving. Not contacted at all.

Client Wizard

The in-game command /hf move starts a linear 7-step wizard:

StepAction
0Introduction — current server info, group ownership warning
1How it works — process overview, key preservation, new UID
2Disconnect from current server
3Browse federation server list, select target
4Load backup (uid.serverId.honsebackup.json), review pair/group summary, send signed move request
5Paste Lodestone verification code, wait for federation peer validation
6Finalize — account created on target, receive new UID, connect, notify remote servers

API Endpoints

POST /api/server-move/start

Accepts a SignedRequest<ServerMovePayload> containing the Lodestone ID, source UID, source server ID, Ed25519 public key, and pair/group backup data.

Validation:

  • Ed25519 signature verified against the public key in the payload (self-authorized identity proof).
  • Source server must be known in the federation list and not banned.
  • Source server must not be the same as the target server.
  • Lodestone ID must not already be registered locally.
  • No other active move in progress for the same character.

On success, a RegistrationRequest is created (same flow as new user registration) and a Lodestone verification code is returned.

POST /api/server-move/validate

Accepts MoveRequestId + SecretCode. Triggers federation peer validation — multiple peers independently check the Lodestone profile for the secret code.

POST /api/server-move/finalize

Accepts MoveRequestId. Creates the user account with a new UID, storing the original public key (no key regeneration). A new client certificate is generated for the user on the target server, producing a new 12-word secret passphrase that the client displays to the user. Returns a ServerMoveFinalizeResponse containing the new UID and MoveRequestId.

Rate Limiting

Defined in ConfigureIpRateLimiting in ServiceConfigurationExtensions.cs:

EndpointDefault Limit
*/server-move/start3 per minute per IP
*/server-move/validate15 per minute per IP
*/server-move/finalize3 per minute per IP

Move States

Tracked in the user_server_moves table:

StateMeaning
InitiatedRequest accepted, processing not yet started
LodestoneRequestedVerification code generated, waiting for user
WaitingForValidationUser triggered validation, federation peers are checking
ValidationCompleteIdentity verified, ready for finalization
CompletedUser account created, move is done
FailedTimed out or encountered an error
CancelledExplicitly cancelled

Moves that remain in a non-terminal state for more than 60 minutes are automatically marked as Failed by the ValidationProcessingService background service.

Database Schema

The user_server_moves table:

ColumnDescription
request_idUnique GUID for the move request
source_uidThe user's UID on their old server
source_server_idFederation ID of the source server
lodestone_idThe user's Lodestone character ID
public_keyThe user's Ed25519 public key (preserved across move)
pair_data_jsonJSON blob of direct pairs and group memberships
stateCurrent move state
user_uidNew UID assigned on the target server (set on completion)
registration_request_idFK to the underlying RegistrationRequest

Local Reference Migration

When a move is finalized on the target server, ProcessCompletedMoveReferencesAsync runs automatically. This migrates all database references from the user's old remote identity to their new local identity.

Tables updated

TableColumns UpdatedDirection
client_pairsOtherUserUID / OtherUserServerIdWhere old user was the "other" in a pair
client_pairsUserUID / UserServerIdWhere old user was the "user" in a pair
group_pairsGroupUserUID / GroupUserServerId + IsLocal = trueAll group memberships
permissionsUserUID / UserServerIdWhere old user owned the permission
permissionsOtherUserUID / OtherUserServerIdWhere old user was the target
group_pair_preferred_permissionsUserUID / UserServerIdGroup permission preferences

In all cases: OldUID / OldServerId becomes NewUID / null (null indicates a local user).

After migration, the old remote User entity is deleted.

SignalR notifications

For each affected local user with an individual pair:

  1. Client_UserRemoveClientPair — removes the old UID@ServerId pair
  2. Client_UserAddClientPair — adds the new local pair with current permissions
  3. Client_UpdateUserIndividualPairStatusDto — updates pair status

For each group the moved user was a member of, all local group members receive:

  1. Client_GroupPairLeft — old remote identity removed from the group
  2. Client_GroupPairJoined — new local identity added to the group with per-member permissions

Edge case

If no remote User entity exists for SourceUID@SourceServerId, migration exits early. This means nobody on the target server had interacted with the moving user.

Remote Server Notification

After the user connects to their new server, the client sends UserFinishServerMove via SignalR with a Dictionary<string, SignedRequest<ServerMoveCompleteDto>> — one signed request per affected remote server.

Client flow

  1. From the backup, collect all distinct server IDs from DirectPairs[].OtherUserServerId and GroupMemberships[].GroupServerId.
  2. For each server (excluding the new home server), create a ServerMoveCompleteDto with MoveRequestId, SourceUID, SourceServerId, NewUID, NewServerId.
  3. Sign each with the user's Ed25519 key.
  4. Send the dictionary to the hub.

Home server flow

MareHubCommandsService.FinishServerMoveAsync:

  1. Validates the move exists and is in Completed state.
  2. Builds CommandUserData with the user's public key.
  3. For each entry, wraps the signed request in a NotifyServerMoveCompleteCommand.
  4. Fire-and-forgets the command to each target server via FederationRouter.RouteCommandToServer.

Remote server processing

When a remote server receives NotifyServerMoveCompleteCommand, it runs ProcessRemoteMoveReferencesAsync. This performs the same reference migration as the local flow but updates to a remote identity:

TableOld ValueNew Value
client_pairsOldUID / OldServerIdNewUID / NewServerId
group_pairsOldUID / OldServerIdNewUID / NewServerId
permissionsOldUID / OldServerIdNewUID / NewServerId
group_pair_preferred_permissionsOldUID / OldServerIdNewUID / NewServerId

A new remote User entity is created for NewUID@NewServerId (inheriting alias and public key), and the old entity is deleted. The same SignalR notification pattern is sent to affected local users.

Security Model

  • Self-authorized: Move requests are signed with the user's Ed25519 private key. The signature is verified against the public key in the payload — no interaction with the source server.
  • Federation validation: Lodestone verification uses the same federation peer validation as registration. Multiple peers independently verify the code.
  • Source server filtering: Moves from servers not in the federation list or with Banned status are rejected.
  • No key regeneration: The user's original Ed25519 key pair is preserved across the move.
  • Standard federation signatures: Remote move notifications use the standard federation signature flow (server header signature + client Ed25519 signature).