diff --git a/chat/consumers.py b/chat/consumers.py
new file mode 100644
index 0000000..7af3987
--- /dev/null
+++ b/chat/consumers.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2024 by Animath
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from channels.generic.websocket import AsyncJsonWebsocketConsumer
+from registration.models import Registration
+
+
+class ChatConsumer(AsyncJsonWebsocketConsumer):
+    """
+    This consumer manages the websocket of the chat interface.
+    """
+    async def connect(self) -> None:
+        """
+        This function is called when a new websocket is trying to connect to the server.
+        We accept only if this is a user of a team of the associated tournament, or a volunteer
+        of the tournament.
+        """
+
+        # Fetch the registration of the current user
+        user = self.scope['user']
+        if user.is_anonymous:
+            # User is not authenticated
+            await self.close()
+            return
+
+        reg = await Registration.objects.aget(user_id=user.id)
+        self.registration = reg
+
+        # Accept the connection
+        await self.accept()
+
+    async def disconnect(self, close_code) -> None:
+        """
+        Called when the websocket got disconnected, for any reason.
+        :param close_code: The error code.
+        """
+        if self.scope['user'].is_anonymous:
+            # User is not authenticated
+            return
+
+    async def receive_json(self, content, **kwargs):
+        """
+        Called when the client sends us some data, parsed as JSON.
+        :param content: The sent data, decoded from JSON text. Must content a `type` field.
+        """
+        # TODO Process chat protocol
diff --git a/chat/static/chat.js b/chat/static/chat.js
new file mode 100644
index 0000000..5a45a83
--- /dev/null
+++ b/chat/static/chat.js
@@ -0,0 +1,52 @@
+(async () => {
+    // check notification permission
+    // This is useful to alert people that they should do something
+    await Notification.requestPermission()
+})()
+
+/**
+ * Display a new notification with the given title and the given body.
+ * @param title The title of the notification
+ * @param body The body of the notification
+ * @param timeout The time (in milliseconds) after that the notification automatically closes. 0 to make indefinite. Default to 5000 ms.
+ * @return Notification
+ */
+function showNotification(title, body, timeout = 5000) {
+    let notif = new Notification(title, {'body': body, 'icon': "/static/tfjm.svg"})
+    if (timeout)
+        setTimeout(() => notif.close(), timeout)
+    return notif
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+    /**
+     * Process the received data from the server.
+     * @param data The received message
+     */
+    function processMessage(data) {
+        // TODO Implement chat protocol
+    }
+
+    function setupSocket(nextDelay = 1000) {
+        // Open a global websocket
+        socket = new WebSocket(
+            (document.location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host + '/ws/chat/'
+        )
+
+        // Listen on websockets and process messages from the server
+        socket.addEventListener('message', e => {
+            // Parse received data as JSON
+            const data = JSON.parse(e.data)
+
+            processMessage(data)
+        })
+
+        // Manage errors
+        socket.addEventListener('close', e => {
+            console.error('Chat socket closed unexpectedly, restarting…')
+            setTimeout(() => setupSocket(2 * nextDelay), nextDelay)
+        })
+    }
+
+    setupSocket()
+})
diff --git a/chat/templates/chat/chat.html b/chat/templates/chat/chat.html
new file mode 100644
index 0000000..cf7de49
--- /dev/null
+++ b/chat/templates/chat/chat.html
@@ -0,0 +1,12 @@
+{% extends "base.html" %}
+
+{% load static %}
+{% load i18n %}
+
+{% block content %}
+{% endblock %}
+
+{% block extrajavascript %}
+    {# This script contains all data for the chat management #}
+    <script src="{% static 'chat.js' %}"></script>
+{% endblock %}
diff --git a/chat/urls.py b/chat/urls.py
index 80ea069..77d52c2 100644
--- a/chat/urls.py
+++ b/chat/urls.py
@@ -1,2 +1,13 @@
 # Copyright (C) 2024 by Animath
 # SPDX-License-Identifier: GPL-3.0-or-later
+
+from django.urls import path
+
+from .views import ChatView
+
+
+app_name = 'chat'
+
+urlpatterns = [
+    path('', ChatView.as_view(), name='chat'),
+]
diff --git a/chat/views.py b/chat/views.py
index 80ea069..8c4ef96 100644
--- a/chat/views.py
+++ b/chat/views.py
@@ -1,2 +1,13 @@
 # Copyright (C) 2024 by Animath
 # SPDX-License-Identifier: GPL-3.0-or-later
+
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.views.generic import TemplateView
+
+
+class ChatView(LoginRequiredMixin, TemplateView):
+    """
+    This view is the main interface of the chat system, which is working
+    with Javascript and websockets.
+    """
+    template_name = "chat/chat.html"
diff --git a/draw/routing.py b/draw/routing.py
deleted file mode 100644
index 8ce6085..0000000
--- a/draw/routing.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (C) 2023 by Animath
-# SPDX-License-Identifier: GPL-3.0-or-later
-
-from django.urls import path
-
-from . import consumers
-
-websocket_urlpatterns = [
-    path("ws/draw/", consumers.DrawConsumer.as_asgi()),
-]
diff --git a/draw/tests.py b/draw/tests.py
index 9f3ec6a..bbb209a 100644
--- a/draw/tests.py
+++ b/draw/tests.py
@@ -14,8 +14,8 @@ from django.contrib.sites.models import Site
 from django.test import TestCase
 from django.urls import reverse
 from participation.models import Team, Tournament
+from tfjm import routing as websocket_routing
 
-from . import routing
 from .models import Draw, Pool, Round, TeamDraw
 
 
@@ -55,7 +55,7 @@ class TestDraw(TestCase):
 
         # Connect to Websocket
         headers = [(b'cookie', self.async_client.cookies.output(header='', sep='; ').encode())]
-        communicator = WebsocketCommunicator(AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)),
+        communicator = WebsocketCommunicator(AuthMiddlewareStack(URLRouter(websocket_routing.websocket_urlpatterns)),
                                              "/ws/draw/", headers)
         connected, subprotocol = await communicator.connect()
         self.assertTrue(connected)
diff --git a/participation/templates/participation/chat.html b/participation/templates/participation/chat.html
deleted file mode 100644
index bf47704..0000000
--- a/participation/templates/participation/chat.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{% extends "base.html" %}
-
-{% load i18n %}
-
-{% block content %}
-    <div class="alert alert-warning">
-    {% blocktrans trimmed %}
-        The chat feature is now out of usage. If you feel that having a chat
-        feature between participants is important, for example to build a
-        team, please contact us.
-    {% endblocktrans %}
-    </div>
-{% endblock %}
diff --git a/participation/urls.py b/participation/urls.py
index 4125a4c..2c97587 100644
--- a/participation/urls.py
+++ b/participation/urls.py
@@ -2,7 +2,6 @@
 # SPDX-License-Identifier: GPL-3.0-or-later
 
 from django.urls import path
-from django.views.generic import TemplateView
 
 from .views import CreateTeamView, FinalNotationSheetTemplateView, GSheetNotificationsView, JoinTeamView, \
     MyParticipationDetailView, MyTeamDetailView, NotationSheetsArchiveView, NoteUpdateView, ParticipationDetailView, \
@@ -74,5 +73,4 @@ urlpatterns = [
     path("pools/passages/<int:pk>/update/", PassageUpdateView.as_view(), name="passage_update"),
     path("pools/passages/<int:pk>/solution/", SynthesisUploadView.as_view(), name="upload_synthesis"),
     path("pools/passages/notes/<int:pk>/", NoteUpdateView.as_view(), name="update_notes"),
-    path("chat/", TemplateView.as_view(template_name="participation/chat.html"), name="chat")
 ]
diff --git a/tfjm/asgi.py b/tfjm/asgi.py
index 3bec7e0..417d54e 100644
--- a/tfjm/asgi.py
+++ b/tfjm/asgi.py
@@ -22,13 +22,13 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tfjm.settings')
 django_asgi_app = get_asgi_application()
 
 # useful since the import must be done after the application initialization
-import draw.routing  # noqa: E402, I202
+import tfjm.routing  # noqa: E402, I202
 
 application = ProtocolTypeRouter(
     {
         "http": django_asgi_app,
         "websocket": AllowedHostsOriginValidator(
-            AuthMiddlewareStack(URLRouter(draw.routing.websocket_urlpatterns))
+            AuthMiddlewareStack(URLRouter(tfjm.routing.websocket_urlpatterns))
         ),
     }
 )
diff --git a/tfjm/routing.py b/tfjm/routing.py
new file mode 100644
index 0000000..08c54d8
--- /dev/null
+++ b/tfjm/routing.py
@@ -0,0 +1,11 @@
+# Copyright (C) 2024 by Animath
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+import chat.consumers
+from django.urls import path
+import draw.consumers
+
+websocket_urlpatterns = [
+    path("ws/chat/", chat.consumers.ChatConsumer.as_asgi()),
+    path("ws/draw/", draw.consumers.DrawConsumer.as_asgi()),
+]
diff --git a/tfjm/templates/navbar.html b/tfjm/templates/navbar.html
index 8e0115d..ecdb4dc 100644
--- a/tfjm/templates/navbar.html
+++ b/tfjm/templates/navbar.html
@@ -62,8 +62,8 @@
                     </li>
                 {% endif %}
             {% endif %}
-            <li class="nav-item active d-none">
-                <a class="nav-link" href="{% url "participation:chat" %}">
+            <li class="nav-item active">
+                <a class="nav-link" href="{% url "chat:chat" %}">
                     <i class="fas fa-comments"></i> {% trans "Chat" %}
                 </a>
             </li>
diff --git a/tfjm/urls.py b/tfjm/urls.py
index a397f07..c506456 100644
--- a/tfjm/urls.py
+++ b/tfjm/urls.py
@@ -37,7 +37,7 @@ urlpatterns = [
     path('search/', AdminSearchView.as_view(), name="haystack_search"),
 
     path('api/', include('api.urls')),
-    # path('chat/', include('chat.urls')),
+    path('chat/', include('chat.urls')),
     path('draw/', include('draw.urls')),
     path('participation/', include('participation.urls')),
     path('registration/', include('registration.urls')),