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:
| Step | Action |
|---|---|
| 0 | Introduction — current server info, group ownership warning |
| 1 | How it works — process overview, key preservation, new UID |
| 2 | Disconnect from current server |
| 3 | Browse federation server list, select target |
| 4 | Load backup (uid.serverId.honsebackup.json), review pair/group summary, send signed move request |
| 5 | Paste Lodestone verification code, wait for federation peer validation |
| 6 | Finalize — 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:
| Endpoint | Default Limit |
|---|---|
*/server-move/start | 3 per minute per IP |
*/server-move/validate | 15 per minute per IP |
*/server-move/finalize | 3 per minute per IP |
Move States
Tracked in the user_server_moves table:
| State | Meaning |
|---|---|
Initiated | Request accepted, processing not yet started |
LodestoneRequested | Verification code generated, waiting for user |
WaitingForValidation | User triggered validation, federation peers are checking |
ValidationComplete | Identity verified, ready for finalization |
Completed | User account created, move is done |
Failed | Timed out or encountered an error |
Cancelled | Explicitly 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:
| Column | Description |
|---|---|
request_id | Unique GUID for the move request |
source_uid | The user's UID on their old server |
source_server_id | Federation ID of the source server |
lodestone_id | The user's Lodestone character ID |
public_key | The user's Ed25519 public key (preserved across move) |
pair_data_json | JSON blob of direct pairs and group memberships |
state | Current move state |
user_uid | New UID assigned on the target server (set on completion) |
registration_request_id | FK 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
| Table | Columns Updated | Direction |
|---|---|---|
client_pairs | OtherUserUID / OtherUserServerId | Where old user was the "other" in a pair |
client_pairs | UserUID / UserServerId | Where old user was the "user" in a pair |
group_pairs | GroupUserUID / GroupUserServerId + IsLocal = true | All group memberships |
permissions | UserUID / UserServerId | Where old user owned the permission |
permissions | OtherUserUID / OtherUserServerId | Where old user was the target |
group_pair_preferred_permissions | UserUID / UserServerId | Group 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:
Client_UserRemoveClientPair— removes the oldUID@ServerIdpairClient_UserAddClientPair— adds the new local pair with current permissionsClient_UpdateUserIndividualPairStatusDto— updates pair status
For each group the moved user was a member of, all local group members receive:
Client_GroupPairLeft— old remote identity removed from the groupClient_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
- From the backup, collect all distinct server IDs from
DirectPairs[].OtherUserServerIdandGroupMemberships[].GroupServerId. - For each server (excluding the new home server), create a
ServerMoveCompleteDtowithMoveRequestId,SourceUID,SourceServerId,NewUID,NewServerId. - Sign each with the user's Ed25519 key.
- Send the dictionary to the hub.
Home server flow
MareHubCommandsService.FinishServerMoveAsync:
- Validates the move exists and is in
Completedstate. - Builds
CommandUserDatawith the user's public key. - For each entry, wraps the signed request in a
NotifyServerMoveCompleteCommand. - 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:
| Table | Old Value | New Value |
|---|---|---|
client_pairs | OldUID / OldServerId | NewUID / NewServerId |
group_pairs | OldUID / OldServerId | NewUID / NewServerId |
permissions | OldUID / OldServerId | NewUID / NewServerId |
group_pair_preferred_permissions | OldUID / OldServerId | NewUID / 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
Bannedstatus 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).