diff --git a/extension/Classes/Api/websocket.php b/extension/Classes/Api/websocket.php
index bbd7695531b1c17e4e45838adaec81d7345e01a3..4d26a32371b7b052193b6dac8499e2dfcb1c19e4 100644
--- a/extension/Classes/Api/websocket.php
+++ b/extension/Classes/Api/websocket.php
@@ -11,6 +11,9 @@ namespace IMATHUZH\Qfq\Api;
 require_once(__DIR__ . '/../../vendor/autoload.php');
 
 use IMATHUZH\Qfq\Core\Form\Chat;
+use IMATHUZH\Qfq\Core\Helper\Logger;
+use IMATHUZH\Qfq\Core\Helper\Path;
+use IMATHUZH\Qfq\Core\Store\Config;
 use Ratchet\Server\IoServer;
 use Ratchet\Http\HttpServer;
 use Ratchet\WebSocket\WsServer;
@@ -25,8 +28,17 @@ use Ratchet\WebSocket\WsServer;
 define('QFQ_API', 'Api call');
 
 $answer = array();
+
+$logFile = Path::absoluteWebsocketLogFile();
+$timestamp = date("Y-m-d H:i:s");
+
 // Define a port number
-define('PORT', 8090);
+$configuredPort = Config::get(SYSTEM_WEBSOCKET_PORT) ?? '';
+define('PORT', $configuredPort);
+
+if (empty($configuredPort)) {
+    Logger::logMessage($timestamp . " - No port defined. Websocket server can not be started. Set the port in QFQ config and restart docker.", $logFile);
+}
 
 try {
     $chat = new Chat();
@@ -40,6 +52,7 @@ try {
         PORT
     );
 
+    Logger::logMessage($timestamp . " - Port: " . $configuredPort . ". Websocket server started!", $logFile);
     $server->run();
 
 } catch (\Throwable $e) {
diff --git a/extension/Classes/Controller/QfqController.php b/extension/Classes/Controller/QfqController.php
index 6cf5822e70d256f3f1defd9275bbe8f14575e9d2..abc3b3bd87b16af2d89fee3489870d1f920095e3 100644
--- a/extension/Classes/Controller/QfqController.php
+++ b/extension/Classes/Controller/QfqController.php
@@ -7,7 +7,10 @@ namespace IMATHUZH\Qfq\Controller;
 
 require_once(__DIR__ . '/../../vendor/autoload.php');
 
+use IMATHUZH\Qfq\Core\Helper\Logger;
+use IMATHUZH\Qfq\Core\Helper\Path;
 use IMATHUZH\Qfq\Core\QuickFormQuery;
+use IMATHUZH\Qfq\Core\Store\Store;
 use IMATHUZH\Qfq\Core\Typo3\T3Handler;
 use TYPO3\CMS\Core\Http\HtmlResponse;
 
@@ -46,6 +49,8 @@ class QfqController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController {
             $html = $qfq->process();
             $flagOk = true;
 
+            $this->websocketStart();
+
         } catch (\UserFormException $e) {
             $html = $e->formatMessage();
 
@@ -98,4 +103,32 @@ class QfqController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController {
         return $content;
     }
 
+    private function websocketStart() {
+        // Websocket preparations for non docker environment
+        $scriptName = Path::urlApi('websocket.php');
+        $command = 'nohup php ' . $scriptName . ' &';
+        $logFile = Path::absoluteWebsocketLogFile();
+        $websocketKillCmd = 'pkill -f ' . $scriptName;
+        $timestamp = date("Y-m-d H:i:s");
+
+        $websocketPort = Store::getVar(SYSTEM_WEBSOCKET_PORT, STORE_SYSTEM);
+
+        // Use pgrep to check if the WebSocket server is already running
+        $running = shell_exec("pgrep -f '$scriptName'");
+
+        // In no docker environment the websocket server start/shut down can be handled automatically.
+        if (!file_exists('/.dockerenv')) {
+            if(!empty($websocketPort) && !$running) {
+                // Execute command and redirect its output to the log file
+                shell_exec($command . ' >> ' . $logFile . ' 2>&1');
+
+            } elseif (empty($websocketPort) && $running) {
+                shell_exec($websocketKillCmd . ' >> ' . $logFile . ' 2>&1');
+
+                $message = "WebSocket server shut down.";
+                Logger::logMessage($timestamp . ' - ' . $message, $logFile);
+            }
+        }
+    }
+
 }
\ No newline at end of file
diff --git a/extension/Classes/Core/AbstractBuildForm.php b/extension/Classes/Core/AbstractBuildForm.php
index abb08ba0fb066108590ce0e165e42a8c4078ce79..1e4843695de2f1391b34f5272cbdafb6c6778175 100644
--- a/extension/Classes/Core/AbstractBuildForm.php
+++ b/extension/Classes/Core/AbstractBuildForm.php
@@ -3118,6 +3118,7 @@ abstract class AbstractBuildForm {
      */
     public function buildChat(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {
         $chatConfig = Support::createChatConfig($formElement, $this->store);
+        $chatConfigJson = json_encode($chatConfig, JSON_UNESCAPED_SLASHES);
 
         $baseUrl = $this->store::getVar(SYSTEM_BASE_URL, STORE_SYSTEM . STORE_EMPTY);
         // Part 1: Build fieldset for chat
@@ -3140,6 +3141,7 @@ abstract class AbstractBuildForm {
         // Part 2: Build chat bubbles content
         $chatWindowAttribute = Support::doAttribute('name', $htmlFormElementName . '-chat');
         $chatWindowAttribute .= Support::doAttribute(ATTRIBUTE_DATA_REFERENCE, $formElement[FE_DATA_REFERENCE] . '-chat');
+        $chatWindowAttribute .= Support::doAttribute('data-chat-config', $chatConfigJson);
 
         $chatHead = '<div class="qfq-chat-window" '. $chatWindowAttribute . '><span class="fas fa-search chat-search-activate"></span><div class="chat-search"><input type="text" class="chat-search-input qfq-skip-dirty" placeholder="Search..." ' . $chatConfig['disabledFlag'] . '><button class="chat-search-btn qfq-skip-dirty" ' . $chatConfig['disabledFlag'] . '>Search</button><span class="chat-search-info"></span></div><div class="chat-messages"><div class="fas fa-arrow-circle-up chat-top-symbol"></div>';
         $chatTail = '</div></div>';
@@ -3238,8 +3240,10 @@ abstract class AbstractBuildForm {
         $inputHtml = "$inputHtml $inputAttribute>$textarea";
 
         $wrapSetupClass = $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_CLASS] ?? '';
-        // value is not needed for chat element. reset it to empty
-        $value = '';
+
+        // Return last message id as value
+        $columnId = array_column($result, $dbColumnNames['id']);
+        $value = max($columnId);
 
         $json = $this->getFormElementForJson($htmlFormElementName, $value, $formElement, $wrapSetupClass);
         $json['chat'] = $chatJson;
diff --git a/extension/Classes/Core/Constants.php b/extension/Classes/Core/Constants.php
index 21505e04c2e9fbdbc0b687bd48c363e34035951c..4ac4ec52f103d2bf0e538bb00db025e0afb855c0 100644
--- a/extension/Classes/Core/Constants.php
+++ b/extension/Classes/Core/Constants.php
@@ -793,6 +793,7 @@ const CSS_REQUIRED_LEFT = 'required-left';
 const SYSTEM_QFQ_PROJECT_PATH = 'qfqProjectPath';
 const SYSTEM_DO_NOT_LOG_COLUMN = 'doNotLogColumn';
 const SYSTEM_PROTECTED_FOLDER_CHECK = 'protectedFolderCheck';
+const SYSTEM_WEBSOCKET_PORT = 'websocketPort';
 
 const EXTRA_ENABLE_SWITCH = 'enableSwitch';
 const EXTRA_COLUMN_NAME_AlIAS_SLUG = 'columnNameAliasSlug';
diff --git a/extension/Classes/Core/Form/Chat.php b/extension/Classes/Core/Form/Chat.php
index 71a06f5de46dbd56bc0e0bbe28a7309fed4c4815..5d04da1d08dc5825d44099f5c90461723fcfa532 100644
--- a/extension/Classes/Core/Form/Chat.php
+++ b/extension/Classes/Core/Form/Chat.php
@@ -9,6 +9,8 @@
 namespace IMATHUZH\Qfq\Core\Form;
 
 
+use IMATHUZH\Qfq\Core\Helper\Logger;
+use IMATHUZH\Qfq\Core\Helper\Path;
 use Ratchet\MessageComponentInterface;
 use Ratchet\ConnectionInterface;
 
@@ -19,34 +21,101 @@ use Ratchet\ConnectionInterface;
  */
 class Chat implements MessageComponentInterface {
     protected $clients;
+    protected $logFile;
+    protected $clientInfo;
+    protected $clientConnections;
 
     public function __construct() {
         $this->clients = new \SplObjectStorage;
+        $this->logFile = Path::absoluteWebsocketLogFile();
+        $this->clientInfo = [];
     }
 
     public function onOpen(ConnectionInterface $conn) {
         $this->clients->attach($conn);
-        echo "New connection! ({$conn->resourceId})\n";
+        $this->clientConnections[$conn->resourceId] = $conn;
+        $timestamp = date("Y-m-d H:i:s");
+        Logger::logMessage($timestamp . " - " . "New connection! ({$conn->resourceId})", $this->logFile);
     }
 
     public function onMessage(ConnectionInterface $from, $msg) {
-        foreach ($this->clients as $client) {
-            if ($from !== $client) {
-                $client->send($msg);
+
+        $data = json_decode($msg);
+        // Catch heartbeat connection keeping
+        if ($data && $data->type == 'heartbeat') {
+            return;
+        }
+
+        if ($data && $data->type == 'config') {
+            // Handle the configuration data
+            foreach ($data->data as $key => $value) {
+                $this->clientInfo[$from->resourceId][$key] = $value;
             }
+
+            $this->updateReceiverIds($from);
+
+            $timestamp = date("Y-m-d H:i:s");
+            Logger::logMessage($timestamp . " - " . "Connection configured! ({$from->resourceId}) Config: " . $msg, $this->logFile);
+            $from->send('Current ID: '. $from->resourceId . ', Receiver IDs: ' . json_encode($this->clientInfo[$from->resourceId]['receiverIds']));
+            return;
         }
-        if ($msg === 'heartbeat') {
-            $from->send('pong');
+
+        // Refresh receiverIds if there exist a new client connection
+        $this->updateReceiverIds($from);
+
+        if (!empty($this->clientInfo[$from->resourceId])) {
+            // send message to client with given receiverId
+            foreach ($this->clientInfo[$from->resourceId]['receiverIds'] as $receiverId) {
+                if (isset($this->clientConnections[$receiverId])) {
+                    $this->clientConnections[$receiverId]->send('Sent from: '. $from->resourceId);
+                    $messages = $this->getMessages($data, $from, $this->clientInfo[$receiverId]);
+                    $this->clientConnections[$receiverId]->send(json_encode($messages, JSON_UNESCAPED_SLASHES));
+                }
+            }
         }
     }
 
     public function onClose(ConnectionInterface $conn) {
         $this->clients->detach($conn);
-        echo "Connection {$conn->resourceId} has disconnected\n";
+        unset($this->clientConnections[$conn->resourceId]);
+        $timestamp = date("Y-m-d H:i:s");
+        Logger::logMessage($timestamp . " - " . "Connection {$conn->resourceId} has disconnected", $this->logFile);
     }
 
     public function onError(ConnectionInterface $conn, \Exception $e) {
-        echo "An error has occurred: {$e->getMessage()}\n";
+        $timestamp = date("Y-m-d H:i:s");
+        Logger::logMessage($timestamp . " - " . "An error has occurred: {$e->getMessage()}", $this->logFile);
         $conn->close();
     }
+
+    private function updateReceiverIds($from) {
+        if (!isset($this->clientInfo[$from->resourceId]['receiverIds'])) {
+            $this->clientInfo[$from->resourceId]['receiverIds'] = array();
+        }
+
+        foreach ($this->clients as $client) {
+            if ($from !== $client && !in_array($client->resourceId, $this->clientInfo[$from->resourceId]['receiverIds']) && $this->clientInfo[$from->resourceId]['grIdGroupList'] === $this->clientInfo[$client->resourceId]['grIdGroupList']) {
+                $this->clientInfo[$from->resourceId]['receiverIds'][] = $client->resourceId;
+            }
+        }
+    }
+
+    private function getMessages($data, $from, $client) {
+        $chatJson = array();
+        $chatJson[$data->messageId]['bubbleClass'] = 'chat-left-bubble';
+        $chatJson[$data->messageId]['username'] = $this->clientInfo[$from->resourceId]['username'];
+
+        if ($this->clientInfo[$from->resourceId]['pIdCreator'] === $client['pIdCreator']) {
+            $chatJson[$data->messageId]['bubbleClass'] = 'chat-right-bubble';
+            $chatJson[$data->messageId]['username'] = '';
+        }
+
+        // current date
+        $timestamp = time();
+        $chatJson[$data->messageId]['title'] = date('d.m.Y H:i', $timestamp);
+        $chatJson[$data->messageId]['message'] = $data->value;
+        $chatJson[$data->messageId]['chatTime'] = date('H:i', $timestamp);
+
+        return $chatJson;
+    }
 }
\ No newline at end of file
diff --git a/extension/Classes/Core/Helper/Path.php b/extension/Classes/Core/Helper/Path.php
index 4c1acc4ec29da3da4e01da3c2c3763d35320a6a7..a071467419d53adbf62bbdffa4ba0b8e77b830c6 100644
--- a/extension/Classes/Core/Helper/Path.php
+++ b/extension/Classes/Core/Helper/Path.php
@@ -78,9 +78,11 @@ class Path {
     private static $overloadAbsoluteQfqLogFile = null;
     private static $overloadAbsoluteMailLogFile = null;
     private static $overloadAbsoluteSqlLogFile = null;
+    private static $overloadAbsoluteWebsocketLogFile = null;
     private const LOG_TO_QFQ_LOG_FILE_DEFAULT = 'qfq.log'; // Don't use directly, use absoluteQfqLogFile()
     private const LOG_TO_MAIL_LOG_FILE_DEFAULT = 'mail.log'; // Don't use directly, use absoluteMailLogFile()
     private const LOG_TO_SQL_LOG_FILE_DEFAULT = 'sql.log'; // Don't use directly, use absoluteSqlLogFile()
+    private const LOG_TO_WEBSOCKET_LOG_FILE_DEFAULT = 'websocket.log';
     private const PROJECT_TO_LOG_DEFAULT = 'log'; // Don't use directly, use absoluteLog()
     private const APP_TO_LOG_IN_PROTECTED = 'fileadmin/protected/log'; // Don't use directly, use absoluteLog()
 
@@ -164,6 +166,17 @@ class Path {
         return self::$overloadAbsoluteMailLogFile;
     }
 
+    /**
+     * @return string
+     * @throws \UserFormException
+     */
+    public static function absoluteWebsocketLogFile(): string {
+        if (is_null(self::$overloadAbsoluteWebsocketLogFile)) {
+            return self::absoluteLog(self::LOG_TO_WEBSOCKET_LOG_FILE_DEFAULT);
+        }
+        return self::$overloadAbsoluteWebsocketLogFile;
+    }
+
     /**
      * @param array $pathPartsToAppend
      * @return string
diff --git a/extension/ext_conf_template.txt b/extension/ext_conf_template.txt
index 677b072fd45c842303994dbe66a4cdc2ea782135..2958e50696b58ecb287a18fb476f8886f0d6e905 100644
--- a/extension/ext_conf_template.txt
+++ b/extension/ext_conf_template.txt
@@ -19,6 +19,9 @@ editInlineReportDarkTheme = 0
 # cat=config/config; type=string; label=Report as File Auto Export:Default is 'no'. If set to 'yes': When a QFQ tt-content record is rendered which does not contain the "file=" keyword, then its body is exported to a file in the qfq-project directory and the tt-content body is replaced by "file=<path_to_file>".
 reportAsFileAutoExport =
 
+# cat=config/config; type=string; label=Websocket server port:Default is empty and no active websocket. If set: Websocket server is running and ready for chat implementation.
+websocketPort =
+
 
 # cat=graphics/config; type=string; label=Command 'inkscape':Default is 'inkscape'. Will be used to convert SVG to images (png). An empty string disables `inkscape`. If it is not available, `convert` will be used instead.
 cmdInkscape = inkscape
diff --git a/javascript/src/Helper/qfqChat.js b/javascript/src/Helper/qfqChat.js
index f9a7599f0d7015de14e6757b6f891efcbc7125a4..d0d8b3859753359d3a3076ed5c2f69f9897fe48a 100644
--- a/javascript/src/Helper/qfqChat.js
+++ b/javascript/src/Helper/qfqChat.js
@@ -3,7 +3,7 @@
  */
 
 /* global console */
-/* global CodeMirror */
+/* global qfqChat */
 /* global $ */
 
 /**
@@ -22,10 +22,10 @@ QfqNS.Helper = QfqNS.Helper || {};
 (function (n) {
     'use strict';
 
-    let qfqChat = function () {
+    let qfqChat = function (form) {
         var chatWindowsElements = document.getElementsByClassName("qfq-chat-window");
         for (var i = 0; i < chatWindowsElements.length; i++) {
-            new ChatWindow(chatWindowsElements[i]);
+            new ChatWindow(chatWindowsElements[i], form);
         }
     }
 
@@ -35,17 +35,22 @@ QfqNS.Helper = QfqNS.Helper || {};
      */
 
     class ChatWindow {
-        constructor(chatWindowElement) {
+        constructor(chatWindowElement, form) {
             this.chatWindow = chatWindowElement;
+            this.form = form;
+            this.elementName = this.chatWindow.parentNode.name;
             this.chatSearch = this.chatWindow.querySelector(".chat-search");
             this.searchInput = this.chatWindow.querySelector(".chat-search-input");
             this.searchBtn = this.chatWindow.querySelector(".chat-search-btn");
             this.chatMessages = this.chatWindow.querySelector(".chat-messages");
             this.topBtn = this.chatWindow.querySelector(".chat-top-symbol");
             this.activateSearchBtn = this.chatWindow.querySelector(".chat-search-activate");
+            this.chatInput = this.chatWindow.nextElementSibling;
             this.currentSearchIndex = 0;
             this.searchResults = [];
             this.lastSearchTerm = '';
+            this.connection = '';
+            this.chatRefresh = false;
             this.init();
         }
 
@@ -86,10 +91,73 @@ QfqNS.Helper = QfqNS.Helper || {};
                 }
             });
 
+            document.addEventListener("visibilitychange", function() {
+                if (this.chatRefresh) {
+                    this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
+                    this.chatRefresh = false;
+                }
+            }.bind(this));
+
+            this.form.on('form.submit.successful', (obj) => {
+                let elementName = this.elementName;
+                let newMessageId = qfqChat.getValue(obj.data, elementName);
+
+                let messageData = {value: this.chatInput.value, messageId: newMessageId};
+                this.connection.send(JSON.stringify(messageData));
+                console.log('send data: ' + JSON.stringify(messageData));
+            });
+
             this.chatMessages.addEventListener('scroll', () => this.checkOverflow());
 
             this.checkOverflow();
             this.scrollToBottom();
+
+            // Build up websocket connection
+            this.connection = new WebSocket('ws://webwork20:46301/ws');
+            this.connection.onopen = function(e) {
+                console.log("Connection established!");
+                let chatJsonConfigString = this.chatWindow.getAttribute('data-chat-config');
+                let chatJsonConfig = JSON.parse(chatJsonConfigString);
+
+                let chatConfig = {
+                    type: "config",
+                    data: chatJsonConfig
+                };
+
+                let keepConnection = {type: "heartbeat"};
+
+                this.connection.send(JSON.stringify(chatConfig));
+                // Send heartbeat message every 30 seconds
+                setInterval(function() {
+                    this.connection.send(JSON.stringify(keepConnection));
+                }.bind(this), 30000);
+            }.bind(this);
+
+            this.connection.onmessage = function(e) {
+                try {
+                    // Try to parse the data as JSON
+                    let decodedData = JSON.parse(e.data);
+                    let entriesArray = Object.entries(decodedData);
+                    // If the parsing succeeds and it's an array with more than one element
+                    if (entriesArray.length > 0) {
+                        let element = this.chatWindow;
+                        qfqChat.refreshChat(element, decodedData);
+
+                        // Set flag if users tab is not active.
+                        if (document.visibilityState !== 'visible') {
+                            this.chatRefresh = true;
+                        }
+                    }
+                } catch (error) {}
+            }.bind(this);
+
+            this.connection.onerror = function(e) {
+                console.error("Connection error!", e);
+            };
+
+            this.connection.onclose = function(e) {
+                console.log("Connection closed!", e);
+            };
         }
 
         scrollToBottom() {
@@ -270,6 +338,18 @@ QfqNS.Helper = QfqNS.Helper || {};
         return chatContainerElement;
     };
 
+    qfqChat.getValue = function (obj, elementName) {
+        let returnValue = null;
+
+        obj["form-update"].forEach(item => {
+            if(item["form-element"] === elementName) {
+                returnValue = item.value;
+            }
+        });
+
+        return returnValue;
+    };
+
     n.qfqChat = qfqChat;
 
 })(QfqNS.Helper);
diff --git a/javascript/src/QfqForm.js b/javascript/src/QfqForm.js
index 250897c9ac0016446cb7b9860da9c4399d134306..7edd0f904468e404154c8d9e2524b5d520d33b09 100644
--- a/javascript/src/QfqForm.js
+++ b/javascript/src/QfqForm.js
@@ -153,7 +153,7 @@ var QfqNS = QfqNS || {};
         //n.Helper.jqxEditor();
         n.Helper.tinyMce();
         n.Helper.codemirror();
-        n.Helper.qfqChat();
+        n.Helper.qfqChat(this.form);
 
         this.form.on('form.submit.before', n.Helper.tinyMce.prepareSave);
         this.form.on('form.validation.before', n.Helper.tinyMce.prepareSave);