diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp
index a0a661887..2fabfbfc7 100644
--- a/src/citra/citra.cpp
+++ b/src/citra/citra.cpp
@@ -81,6 +81,9 @@ static void OnStateChanged(const Network::RoomMember::State& state) {
     case Network::RoomMember::State::Joined:
         LOG_DEBUG(Network, "Successfully joined to the room");
         break;
+    case Network::RoomMember::State::Moderator:
+        LOG_DEBUG(Network, "Successfully joined the room as a moderator");
+        break;
     default:
         break;
     }
diff --git a/src/citra_qt/multiplayer/chat_room.cpp b/src/citra_qt/multiplayer/chat_room.cpp
index 4c4150d6a..85ac53987 100644
--- a/src/citra_qt/multiplayer/chat_room.cpp
+++ b/src/citra_qt/multiplayer/chat_room.cpp
@@ -306,7 +306,9 @@ void ChatRoom::OnStatusMessageReceive(const Network::StatusMessageEntry& status_
 
 void ChatRoom::OnSendChat() {
     if (auto room = Network::GetRoomMember().lock()) {
-        if (room->GetState() != Network::RoomMember::State::Joined) {
+        if (room->GetState() != Network::RoomMember::State::Joined &&
+            room->GetState() != Network::RoomMember::State::Moderator) {
+
             return;
         }
         auto message = ui->chat_message->text().toStdString();
diff --git a/src/citra_qt/multiplayer/client_room.cpp b/src/citra_qt/multiplayer/client_room.cpp
index d87a3e6e1..458f8c864 100644
--- a/src/citra_qt/multiplayer/client_room.cpp
+++ b/src/citra_qt/multiplayer/client_room.cpp
@@ -72,9 +72,12 @@ void ClientRoomWindow::OnRoomUpdate(const Network::RoomInformation& info) {
 }
 
 void ClientRoomWindow::OnStateChange(const Network::RoomMember::State& state) {
-    if (state == Network::RoomMember::State::Joined) {
+    if (state == Network::RoomMember::State::Joined ||
+        state == Network::RoomMember::State::Moderator) {
+
         ui->chat->Clear();
         ui->chat->AppendStatusMessage(tr("Connected"));
+        SetModPerms(state == Network::RoomMember::State::Moderator);
     }
     UpdateView();
 }
diff --git a/src/citra_qt/multiplayer/client_room.h b/src/citra_qt/multiplayer/client_room.h
index c40d324c3..584a51642 100644
--- a/src/citra_qt/multiplayer/client_room.h
+++ b/src/citra_qt/multiplayer/client_room.h
@@ -18,7 +18,6 @@ public:
     ~ClientRoomWindow();
 
     void RetranslateUi();
-    void SetModPerms(bool is_mod);
 
 public slots:
     void OnRoomUpdate(const Network::RoomInformation&);
@@ -32,6 +31,7 @@ signals:
 private:
     void Disconnect();
     void UpdateView();
+    void SetModPerms(bool is_mod);
 
     QStandardItemModel* player_list;
     std::unique_ptr<Ui::ClientRoom> ui;
diff --git a/src/citra_qt/multiplayer/direct_connect.cpp b/src/citra_qt/multiplayer/direct_connect.cpp
index 3870c1e25..c14edde76 100644
--- a/src/citra_qt/multiplayer/direct_connect.cpp
+++ b/src/citra_qt/multiplayer/direct_connect.cpp
@@ -63,7 +63,7 @@ void DirectConnectWindow::Connect() {
         // Prevent the user from trying to join a room while they are already joining.
         if (member->GetState() == Network::RoomMember::State::Joining) {
             return;
-        } else if (member->GetState() == Network::RoomMember::State::Joined) {
+        } else if (member->IsConnected()) {
             // And ask if they want to leave the room if they are already in one.
             if (!NetworkMessage::WarnDisconnect()) {
                 return;
@@ -122,7 +122,9 @@ void DirectConnectWindow::OnConnection() {
     EndConnecting();
 
     if (auto room_member = Network::GetRoomMember().lock()) {
-        if (room_member->GetState() == Network::RoomMember::State::Joined) {
+        if (room_member->GetState() == Network::RoomMember::State::Joined ||
+            room_member->GetState() == Network::RoomMember::State::Moderator) {
+
             close();
         }
     }
diff --git a/src/citra_qt/multiplayer/host_room.cpp b/src/citra_qt/multiplayer/host_room.cpp
index d5f093e48..2dbc74d6c 100644
--- a/src/citra_qt/multiplayer/host_room.cpp
+++ b/src/citra_qt/multiplayer/host_room.cpp
@@ -113,7 +113,7 @@ void HostRoomWindow::Host() {
     if (auto member = Network::GetRoomMember().lock()) {
         if (member->GetState() == Network::RoomMember::State::Joining) {
             return;
-        } else if (member->GetState() == Network::RoomMember::State::Joined) {
+        } else if (member->IsConnected()) {
             auto parent = static_cast<MultiplayerState*>(parentWidget());
             if (!parent->OnCloseRoom()) {
                 close();
diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/citra_qt/multiplayer/lobby.cpp
index bb1807776..3b48b3a95 100644
--- a/src/citra_qt/multiplayer/lobby.cpp
+++ b/src/citra_qt/multiplayer/lobby.cpp
@@ -109,7 +109,7 @@ void Lobby::OnJoinRoom(const QModelIndex& source) {
         // Prevent the user from trying to join a room while they are already joining.
         if (member->GetState() == Network::RoomMember::State::Joining) {
             return;
-        } else if (member->GetState() == Network::RoomMember::State::Joined) {
+        } else if (member->IsConnected()) {
             // And ask if they want to leave the room if they are already in one.
             if (!NetworkMessage::WarnDisconnect()) {
                 return;
diff --git a/src/citra_qt/multiplayer/state.cpp b/src/citra_qt/multiplayer/state.cpp
index b14e79f9b..75d74e189 100644
--- a/src/citra_qt/multiplayer/state.cpp
+++ b/src/citra_qt/multiplayer/state.cpp
@@ -89,7 +89,9 @@ void MultiplayerState::retranslateUi() {
 
     if (current_state == Network::RoomMember::State::Uninitialized) {
         status_text->setText(tr("Not Connected. Click here to find a room!"));
-    } else if (current_state == Network::RoomMember::State::Joined) {
+    } else if (current_state == Network::RoomMember::State::Joined ||
+               current_state == Network::RoomMember::State::Moderator) {
+
         status_text->setText(tr("Connected"));
     } else {
         status_text->setText(tr("Not Connected"));
@@ -107,7 +109,9 @@ void MultiplayerState::retranslateUi() {
 
 void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& state) {
     LOG_DEBUG(Frontend, "Network State: {}", Network::GetStateStr(state));
-    if (state == Network::RoomMember::State::Joined) {
+    if (state == Network::RoomMember::State::Joined ||
+        state == Network::RoomMember::State::Moderator) {
+
         OnOpenNetworkRoom();
         status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16));
         status_text->setText(tr("Connected"));
@@ -183,7 +187,9 @@ void MultiplayerState::OnAnnounceFailed(const Common::WebResult& result) {
 void MultiplayerState::UpdateThemedIcons() {
     if (show_notification) {
         status_icon->setPixmap(QIcon::fromTheme("connected_notification").pixmap(16));
-    } else if (current_state == Network::RoomMember::State::Joined) {
+    } else if (current_state == Network::RoomMember::State::Joined ||
+               current_state == Network::RoomMember::State::Moderator) {
+
         status_icon->setPixmap(QIcon::fromTheme("connected").pixmap(16));
     } else {
         status_icon->setPixmap(QIcon::fromTheme("disconnected").pixmap(16));
@@ -258,12 +264,6 @@ void MultiplayerState::OnOpenNetworkRoom() {
                 connect(client_room, &ClientRoomWindow::ShowNotification, this,
                         &MultiplayerState::ShowNotification);
             }
-            const std::string host_username = member->GetRoomInformation().host_username;
-            if (host_username.empty()) {
-                client_room->SetModPerms(false);
-            } else {
-                client_room->SetModPerms(member->GetUsername() == host_username);
-            }
             BringWidgetToFront(client_room);
             return;
         }
diff --git a/src/core/hle/service/nwm/nwm_uds.cpp b/src/core/hle/service/nwm/nwm_uds.cpp
index 755da4f1d..bf2d408c1 100644
--- a/src/core/hle/service/nwm/nwm_uds.cpp
+++ b/src/core/hle/service/nwm/nwm_uds.cpp
@@ -140,7 +140,9 @@ std::list<Network::WifiPacket> GetReceivedBeacons(const MacAddress& sender) {
 /// Sends a WifiPacket to the room we're currently connected to.
 void SendPacket(Network::WifiPacket& packet) {
     if (auto room_member = Network::GetRoomMember().lock()) {
-        if (room_member->GetState() == Network::RoomMember::State::Joined) {
+        if (room_member->GetState() == Network::RoomMember::State::Joined ||
+            room_member->GetState() == Network::RoomMember::State::Moderator) {
+
             packet.transmitter_address = room_member->GetMacAddress();
             room_member->SendWifiPacket(packet);
         }
diff --git a/src/network/room.cpp b/src/network/room.cpp
index 394e8644f..5386977e5 100644
--- a/src/network/room.cpp
+++ b/src/network/room.cpp
@@ -155,6 +155,12 @@ public:
      */
     void SendJoinSuccess(ENetPeer* client, MacAddress mac_address);
 
+    /**
+     * Notifies the member that its connection attempt was successful,
+     * and it is now part of the room, and it has been granted mod permissions.
+     */
+    void SendJoinSuccessAsMod(ENetPeer* client, MacAddress mac_address);
+
     /**
      * Sends a IdHostKicked message telling the client that they have been kicked.
      */
@@ -401,7 +407,11 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) {
 
     // Notify everyone that the room information has changed.
     BroadcastRoomInformation();
-    SendJoinSuccess(event->peer, preferred_mac);
+    if (HasModPermission(event->peer)) {
+        SendJoinSuccessAsMod(event->peer, preferred_mac);
+    } else {
+        SendJoinSuccess(event->peer, preferred_mac);
+    }
 }
 
 void Room::RoomImpl::HandleModKickPacket(const ENetEvent* event) {
@@ -588,10 +598,11 @@ bool Room::RoomImpl::HasModPermission(const ENetPeer* client) const {
     if (sending_member == members.end()) {
         return false;
     }
-    if (sending_member->user_data.username != room_information.host_username) {
-        return false;
-    }
-    return true;
+    if (sending_member->user_data.moderator) // Community moderator
+        return true;
+    if (sending_member->user_data.username == room_information.host_username) // Room host
+        return true;
+    return false;
 }
 
 void Room::RoomImpl::SendNameCollision(ENetPeer* client) {
@@ -665,6 +676,16 @@ void Room::RoomImpl::SendJoinSuccess(ENetPeer* client, MacAddress mac_address) {
     enet_host_flush(server);
 }
 
+void Room::RoomImpl::SendJoinSuccessAsMod(ENetPeer* client, MacAddress mac_address) {
+    Packet packet;
+    packet << static_cast<u8>(IdJoinSuccessAsMod);
+    packet << mac_address;
+    ENetPacket* enet_packet =
+        enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
+    enet_peer_send(client, 0, enet_packet);
+    enet_host_flush(server);
+}
+
 void Room::RoomImpl::SendUserKicked(ENetPeer* client) {
     Packet packet;
     packet << static_cast<u8>(IdHostKicked);
diff --git a/src/network/room.h b/src/network/room.h
index 3181e84d7..5781631d7 100644
--- a/src/network/room.h
+++ b/src/network/room.h
@@ -74,6 +74,7 @@ enum RoomMessageTypes : u8 {
     IdModBanListResponse,
     IdModPermissionDenied,
     IdModNoSuchUser,
+    IdJoinSuccessAsMod,
 };
 
 /// Types of system status messages
diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp
index 40d7e7068..8a846ee04 100644
--- a/src/network/room_member.cpp
+++ b/src/network/room_member.cpp
@@ -151,7 +151,7 @@ void RoomMember::RoomMemberImpl::SetError(const Error new_error) {
 }
 
 bool RoomMember::RoomMemberImpl::IsConnected() const {
-    return state == State::Joining || state == State::Joined;
+    return state == State::Joining || state == State::Joined || state == State::Moderator;
 }
 
 void RoomMember::RoomMemberImpl::MemberLoop() {
@@ -176,12 +176,17 @@ void RoomMember::RoomMemberImpl::MemberLoop() {
                     HandleRoomInformationPacket(&event);
                     break;
                 case IdJoinSuccess:
+                case IdJoinSuccessAsMod:
                     // The join request was successful, we are now in the room.
                     // If we joined successfully, there must be at least one client in the room: us.
                     ASSERT_MSG(member_information.size() > 0,
                                "We have not yet received member information.");
                     HandleJoinPacket(&event); // Get the MAC Address for the client
-                    SetState(State::Joined);
+                    if (event.packet->data[0] == IdJoinSuccessAsMod) {
+                        SetState(State::Moderator);
+                    } else {
+                        SetState(State::Joined);
+                    }
                     break;
                 case IdModBanListResponse:
                     HandleModBanListResponsePacket(&event);
@@ -232,7 +237,7 @@ void RoomMember::RoomMemberImpl::MemberLoop() {
                 enet_packet_destroy(event.packet);
                 break;
             case ENET_EVENT_TYPE_DISCONNECT:
-                if (state == State::Joined) {
+                if (state == State::Joined || state == State::Moderator) {
                     SetState(State::Idle);
                     SetError(Error::LostConnection);
                 }
@@ -331,7 +336,6 @@ void RoomMember::RoomMemberImpl::HandleJoinPacket(const ENetEvent* event) {
 
     // Parse the MAC Address from the packet
     packet >> mac_address;
-    SetState(State::Joined);
 }
 
 void RoomMember::RoomMemberImpl::HandleWifiPackets(const ENetEvent* event) {
diff --git a/src/network/room_member.h b/src/network/room_member.h
index 65d1c64eb..3410abac1 100644
--- a/src/network/room_member.h
+++ b/src/network/room_member.h
@@ -59,7 +59,8 @@ public:
         Uninitialized, ///< Not initialized
         Idle,          ///< Default state (i.e. not connected)
         Joining,       ///< The client is attempting to join a room.
-        Joined, ///< The client is connected to the room and is ready to send/receive packets.
+        Joined,    ///< The client is connected to the room and is ready to send/receive packets.
+        Moderator, ///< The client is connnected to the room and is granted mod permissions.
     };
 
     enum class Error : u8 {
@@ -270,6 +271,8 @@ static const char* GetStateStr(const RoomMember::State& s) {
         return "Joining";
     case RoomMember::State::Joined:
         return "Joined";
+    case RoomMember::State::Moderator:
+        return "Moderator";
     }
     return "Unknown";
 }
diff --git a/src/network/verify_user.h b/src/network/verify_user.h
index 74e154331..01b9877c8 100644
--- a/src/network/verify_user.h
+++ b/src/network/verify_user.h
@@ -13,6 +13,7 @@ struct UserData {
     std::string username;
     std::string display_name;
     std::string avatar_url;
+    bool moderator = false; ///< Whether the user is a Citra Moderator.
 };
 
 /**
diff --git a/src/web_service/verify_user_jwt.cpp b/src/web_service/verify_user_jwt.cpp
index fb7b7281d..27e08db9e 100644
--- a/src/web_service/verify_user_jwt.cpp
+++ b/src/web_service/verify_user_jwt.cpp
@@ -50,6 +50,10 @@ Network::VerifyUser::UserData VerifyUserJWT::LoadUserData(const std::string& ver
     if (decoded.payload().has_claim("avatarUrl")) {
         user_data.avatar_url = decoded.payload().get_claim_value<std::string>("avatarUrl");
     }
+    if (decoded.payload().has_claim("roles")) {
+        auto roles = decoded.payload().get_claim_value<std::vector<std::string>>("roles");
+        user_data.moderator = std::find(roles.begin(), roles.end(), "moderator") != roles.end();
+    }
     return user_data;
 }