diff --git a/public/index.html b/public/index.html
index 4f47abdebd44def06142405d80f96ccacb543c94..369b33f73eb472472a8c77aab87f8ec6866611df 100644
--- a/public/index.html
+++ b/public/index.html
@@ -2,6 +2,22 @@
 <html lang="en">
   <head>
     <meta charset="UTF-8" />
+    <link rel="manifest" href="manifest.json">
+    <link rel="shortcut icon" href="logo.png">
+    <script>
+      if (navigator.serviceWorker) {
+        navigator.serviceWorker
+          .register("service-worker.js")
+          .then(
+            (registration) =>
+              console.log(
+                `Service worker registered on scope ${registration.scope}`,
+              ),
+            (reason) =>
+              console.log(`Service worker failed to register ~ ${reason}`),
+          )
+      }
+    </script>
   </head>
   <body>
     <div>
diff --git a/public/logo.png b/public/logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..e4266dca24a4059dcb0a7eec22db9384378d0bf3
Binary files /dev/null and b/public/logo.png differ
diff --git a/public/manifest.json b/public/manifest.json
new file mode 100644
index 0000000000000000000000000000000000000000..1b94f0d658806221b9b2bec412b6311ba6a108a3
--- /dev/null
+++ b/public/manifest.json
@@ -0,0 +1,13 @@
+{
+  "short_name": "Drawing App",
+  "name": "Drawing App",
+  "icons": [
+    {
+      "src": "/logo.png",
+      "type": "image/png",
+      "sizes": "144x144"
+    }
+  ],
+  "display": "standalone",
+  "start_url": "/index.html"
+}
diff --git a/public/service-worker.js b/public/service-worker.js
new file mode 100644
index 0000000000000000000000000000000000000000..006388a3b8328861fec68f7f1d5c7d0bf1c82c9b
--- /dev/null
+++ b/public/service-worker.js
@@ -0,0 +1,66 @@
+self.addEventListener("install", (event) => {
+  console.info("Service worker installed.", event)
+})
+
+self.addEventListener("activate", (event) => {
+  console.info("Service worker activated.", event)
+})
+
+const CACHE_NAME = "APP-V0"
+const FILES_TO_CACHE = [
+  "/index.html",
+  "/js/app.js",
+  "/manifest.json",
+]
+const FILE_ALIASES = new Map([
+  ["/", "/index.html"]
+])
+
+const normalizeUrl = (url) => {
+  const url_ = new URL(url)
+  url_.pathname = url_.pathname.replace(/\/+/g, "/").replace(/\/$/, "")
+  if (FILE_ALIASES.has(url_.pathname)) {
+    url_.pathname = FILE_ALIASES.get(url_.pathname)
+  }
+  return url_.href
+}
+
+self.addEventListener("install", async (event) => {
+  const cache = await caches.open(CACHE_NAME)
+  const additions = cache.addAll(FILES_TO_CACHE)
+  await additions
+  console.info(`Files cached: [\n  ${FILES_TO_CACHE.join(`,\n  `)}\n]`)
+})
+
+self.addEventListener("activate", async (event) => {
+  const oldCacheKeys = (await caches.keys()).filter((key) => key != CACHE_NAME)
+  oldCacheKeys.forEach((key) => caches.delete(key))
+})
+
+
+self.addEventListener("fetch", (event) => {
+  const normalizedUrl = normalizeUrl(event.request.url)
+  let response = fetch(event.request)
+  if (FILES_TO_CACHE.includes(normalizedUrl)) {
+    response = response
+      .then(async (response) => {
+        const cache = await caches.open(CACHE_NAME)
+        await cache.put(normalizedUrl, response.clone())
+        return response
+      })
+      .catch(() => caches.match(normalizedUrl))
+      .catch(e => null)
+  }
+  event.respondWith(
+    fetch(event.request)
+      .then(async (response) => {
+        if (FILES_TO_CACHE.includes(new URL(normalizedUrl).pathname)) {
+          const cache = await caches.open(CACHE_NAME)
+          await cache.put(normalizedUrl, response.clone())
+        }
+        return response
+      })
+      .catch(() => caches.match(normalizedUrl))
+      .catch(e => null)
+  )
+})