diff --git a/Makefile b/Makefile
index 0aa47756d72dcfcaff88efb673b5fa5044f8e05a..4c64a38d01f0273059e29f6dac0448967b005b86 100644
--- a/Makefile
+++ b/Makefile
@@ -43,7 +43,7 @@ LIBS_DIR = $(abspath libs)
 TEST_CORE_DIR = $(abspath test/core/)
 TEST_EXT_DIR = $(abspath test/ext)
 TEST_SSS_DIR =$(abspath test/server_side_scripting)
-LIBS = fonts css/fonts css/bootstrap.css css/bootstrap-icons.css js/state-machine.js js/jquery.js js/showdown.js js/dropzone.js css/dropzone.css js/loglevel.js js/leaflet.js css/leaflet.css css/images js/leaflet-latlng-graticule.js js/proj4.js js/proj4leaflet.js js/leaflet-coordinates.js css/leaflet-coordinates.css js/leaflet-graticule.js js/bootstrap-select.js css/bootstrap-select.css js/bootstrap-autocomplete.min.js js/plotly.js js/pako.js js/utif.js js/bootstrap.js
+LIBS = fonts css/fonts css/bootstrap.css css/bootstrap-icons.css js/state-machine.js js/jquery.js js/showdown.js js/dropzone.js css/dropzone.css js/loglevel.js js/leaflet.js css/leaflet.css css/images js/leaflet-latlng-graticule.js js/proj4.js js/proj4leaflet.js js/leaflet-coordinates.js css/leaflet-coordinates.css js/leaflet-graticule.js js/bootstrap-select.js css/bootstrap-select.css js/bootstrap-autocomplete.min.js js/plotly.js js/pako.js js/utif.js js/bootstrap.js js/qrcode.js
 
 
 TEST_LIBS = $(LIBS) js/qunit.js css/qunit.css $(subst $(TEST_CORE_DIR)/,,$(shell find $(TEST_CORE_DIR)/))
@@ -299,6 +299,9 @@ $(LIBS_DIR)/js/pako.js: unzip $(LIBS_DIR)/js
 $(LIBS_DIR)/js/utif.js: unzip $(LIBS_DIR)/js
 	ln -s $(LIBS_DIR)/UTIF-8205c1f/UTIF.js $@
 
+$(LIBS_DIR)/js/qrcode.js: unzip $(LIBS_DIR)/js
+	ln -s $(LIBS_DIR)/qrcode-1.4.4/qrcode.min.js $@
+
 
 $(addprefix $(LIBS_DIR)/, js css):
 	mkdir $@ || true
diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties
index 386eb8bd22bd3f32b94d9b9ea4714b80e355ea8e..e0432268041a49be4799193b36abf68cb2518abe 100644
--- a/build.properties.d/00_default.properties
+++ b/build.properties.d/00_default.properties
@@ -152,4 +152,6 @@ MODULE_DEPENDENCIES=(
     ext_trigger_crawler_form.js
     ext_bookmarks.js
     ext_cosmetics.js
+    qrcode.js
+    ext_qrcode.js
 )
diff --git a/libs/qrcode-1.4.4.zip b/libs/qrcode-1.4.4.zip
new file mode 100644
index 0000000000000000000000000000000000000000..ed1e0f854985cfcec8cd4f419fe27c195f39c22c
Binary files /dev/null and b/libs/qrcode-1.4.4.zip differ
diff --git a/src/core/js/ext_map.js b/src/core/js/ext_map.js
index 719d4f77943fdfca562961838dc02d95d5fd5cb1..c20cdefc2e9de73a21db81e3a7d5ebacebe73416 100644
--- a/src/core/js/ext_map.js
+++ b/src/core/js/ext_map.js
@@ -1470,7 +1470,7 @@ var caosdb_map = new function () {
          */
         this.make_entity_name_label = function (entity) {
             const name = getEntityName(entity);
-            const id = getEntityId(entity);
+            const id = getEntityID(entity);
 
             const entity_on_page = $(`#${id}`).length > 0;
             const href = entity_on_page ? `#${id}` : connection.getBasePath() + `Entity/${id}`
diff --git a/src/core/js/ext_qrcode.js b/src/core/js/ext_qrcode.js
new file mode 100644
index 0000000000000000000000000000000000000000..a3aefe3ae892ffe02b99a17664003d6b8965baa9
--- /dev/null
+++ b/src/core/js/ext_qrcode.js
@@ -0,0 +1,188 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+"use strict";
+
+/**
+ * Adds QR-Code generation to entities.
+ *
+ * @author Timm Fitschen
+ */
+var ext_qrcode = function ($, connection, getEntityVersion, getEntityID, QRCode, logger) {
+
+  const _buttons_list_class = "caosdb-v-entity-header-buttons-list";
+  const _qrcode_button_class = "caosdb-f-entity-qrcode-button";
+  const _qrcode_canvas_container = "caosdb-f-entity-qrcode";
+  const _qrcode_link_container = "caosdb-f-entity-qrcode-link";
+  const _qrcode_icon = `<i class="bi bi-upc"></i>`;
+
+  /**
+   * Create a new QR Code and a caption with a link, either linking to the
+   * entity head or to the exact version of the entity, based on the selected
+   * radio buttons and insert it into the modal.
+   *
+   * @param {HTMLElement} modal
+   * @param {string} entity_id
+   * @param {string} entity_version
+   */
+  var update_qrcode = function(modal, entity_id, entity_version) {
+    modal = $(modal);
+    const uri = modal.find("input[name=entity-qrcode-versioned]:checked").val();
+    var display_version = "";
+    if(uri.indexOf("@") > -1) {
+      display_version = `@${entity_version.substring(0,8)}`;
+    }
+    const description = `Entity <a href="${uri}">${entity_id}${display_version}</a>`;
+    modal.find(`.${_qrcode_canvas_container}`).empty();
+    modal.find(`.${_qrcode_link_container}`).empty().append(description);
+    QRCode.toCanvas(uri, {"scale": 6}).then((canvas) => {
+      modal.find(`.${_qrcode_canvas_container}`).empty().append(canvas);
+    }).catch(logger.error);
+  }
+
+  /**
+   * Create modal which shows the QR Code and a form where the user can choose
+   * whether the QR Code links to the entity head or the exact version of the
+   * entity.
+   *
+   * @param {string} modal_id - id of the resulting HTMLElement
+   * @param {string} entity_id
+   * @param {string} entity_version
+   * @return {HTMLElement} the resulting modal.
+   */
+  var create_qrcode_modal = function(modal_id, entity_id, entity_version) {
+    const uri = `${connection.getEntityUri([entity_id])}`;
+    const short_version = entity_version.substring(0,8);
+    const modal = $(`<div class="modal fade" id="${modal_id}" tabindex="-1" aria-labelledby="${modal_id}-label" aria-hidden="true">
+      <div class="modal-dialog">
+        <div class="modal-content">
+          <div class="modal-header">
+            <h4 class="modal-title" id="${modal_id}-label">QR Code</h5>
+            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+          </div>
+          <div class="modal-body text-center">
+            <div class="${_qrcode_canvas_container}"></div>
+            <div class="${_qrcode_link_container}"></div>
+          </div>
+          <div class="modal-footer justify-content-start">
+            <form>
+            <div class="form-check">
+              <label class="form-check-label">
+                <input value="${uri}" class="form-check-input" type="radio" name="entity-qrcode-versioned" checked>
+                Link to this entity.
+              </label>
+            </div>
+            <div class="form-check">
+              <label class="form-check-label" for="flexRadioDefault1">
+                <input value="${uri}@${entity_version}" class="form-check-input" type="radio" name="entity-qrcode-versioned">
+                Link to this exact version of this entity.
+              </label>
+            </div>
+            </form>
+          </div>
+        </div>
+      </div>
+    </div>`);
+    modal.find("form").change(() => {update_qrcode(modal, entity_id, entity_version);});
+    return modal[0];
+  }
+
+  /**
+   * Click handler of the QR Code button. The click event opens a modal showing
+   * the QR Code and a form where the user can choose whether the QR Code links
+   * to the entity head or the exact version of the entity.
+   *
+   * @param {string} entity_id
+   * @param {string} entity_version
+   */
+  var qrcode_button_click_handler = function(entity_id, entity_version) {
+    const modal_id = `qrcode-modal-${entity_id}-${entity_version}`;
+    var modal_element = document.getElementById(modal_id);
+    if(modal_element) {
+      // toggle modal
+      const modal = bootstrap.Modal.getInstance(modal_element);
+      modal.toggle();
+    } else {
+      modal_element = create_qrcode_modal(modal_id, entity_id, entity_version);
+      update_qrcode(modal_element, entity_id, entity_version);
+      $("body").append(modal_element);
+      const options = {};
+      const modal = new bootstrap.Modal(modal_element, options);
+      modal.show();
+    }
+  }
+
+  /**
+   * Create a button which opens the QR Code modal on click.
+   *
+   * @param {string} entity_id
+   * @param {string} entity_version
+   * @return {HTMLElement} the newly created button.
+   */
+  var create_qrcode_button = function (entity_id, entity_version) {
+    const button = $(`<button title="Create QR Code" type="button" class="${_qrcode_button_class} caosdb-v-entity-qrcode-button btn">${_qrcode_icon}</button>`);
+    button.click(() => {qrcode_button_click_handler(entity_id, entity_version);});
+    return button[0];
+  }
+
+  /**
+   * Add a qrcode button to a given entity.
+   * @param {HTMLElement} entity
+   */
+  var add_qrcode_to_entity = function (entity) {
+    const entity_id = getEntityID(entity);
+    const entity_version = getEntityVersion(entity);
+
+    $(entity).find(`.${_buttons_list_class}`).append(create_qrcode_button(entity));
+  }
+
+  var remove_qrcode_button = function (entity) {
+    $(entity).find(`.${_buttons_list_class} .${_qrcode_button_class}`).remove();
+  }
+
+  /**
+   * Initialize this module and append a QR Code button to all entities panels on the page.
+   *
+   * Removes all respective buttons if present before adding a new one.
+   */
+  var init = function () {
+    for(let entity of $(".caosdb-entity-panel")) {
+      remove_qrcode_button(entity);
+      add_qrcode_to_entity(entity);
+    }
+  };
+
+  return {
+    update_qrcode: update_qrcode,
+    add_qrcode_to_entity: add_qrcode_to_entity,
+    remove_qrcode_button: remove_qrcode_button,
+    create_qrcode_button: create_qrcode_button,
+    create_qrcode_modal: create_qrcode_modal,
+    qrcode_button_click_handler: qrcode_button_click_handler,
+    init: init
+  };
+
+}($, connection, getEntityVersion, getEntityID, QRCode, console);
+
+$(document).ready(function () {
+  if("${BUILD_MODULE_EXT_QRCODE}"=="ENABLED") {
+    caosdb_modules.register(ext_qrcode);
+  }
+});
diff --git a/src/core/js/fileupload.js b/src/core/js/fileupload.js
index 31d86589286f1761f481cc9d9bb6a557a63cbce1..8a988e86a6a9b5bbb39f39a37d63b32b144c748d 100644
--- a/src/core/js/fileupload.js
+++ b/src/core/js/fileupload.js
@@ -156,7 +156,7 @@ var fileupload = new function() {
         // get property-value input element (in case of FILE property)
         var input = $(property).find(".caosdb-f-property-value input");
         var set_value = function(entity) {
-            input.val(getEntityId(entity));
+            input.val(getEntityID(entity));
         }
 
         if (input.length == 0) {
@@ -207,7 +207,7 @@ var fileupload = new function() {
                 getEntityName(entity) + `</code> has been uploaded.</div>`);
 
             input.after(`<a class="btn btn-secondary btn-sm"
-  href="` + connection.getEntityUri([getEntityId(entity)]) + `" target= "_blank">` +
+  href="` + connection.getEntityUri([getEntityID(entity)]) + `" target= "_blank">` +
                 getEntityName(entity) + `</a>`);
 
         };
diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js
index 74dc62be15551f987707253ca201777f7c3929ac..dbb4e269a247cbbc40f1ea58623ec7b515dc2d57 100644
--- a/src/core/js/webcaosdb.js
+++ b/src/core/js/webcaosdb.js
@@ -812,7 +812,7 @@ this.transaction = new function () {
                     $(updatePanel).insertBefore(entity);
                     // create and add waiting notification
                     updatePanel.appendChild(transaction.update.createWaitRetrieveNotification());
-                    let entityId = getEntityId(entity);
+                    let entityId = getEntityID(entity);
                     transaction.update.retrieveOldEntityXmlString(entityId).then(xmlstr => {
                         app.openForm(xmlstr);
                     }, err => {
@@ -1524,19 +1524,6 @@ function removeAllWaitingNotifications(elem) {
     return elem;
 }
 
-/**
- * Extract the ID of an entity by parsing the textContent of the first occuring element with
- * class `caosdb-id`.
- *
- * @param {HTMLElement} entity
- * @returns {Number} ID of entity.
- */
-function getEntityId(entity) {
-    let id = Number.parseInt(entity.getElementsByClassName("caosdb-id")[0].textContent);
-    if (isNaN(id)) throw new Error("id was NaN");
-    return id;
-}
-
 // TODO remove and use connection.post
 /**
  * Post an xml document to basepath/Entity
diff --git a/test/core/js/modules/ext_qrcode.js.js b/test/core/js/modules/ext_qrcode.js.js
new file mode 100644
index 0000000000000000000000000000000000000000..35707cc83c9c46a4f235ed2e3a8dfd429e6db33e
--- /dev/null
+++ b/test/core/js/modules/ext_qrcode.js.js
@@ -0,0 +1,75 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+'use strict';
+
+QUnit.module("ext_qrcode.js", {
+    before: function (assert) {
+        // setup before module
+    },
+    beforeEach: function (assert) {
+        // setup before each test
+        $(document.body).append('<div data-entity-id="eid123" data-version-id="vid234" id="ext-qrcode-test-entity" class="caosdb-entity-panel"><div class="caosdb-v-entity-header-buttons-list"></div></div>');
+    },
+    afterEach: function (assert) {
+        // teardown after each test
+        const modal = bootstrap.Modal.getInstance($(".modal")[0]);
+        if(modal) modal.dispose();
+        $("#ext-qrcode-test-entity").remove();
+        $(".modal").remove();
+    },
+    after: function (assert) {
+        // teardown after module
+    }
+});
+
+QUnit.test("init", function (assert) {
+    assert.ok(ext_qrcode.init, "init available");
+    assert.equal($(".caosdb-f-entity-qrcode-button").length, 0, "no button before.");
+    ext_qrcode.init();
+    assert.equal($(".caosdb-f-entity-qrcode-button").length, 1, "button has been added.");
+});
+
+QUnit.test("create_qrcode_button", function(assert) {
+  assert.equal(ext_qrcode.create_qrcode_button("entityid", "versionid").tagName, "BUTTON", "create_qrcode_button creates a button");
+});
+
+
+QUnit.test("qrcode_button_click_handler", function(assert) {
+  var done = assert.async();
+  assert.equal($("#qrcode-modal-entityid-versionid").length, 0, "no modal before first click");
+  ext_qrcode.qrcode_button_click_handler("entityid", "versionid")
+  $("#qrcode-modal-entityid-versionid").on("shown.bs.modal", done);
+  assert.equal($("#qrcode-modal-entityid-versionid").length, 1, "first click added the modal");
+});
+
+QUnit.test("update_qrcode", function(assert) {
+  // create modal
+  const entity_id = "eid456";
+  const entity_version = "vid3564";
+  const modal_id = `qrcode-modal-${entity_id}-${entity_version}`;
+  const modal_element = ext_qrcode.create_qrcode_modal(modal_id, entity_id, entity_version);
+  $(document.body).append(modal_element);
+
+  assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode *").length, 0);
+  assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode-link *").length, 0);
+
+  // TODO call update_qrcode
+});
diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js
index 52bf4ada52d2ce59b59d8615c89ca5796343622b..ad26e8455fb7ca16a9e6e9687606b6aba1d0e7e6 100644
--- a/test/core/js/modules/webcaosdb.js.js
+++ b/test/core/js/modules/webcaosdb.js.js
@@ -91,28 +91,6 @@ QUnit.test("injectTemplate", async function (assert) {
     assert.equal(xml2str(result_xml), "<newroot xmlns=\"http://www.w3.org/1999/xhtml\">content</newroot>");
 });
 
-QUnit.test("getEntityId", function (assert) {
-    assert.ok(getEntityId, "function available");
-    let okElem = $('<div><div class="caosdb-id">1234</div></div>')[0];
-    let notOkElem = $('<div><div class="caosdb-id">asdf</div></div>')[0];
-    let emptyElem = $('<div></div>')[0];
-
-    assert.throws(() => {
-        getEntityId();
-    }, "no parameter throws");
-    assert.throws(() => {
-        getEntityId(null);
-    }, "null parameter throws");
-    assert.throws(() => {
-        getEntityId(notOkElem);
-    }, "on-integer ID throws");
-    assert.throws(() => {
-        getEntityId(empty);
-    }, "empty elem throws");
-
-    assert.equal("1234", getEntityId(okElem), "ID found");
-});
-
 QUnit.test("asyncXslt", function (assert) {
     let xml_str = '<root/>';
     let xsl_str = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:output method="html" /><xsl:template match="root"><newroot/></xsl:template></xsl:stylesheet>';
@@ -920,7 +898,7 @@ QUnit.test("createCarouselNav", function (assert) {
         assert.equal($(carousel).find("." + preview.classNamePreviewCarouselNav).length, 1, "carousel has nav");
         assert.equal($(carousel).find(".carousel-inner").length, 1, "carousel has inner");
         for (let i = 0; i < correct_order_id.length; i++) {
-            assert.equal(getEntityId($(carousel).find('.carousel-item')[i]), correct_order_id[i], "entities ids are in order")
+            assert.equal(getEntityID($(carousel).find('.carousel-item')[i]), correct_order_id[i], "entities ids are in order")
         }
 
         assert.ok(carousel.id, "has id");