diff --git a/js/src/App.vue b/js/src/App.vue
index 218a48cdc..f0ef0bfe6 100644
--- a/js/src/App.vue
+++ b/js/src/App.vue
@@ -117,11 +117,38 @@ export default class App extends Vue {
     window.addEventListener("offline", () => {
       this.online = false;
       this.showOfflineNetworkWarning();
-      console.log("offline");
+      console.debug("offline");
     });
     window.addEventListener("online", () => {
       this.online = true;
-      console.log("online");
+      console.debug("online");
+    });
+    document.addEventListener("refreshApp", (event: Event) => {
+      this.$buefy.snackbar.open({
+        queue: false,
+        indefinite: true,
+        type: "is-primary",
+        actionText: this.$t("Update app") as string,
+        cancelText: this.$t("Ignore") as string,
+        message: this.$t("A new version is available.") as string,
+        onAction: async () => {
+          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+          // @ts-ignore
+          const detail = event.detail;
+          const registration = detail as ServiceWorkerRegistration;
+          try {
+            await this.refreshApp(registration);
+            window.location.reload();
+          } catch (err) {
+            console.error(err);
+            this.$notifier.error(
+              this.$t(
+                "An error has occured while refreshing the page."
+              ) as string
+            );
+          }
+        },
+      });
     });
 
     this.interval = setInterval(async () => {
@@ -138,6 +165,30 @@ export default class App extends Vue {
     }, 60000);
   }
 
+  private async refreshApp(
+    registration: ServiceWorkerRegistration
+  ): Promise<any> {
+    const worker = registration.waiting;
+    if (!worker) {
+      return Promise.resolve();
+    }
+    console.debug("Doing worker.skipWaiting().");
+    return new Promise((resolve, reject) => {
+      const channel = new MessageChannel();
+
+      channel.port1.onmessage = (event) => {
+        console.debug("Done worker.skipWaiting().");
+        if (event.data.error) {
+          reject(event.data);
+        } else {
+          resolve(event.data);
+        }
+      };
+      console.debug("calling skip waiting");
+      worker?.postMessage({ type: "skip-waiting" }, [channel.port2]);
+    });
+  }
+
   showOfflineNetworkWarning(): void {
     this.$notifier.error(this.$t("You are offline") as string);
   }
diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json
index 3cf0514d1..787d95548 100644
--- a/js/src/i18n/en_US.json
+++ b/js/src/i18n/en_US.json
@@ -1047,5 +1047,11 @@
   "Share this group": "Share this group",
   "This group is accessible only through it's link. Be careful where you post this link.": "This group is accessible only through it's link. Be careful where you post this link.",
   "{count} members": "No members|One member|{count} members",
-  "Share": "Share"
+  "Share": "Share",
+  "Update app": "Update app",
+  "Ignore": "Ignore",
+  "A new version is available.": "A new version is available.",
+  "An error has occured while refreshing the page.": "An error has occured while refreshing the page.",
+  "Join group {group}": "Join group {group}",
+  "Public preview": "Public preview"
 }
diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json
index 2215a30c7..f5b8257f4 100644
--- a/js/src/i18n/fr_FR.json
+++ b/js/src/i18n/fr_FR.json
@@ -1138,5 +1138,11 @@
   "Share this group": "Partager ce groupe",
   "This group is accessible only through it's link. Be careful where you post this link.": "Ce groupe est accessible uniquement à travers son lien. Faites attention où vous le diffusez.",
   "{count} members": "Aucun membre|Un⋅e membre|{count} membres",
-  "Share": "Partager"
+  "Share": "Partager",
+  "Update app": "Mettre à jour",
+  "Ignore": "Ignorer",
+  "A new version is available.": "Une nouvelle version est disponible.",
+  "An error has occured while refreshing the page.": "Une erreur est survenue lors du rafraîchissement de la page.",
+  "Join group {group}": "Rejoindre le groupe {group}",
+  "Public preview": "Aperçu public"
 }
diff --git a/js/src/registerServiceWorker.ts b/js/src/registerServiceWorker.ts
index 141c1daa4..ee0de1d9b 100644
--- a/js/src/registerServiceWorker.ts
+++ b/js/src/registerServiceWorker.ts
@@ -5,25 +5,27 @@ import { register } from "register-service-worker";
 if ("serviceWorker" in navigator && isProduction()) {
   register(`${process.env.BASE_URL}service-worker.js`, {
     ready() {
-      console.log(
+      console.debug(
         "App is being served from cache by a service worker.\n" +
           "For more details, visit https://goo.gl/AFskqB"
       );
     },
     registered() {
-      console.log("Service worker has been registered.");
+      console.debug("Service worker has been registered.");
     },
     cached() {
-      console.log("Content has been cached for offline use.");
+      console.debug("Content has been cached for offline use.");
     },
     updatefound() {
-      console.log("New content is downloading.");
+      console.debug("New content is downloading.");
     },
-    updated() {
-      console.log("New content is available; please refresh.");
+    updated(registration: ServiceWorkerRegistration) {
+      const event = new CustomEvent("refreshApp", { detail: registration });
+      document.dispatchEvent(event);
+      console.debug("New content is available; please refresh.");
     },
     offline() {
-      console.log(
+      console.debug(
         "No internet connection found. App is running in offline mode."
       );
     },
@@ -34,6 +36,5 @@ if ("serviceWorker" in navigator && isProduction()) {
 }
 
 function isProduction(): boolean {
-  return true;
-  // return process.env.NODE_ENV === "production";
+  return process.env.NODE_ENV === "production";
 }
diff --git a/js/src/service-worker.ts b/js/src/service-worker.ts
index 7ff855658..e87c5cce7 100644
--- a/js/src/service-worker.ts
+++ b/js/src/service-worker.ts
@@ -124,3 +124,17 @@ self.addEventListener("notificationclick", function (event: NotificationEvent) {
     })()
   );
 });
+
+self.addEventListener("message", (event: ExtendableMessageEvent) => {
+  const replyPort = event.ports[0];
+  const message = event.data;
+  if (replyPort && message && message.type === "skip-waiting") {
+    console.log("doing skip waiting");
+    event.waitUntil(
+      self.skipWaiting().then(
+        () => replyPort.postMessage({ error: null }),
+        (error) => replyPort.postMessage({ error })
+      )
+    );
+  }
+});