diff --git a/dChatServer/ChatPacketHandler.cpp b/dChatServer/ChatPacketHandler.cpp
index 39f49944..ab19886b 100644
--- a/dChatServer/ChatPacketHandler.cpp
+++ b/dChatServer/ChatPacketHandler.cpp
@@ -20,7 +20,7 @@
 #include "eGameMasterLevel.h"
 #include "ChatPackets.h"
 #include "json.hpp"
-#include "Web.h"
+#include "ChatWeb.h"
 
 void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) {
 	//Get from the packet which player we want to do something with:
@@ -428,50 +428,41 @@ void ChatPacketHandler::HandleShowAll(Packet* packet) {
 // that are sent to the server. Because of this, there are large gaps of unused data in chat messages
 void ChatPacketHandler::HandleChatMessage(Packet* packet) {
 	CINSTREAM_SKIP_HEADER;
-	LWOOBJID playerID;
-	inStream.Read(playerID);
-	LOG("Got a message from player %llu", playerID);
+	ChatMessage data;
+	LWOOBJID sender;
+	inStream.Read(sender);
+	LOG("Got a message from player %llu", sender);
 
-	const auto& sender = Game::playerContainer.GetPlayerData(playerID);
-	if (!sender || sender.GetIsMuted()) return;
+	data.sender = Game::playerContainer.GetPlayerData(sender);
+	if (!data.sender || data.sender.GetIsMuted()) return;
 
 	eChatChannel channel;
 	uint32_t size;
 
 	inStream.IgnoreBytes(4);
-	inStream.Read(channel);
+	inStream.Read(data.channel);
 	inStream.Read(size);
 	inStream.IgnoreBytes(77);
-	LOG("message size: %u", size);	
-	LUWString message(size);
-	inStream.Read(message);
+	LOG("message size: %u", size);
+	data.message = LUWString(size);
+	inStream.Read(data.message);
 
-	LOG("Got message %s from (%s) via [%s]: %s", message.GetAsString().c_str(), sender.playerName.c_str(), StringifiedEnum::ToString(channel).data(), message.GetAsString().c_str());
+	LOG("Got message from (%s) via [%s]: %s", data.sender.playerName.c_str(), StringifiedEnum::ToString(data.channel).data(), data.message.GetAsString().c_str());
 	
-	// build chat json data
-	nlohmann::json data;
-	data["playerName"] = sender.playerName;
-	data["message"] = message.GetAsString();
-	auto& zoneID = data["zone_id"];
-	zoneID["map_id"] = sender.zoneID.GetMapID();
-	zoneID["instance_id"] = sender.zoneID.GetInstanceID();
-	zoneID["clone_id"] = sender.zoneID.GetCloneID();
 	
 	switch (channel) {
 	case eChatChannel::LOCAL: {
-		Game::web.SendWSMessage("chat_local", data);
 		break;
 	}
 	case eChatChannel::TEAM: {
-		auto* team = Game::playerContainer.GetTeam(playerID);
+		auto* team = Game::playerContainer.GetTeam(data.sender.playerID);
 		if (team == nullptr) return;
+		data.teamID = team->teamID;
 
 		for (const auto memberId : team->memberIDs) {
 			const auto& otherMember = Game::playerContainer.GetPlayerData(memberId);
 			if (!otherMember) return;
-			SendPrivateChatMessage(sender, otherMember, otherMember, message, eChatChannel::TEAM, eChatMessageResponseCode::SENT);
-			data["teamID"] = team->teamID;
-			Game::web.SendWSMessage("chat_team", data);
+			SendPrivateChatMessage(data.sender, otherMember, otherMember, data.message, eChatChannel::TEAM, eChatMessageResponseCode::SENT);
 		}
 		break;
 	}
@@ -479,70 +470,68 @@ void ChatPacketHandler::HandleChatMessage(Packet* packet) {
 		LOG("Unhandled Chat channel [%s]", StringifiedEnum::ToString(channel).data());
 		break;
 	}
+	ChatWeb::SendWSChatMessage(data);
 }
 
 // the structure the client uses to send this packet is shared in many chat messages
 // that are sent to the server. Because of this, there are large gaps of unused data in chat messages
 void ChatPacketHandler::HandlePrivateChatMessage(Packet* packet) {
+	ChatMessage data;
+	data.channel = eChatChannel::GENERAL;
+
 	CINSTREAM_SKIP_HEADER;
 	LWOOBJID playerID;
 	inStream.Read(playerID);
 
-	const auto& sender = Game::playerContainer.GetPlayerData(playerID);
-	if (!sender || sender.GetIsMuted()) return;
+	data.sender = Game::playerContainer.GetPlayerData(playerID);
+	if (!data.sender || data.sender.GetIsMuted()) return;
 
-	eChatChannel channel;
 	uint32_t size;
-	LUWString LUReceiverName;
 
 	inStream.IgnoreBytes(4);
-	inStream.Read(channel);
-	if (channel != eChatChannel::PRIVATE_CHAT) LOG("WARNING: Received Private chat with the wrong channel!");
+	inStream.Read(data.channel);
+	if (data.channel != eChatChannel::PRIVATE_CHAT) LOG("WARNING: Received Private chat with the wrong channel!");
 
 	inStream.Read(size);
 	inStream.IgnoreBytes(77);
 
+	LUWString LUReceiverName;
 	inStream.Read(LUReceiverName);
 	auto receiverName = LUReceiverName.GetAsString();
 	inStream.IgnoreBytes(2);
 
-	LUWString message(size);
-	inStream.Read(message);
+	data.message = LUWString(size);
+	inStream.Read(data.message);
 
-	LOG("Got a message from (%s) via [%s]: %s to %s", sender.playerName.c_str(), StringifiedEnum::ToString(channel).data(), message.GetAsString().c_str(), receiverName.c_str());
+	LOG("Got a message from (%s) via [%s]: %s to %s", data.sender.playerName.c_str(), StringifiedEnum::ToString(data.channel).data(), data.message.GetAsString().c_str(), receiverName.c_str());
 
-	const auto& receiver = Game::playerContainer.GetPlayerData(receiverName);
-	if (!receiver) {
+	data.receiver = Game::playerContainer.GetPlayerData(receiverName);
+	if (!data.receiver) {
 		PlayerData otherPlayer;
 		otherPlayer.playerName = receiverName;
 		auto responseType = Database::Get()->GetCharacterInfo(receiverName)
 			? eChatMessageResponseCode::NOTONLINE
 			: eChatMessageResponseCode::GENERALERROR;
 
-		SendPrivateChatMessage(sender, otherPlayer, sender, message, eChatChannel::GENERAL, responseType);
+		SendPrivateChatMessage(data.sender, otherPlayer, data.sender, data.message, data.channel, responseType);
 		return;
 	}
 
 	// Check to see if they are friends
 	// only freinds can whispr each other
-	for (const auto& fr : receiver.friends) {
-		if (fr.friendID == sender.playerID) {
-			nlohmann::json data;
-			data["sender"] = sender.playerName;
-			data["receiver"] = receiverName;
-			data["message"] = message.GetAsString();
-			data["zone_id"]["map_id"] = sender.zoneID.GetMapID();
-			data["zone_id"]["instance_id"] = sender.zoneID.GetInstanceID();
-			data["zone_id"]["clone_id"] = sender.zoneID.GetCloneID();
-			Game::web.SendWSMessage("chat_private", data);
-			//To the sender:
-			SendPrivateChatMessage(sender, receiver, sender, message, eChatChannel::PRIVATE_CHAT, eChatMessageResponseCode::SENT);
-			//To the receiver:
-			SendPrivateChatMessage(sender, receiver, receiver, message, eChatChannel::PRIVATE_CHAT, eChatMessageResponseCode::RECEIVEDNEWWHISPER);
+	for (const auto& fr : data.receiver.friends) {
+		if (fr.friendID == data.sender.playerID) {
+			data.channel = eChatChannel::PRIVATE_CHAT;
+			// To the sender:
+			SendPrivateChatMessage(data.sender, data.receiver, data.sender, data.message, data.channel, eChatMessageResponseCode::SENT);
+			// To the receiver:
+			SendPrivateChatMessage(data.sender, data.receiver, data.receiver, data.message, data.channel, eChatMessageResponseCode::RECEIVEDNEWWHISPER);
+			// To the WebSocket
+			ChatWeb::SendWSChatMessage(data);
 			return;
 		}
 	}
-	SendPrivateChatMessage(sender, receiver, sender, message, eChatChannel::GENERAL, eChatMessageResponseCode::NOTFRIENDS);
+	SendPrivateChatMessage(data.sender, data.receiver, data.sender, data.message, data.channel, eChatMessageResponseCode::NOTFRIENDS);
 }
 
 void ChatPacketHandler::SendPrivateChatMessage(const PlayerData& sender, const PlayerData& receiver, const PlayerData& routeTo, const LUWString& message, const eChatChannel channel, const eChatMessageResponseCode responseCode) {
diff --git a/dChatServer/ChatPacketHandler.h b/dChatServer/ChatPacketHandler.h
index def9c9b9..e29a150f 100644
--- a/dChatServer/ChatPacketHandler.h
+++ b/dChatServer/ChatPacketHandler.h
@@ -44,6 +44,14 @@ enum class eChatMessageResponseCode : uint8_t {
     RECEIVERFREETRIAL,
 };
 
+struct ChatMessage {
+	LUWString message;
+	PlayerData sender;
+	PlayerData receiver;
+	eChatChannel channel;
+	LWOOBJID teamID;
+};
+
 namespace ChatPacketHandler {
 	void HandleFriendlistRequest(Packet* packet);
 	void HandleFriendRequest(Packet* packet);
diff --git a/dChatServer/ChatWeb.cpp b/dChatServer/ChatWeb.cpp
index 9ff1d587..6216fdd6 100644
--- a/dChatServer/ChatWeb.cpp
+++ b/dChatServer/ChatWeb.cpp
@@ -56,7 +56,6 @@ void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) {
 	}
 }
 
-
 void HandleWSChat(mg_connection* connection, json data) {
 	auto check = JSONUtils::CheckRequiredData(data, { "user", "message" });
 	if (!check.empty()) {
@@ -65,41 +64,80 @@ void HandleWSChat(mg_connection* connection, json data) {
 		const auto user = data["user"].get<std::string>();
 		const auto message = data["message"].get<std::string>();
 		LOG_DEBUG("EXTERNAL Chat message from %s: %s", user.c_str(), message.c_str());
-		//TODO: Send chat message to  corret world server to broadcast to players
+		// TODO: use chat filter and respond if the message isn't allowed
+		// TODO: Send chat message to  corret world server to broadcast to players
 	}
 }
 
-void ChatWeb::RegisterRoutes() {
-	// REST API v1 routes
-	std::string v1_route = "/api/v1/";
-	Game::web.RegisterHTTPRoute({
-		.path = v1_route + "players",
-		.method = eHTTPMethod::GET,
-		.handle = HandleHTTPPlayersRequest
-	});
+namespace ChatWeb {
+	void RegisterRoutes() {
+		// REST API v1 routes
+		std::string v1_route = "/api/v1/";
+		Game::web.RegisterHTTPRoute({
+			.path = v1_route + "players",
+			.method = eHTTPMethod::GET,
+			.handle = HandleHTTPPlayersRequest
+		});
 
-	Game::web.RegisterHTTPRoute({
-		.path = v1_route + "teams",
-		.method = eHTTPMethod::GET,
-		.handle = HandleHTTPTeamsRequest
-	});
+		Game::web.RegisterHTTPRoute({
+			.path = v1_route + "teams",
+			.method = eHTTPMethod::GET,
+			.handle = HandleHTTPTeamsRequest
+		});
 
-	Game::web.RegisterHTTPRoute({
-		.path = v1_route + "announce",
-		.method = eHTTPMethod::POST,
-		.handle = HandleHTTPAnnounceRequest
-	});
+		Game::web.RegisterHTTPRoute({
+			.path = v1_route + "announce",
+			.method = eHTTPMethod::POST,
+			.handle = HandleHTTPAnnounceRequest
+		});
 
-	// WebSocket Events
-	Game::web.RegisterWSEvent({
-		.name = "chat",
-		.handle = HandleWSChat
-	});
+		// WebSocket Events
+		Game::web.RegisterWSEvent({
+			.name = "chat",
+			.handle = HandleWSChat
+		});
 
-	// WebSocket subscriptions
-	Game::web.RegisterWSSubscription("chat_local");
-	Game::web.RegisterWSSubscription("chat_team");
-	Game::web.RegisterWSSubscription("chat_private");
-	Game::web.RegisterWSSubscription("player");
-	Game::web.RegisterWSSubscription("team");
+		// WebSocket subscriptions
+		Game::web.RegisterWSSubscription("chat_local");
+		Game::web.RegisterWSSubscription("chat_team");
+		Game::web.RegisterWSSubscription("chat_private");
+		Game::web.RegisterWSSubscription("chat");
+		Game::web.RegisterWSSubscription("player");
+		Game::web.RegisterWSSubscription("team");
+	}
+
+	void SendWSPlayerUpdate(const PlayerData& player, eActivityType activityType) {
+		json data;
+		data["player_data"] = player;
+		data["update_type"] = magic_enum::enum_name(activityType);
+		Game::web.SendWSMessage("player_update", data);
+	}
+
+	void SendWSChatMessage(const ChatMessage& chatMessage) {
+		json data;
+		data["message"] = chatMessage.message.GetAsString();
+		data["sender"] = chatMessage.sender;
+		
+		data["channel"] = magic_enum::enum_name(chatMessage.channel);
+
+		std::string event = "chat"; // generic catch all
+		switch (chatMessage.channel) {
+		case eChatChannel::LOCAL:
+			event = "chat_local";
+			break;
+		case eChatChannel::TEAM:
+			event = "chat_team";
+			data["teamID"] = chatMessage.teamID;
+			break;
+		case eChatChannel::PRIVATE_CHAT:
+			data["receiver"] = chatMessage.receiver;
+			event = "chat_private";
+			break;
+		default:
+			LOG_DEBUG("Unhandled Chat channel [%s] in websocket send", StringifiedEnum::ToString(chatMessage.channel).data());
+			break;
+		}
+		Game::web.SendWSMessage(event, data);
+	}
 }
+
diff --git a/dChatServer/ChatWeb.h b/dChatServer/ChatWeb.h
index c2a28bc9..67b13aa1 100644
--- a/dChatServer/ChatWeb.h
+++ b/dChatServer/ChatWeb.h
@@ -5,9 +5,14 @@
 #include <functional>
 
 #include "Web.h"
+#include "PlayerContainer.h"
+#include "IActivityLog.h"
+#include "ChatPacketHandler.h"
 
 namespace ChatWeb {
 	void RegisterRoutes();
+	void SendWSPlayerUpdate(const PlayerData& player, eActivityType activityType);
+	void SendWSChatMessage(const ChatMessage& chatMessage);
 };
 
 
diff --git a/dChatServer/PlayerContainer.cpp b/dChatServer/PlayerContainer.cpp
index 82973ae8..f26ecb51 100644
--- a/dChatServer/PlayerContainer.cpp
+++ b/dChatServer/PlayerContainer.cpp
@@ -12,8 +12,7 @@
 #include "ChatPackets.h"
 #include "dConfig.h"
 #include "MessageType/Chat.h"
-#include "json.hpp"
-#include "Web.h"
+#include "ChatWeb.h"
 
 void PlayerContainer::Initialize() {
 	m_MaxNumberOfBestFriends =
@@ -60,18 +59,8 @@ void PlayerContainer::InsertPlayer(Packet* packet) {
 	m_PlayerCount++;
 
 	LOG("Added user: %s (%llu), zone: %i", data.playerName.c_str(), data.playerID, data.zoneID.GetMapID());
-	// Send to connected websockets
-	nlohmann::json wsdata;
-	wsdata["action"] = "character_update";
-	wsdata["type"] = "add";
-	wsdata["playerName"] = data.playerName;
-	wsdata["playerID"] = data.playerID;
-	auto& zoneID = wsdata["zone_id"];
-	zoneID["map_id"] = data.zoneID.GetMapID();
-	zoneID["instance_id"] = data.zoneID.GetInstanceID();
-	zoneID["clone_id"] = data.zoneID.GetCloneID();
-	Game::web.SendWSMessage("player", wsdata);
-	Database::Get()->UpdateActivityLog(data.playerID, eActivityType::PlayerLoggedIn, data.zoneID.GetMapID());
+	ChatWeb::SendWSPlayerUpdate(data, isLogin ? eActivityType::PlayerLoggedIn : eActivityType::PlayerChangedZone);
+	Database::Get()->UpdateActivityLog(data.playerID, isLogin ? eActivityType::PlayerLoggedIn : eActivityType::PlayerChangedZone, data.zoneID.GetMapID());
 	m_PlayersToRemove.erase(playerId);
 }
 
@@ -125,12 +114,7 @@ void PlayerContainer::RemovePlayer(const LWOOBJID playerID) {
 		}
 	}
 
-	nlohmann::json wsdata;
-	wsdata["action"] = "character_update";
-	wsdata["type"] = "remove";
-	wsdata["playerName"] = player.playerName;
-	wsdata["playerID"] = player.playerID;
-	Game::web.SendWSMessage("player", wsdata);
+	ChatWeb::SendWSPlayerUpdate(player, eActivityType::PlayerLoggedOut);
 
 	m_PlayerCount--;
 	LOG("Removed user: %llu", playerID);
diff --git a/dDatabase/GameDatabase/ITables/IActivityLog.h b/dDatabase/GameDatabase/ITables/IActivityLog.h
index a67b61a4..98aa79db 100644
--- a/dDatabase/GameDatabase/ITables/IActivityLog.h
+++ b/dDatabase/GameDatabase/ITables/IActivityLog.h
@@ -8,6 +8,7 @@
 enum class eActivityType : uint32_t {
 	PlayerLoggedIn,
 	PlayerLoggedOut,
+	PlayerChangedZone,
 };
 
 class IActivityLog {