diff --git a/docs/deploying.md b/docs/deploying.md
index 68dd230e42b84515e1a15293f79f547946cac744..9a012bb9f28b8907662347988b7844a375763500 100644
--- a/docs/deploying.md
+++ b/docs/deploying.md
@@ -14,6 +14,11 @@ source venv/bin/activate
 pip install repo/requirements.txt
 ```
 
+### TODO channels integration
+
+- redis layer
+- asgi webserver, like guvicorn, daphne, ...
+
 ### Configuration
 TODO
 
@@ -29,7 +34,7 @@ Create a systemd service to run periodic tasks such as sending reminder e-mails
 been enabled.
 
 #### `wahlfang-reminders.timer`
-```
+```ini
 [Unit]
 Description=Wahlfang Election Reminders Timer
 
@@ -41,7 +46,7 @@ WantedBy=timers.target
 ```
 
 #### `wahlfang-reminders.service`
-```
+```ini
 [Unit]
 Description=Wahlfang Election reminders
 
@@ -61,7 +66,7 @@ Example gunicorn systemd service. Assumes wahlfang has been cloned to `/srv/wahl
 all requirements and gunicorn in `/srv/wahlfang/venv`.
 
 #### `gunicorn.service`
-```
+```ini
 [Unit]
 Description=gunicorn daemon
 Requires=gunicorn.socket
@@ -87,7 +92,7 @@ WantedBy=multi-user.target
 #### `gunicorn.socket`
 A corresponding systemd.socket file for socket activation.
 
-```
+```ini
 [Unit]
 Description=gunicorn socket
 
diff --git a/management/consumers.py b/management/consumers.py
new file mode 100644
index 0000000000000000000000000000000000000000..3c662b4f94f4d954f2b3bd9ee1a7d5ed2390ced8
--- /dev/null
+++ b/management/consumers.py
@@ -0,0 +1,39 @@
+import json
+
+from channels.generic.websocket import AsyncWebsocketConsumer
+
+
+class ElectionConsumer(AsyncWebsocketConsumer):
+
+    async def connect(self):
+        self.group = "Election-" + self.scope['url_route']['kwargs']['pk']  # pylint: disable=W0201
+        await self.channel_layer.group_add(self.group, self.channel_name)
+        await self.accept()
+
+    async def disconnect(self, code):
+        await self.channel_layer.group_discard(self.group, self.channel_name)
+
+    async def send_reload(self, event):
+        await self.send(text_data=json.dumps({
+            'reload': True,
+        }))
+
+
+class SessionConsumer(AsyncWebsocketConsumer):
+
+    async def connect(self):
+        session = self.scope['url_route']['kwargs']['pk']
+        # reload if a new voter logged in, or if a election of the session was changed (like added)
+        self.groups = ["Login-Session-" + session, "Session-" + session]  # pylint: disable=W0201
+        for group in self.groups:
+            await self.channel_layer.group_add(group, self.channel_name)
+        await self.accept()
+
+    async def disconnect(self, code):
+        for group in self.groups:
+            await self.channel_layer.group_discard(group, self.channel_name)
+
+    async def send_reload(self, event):
+        await self.send(text_data=json.dumps({
+            'reload': True,
+        }))
diff --git a/management/routing.py b/management/routing.py
new file mode 100644
index 0000000000000000000000000000000000000000..51d0f8c2ccf77381eeee9b68be845b747301adf9
--- /dev/null
+++ b/management/routing.py
@@ -0,0 +1,8 @@
+from channels.routing import URLRouter
+from django.urls import re_path
+from . import consumers
+
+websocket_urlpatterns = URLRouter([
+    re_path(r'election/(?P<pk>\d+)$', consumers.ElectionConsumer.as_asgi()),
+    re_path(r'meeting/(?P<pk>\d+)$', consumers.SessionConsumer.as_asgi()),
+])
diff --git a/management/templates/management/add_election.html b/management/templates/management/add_election.html
index 41a8ac047ad5adf18aaa27edcf9e8f306703a1a8..996ff478b119d1d86deb9f603bded80e13b7814a 100644
--- a/management/templates/management/add_election.html
+++ b/management/templates/management/add_election.html
@@ -94,6 +94,8 @@
       </div>
     </div>
   </div>
+{% endblock %}
+{% block footer_scripts %}
   <script src="{% static "js/jquery-3.5.1.min.js" %}"
           integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="></script>
   <script src="{% static "bootstrap-4.5.3-dist/js/bootstrap.min.js" %}"
diff --git a/management/templates/management/add_session.html b/management/templates/management/add_session.html
index a031ebcb93114e7db54e1dd67f2fdb9012d9fbe2..b87df27e5e833c731329c947628be890491dfa74 100644
--- a/management/templates/management/add_session.html
+++ b/management/templates/management/add_session.html
@@ -85,6 +85,9 @@
       </div>
     </div>
   </div>
+{% endblock %}
+
+{% block footer_scripts %}
   <script src="{% static "js/jquery-3.5.1.min.js" %}"
           integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="></script>
   <script src="{% static "bootstrap-4.5.3-dist/js/bootstrap.min.js" %}"
diff --git a/management/templates/management/application.html b/management/templates/management/application.html
index ea113a88a9f0e5c24a929728cbe34d610e616220..8fb4da6a956e1d6b53e3833474ed302cd90f97af 100644
--- a/management/templates/management/application.html
+++ b/management/templates/management/application.html
@@ -82,6 +82,5 @@
 {% endblock %}
 
 {% block footer_scripts %}
-  {{ block.super }}
-  <script src="{% static 'management/js/application.js' %}"></script>
+  <script src="{% static "js/application.js" %}"></script>
 {% endblock %}
diff --git a/management/templates/management/election.html b/management/templates/management/election.html
index d4a6b3a447111ff931593cab073b3f66544b15af..ffcbe4b110b1d671a7b8c72b8e4709c1fe9824fa 100644
--- a/management/templates/management/election.html
+++ b/management/templates/management/election.html
@@ -1,5 +1,6 @@
 {% extends 'management/base.html' %}
 {% load crispy_forms_filters %}
+{% load static %}
 
 {% block content %}
   <div class="row justify-content-center">
@@ -157,3 +158,11 @@
     </div>
   </div>
 {% endblock %}
+
+{% block footer_scripts %}
+  {#  Automatic reload of the page: #}
+  {#    - if another vote was cast#}
+  <script src="{% static "js/jquery-3.5.1.min.js" %}"
+          integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="></script>
+  <script src="{% static "js/reload.js" %}"></script>
+{% endblock %}
diff --git a/management/templates/management/index.html b/management/templates/management/index.html
index d6fff7b3e30741b89e2c67b4b3b571f33253704e..4b888882945abd2982674655aab7a473ebde1684 100644
--- a/management/templates/management/index.html
+++ b/management/templates/management/index.html
@@ -54,7 +54,9 @@
       </div>
     </div>
   {% endfor %}
+{% endblock %}
 
+{% block footer_scripts %}
   <script src="{% static "js/jquery-3.5.1.slim.min.js" %}"
           integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"></script>
   <script src="{% static "bootstrap-4.5.3-dist/js/bootstrap.min.js" %}"
diff --git a/management/templates/management/session.html b/management/templates/management/session.html
index 479ba6a8ae3ced2f6470a49680d864cbe924ecf6..5d4ecd582b75ccffbb1e102bab12c5050a00f97c 100644
--- a/management/templates/management/session.html
+++ b/management/templates/management/session.html
@@ -152,9 +152,17 @@
       </div>
     </div>
   </div>
+{% endblock %}
+
+{% block footer_scripts %}
+  {#  Automatic reload of the page: #}
+  {#    - either if the start date / end date of a election is due#}
+  {#    - or if the admin started / stopped one election and the page is notified with a websocket#}
+  {#    - or if a voter has logged in #}
+  <script src="{% static "js/jquery-3.5.1.min.js" %}"
+          integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="></script>
+  <script src="{% static "js/reload.js" %}"></script>
 
-  <script src="{% static "js/jquery-3.5.1.slim.min.js" %}"
-          integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"></script>
   <script src="{% static "js/popper-1.16.1.min.js" %}"
           integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN"></script>
   <script src="{% static "bootstrap-4.5.3-dist/js/bootstrap.min.js" %}"
diff --git a/management/templates/management/session_election_item.html b/management/templates/management/session_election_item.html
index 8ef5ba290337a3529625cf5d060b5a905398f672..e3053942612cef25870225fa5e598507115d8354 100644
--- a/management/templates/management/session_election_item.html
+++ b/management/templates/management/session_election_item.html
@@ -10,12 +10,14 @@
   <small class="float-right">
     {% if not election.started and election.start_date %}
       <span class="right-margin">Starts at {{ election.start_date|date:"Y-m-d H:i:s" }}</span>
+      {# Time for automatic reload #}
+      <div class="d-none time">{{ election.start_date|date:"U" }}|</div>
     {% elif not election.started %}
       <span class="right-margin">Needs to be started manually</span>
     {% elif election.is_open and election.end_date %}
-      <span class="right-margin">
-      Open until {{ election.end_date|date:"Y-m-d H:i:s" }}
-    </span>
+      <span class="right-margin">Open until {{ election.end_date|date:"Y-m-d H:i:s" }}</span>
+      {# Time for automatic reload #}
+      <div class="d-none time">{{ election.end_date|date:"U" }}|</div>
     {% elif election.closed %}
       <span class="right-margin">Closed</span>
     {% else %}
diff --git a/management/templates/management/session_settings.html b/management/templates/management/session_settings.html
index 95affadf354431d6d9afefec1d9e665c6378583f..190d56d199df24438f3c364df52a0de0866e254a 100644
--- a/management/templates/management/session_settings.html
+++ b/management/templates/management/session_settings.html
@@ -97,6 +97,9 @@
       </div>
     </div>
   </div>
+{% endblock %}
+
+{% block footer_scripts %}
   <script src="{% static "js/jquery-3.5.1.min.js" %}"
           integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="></script>
   <script src="{% static "bootstrap-4.5.3-dist/js/bootstrap.min.js" %}"
diff --git a/requirements.txt b/requirements.txt
index 091714695c1bca9a19ab0b3db424f9186df96fd7..5e827c6837b55a87d58f24a429d765c4fdae0939 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,4 +7,5 @@ django-ratelimit==3.0.*
 django-auth-ldap==2.2.*
 qrcode==6.1
 latex==0.7.*
-django_prometheus==2.1.*
\ No newline at end of file
+django_prometheus==2.1.*
+channels==3.0.*
\ No newline at end of file
diff --git a/management/static/management/js/application.js b/static/js/application.js
similarity index 100%
rename from management/static/management/js/application.js
rename to static/js/application.js
diff --git a/static/js/reload.js b/static/js/reload.js
new file mode 100644
index 0000000000000000000000000000000000000000..21e9c0eaf6be7358d75e93ed911fe129b71c13eb
--- /dev/null
+++ b/static/js/reload.js
@@ -0,0 +1,48 @@
+$(document).ready(() => {
+  let timeout;
+
+  function reload_callback() {
+    setup_date_reload();
+  }
+
+  function reload() {
+    console.log("Reloading")
+    //wait a random time, to not overload the server if everyone reloads at the same time
+    window.setTimeout(() => $("#content").load(location.pathname + " #content", reload_callback), Math.random() * 1000)
+  }
+
+  function setup_date_reload() {
+    //setup a timer to reload the page if a start or end date of a election passed
+    clearTimeout(timeout);
+    const now_ms = new Date().getTime();
+    const times = $(".time").text().split('|').map(u_time => parseInt(u_time));
+    const wait_ms = times.map(time => (time + 5) * 1000 - now_ms).filter(t => t > 10 * 1000);
+    const min_ms = Math.min(...wait_ms);
+    if (min_ms < 24 * 60 * 60 * 1000) {
+      console.log("Reloading in " + (min_ms / 1000) + "s");
+      timeout = setTimeout(reload, min_ms);
+    }
+  }
+
+  function setup_websocket() {
+    const ws = new WebSocket(location.href.replace("http", "ws"));
+    ws.onmessage = function (e) {
+      const message = JSON.parse(e.data)
+      if (message.reload) {
+        reload();
+      }
+    }
+    ws.onopen = function (e) {
+      console.log("Websocket connected");
+    }
+    ws.onerror = function (e) {
+      console.error("Websocket ERROR. Site will not reload automatically");
+    }
+    ws.onclose = function (e) {
+      console.error("Websocket Closed. Site will not reload automatically");
+    }
+  }
+
+  setup_date_reload();
+  setup_websocket();
+})
diff --git a/vote/consumers.py b/vote/consumers.py
new file mode 100644
index 0000000000000000000000000000000000000000..628fbfa62b79d011f2326bdd133c35203730e1d2
--- /dev/null
+++ b/vote/consumers.py
@@ -0,0 +1,30 @@
+import json
+
+from channels.generic.websocket import AsyncWebsocketConsumer
+from channels.db import database_sync_to_async
+
+from vote.models import Session
+
+
+class VoteConsumer(AsyncWebsocketConsumer):
+
+    async def connect(self):
+        self.group = await database_sync_to_async(self.get_session_key)()  # pylint: disable=W0201
+        await self.channel_layer.group_add(self.group, self.channel_name)
+        await self.accept()
+
+    async def disconnect(self, code):
+        await self.channel_layer.group_discard(self.group, self.channel_name)
+
+    async def send_reload(self, event):
+        await self.send(text_data=json.dumps({
+            'reload': True,
+        }))
+
+    def get_session_key(self):
+        if 'uuid' in self.scope['url_route']['kwargs']:
+            uuid = self.scope['url_route']['kwargs']['uuid']
+            session = Session.objects.get(spectator_token=uuid)
+        else:
+            session = self.scope['user'].session
+        return "Session-" + str(session.pk)
diff --git a/vote/forms.py b/vote/forms.py
index 2f35889c15a89558fc137e15e4e3524cb1a2eedf..7d767f1b4f152367dca236a2ab262a0ac0accfd8 100644
--- a/vote/forms.py
+++ b/vote/forms.py
@@ -1,3 +1,5 @@
+from asgiref.sync import async_to_sync
+from channels.layers import get_channel_layer
 from django import forms
 from django.contrib.auth import authenticate
 from django.db import transaction
@@ -122,6 +124,12 @@ class VoteForm(forms.Form):
             with transaction.atomic():
                 Vote.objects.bulk_create(votes)
                 can_vote.delete()
+            # notify manager that new votes were cast
+            group = "Election-" + str(self.election.pk)
+            async_to_sync(get_channel_layer().group_send)(
+                group,
+                {'type': 'send_reload'}
+            )
 
         return votes
 
diff --git a/vote/models.py b/vote/models.py
index 435c559eca40e764e11381a3e14c047c85ca2c12..a142f0b574e11f390df9449fcc1f86eacc82d645 100644
--- a/vote/models.py
+++ b/vote/models.py
@@ -9,6 +9,8 @@ from io import BytesIO
 
 import PIL
 from PIL import Image
+from asgiref.sync import async_to_sync
+from channels.layers import get_channel_layer
 from django.conf import settings
 from django.contrib.auth import password_validation
 from django.contrib.auth.hashers import (
@@ -165,6 +167,15 @@ class Election(models.Model):
             return 0
         return int(self.votes.count() / self.applications.count())
 
+    def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
+        super().save(force_insert, force_update, using, update_fields)
+        # notify users to reload their page
+        group = "Session-" + str(self.session.pk)
+        async_to_sync(get_channel_layer().group_send)(
+            group,
+            {'type': 'send_reload'}
+        )
+
     def __str__(self):
         return self.title
 
@@ -201,6 +212,12 @@ class Voter(models.Model):
         if self._password is not None:
             password_validation.password_changed(self._password, self)
             self._password = None
+        # notify manager to reload their page, if the user logged in
+        group = "Login-Session-" + str(self.session.pk)
+        async_to_sync(get_channel_layer().group_send)(
+            group,
+            {'type': 'send_reload'}
+        )
 
     def set_password(self, raw_password=None):
         if not raw_password:
@@ -506,3 +523,5 @@ class Vote(models.Model):
     election = models.ForeignKey(Election, related_name='votes', on_delete=models.CASCADE)
     candidate = models.ForeignKey(Application, related_name='votes', on_delete=models.CASCADE)
     vote = models.CharField(choices=VOTE_CHOICES, max_length=max(len(x[0]) for x in VOTE_CHOICES))
+    # save method is not called on bulk_create in forms.VoteForm.
+    # The model update listener for websockets is implemented in the form.
diff --git a/vote/routing.py b/vote/routing.py
new file mode 100644
index 0000000000000000000000000000000000000000..2bd2bca7598406a8dc5290b4a36a0abffd7c7621
--- /dev/null
+++ b/vote/routing.py
@@ -0,0 +1,8 @@
+from channels.routing import URLRouter
+from django.urls import path, re_path
+from . import consumers
+
+websocket_urlpatterns = URLRouter([
+    path('', consumers.VoteConsumer.as_asgi()),
+    re_path(r'spectator/(?P<uuid>.+)$', consumers.VoteConsumer.as_asgi()),
+])
diff --git a/vote/templates/vote/application.html b/vote/templates/vote/application.html
index 6a95b212c3a384e7419da8ff7ed1574300f289f8..72e42dcfd264f65304cf6f15ad42e2d1329f7b80 100644
--- a/vote/templates/vote/application.html
+++ b/vote/templates/vote/application.html
@@ -77,6 +77,5 @@
 {% endblock %}
 
 {% block footer_scripts %}
-  {{ block.super }}
-  <script src="{% static 'management/js/application.js' %}"></script>
+  <script src="{% static "js/application.js" %}"></script>
 {% endblock %}
diff --git a/vote/templates/vote/base.html b/vote/templates/vote/base.html
index 692269c3bce300cead636179ed1bc9ca0d8b04cd..69875796bcd0152d8167e62924bfd30e5dd52f55 100644
--- a/vote/templates/vote/base.html
+++ b/vote/templates/vote/base.html
@@ -79,12 +79,6 @@
   </article>
 
 </div>
-{% comment "Not needed :)" %}
-  <script src="{% static "js/jquery-3.5.1.slim.min.js" %}"
-          integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"></script>
-  <script src="{% static "bootstrap-4.5.3-dist/js/bootstrap.min.js" %}"
-          integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s"></script>
-{% endcomment %}
 {% block footer_scripts %}
 {% endblock %}
 </body>
diff --git a/vote/templates/vote/index.html b/vote/templates/vote/index.html
index 7fa80781df062961f09ad6ba0a5f90546bb1c66f..df56c2748eafdac5f73e1abf03e55433ec9eac91 100644
--- a/vote/templates/vote/index.html
+++ b/vote/templates/vote/index.html
@@ -64,3 +64,12 @@
     </div>
   </div>
 {% endblock %}
+
+{% block footer_scripts %}
+  {#  Automatic reload of the page: #}
+  {#    - either if the start date / end date of a election is due#}
+  {#    - or if the admin started / stopped one election and the page is notified with a websocket#}
+  <script src="{% static "js/jquery-3.5.1.slim.min.js" %}"
+          integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"></script>
+  <script src="{% static "js/reload.js" %}"></script>
+{% endblock %}
diff --git a/vote/templates/vote/index_election_item.html b/vote/templates/vote/index_election_item.html
index 4e1b1cc78690a3a75f3ae2cb9f6a6d9c56dc0f44..d9ceaf28df2b99941e38912f6f77ba9da57d2d06 100644
--- a/vote/templates/vote/index_election_item.html
+++ b/vote/templates/vote/index_election_item.html
@@ -16,6 +16,9 @@
     {% if election.end_date %}
       <small class="text-muted">Voting Period: {{ election.start_date|date:"D Y-m-d H:i:s" }}
         - {{ election.end_date|date:"D Y-m-d H:i:s" }} (UTC{{ election.end_date|date:"O" }})</small>
+      <!-- hidden start and end time for javascript  | is used to delimit values-->
+      <div class="d-none time">{{ election.start_date|date:"U" }}|</div>
+      <div class="d-none time">{{ election.end_date|date:"U" }}|</div>
     {% endif %}
     <hr>
     <div class="list-group mt-3">
diff --git a/vote/templates/vote/spectator.html b/vote/templates/vote/spectator.html
index d480e5e08ae113ad1cb7f273421c16ad62a50c20..6b352bde4a70d63846ef909a01c61cc2a15f97b3 100644
--- a/vote/templates/vote/spectator.html
+++ b/vote/templates/vote/spectator.html
@@ -65,3 +65,12 @@
     </div>
   </div>
 {% endblock %}
+
+{% block footer_scripts %}
+  {#  Automatic reload of the page: #}
+  {#    - either if the start date / end date of a election is due#}
+  {#    - or if the admin started / stopped one election and the page is notified with a websocket#}
+  <script src="{% static "js/jquery-3.5.1.slim.min.js" %}"
+          integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"></script>
+  <script src="{% static "js/reload.js" %}"></script>
+{% endblock %}
diff --git a/vote/templates/vote/spectator_election_item.html b/vote/templates/vote/spectator_election_item.html
index fc217d9f208c03160573413f49fdba10dff9c38a..538b2943dc1463af9939a01f07bb0f87aae23c6d 100644
--- a/vote/templates/vote/spectator_election_item.html
+++ b/vote/templates/vote/spectator_election_item.html
@@ -4,6 +4,9 @@
     {% if election.end_date %}
       <small class="text-muted">Voting Period: {{ election.start_date|date:"D Y-m-d H:i:s" }}
         - {{ election.end_date|date:"D Y-m-d H:i:s" }} (UTC{{ election.end_date|date:"O" }})</small>
+      <!-- hidden start and end time for javascript  | is used to delimit values-->
+      <div class="d-none time">{{ election.start_date|date:"U" }}|</div>
+      <div class="d-none time">{{ election.end_date|date:"U" }}|</div>
     {% endif %}
     <hr>
     <div class="list-group mt-3">
diff --git a/vote/templates/vote/vote.html b/vote/templates/vote/vote.html
index f7f3467a27b9bf40cf90ca643ae01733e3c5c27b..f055095442c7e8fbf6f59c568413c2de77578498 100644
--- a/vote/templates/vote/vote.html
+++ b/vote/templates/vote/vote.html
@@ -117,6 +117,5 @@
 {% endblock %}
 
 {% block footer_scripts %}
-  {{ block.super }}
   <script src="{% static "vote/js/vote.js" %}"></script>
 {% endblock %}
diff --git a/vote/views.py b/vote/views.py
index 9aa170f7f5d26d883ef21f34dfdb075a37a7a744..dd68fcd040015c69b0f4003fb2ad7a0a51a28f2b 100644
--- a/vote/views.py
+++ b/vote/views.py
@@ -1,3 +1,5 @@
+import sys
+
 from django.conf import settings
 from django.contrib import messages
 from django.contrib.auth import authenticate, login, views as auth_views
@@ -60,10 +62,19 @@ def index(request):
         (e, voter.can_vote(e), voter.application.filter(election=e).exists())
         for e in voter.session.elections.order_by('pk')
     ]
-    open_elections = [e for e in elections if e[0].is_open]
-    upcoming_elections = [e for e in elections if not e[0].started]
-    published_elections = [e for e in elections if e[0].closed and int(e[0].result_published)]
-    closed_elections = [e for e in elections if e[0].closed and not int(e[0].result_published)]
+
+    def date_asc(e):
+        date = e[0].start_date
+        return date.timestamp() if date else sys.maxsize
+
+    def date_desc(e):
+        date = e[0].start_date
+        return -date.timestamp() if date else -sys.maxsize
+
+    open_elections = sorted([e for e in elections if e[0].is_open], key=date_desc)
+    upcoming_elections = sorted([e for e in elections if not e[0].started], key=date_asc)
+    published_elections = sorted([e for e in elections if e[0].closed and int(e[0].result_published)], key=date_desc)
+    closed_elections = sorted([e for e in elections if e[0].closed and not int(e[0].result_published)], key=date_desc)
     context = {
         'title': voter.session.title,
         'meeting_link': voter.session.meeting_link,
@@ -166,10 +177,19 @@ def help_page(request):
 def spectator(request, uuid):
     session = get_object_or_404(Session.objects, spectator_token=uuid)
     elections = session.elections.all()
-    open_elections = [e for e in elections if e.is_open]
-    upcoming_elections = [e for e in elections if not e.started]
-    published_elections = [e for e in elections if e.closed and int(e.result_published)]
-    closed_elections = [e for e in elections if e.closed and not int(e.result_published)]
+
+    def date_asc(e):
+        date = e.start_date
+        return date.timestamp() if date else sys.maxsize
+
+    def date_desc(e):
+        date = e.start_date
+        return -date.timestamp() if date else -sys.maxsize
+
+    open_elections = sorted([e for e in elections if e.is_open], key=date_desc)
+    upcoming_elections = sorted([e for e in elections if not e.started], key=date_asc)
+    published_elections = sorted([e for e in elections if e.closed and int(e.result_published)], key=date_desc)
+    closed_elections = sorted([e for e in elections if e.closed and not int(e.result_published)], key=date_desc)
     context = {
         'title': session.title,
         'meeting_link': session.meeting_link,
diff --git a/wahlfang/asgi.py b/wahlfang/asgi.py
index f29b0820da5c1b47fc9ef738f7c0a100a30bd78d..cfd595d791a2751a7786e67ac2b27f76f1bc6288 100644
--- a/wahlfang/asgi.py
+++ b/wahlfang/asgi.py
@@ -9,8 +9,14 @@ https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
 
 import os
 
+from channels.auth import AuthMiddlewareStack
+from channels.routing import ProtocolTypeRouter, URLRouter
 from django.core.asgi import get_asgi_application
+import wahlfang.routing
 
 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wahlfang.settings')
 
-application = get_asgi_application()
+application = ProtocolTypeRouter({
+    "https": get_asgi_application(),
+    "websocket": AuthMiddlewareStack(wahlfang.routing.websocket_urlpatterns),
+})
diff --git a/wahlfang/routing.py b/wahlfang/routing.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2fb30158fc467e0a984392f22c883c470de64fa
--- /dev/null
+++ b/wahlfang/routing.py
@@ -0,0 +1,10 @@
+from channels.routing import URLRouter
+from django.urls import path
+
+import management.routing
+import vote.routing
+
+websocket_urlpatterns = URLRouter([
+    path('', vote.routing.websocket_urlpatterns),
+    path('management/', management.routing.websocket_urlpatterns),
+])
diff --git a/wahlfang/settings.py b/wahlfang/settings.py
index c7341182c89b40b2c80761cc132fde0a6c7d860c..965d5c4c7bbc9dd49492b4746e79035f1277e27d 100644
--- a/wahlfang/settings.py
+++ b/wahlfang/settings.py
@@ -52,6 +52,7 @@ INSTALLED_APPS = [
     'crispy_forms',
     'vote',
     'management',
+    'channels',
 ]
 
 if EXPORT_PROMETHEUS_METRICS:
@@ -99,7 +100,13 @@ AUTHENTICATION_BACKENDS = {
     'django.contrib.auth.backends.ModelBackend'
 }
 
-WSGI_APPLICATION = 'wahlfang.wsgi.application'
+ASGI_APPLICATION = 'wahlfang.asgi.application'
+
+CHANNEL_LAYERS = {
+    "default": {
+        "BACKEND": "channels.layers.InMemoryChannelLayer"
+    }
+}
 
 # Database
 # https://docs.djangoproject.com/en/3.0/ref/settings/#databases