diff --git a/Dockerfile b/Dockerfile
index 4472a7837250cabd6b32f87731ff63f80292ba95..137fba83027d04a5987961f75793a60186b7033b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM registry.gitlab.com/arkindex/backend:base-0.10.1
+FROM registry.gitlab.com/arkindex/backend:base-0.10.3
 
 ARG COMMON_BRANCH=master
 ARG COMMON_ID=9855787
@@ -26,12 +26,12 @@ RUN \
 # Install arkindex and its deps
 # Uses a source archive instead of full local copy to speedup docker build
 COPY dist/arkindex-*.tar.gz /tmp/arkindex.tar.gz
-RUN pip install /tmp/arkindex.tar.gz gunicorn==19.9 && rm /tmp/arkindex.tar.gz
+RUN pip install /tmp/arkindex.tar.gz && rm /tmp/arkindex.tar.gz
 
 # Allow access to logs
 RUN mkdir -p /logs
 RUN chown -R ark:teklia /logs
 
-# Run through supervisor
+# Run with Daphne
 EXPOSE 80
-CMD ["gunicorn", "--access-logfile=-", "--capture-output", "--bind=0.0.0.0:80", "arkindex.project.wsgi"]
+CMD ["daphne", "--verbosity=1", "--bind=0.0.0.0", "--port=80", "arkindex.project.asgi:application"]
diff --git a/arkindex/project/asgi.py b/arkindex/project/asgi.py
new file mode 100644
index 0000000000000000000000000000000000000000..0b555c966c18d02f2f312313464a0d8ede1a1673
--- /dev/null
+++ b/arkindex/project/asgi.py
@@ -0,0 +1,13 @@
+"""
+ASGI entrypoint. Configures Django and then runs the application
+defined in the ASGI_APPLICATION setting.
+"""
+
+import os
+import django
+from channels.routing import get_default_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "arkindex.project.settings")
+os.environ['ALL_CHECKS'] = 'true'
+django.setup()
+application = get_default_application()
diff --git a/arkindex/project/consumers.py b/arkindex/project/consumers.py
new file mode 100644
index 0000000000000000000000000000000000000000..45b64e7d5b47855ebbc08d8755ff2f3bf826f557
--- /dev/null
+++ b/arkindex/project/consumers.py
@@ -0,0 +1,46 @@
+from channels.exceptions import DenyConnection
+from channels.generic.websocket import AsyncJsonWebsocketConsumer
+
+
+class ConsumerPermission(object):
+    """
+    Base class for permission classes on Channels consumers.
+    """
+
+    def has_permission(self, consumer):
+        return True
+
+
+class IsAuthenticated(ConsumerPermission):
+    """
+    Restrict connections to authenticated users.
+    """
+
+    def has_permission(self, consumer):
+        if not consumer.user.is_authenticated:
+            return False
+        return super().has_permission(consumer)
+
+
+class IsAdmin(ConsumerPermission):
+    """
+    Restrict connections to admin users.
+    """
+
+    def has_permission(self, consumer):
+        if not consumer.user.is_authenticated or not consumer.user.is_admin:
+            return False
+        return super().has_permission(consumer)
+
+
+class AuthConsumer(AsyncJsonWebsocketConsumer):
+    permission_classes = ()
+
+    async def connect(self):
+        self.user = self.scope["user"]
+        for permission_class in self.permission_classes:
+            if not permission_class().has_permission(self):
+                # Raise instead of using self.close() because it allows subclasses
+                # to call await super().connect() and continue only when connections are accepted
+                raise DenyConnection
+        await self.accept()
diff --git a/arkindex/project/routing.py b/arkindex/project/routing.py
new file mode 100644
index 0000000000000000000000000000000000000000..60f19878e014f3d94172b9f37f8f39f234636245
--- /dev/null
+++ b/arkindex/project/routing.py
@@ -0,0 +1,12 @@
+from channels.auth import AuthMiddlewareStack
+from channels.routing import ProtocolTypeRouter, URLRouter
+from channels.security.websocket import AllowedHostsOriginValidator
+
+application = ProtocolTypeRouter({
+    'websocket': AllowedHostsOriginValidator(
+        AuthMiddlewareStack(
+            URLRouter([
+            ]),
+        ),
+    ),
+})
diff --git a/arkindex/project/settings.py b/arkindex/project/settings.py
index 324817c9c7b7d7f22a9b0f65408ccbb0ca8782f6..d860f56880919a39b2ea7de681c18a88e785caec 100644
--- a/arkindex/project/settings.py
+++ b/arkindex/project/settings.py
@@ -117,6 +117,7 @@ INSTALLED_APPS = [
     'django_admin_hstore_widget',
 
     # Tools
+    'channels',
     'rest_framework',
     'rest_framework.authtoken',
     'django_filters',
@@ -162,6 +163,7 @@ TEMPLATES = [
 ]
 
 WSGI_APPLICATION = 'arkindex.project.wsgi.application'
+ASGI_APPLICATION = 'arkindex.project.routing.application'
 
 
 # Password validation
@@ -277,6 +279,18 @@ CACHES = {
     }
 }
 
+# Django Channels layer using Redis
+CHANNEL_LAYERS = {
+    "default": {
+        "BACKEND": "channels_redis.core.RedisChannelLayer",
+        "CONFIG": {
+            "hosts": [
+                (os.environ.get('REDIS_HOST', 'localhost'), 6379)
+            ],
+        },
+    },
+}
+
 LOGGING = {
     'version': 1,
     'disable_existing_loggers': False,
@@ -319,6 +333,10 @@ LOGGING = {
             'level': 'INFO',
             'propagate': True,
         },
+        'daphne.server': {
+            'handlers': ['console_debug'],
+            'level': 'INFO',
+        },
         'arkindex.documents.importer': {
             'handlers': ['importers'],
             'level': 'DEBUG',
@@ -363,6 +381,8 @@ CSRF_COOKIE_DOMAIN = os.environ.get('COOKIE_DOMAIN')
 
 SESSION_COOKIE_NAME = 'arkindex.auth'
 SESSION_COOKIE_DOMAIN = os.environ.get('COOKIE_DOMAIN')
+# Required for authentication over websockets
+SESSION_COOKIE_HTTPONLY = False
 
 CORS_ORIGIN_WHITELIST = env2list('CORS_ORIGIN_WHITELIST', default=[
     'universalviewer.io',
diff --git a/base/requirements.txt b/base/requirements.txt
index e35efd23de762bd382e8bcdbacd1ed4f768d05b5..97f050776216516a473d97ecb5ff30c57fe828ab 100644
--- a/base/requirements.txt
+++ b/base/requirements.txt
@@ -2,9 +2,11 @@ boto3==1.9
 cryptography>=2.7
 Django==2.2
 elasticsearch==6.2.0
+hiredis==1.0.0
 ijson==2.3
 lxml==4.2.3
 openpyxl==2.4.9
 Pillow==4.3.0
 psycopg2==2.7.3.2
 python-Levenshtein==0.12.0
+Twisted==19.7.0
diff --git a/requirements.txt b/requirements.txt
index 0ca760efe9c3a468e3046dfa830ca216aff9eb99..dddc7b6d1ef49ed37b92d80a600f050befebaf5a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,6 +2,8 @@
 
 arkindex-common==0.2.0
 certifi==2017.7.27.1
+channels==2.3.1
+channels-redis==2.4.1
 chardet==3.0.4
 django-admin-hstore-widget==1.0.1
 django-cors-headers==2.4.0