diff --git a/Common/Phantom.Common.Messages.Web/ToController/LogOutMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/LogOutMessage.cs
new file mode 100644
index 0000000..a89519a
--- /dev/null
+++ b/Common/Phantom.Common.Messages.Web/ToController/LogOutMessage.cs
@@ -0,0 +1,10 @@
+using System.Collections.Immutable;
+using MemoryPack;
+
+namespace Phantom.Common.Messages.Web.ToController;
+
+[MemoryPackable(GenerateType.VersionTolerant)]
+public sealed partial record LogOutMessage(
+	[property: MemoryPackOrder(0)] Guid UserGuid,
+	[property: MemoryPackOrder(1)] ImmutableArray<byte> SessionToken
+) : IMessageToController;
diff --git a/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs b/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs
index cb97742..bf48c78 100644
--- a/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs
+++ b/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs
@@ -24,21 +24,22 @@ public static class WebMessageRegistries {
 		ToController.Add<RegisterWebMessage>(0);
 		ToController.Add<UnregisterWebMessage>(1);
 		ToController.Add<LogInMessage, LogInSuccess?>(2);
-		ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(3);
-		ToController.Add<CreateUserMessage, CreateUserResult>(4);
-		ToController.Add<DeleteUserMessage, DeleteUserResult>(5);
-		ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(6);
-		ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(7);
-		ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(8);
-		ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(9);
-		ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(10);
-		ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(11);
-		ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(12);
-		ToController.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(13);
-		ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(14);
-		ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(15);
-		ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(16);
-		ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(17);
+		ToController.Add<LogOutMessage>(3);
+		ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(4);
+		ToController.Add<CreateUserMessage, CreateUserResult>(5);
+		ToController.Add<DeleteUserMessage, DeleteUserResult>(6);
+		ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(7);
+		ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(8);
+		ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(9);
+		ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(10);
+		ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(11);
+		ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(12);
+		ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(13);
+		ToController.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(14);
+		ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(15);
+		ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(16);
+		ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(17);
+		ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(18);
 		ToController.Add<ReplyMessage>(127);
 		
 		ToWeb.Add<RegisterWebResultMessage>(0);
diff --git a/Controller/Phantom.Controller.Services/ControllerServices.cs b/Controller/Phantom.Controller.Services/ControllerServices.cs
index 7e9abc2..6b70bb9 100644
--- a/Controller/Phantom.Controller.Services/ControllerServices.cs
+++ b/Controller/Phantom.Controller.Services/ControllerServices.cs
@@ -59,7 +59,7 @@ public sealed class ControllerServices : IDisposable {
 		this.PermissionManager = new PermissionManager(dbProvider);
 
 		this.UserRoleManager = new UserRoleManager(dbProvider);
-		this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager);
+		this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager, dbProvider);
 		this.AuditLogManager = new AuditLogManager(dbProvider);
 		this.EventLogManager = new EventLogManager(ActorSystem, dbProvider, shutdownCancellationToken);
 		
diff --git a/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs b/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs
index 881de92..d429b4d 100644
--- a/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs
+++ b/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs
@@ -69,6 +69,8 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
 		
 		ReceiveAsync<RegisterWebMessage>(HandleRegisterWeb);
 		Receive<UnregisterWebMessage>(HandleUnregisterWeb);
+		ReceiveAndReplyLater<LogInMessage, LogInSuccess?>(HandleLogIn);
+		Receive<LogOutMessage>(HandleLogOut);
 		ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(HandleCreateOrUpdateAdministratorUser);
 		ReceiveAndReplyLater<CreateUserMessage, CreateUserResult>(HandleCreateUser);
 		ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(HandleGetUsers);
@@ -84,7 +86,6 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
 		ReceiveAndReply<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(HandleGetAgentJavaRuntimes);
 		ReceiveAndReplyLater<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(HandleGetAuditLog);
 		ReceiveAndReplyLater<GetEventLogMessage, ImmutableArray<EventLogItem>>(HandleGetEventLog);
-		ReceiveAndReplyLater<LogInMessage, LogInSuccess?>(HandleLogIn);
 		Receive<ReplyMessage>(HandleReply);
 	}
 
@@ -96,6 +97,14 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
 		connection.Close();
 	}
 
+	private Task<LogInSuccess?> HandleLogIn(LogInMessage message) {
+		return userLoginManager.LogIn(message.Username, message.Password);
+	}
+
+	private void HandleLogOut(LogOutMessage message) {
+		_ = userLoginManager.LogOut(message.UserGuid, message.SessionToken);
+	}
+	
 	private Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message) {
 		return userManager.CreateOrUpdateAdministrator(message.Username, message.Password);
 	}
@@ -156,10 +165,6 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
 		return eventLogManager.GetMostRecentItems(message.Count);
 	}
 
-	private Task<LogInSuccess?> HandleLogIn(LogInMessage message) {
-		return userLoginManager.LogIn(message.Username, message.Password);
-	}
-
 	private void HandleReply(ReplyMessage message) {
 		connection.Receive(message);
 	}
diff --git a/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs b/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs
index 889949a..e9f27cc 100644
--- a/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs
+++ b/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs
@@ -2,19 +2,23 @@
 using System.Collections.Immutable;
 using System.Security.Cryptography;
 using Phantom.Common.Data.Web.Users;
+using Phantom.Controller.Database;
+using Phantom.Controller.Database.Repositories;
 
 namespace Phantom.Controller.Services.Users; 
 
 sealed class UserLoginManager {
 	private const int SessionIdBytes = 20;
-	private readonly ConcurrentDictionary<string, List<ImmutableArray<byte>>> sessionTokensByUsername = new ();
+	private readonly ConcurrentDictionary<Guid, List<ImmutableArray<byte>>> sessionTokensByUserGuid = new ();
 	
 	private readonly UserManager userManager;
 	private readonly PermissionManager permissionManager;
+	private readonly IDbContextProvider dbProvider;
 	
-	public UserLoginManager(UserManager userManager, PermissionManager permissionManager) {
+	public UserLoginManager(UserManager userManager, PermissionManager permissionManager, IDbContextProvider dbProvider) {
 		this.userManager = userManager;
 		this.permissionManager = permissionManager;
+		this.dbProvider = dbProvider;
 	}
 
 	public async Task<LogInSuccess?> LogIn(string username, string password) {
@@ -24,11 +28,37 @@ sealed class UserLoginManager {
 		}
 
 		var token = ImmutableArray.Create(RandomNumberGenerator.GetBytes(SessionIdBytes));
-		var sessionTokens = sessionTokensByUsername.GetOrAdd(username, static _ => new List<ImmutableArray<byte>>());
+		var sessionTokens = sessionTokensByUserGuid.GetOrAdd(user.UserGuid, static _ => new List<ImmutableArray<byte>>());
 		lock (sessionTokens) {
 			sessionTokens.Add(token);
 		}
-		
+
+		await using (var db = dbProvider.Lazy()) {
+			var auditLogWriter = new AuditLogRepository(db).Writer(user.UserGuid);
+			auditLogWriter.UserLoggedIn(user);
+			
+			await db.Ctx.SaveChangesAsync();
+		}
+
 		return new LogInSuccess(user.UserGuid, await permissionManager.FetchPermissionsForUserId(user.UserGuid), token);
 	}
+
+	public async Task LogOut(Guid userGuid, ImmutableArray<byte> sessionToken) {
+		if (!sessionTokensByUserGuid.TryGetValue(userGuid, out var sessionTokens)) {
+			return;
+		}
+
+		lock (sessionTokens) {
+			if (sessionTokens.RemoveAll(token => token.SequenceEqual(sessionToken)) == 0) {
+				return;
+			}
+		}
+
+		await using var db = dbProvider.Lazy();
+		
+		var auditLogWriter = new AuditLogRepository(db).Writer(userGuid);
+		auditLogWriter.UserLoggedOut(userGuid);
+			
+		await db.Ctx.SaveChangesAsync();
+	}
 }
diff --git a/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs b/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs
index 3f6432a..f9d27d4 100644
--- a/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs
+++ b/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs
@@ -53,8 +53,8 @@ public sealed class UserLoginManager {
 
 	public async Task LogOut() {
 		var stored = await sessionBrowserStorage.Delete();
-		if (stored != null) {
-			sessionManager.Remove(stored.UserGuid, stored.Token);
+		if (stored != null && sessionManager.Remove(stored.UserGuid, stored.Token)) {
+			await controllerConnection.Send(new LogOutMessage(stored.UserGuid, stored.Token));
 		}
 
 		await navigation.NavigateTo(string.Empty);
diff --git a/Web/Phantom.Web.Services/Authentication/UserSessionManager.cs b/Web/Phantom.Web.Services/Authentication/UserSessionManager.cs
index 3e5e424..3c830a1 100644
--- a/Web/Phantom.Web.Services/Authentication/UserSessionManager.cs
+++ b/Web/Phantom.Web.Services/Authentication/UserSessionManager.cs
@@ -23,9 +23,13 @@ public sealed class UserSessionManager {
 		return userSessions.TryGetValue(userGuid, out var sessions) && sessions.HasToken(token) ? sessions.UserInfo : null;
 	}
 
-	internal void Remove(Guid userGuid, ImmutableArray<byte> token) {
+	internal bool Remove(Guid userGuid, ImmutableArray<byte> token) {
 		if (userSessions.TryGetValue(userGuid, out var sessions)) {
 			sessions.RemoveToken(token);
+			return true;
+		}
+		else {
+			return false;
 		}
 	}
 }