diff --git a/aleksis/core/schoolapps/settings.py b/aleksis/core/schoolapps/settings.py
index 36fd54aa760b3c7a73a72754b972a28b659b2241..5d59cc743dcd60426eee3f471799a3bbbe5370e7 100644
--- a/aleksis/core/schoolapps/settings.py
+++ b/aleksis/core/schoolapps/settings.py
@@ -192,36 +192,6 @@ else:
         }
     }
 
-# PWA
-PWA_APP_NAME = 'SchoolApps'
-PWA_APP_DESCRIPTION = "Eine Sammlung an nützlichen Apps für den Schulalltag am Katharineum zu Lübeck"
-PWA_APP_THEME_COLOR = '#da1f3d'
-PWA_APP_BACKGROUND_COLOR = '#ffffff'
-PWA_APP_DISPLAY = 'standalone'
-PWA_APP_SCOPE = '/'
-PWA_APP_ORIENTATION = 'any'
-PWA_APP_START_URL = '/'
-PWA_APP_ICONS = [
-    {
-        "src": "/static/icons/android_192.png",
-        "sizes": "192x192",
-        "type": "image/png"
-    },
-    {
-        "src": "/static/icons/android_512.png",
-        "sizes": "512x512",
-        "type": "image/png"
-    }
-]
-PWA_APP_SPLASH_SCREEN = [
-    {
-        'src': '/static/icons/android_512.png',
-        'media': '(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)'
-    }
-]
-PWA_APP_DIR = 'ltr'
-PWA_SERVICE_WORKER_PATH = os.path.join(BASE_DIR, 'static/common', 'serviceworker.js')
-PWA_APP_LANG = 'de-DE'
 
 LOGGING = {
     'version': 1,
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index 55f09f24db5d70baba3d678f59773f41209a4448..8fe5e7f5e8356e78b22127291ffc21f48c0a01d5 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -72,7 +72,8 @@ INSTALLED_APPS = [
     "aleksis.core",
     "impersonate",
     "two_factor",
-    "material"
+    "material",
+    "pwa"
 ]
 
 INSTALLED_APPS += get_app_packages()
@@ -84,7 +85,6 @@ STATICFILES_FINDERS = [
     "sass_processor.finders.CssFinder",
 ]
 
-
 MIDDLEWARE = [
     #    'django.middleware.cache.UpdateCacheMiddleware',
     "django.middleware.security.SecurityMiddleware",
@@ -125,15 +125,14 @@ TEMPLATES = [
 ]
 
 THUMBNAIL_PROCESSORS = (
-    "image_cropping.thumbnail_processors.crop_corners",
-) + thumbnail_settings.THUMBNAIL_PROCESSORS
+                           "image_cropping.thumbnail_processors.crop_corners",
+                       ) + thumbnail_settings.THUMBNAIL_PROCESSORS
 
 # Already included by base template / Bootstrap
 IMAGE_CROPPING_JQUERY_URL = None
 
 WSGI_APPLICATION = "aleksis.core.wsgi.application"
 
-
 # Database
 # https://docs.djangoproject.com/en/2.1/ref/settings/#databases
 
@@ -161,10 +160,10 @@ if _settings.get("caching.memcached.enabled", True):
 # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators
 
 AUTH_PASSWORD_VALIDATORS = [
-    {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",},
-    {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",},
-    {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",},
-    {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",},
+    {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", },
+    {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", },
+    {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", },
+    {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", },
 ]
 
 # Authentication backends are dynamically populated
@@ -230,7 +229,8 @@ STATIC_ROOT = _settings.get("static.root", os.path.join(BASE_DIR, "static"))
 MEDIA_ROOT = _settings.get("media.root", os.path.join(BASE_DIR, "media"))
 NODE_MODULES_ROOT = _settings.get("node_modules.root", os.path.join(BASE_DIR, "node_modules"))
 
-YARN_INSTALLED_APPS = ["datatables", "highlight.js", "jquery", "manup", "materialize-css", "moment", "popper.js", "prop-types", "react", "react-dom", "material-design-icons-iconfont", "select2"]
+YARN_INSTALLED_APPS = ["datatables", "highlight.js", "jquery", "manup", "materialize-css", "moment", "popper.js",
+                       "prop-types", "react", "react-dom", "material-design-icons-iconfont", "select2"]
 
 JS_URL = _settings.get("js_assets.url", STATIC_URL)
 JS_ROOT = _settings.get("js_assets.root", NODE_MODULES_ROOT + "/node_modules")
@@ -304,3 +304,49 @@ if _settings.get("2fa.twilio.sid", None):
     TWILIO_CALLER_ID = _settings.get("2fa.twilio.callerid")
 
 _settings.populate_obj(sys.modules[__name__])
+
+PWA_APP_NAME = 'AlekSIS'  # dbsettings
+PWA_APP_DESCRIPTION = "AlekSIS – The free school information system"  # dbsettings
+PWA_APP_THEME_COLOR = _settings.get("pwa.color", "#da1f3d")  # dbsettings
+PWA_APP_BACKGROUND_COLOR = '#ffffff'
+PWA_APP_DISPLAY = 'standalone'
+PWA_APP_SCOPE = '/'
+PWA_APP_ORIENTATION = 'any'
+PWA_APP_START_URL = '/'
+PWA_APP_ICONS = [  # three icons to upload dbsettings
+    {
+        "src": "/static/icons/android_192.png",
+        "sizes": "192x192"
+    },
+    {
+        "src": "/static/icons/android_512.png",
+        "sizes": "512x512"
+    }
+]
+PWA_APP_ICONS_APPLE = [
+    {
+        "src": "/static/icons/apple_76.png",
+        "sizes": "76x76"
+    },
+    {
+        "src": "/static/icons/apple_114.png",
+        "sizes": "114x114"
+    },
+    {
+        "src": "/static/icons/apple_152.png",
+        "sizes": "152x152"
+    },
+    {
+        "src": "/static/icons/apple_180.png",
+        "sizes": "180x180"
+    },
+]
+PWA_APP_SPLASH_SCREEN = [
+    {
+        'src': '/static/icons/android_512.png',
+        'media': '(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)'
+    }
+]
+PWA_APP_DIR = 'ltr'
+PWA_SERVICE_WORKER_PATH = os.path.join(STATIC_ROOT, 'js', 'serviceworker.js')
+# PWA_APP_LANG = 'de-DE'
diff --git a/aleksis/core/static/common/favicon.ico b/aleksis/core/static/common/favicon.ico
deleted file mode 100644
index 1405a12165083ca70bcdfffeacc527f5986a3785..0000000000000000000000000000000000000000
Binary files a/aleksis/core/static/common/favicon.ico and /dev/null differ
diff --git a/aleksis/core/static/common/logo.png b/aleksis/core/static/common/logo.png
deleted file mode 100644
index 005e0b6c64a3475a0ccaa875caaf7673c14b8fd2..0000000000000000000000000000000000000000
Binary files a/aleksis/core/static/common/logo.png and /dev/null differ
diff --git a/aleksis/core/static/common/serviceworker.js b/aleksis/core/static/common/serviceworker.js
deleted file mode 100644
index 76a52613b4a8a08e45d06b1fdcc54a74e47f5913..0000000000000000000000000000000000000000
--- a/aleksis/core/static/common/serviceworker.js
+++ /dev/null
@@ -1,115 +0,0 @@
-//This is the SchoolApps service worker
-
-const CACHE = "schoolapps-cache";
-
-const precacheFiles = [
-    '',
-    '/faq/',
-];
-
-const offlineFallbackPage = '/offline';
-
-const avoidCachingPaths = [
-    '/admin',
-    '/settings',
-    '/support',
-    '/tools',
-    '/faq/ask',
-    '/aub/apply_for',
-    '/aub/check1',
-    '/aub/check2',
-    '/aktuell.pdf',
-    '/accounts/login',
-    '/timetable/aktuell.pdf',
-    '/api',
-];
-
-function pathComparer(requestUrl, pathRegEx) {
-  return requestUrl.match(new RegExp(pathRegEx));
-}
-
-function comparePaths(requestUrl, pathsArray) {
-  if (requestUrl) {
-    for (let index = 0; index < pathsArray.length; index++) {
-      const pathRegEx = pathsArray[index];
-      if (pathComparer(requestUrl, pathRegEx)) {
-        return true;
-      }
-    }
-  }
-
-  return false;
-}
-
-self.addEventListener("install", function (event) {
-  console.log("[SchoolApps PWA] Install Event processing.");
-
-  console.log("[SchoolApps PWA] Skipping waiting on install.");
-  self.skipWaiting();
-
-  event.waitUntil(
-    caches.open(CACHE).then(function (cache) {
-      console.log("[SchoolApps PWA] Caching pages during install.");
-
-      return cache.addAll(precacheFiles).then(function () {
-        return cache.add(offlineFallbackPage);
-      });
-    })
-  );
-});
-
-// Allow sw to control of current page
-self.addEventListener("activate", function (event) {
-  console.log("[SchoolApps PWA] Claiming clients for current page.");
-  event.waitUntil(self.clients.claim());
-});
-
-// If any fetch fails, it will look for the request in the cache and serve it from there first
-self.addEventListener("fetch", function (event) {
-  if (event.request.method !== "GET") return;
-  networkFirstFetch(event);
-});
-
-function networkFirstFetch(event) {
-  event.respondWith(
-    fetch(event.request)
-      .then(function (response) {
-        // If request was successful, add or update it in the cache
-        console.log("[SchoolApps PWA] Network request successful.");
-        event.waitUntil(updateCache(event.request, response.clone()));
-        return response;
-      })
-      .catch(function (error) {
-        console.log("[SchoolApps PWA] Network request failed. Serving content from cache: " + error);
-        return fromCache(event);
-      })
-  );
-}
-
-function fromCache(event) {
-  // Check to see if you have it in the cache
-  // Return response
-  // If not in the cache, then return offline fallback page
-  return caches.open(CACHE).then(function (cache) {
-    return cache.match(event.request)
-    .then(function (matching) {
-      if (!matching || matching.status === 404) {
-        console.log("[SchoolApps PWA] Cache request failed. Serving offline fallback page.");
-        // Use the precached offline page as fallback
-        return caches.match(offlineFallbackPage)
-      }
-
-      return matching;
-    });
-  });
-}
-
-function updateCache(request, response) {
-  if (!comparePaths(request.url, avoidCachingPaths)) {
-    return caches.open(CACHE).then(function (cache) {
-      return cache.put(request, response);
-    });
-  }
-
-  return Promise.resolve();
-}
diff --git a/aleksis/core/static/js/serviceworker.js b/aleksis/core/static/js/serviceworker.js
new file mode 100644
index 0000000000000000000000000000000000000000..93d9797c78db9756cba6f1f6caa46fc8c14ae57b
--- /dev/null
+++ b/aleksis/core/static/js/serviceworker.js
@@ -0,0 +1,105 @@
+// This is the AlekSIS service worker
+
+const CACHE = "aleksis-cache";
+
+const precacheFiles = [
+    '',
+];
+
+const offlineFallbackPage = '/offline';
+
+const avoidCachingPaths = [
+    '/admin',
+    '/settings',
+    '/accounts/login'
+]; // TODO: More paths are needed
+
+function pathComparer(requestUrl, pathRegEx) {
+    return requestUrl.match(new RegExp(pathRegEx));
+}
+
+function comparePaths(requestUrl, pathsArray) {
+    if (requestUrl) {
+        for (let index = 0; index < pathsArray.length; index++) {
+            const pathRegEx = pathsArray[index];
+            if (pathComparer(requestUrl, pathRegEx)) {
+                return true;
+            }
+        }
+    }
+
+    return false;
+}
+
+self.addEventListener("install", function (event) {
+    console.log("[AlekSIS PWA] Install Event processing.");
+
+    console.log("[AlekSIS PWA] Skipping waiting on install.");
+    self.skipWaiting();
+
+    event.waitUntil(
+        caches.open(CACHE).then(function (cache) {
+            console.log("[AlekSIS PWA] Caching pages during install.");
+
+            return cache.addAll(precacheFiles).then(function () {
+                return cache.add(offlineFallbackPage);
+            });
+        })
+    );
+});
+
+// Allow sw to control of current page
+self.addEventListener("activate", function (event) {
+    console.log("[AlekSIS PWA] Claiming clients for current page.");
+    event.waitUntil(self.clients.claim());
+});
+
+// If any fetch fails, it will look for the request in the cache and serve it from there first
+self.addEventListener("fetch", function (event) {
+    if (event.request.method !== "GET") return;
+    networkFirstFetch(event);
+});
+
+function networkFirstFetch(event) {
+    event.respondWith(
+        fetch(event.request)
+            .then(function (response) {
+                // If request was successful, add or update it in the cache
+                console.log("[AlekSIS PWA] Network request successful.");
+                event.waitUntil(updateCache(event.request, response.clone()));
+                return response;
+            })
+            .catch(function (error) {
+                console.log("[AlekSIS PWA] Network request failed. Serving content from cache: " + error);
+                return fromCache(event);
+            })
+    );
+}
+
+function fromCache(event) {
+    // Check to see if you have it in the cache
+    // Return response
+    // If not in the cache, then return offline fallback page
+    return caches.open(CACHE).then(function (cache) {
+        return cache.match(event.request)
+            .then(function (matching) {
+                if (!matching || matching.status === 404) {
+                    console.log("[AlekSIS PWA] Cache request failed. Serving offline fallback page.");
+                    // Use the precached offline page as fallback
+                    return caches.match(offlineFallbackPage)
+                }
+
+                return matching;
+            });
+    });
+}
+
+function updateCache(request, response) {
+    if (!comparePaths(request.url, avoidCachingPaths)) {
+        return caches.open(CACHE).then(function (cache) {
+            return cache.put(request, response);
+        });
+    }
+
+    return Promise.resolve();
+}
diff --git a/aleksis/core/templates/core/base.html b/aleksis/core/templates/core/base.html
index 62635310eb8c158a9efb816b9d9a9d04d6bb1c91..854a16afb3c44f281c84bcd93b704b8c66faf3f4 100644
--- a/aleksis/core/templates/core/base.html
+++ b/aleksis/core/templates/core/base.html
@@ -1,7 +1,7 @@
 {# -*- engine:django -*- #}
 
 
-{% load i18n menu_generator static sass_tags any_js %}
+{% load i18n menu_generator static sass_tags any_js pwa %}
 
 
 <!DOCTYPE html>
@@ -14,7 +14,13 @@
 
   <title>School Information System</title>
 
-  {% include 'core/icons.html' %}
+  {# Favicons #}
+  <link href="{% static "icons/favicon_16.png" %}" rel="icon" type="image/png" sizes="16x16">
+  <link href="{% static "icons/favicon_32.png" %}" rel="icon" type="image/png" sizes="32x32">
+  <link href="{% static "icons/favicon_48.png" %}" rel="icon" type="image/png" sizes="48x48">
+
+  <!-- PWA -->
+  {% progressive_web_app_meta %}
 
   {# CSS #}
   {% include_css "material-design-icons" %}
diff --git a/aleksis/core/templates/core/icons.html b/aleksis/core/templates/core/icons.html
deleted file mode 100644
index 53e98aadd7f53a25431dfc1fa6d72aa9b53617a1..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/core/icons.html
+++ /dev/null
@@ -1,7 +0,0 @@
-{# -*- engine:django -*- #}
-
-{% load static %}
-
-<link rel="shortcut icon" type="image/png" href="{% static 'img/aleksis-logo.png' %}" />
-<link rel="shortcut icon" sizes="196x196" href="{% static 'img/aleksis-logo.png' %}" />
-<link rel="apple-touch-icon" href="{% static 'img/aleksis-logo.png' %}" />
diff --git a/aleksis/core/templates/partials/header.html b/aleksis/core/templates/partials/header.html
index ceb4065c957413766d0716a3bffe7c4a4953d5d9..c3edc1caa05eb2e6863ffef1ea3e7672059b05bb 100644
--- a/aleksis/core/templates/partials/header.html
+++ b/aleksis/core/templates/partials/header.html
@@ -68,10 +68,8 @@
     <!-- Favicon -->
     <link rel="shortcut icon" type="image/x-icon" href="{% static 'common/favicon.ico' %}">
 
-    <!-- PWA -->
-    {% progressive_web_app_meta %}
 
-    <!--------->
+  <!--------->
     <!-- CSS -->
     <!--------->
     <link href="{% static 'css/materialdesignicons-webfont/material-icons.css' %}" rel="stylesheet">
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index 17fce01f776afbfbdc6bb2eb83cd5a23d7cb4ce5..2e3b0c98dbdd9831cc7d2e5a19d2511910c64e63 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -11,6 +11,7 @@ from two_factor.urls import urlpatterns as tf_urls
 from . import views
 
 urlpatterns = [
+    path("", include("pwa.urls"), name="pwa"),
     path('offline/', views.offline, name='offline'),
     path("admin/", admin.site.urls),
     path("data_management/", views.data_management, name="data_management"),
diff --git a/poetry.lock b/poetry.lock
index 4c091fe70c03b2ae1ba6f84157c3d5b05c19906e..cd62881947c361493e1275b5b46b8a8bec59cb7b 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -161,9 +161,6 @@ optional = false
 python-versions = "*"
 version = "5.0.6"
 
-[package.dependencies]
-six = "*"
-
 [[package]]
 category = "dev"
 description = "Code coverage measurement for Python"
@@ -427,6 +424,17 @@ version = ">=7.0.2"
 phonenumbers = ["phonenumbers (>=7.0.2)"]
 phonenumberslite = ["phonenumberslite (>=7.0.2)"]
 
+[[package]]
+category = "main"
+description = "A Django app to include a manifest.json and Service Worker instance to enable progressive web app behavior"
+name = "django-pwa"
+optional = false
+python-versions = "*"
+version = "1.0.6"
+
+[package.dependencies]
+django = ">=1.8"
+
 [[package]]
 category = "main"
 description = "SASS processor to compile SCSS files into *.css, while rendering, or offline."
@@ -1226,7 +1234,7 @@ category = "main"
 description = "YAML parser and emitter for Python"
 name = "pyyaml"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = "*"
 version = "5.2"
 
 [[package]]
@@ -1650,7 +1658,7 @@ testing = ["pathlib2", "contextlib2", "unittest2"]
 ldap = ["django-auth-ldap"]
 
 [metadata]
-content-hash = "40917beab5394838573def3e2c8a6719f37040d826df74b3cdc9b52262ad0290"
+content-hash = "7c5cc3497e09ab1ef53c542aeba9787d5045524e6f1a9c299454f18aa4d592a6"
 python-versions = "^3.7"
 
 [metadata.files]
@@ -1828,6 +1836,10 @@ django-phonenumber-field = [
     {file = "django-phonenumber-field-3.0.1.tar.gz", hash = "sha256:794ebbc3068a7af75aa72a80cb0cec67e714ff8409a965968040f1fd210b2d97"},
     {file = "django_phonenumber_field-3.0.1-py3-none-any.whl", hash = "sha256:1ab19f723928582fed412bd9844221fa4ff466276d8526b8b4a9913ee1487c5e"},
 ]
+django-pwa = [
+    {file = "django-pwa-1.0.6.tar.gz", hash = "sha256:b3f1ad0c5241fae4c7505423540de4db93077d7c88416ff6d2af545ffe209f34"},
+    {file = "django_pwa-1.0.6-py3-none-any.whl", hash = "sha256:9306105fcb637ae16fea6527be4b147d45fd53db85efb1d4f61dfea6bf793e56"},
+]
 django-sass-processor = [
     {file = "django-sass-processor-0.8.tar.gz", hash = "sha256:e039551994feaaba6fcf880412b25a772dd313162a34cbb4289814988cfae340"},
 ]
@@ -2402,7 +2414,6 @@ urllib3 = [
 ]
 wcwidth = [
     {file = "wcwidth-0.1.8-py2.py3-none-any.whl", hash = "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603"},
-    {file = "wcwidth-0.1.8.tar.gz", hash = "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"},
 ]
 yubiotp = [
     {file = "YubiOTP-0.2.2.post1-py2.py3-none-any.whl", hash = "sha256:7e281801b24678f4bda855ce8ab975a7688a912f5a6cb22b6c2b16263a93cbd2"},
diff --git a/pyproject.toml b/pyproject.toml
index a1b518307a593aaca9af20c67dc0027166e5a8cc..ea95aa00dd4b6873da4982ec10d5d8203693c190 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -50,6 +50,7 @@ django-two-factor-auth = { version = "^1.10.0", extras = [ "YubiKey", "phonenumb
 django-yarnpkg = "^6.0"
 django-dbsettings = "^1.0.0"
 django-material = "^1.6.0"
+django-pwa = "^1.0.6"
 
 [tool.poetry.extras]
 ldap = ["django-auth-ldap"]
diff --git a/requirements.txt b/requirements.txt
index 8b767d1c91300b74fb4bce041688565787ae8949..08d6e1aa88a09416668bf8e0238fe0ef44cff4bf 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,6 +4,5 @@ django-filter
 django_react_templatetags
 PyPDF2
 martor
-django-pwa
 django_widget_tweaks
 ics