From 2a4330bc5a108dc66e04a7a434aef176b48fd351 Mon Sep 17 00:00:00 2001
From: Philipp Berger <philipp.berger@nexenio.com>
Date: Fri, 10 Sep 2021 19:11:17 +0200
Subject: [PATCH] chore: release v2.0.0

---
 CHANGELOG.md                                  |  26 +-
 e2e/.gitignore                                |   3 +
 e2e/certs/.keep                               |   0
 e2e/cypress/plugins/index.js                  |  13 +-
 e2e/package.json                              |   7 +-
 .../helper/signHealthDepartment.js            | 115 +++++++
 .../helper/ui/tracking.helper.js              |  33 +-
 .../locations/downloadPDF/downloadPDF.spec.js |  34 +-
 e2e/specs/locations/helpers/functions.js      |   6 +-
 .../locations/location/location.helper.js     |   1 +
 .../webapp/checkin/checkinByLinkFlow.spec.js  |   8 +-
 .../workflow/senarios/contactForm.spec.js     |   3 +-
 e2e/support/index.js                          |   1 +
 e2e/yarn.lock                                 | 105 +++++-
 package.json                                  |  91 +++---
 scripts/generateCertificates.sh               |   9 +-
 .../config/custom-environment-variables.js    |  17 +
 services/backend/config/default.js            |  21 ++
 services/backend/package.json                 |   4 +-
 services/backend/src/@types/express.d.ts      |  19 +-
 services/backend/src/app.js                   |   8 +-
 services/backend/src/constants/auditLog.ts    |  42 +++
 .../migrations/2021.08.06-0710-auditLogs.js   |  74 +++++
 ...dApprovedAtAttributeToLocationTransfers.js |  25 ++
 ...021.08.12-1307-createNotificationChunks.js |  25 ++
 ...-1321-addContactInfosToHealthDepartment.js |  17 +
 .../2021.08.16-1926-createRiskLevels.js       |  37 +++
 ...cationsEnabledColumnToHealthDepartments.js |  10 +
 ...21.09.02-1530-employeeNonRequiredFields.js |  69 ++++
 ...2021.09.03-1900-addNotificationMessages.js |  70 ++++
 .../src/database/models/healthDepartment.js   |  21 ++
 .../models/healthDepartmentAuditLog.js        |  46 +++
 .../models/healthDepartmentEmployee.js        |  10 +-
 .../src/database/models/locationTransfer.js   |   5 +
 .../database/models/locationTransferTrace.js  |   4 +
 .../src/database/models/notificationChunk.js  |  13 +
 .../database/models/notificationMessage.js    |  35 ++
 .../backend/src/database/models/riskLevel.js  |  24 ++
 .../createDefaultNotificationMessages.js      | 261 +++++++++++++++
 services/backend/src/middlewares/rateLimit.js |  15 +
 .../backend/src/middlewares/requestMetrics.js |  53 ++-
 .../backend/src/middlewares/requireUser.js    |  11 +-
 .../passport/localHealthDepartmentEmployee.js |  16 +-
 .../backend/src/passport/localOperator.js     |   3 +-
 .../src/routes/internal/en2end.schema.js      |  11 +
 .../backend/src/routes/internal/end2end.js    |  28 +-
 services/backend/src/routes/internal/jobs.js  |  70 +++-
 services/backend/src/routes/v3/auth.js        |  30 +-
 services/backend/src/routes/v3/badges.js      |  26 +-
 .../routes/v3/healthDepartmentEmployees.js    | 178 +++++++---
 .../v3/healthDepartmentEmployees.openapi.yaml |  43 ++-
 .../v3/healthDepartmentEmployees.schemas.js   |   5 +
 .../v3/healthDepartmentEmployees/locations.js |   1 +
 .../v3/healthDepartmentEmployees/password.js  |  34 ++
 .../src/routes/v3/healthDepartments.js        |  25 ++
 .../routes/v3/healthDepartments.openapi.yaml  |  36 +-
 .../routes/v3/healthDepartments.schemas.js    |   6 +
 services/backend/src/routes/v3/keys/badges.js | 165 ++++++++--
 .../src/routes/v3/keys/badges.schemas.js      |  45 +--
 services/backend/src/routes/v3/keys/daily.js  | 169 ++++++++--
 .../src/routes/v3/keys/daily.schemas.js       |  46 +--
 .../src/routes/v3/locationTransfers.js        | 144 +++++++-
 .../routes/v3/locationTransfers.openapi.yaml  |   6 +
 .../backend/src/routes/v3/notifications.js    |  21 +-
 .../backend/src/routes/v3/tracingProcesses.js |   2 +-
 .../src/routes/v3/tracingProcesses.schemas.js |  14 +-
 services/backend/src/routes/v4.js             |   4 +
 services/backend/src/routes/v4/auth.js        |  25 +-
 .../src/routes/v4/healthDepartments.js        |  43 ++-
 .../routes/v4/healthDepartments.schemas.js    |   5 +
 .../src/routes/v4/notifications.schemas.ts    |   5 +
 .../backend/src/routes/v4/notifications.ts    |  59 ++++
 .../src/routes/v4/riskLevels.schemas.ts       |  14 +
 services/backend/src/routes/v4/riskLevels.ts  | 159 +++++++++
 services/backend/src/utils/bloomFilter.js     |   3 +-
 services/backend/src/utils/generators.js      |   4 +-
 services/backend/src/utils/hdAuditLog.ts      | 307 ++++++++++++++++++
 services/backend/src/utils/ipChecks.js        |   6 +-
 services/backend/src/utils/logger.js          |   1 +
 .../notifications/notificationsConfig.js      |  50 +++
 .../notificationsV3.js}                       |   7 +-
 .../notifications/notificationsV4.test.js     |  26 ++
 .../utils/notifications/notificationsV4.ts    | 230 +++++++++++++
 services/backend/src/utils/redis.js           |   1 +
 services/backend/src/utils/redisCache.ts      |   3 +-
 services/backend/src/utils/signedKeys.js      |  18 +-
 services/backend/yarn.lock                    |  23 +-
 services/contact-form/package.json            |   2 +-
 services/contact-form/yarn.lock               |   6 +-
 services/elb/shared/proxy.nginx.conf          |   3 +
 services/health-department/.iyarc             |   6 +
 services/health-department/package.json       |   2 +-
 services/health-department/src/ant.css        |   8 +
 .../health-department/src/assets/Bell.svg     |  14 +
 .../health-department/src/assets/BellIcon.svg |  13 +
 .../src/components/App/App.styled.js          |   1 -
 .../HeaderRow/HeaderRow.react.js              |   6 +-
 .../History/Header/Header.react.js            |   4 +-
 .../ContactConfirmationButton.react.js        | 109 ++++---
 .../ContactConfirmationButton.styled.js       |   4 +
 .../NotificationTrigger.react.js              |  87 +++++
 .../NotificationTrigger.styled.js             |  16 +
 .../NotificationTrigger/index.js              |   1 +
 .../HistoryTable/HistoryTable.react.js        |  35 +-
 .../ProcessDetails/InfoRow/InfoRow.react.js   |   6 +-
 .../Note/EmptyNote/EmptyNote.react.js         |  17 +
 .../Note/EmptyNote/EmptyNote.styled.js        |  30 ++
 .../ProcessDetails/Note/EmptyNote/index.js    |   1 +
 .../App/ProcessDetails/Note/Note.react.js     |  23 ++
 .../NoteButtonWrapper.react.js                |  33 ++
 .../NoteButtonWrapper.styled.js               |  16 +
 .../NoteProcess/NoteButtonWrapper/index.js    |   1 +
 .../Note/NoteProcess/NoteProcess.helper.js    |  70 ++++
 .../Note/NoteProcess/NoteProcess.react.js     | 122 +++++++
 .../Note/NoteProcess/NoteProcess.styled.js    |  24 ++
 .../ProcessDetails/Note/NoteProcess/index.js  |   1 +
 .../App/ProcessDetails/Note/index.js          |   1 +
 .../ProcessDetails/ProcessDetails.react.js    |   7 +-
 .../AuditLogsDownloads.react.js               |  76 +++++
 .../AuditLogsDownloads.styled.js              |  25 ++
 .../DownloadFiles/EmailToUuidListDownload.js  |  42 +++
 .../AuditLogsDownloads/DownloadFiles/index.js |   5 +
 .../App/Profile/AuditLogsDownloads/index.js   |   1 +
 .../ChangePasswordView.react.js               |  14 +-
 .../ContactInformation.react.js               | 164 ++++++++++
 .../ContactInformation.styled.js              |  13 +
 .../App/Profile/ContactInformation/index.js   |   1 +
 .../DownloadSigningTool.react.js              |  58 ++--
 .../DownloadSigningTool.styled.js             |   1 -
 .../components/App/Profile/Profile.react.js   |  29 +-
 .../components/App/Profile/Profile.styled.js  |  34 +-
 .../TrackingList/Table/Entry/Entry.react.js   |  17 +-
 .../ManualSearchNameDisplay.react.js          |   4 +-
 .../SelectAssignee/SelectAssignee.react.js    |   5 +-
 .../TrackingList/Table/Header/Header.react.js |   3 +-
 .../TrackingList/Table/Table.styled.js        |   2 +-
 .../EmployeeList/EmployeeList.styled.js       |   2 +-
 .../VerificationTag/VerificationTag.react.js  |  22 +-
 .../ContactPersons/ContactPersons.react.js    |   8 +-
 .../ContactPersons/Export/Export.styled.js    |   2 +-
 .../GuestList/GuestList.react.js              |   6 +-
 .../GuestListTable/GuestListTable.react.js    |  44 +++
 .../GuestListTable/Notified/Notified.react.js |  21 ++
 .../GuestListTable/Notified/index.js          |   1 +
 .../ContactPersons/Notify/Notify.helper.js    |  11 +
 .../ContactPersons/Notify/Notify.react.js     |  61 ++++
 .../ContactPersons/Notify/Notify.styled.js    |  19 ++
 .../ContactPersons/Notify/index.js            |   1 +
 .../DataRequestModal.react.js                 |  26 +-
 .../NotificationModal.helper.js               |  37 +++
 .../NotificationModal.react.js                | 273 ++++++++++++++++
 .../NotificationModal.styled.js               |  51 +++
 .../App/modals/NotificationModal/index.js     |   1 +
 .../src/components/general/Divider.styled.js  |   8 +
 .../src/components/general/index.js           |   6 +-
 .../components/hooks/useLocationTransfers.js  |  60 ++++
 .../hooks/useLocationWithTransfers.js         |  52 ---
 .../src/components/hooks/useValidators.js     |  46 +++
 .../src/constants/riskLevels.js               |   2 +
 .../health-department/src/constants/sormas.js |   1 +
 .../src/constants/valueLength.js              |   6 +-
 .../src/errors/InvalidNoteSignatureError.js   |   7 +
 .../health-department/src/messages/de.json    |  36 +-
 .../health-department/src/messages/en.json    |  36 +-
 services/health-department/src/network/api.js |  43 ++-
 .../src/utils/cryptoKeyOperations.js          |  49 ++-
 .../src/utils/cryptoOperations.js             |   1 +
 .../health-department/src/utils/decryption.js |  11 +-
 .../src/utils/validatorRules.helper.js        |   1 -
 services/health-department/yarn.lock          |   6 +-
 services/locations/package.json               |   2 +-
 .../GenerateQRCodes/GenerateQRCodes.helper.js |   6 +-
 .../CompletedDataRequests.react.js            |  47 +--
 .../Authentication/Footer/Footer.react.js     |   2 +-
 .../ShareData/FinishStep/FinishStep.react.js  |  12 +-
 .../components/ShareData/ShareData.react.js   |   7 +-
 services/locations/src/messages/de.json       |  91 +++++-
 services/locations/src/messages/en.json       |  87 ++++-
 services/locations/src/utils/downloadPDF.js   |  13 +-
 services/locations/src/utils/qrCodeData.js    |   4 +-
 services/locations/src/utils/time.js          |   7 +
 services/locations/yarn.lock                  |   6 +-
 services/scanner/package.json                 |   2 +-
 services/scanner/yarn.lock                    |   6 +-
 services/webapp/package.json                  |   4 +-
 .../ContactInformation.react.js               |  58 ++--
 .../src/components/History/History.react.js   |  32 +-
 services/webapp/yarn.lock                     |   6 +-
 yarn.lock                                     |  35 +-
 189 files changed, 5342 insertions(+), 713 deletions(-)
 create mode 100644 e2e/certs/.keep
 create mode 100644 e2e/specs/health-department/helper/signHealthDepartment.js
 create mode 100644 services/backend/src/constants/auditLog.ts
 create mode 100644 services/backend/src/database/migrations/2021.08.06-0710-auditLogs.js
 create mode 100644 services/backend/src/database/migrations/2021.08.06-1411-addApprovedAtAttributeToLocationTransfers.js
 create mode 100644 services/backend/src/database/migrations/2021.08.12-1307-createNotificationChunks.js
 create mode 100644 services/backend/src/database/migrations/2021.08.13-1321-addContactInfosToHealthDepartment.js
 create mode 100644 services/backend/src/database/migrations/2021.08.16-1926-createRiskLevels.js
 create mode 100644 services/backend/src/database/migrations/2021.08.30-1100-addNotificationsEnabledColumnToHealthDepartments.js
 create mode 100644 services/backend/src/database/migrations/2021.09.02-1530-employeeNonRequiredFields.js
 create mode 100644 services/backend/src/database/migrations/2021.09.03-1900-addNotificationMessages.js
 create mode 100644 services/backend/src/database/models/healthDepartmentAuditLog.js
 create mode 100644 services/backend/src/database/models/notificationChunk.js
 create mode 100644 services/backend/src/database/models/notificationMessage.js
 create mode 100644 services/backend/src/database/models/riskLevel.js
 create mode 100644 services/backend/src/database/seeds/createDefaultNotificationMessages.js
 create mode 100644 services/backend/src/routes/internal/en2end.schema.js
 create mode 100644 services/backend/src/routes/v4/notifications.schemas.ts
 create mode 100644 services/backend/src/routes/v4/notifications.ts
 create mode 100644 services/backend/src/routes/v4/riskLevels.schemas.ts
 create mode 100644 services/backend/src/routes/v4/riskLevels.ts
 create mode 100644 services/backend/src/utils/hdAuditLog.ts
 create mode 100644 services/backend/src/utils/notifications/notificationsConfig.js
 rename services/backend/src/utils/{notifications.js => notifications/notificationsV3.js} (95%)
 create mode 100644 services/backend/src/utils/notifications/notificationsV4.test.js
 create mode 100644 services/backend/src/utils/notifications/notificationsV4.ts
 create mode 100644 services/health-department/src/assets/Bell.svg
 create mode 100644 services/health-department/src/assets/BellIcon.svg
 create mode 100644 services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/NotificationTrigger/NotificationTrigger.react.js
 create mode 100644 services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/NotificationTrigger/NotificationTrigger.styled.js
 create mode 100644 services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/NotificationTrigger/index.js
 create mode 100644 services/health-department/src/components/App/ProcessDetails/Note/EmptyNote/EmptyNote.react.js
 create mode 100644 services/health-department/src/components/App/ProcessDetails/Note/EmptyNote/EmptyNote.styled.js
 create mode 100644 services/health-department/src/components/App/ProcessDetails/Note/EmptyNote/index.js
 create mode 100644 services/health-department/src/components/App/ProcessDetails/Note/Note.react.js
 create mode 100644 services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteButtonWrapper/NoteButtonWrapper.react.js
 create mode 100644 services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteButtonWrapper/NoteButtonWrapper.styled.js
 create mode 100644 services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteButtonWrapper/index.js
 create mode 100644 services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteProcess.helper.js
 create mode 100644 services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteProcess.react.js
 create mode 100644 services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteProcess.styled.js
 create mode 100644 services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/index.js
 create mode 100644 services/health-department/src/components/App/ProcessDetails/Note/index.js
 create mode 100644 services/health-department/src/components/App/Profile/AuditLogsDownloads/AuditLogsDownloads.react.js
 create mode 100644 services/health-department/src/components/App/Profile/AuditLogsDownloads/AuditLogsDownloads.styled.js
 create mode 100644 services/health-department/src/components/App/Profile/AuditLogsDownloads/DownloadFiles/EmailToUuidListDownload.js
 create mode 100644 services/health-department/src/components/App/Profile/AuditLogsDownloads/DownloadFiles/index.js
 create mode 100644 services/health-department/src/components/App/Profile/AuditLogsDownloads/index.js
 create mode 100644 services/health-department/src/components/App/Profile/ContactInformation/ContactInformation.react.js
 create mode 100644 services/health-department/src/components/App/Profile/ContactInformation/ContactInformation.styled.js
 create mode 100644 services/health-department/src/components/App/Profile/ContactInformation/index.js
 create mode 100644 services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/GuestList/GuestListTable/Notified/Notified.react.js
 create mode 100644 services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/GuestList/GuestListTable/Notified/index.js
 create mode 100644 services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Notify/Notify.helper.js
 create mode 100644 services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Notify/Notify.react.js
 create mode 100644 services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Notify/Notify.styled.js
 create mode 100644 services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Notify/index.js
 create mode 100644 services/health-department/src/components/App/modals/NotificationModal/NotificationModal.helper.js
 create mode 100644 services/health-department/src/components/App/modals/NotificationModal/NotificationModal.react.js
 create mode 100644 services/health-department/src/components/App/modals/NotificationModal/NotificationModal.styled.js
 create mode 100644 services/health-department/src/components/App/modals/NotificationModal/index.js
 create mode 100644 services/health-department/src/components/general/Divider.styled.js
 create mode 100644 services/health-department/src/components/hooks/useLocationTransfers.js
 delete mode 100644 services/health-department/src/components/hooks/useLocationWithTransfers.js
 create mode 100644 services/health-department/src/components/hooks/useValidators.js
 create mode 100644 services/health-department/src/constants/riskLevels.js
 create mode 100644 services/health-department/src/errors/InvalidNoteSignatureError.js
 create mode 100644 services/locations/src/utils/time.js

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 82b0a64..31046c7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,30 @@
 # Changelog
 
+### 2.0.0 (2021-09-10)
+* **backend:** fix: strict JWT schema check for signing app
+* **backend:** fix: netmask of ipv6 addresses
+* **backend:** feat: add v4 notification endpoint
+* **backend:** feat: restrict location transfers endpoint
+* **backend:** feat: add health department account based rate limits
+* **backend:** feat: add rate limits for notifications and bloomfilter endpoints
+* **backend:** feat: add phone number and email to health departments
+* **backend:** feat: deny daily key rotation from unsigned health departments
+* **backend:** perf: disable default etag generation
+* **locations:** fix: redirection to login after successful share data request
+* **locations:** fix: typo in authentication footer
+* **locations:** fix: time display in completed share data requests
+* **locations:** fix: values in csv download for qr codes
+* **health-department:** fix: mismatch between list entries and counter
+* **health-department:** fix: view update after venue owner completes share data request
+* **health-department:** fix: misleading location name property in proccess details
+* **health-department:** feat: improved usability in the timepicker when searching for locations
+* **health-department:** feat: add note to tracing processes
+* **health-department:** feat: possibility to trigger notifications for a specific location
+* **health-department:** feat: possibility to trigger notifications for specific contacts
+* **health-department:** feat: possibility to add public contact informations
+* **health-department:** feat: possibility for admins to download the audit logfile
+* **webapp:** fix: user transfer creation before approval
+
 ### 1.9.2 (2021-09-01)
 * **locations:** fix: domain specific email validation
 * **scanner:** feat: deny check ins of v3 badges
@@ -8,7 +33,6 @@
 * **health-department:** fix: handling of unregistered badges in tracing processes
 
 ### 1.9.0 (2021-08-18)
-
 * **backend:** fix: openAPI JSON
 * **backend:** feat: send email to operator after approved location transfer
 * **backend:** feat: add note to tracing proccess
diff --git a/e2e/.gitignore b/e2e/.gitignore
index 2948967..3707aeb 100644
--- a/e2e/.gitignore
+++ b/e2e/.gitignore
@@ -20,3 +20,6 @@ tsconfig.json
 
 cypress/fixtures
 cypress/screenshots
+
+# certificates
+certs/*.pfx
\ No newline at end of file
diff --git a/e2e/certs/.keep b/e2e/certs/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/e2e/cypress/plugins/index.js b/e2e/cypress/plugins/index.js
index bfa34d3..9162bae 100644
--- a/e2e/cypress/plugins/index.js
+++ b/e2e/cypress/plugins/index.js
@@ -28,13 +28,14 @@ function generateCameraStream(path) {
 }
 
 function getConfigurationByFile(file) {
-  const pathToConfigFile = path.resolve('..', 'e2e/config', `${file}.json`)
+  const pathToConfigFile = path.resolve('..', 'e2e/config', `${file}.json`);
   console.log('Path to config:' + pathToConfigFile);
   console.log('Read config:' + pathToConfigFile);
-  return fse.readJson(pathToConfigFile)
+  return fse.readJson(pathToConfigFile);
 }
 
 module.exports = (on, config) => {
+  require('cypress-fail-fast/plugin')(on, config);
   on('task', {
     setCameraImage: async path => {
       await generateCameraStream(path);
@@ -52,7 +53,7 @@ module.exports = (on, config) => {
       }
       return false;
     },
-    dbQuery: query => postgres(query.query, query.connection)
+    dbQuery: query => postgres(query.query, query.connection),
   });
 
   on('before:browser:launch', async (browser = {}, launchOptions) => {
@@ -63,7 +64,7 @@ module.exports = (on, config) => {
       launchOptions.args.push(
         '--use-file-for-fake-video-capture=/tmp/luca/stream.mjpeg'
       );
-      launchOptions.preferences.default["download"] = {
+      launchOptions.preferences.default['download'] = {
         default_directory: path.join(__dirname, 'downloads'),
       };
     }
@@ -71,6 +72,6 @@ module.exports = (on, config) => {
   });
 
   //switching between envs
-  const file = config.env.configFile || 'local'
-  return getConfigurationByFile(file)
+  const file = config.env.configFile || 'local';
+  return getConfigurationByFile(file);
 };
diff --git a/e2e/package.json b/e2e/package.json
index ce3a6fb..6496b3b 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -1,6 +1,6 @@
 {
   "name": "e2e",
-  "version": "1.9.2",
+  "version": "2.0.0",
   "main": "index.js",
   "private": true,
   "engines": {
@@ -19,9 +19,12 @@
     "faker": "5.5.3",
     "fluent-ffmpeg": "2.1.2",
     "fs-extra": "10.0.0",
-    "moment": "2.29.1"
+    "jsonwebtoken": "8.5.1",
+    "moment": "2.29.1",
+    "node-forge": "0.10.0"
   },
   "devDependencies": {
+    "cypress-fail-fast": "3.1.0",
     "cypress-postgres": "1.1.1",
     "eslint-plugin-sonarjs": "0.5.0",
     "prettier": "2.2.1"
diff --git a/e2e/specs/health-department/helper/signHealthDepartment.js b/e2e/specs/health-department/helper/signHealthDepartment.js
new file mode 100644
index 0000000..369fd5a
--- /dev/null
+++ b/e2e/specs/health-department/helper/signHealthDepartment.js
@@ -0,0 +1,115 @@
+import forge, { pkcs12 } from 'node-forge';
+import jwt from 'jsonwebtoken';
+
+const JWT_ALGORITHM = 'RS512';
+const PASSWORD = 'testing';
+
+const decryptPkcs12 = p12file => {
+  const p12Asn1 = forge.asn1.fromDer(p12file);
+  const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, PASSWORD);
+
+  const certBags = p12.getBags({ bagType: forge.pki.oids.certBag })[
+    forge.pki.oids.certBag
+  ];
+
+  if (!certBags || certBags.length === 0) {
+    throw new Error('Certificate not found');
+  }
+
+  const privateKeyBags = p12.getBags({
+    bagType: forge.pki.oids.pkcs8ShroudedKeyBag,
+  })[forge.pki.oids.pkcs8ShroudedKeyBag];
+
+  if (!privateKeyBags || privateKeyBags.length === 0) {
+    throw new Error('Private Key not found');
+  }
+
+  const certBag = certBags[0];
+  const privateKeyBag = privateKeyBags[0];
+
+  if (!certBag.cert) {
+    throw new Error('Certificate not found');
+  }
+
+  if (!privateKeyBag.key) {
+    throw new Error('Private key not found');
+  }
+
+  const certDer = forge.asn1.toDer(forge.pki.certificateToAsn1(certBag.cert));
+  const md = forge.md.sha1.create();
+  md.update(certDer.data);
+  const fingerprint = md.digest().toHex();
+
+  const commonName = certBag.cert.subject.getField('CN')?.value;
+  const serialName = certBag.cert.subject.getField({ name: 'serialName' })
+    ?.value;
+
+  const cert = forge.pki.certificateToPem(certBag.cert);
+  const publicKey = forge.pki.publicKeyToPem(certBag.cert.publicKey);
+  const privateKey = forge.pki.privateKeyToPem(privateKeyBag.key);
+  return {
+    cert: cert.replace(/\r\n/g, '\n'),
+    publicKey: publicKey.replace(/\r\n/g, '\n'),
+    privateKey: privateKey.replace(/\r\n/g, '\n'),
+    fingerprint,
+    commonName,
+    serialName,
+  };
+};
+
+const createSignedPublicKeys = (healthDepartment, p12file) => {
+  const pkcs12 = decryptPkcs12(p12file);
+
+  const signedPublicHDSKP = jwt.sign(
+    {
+      sub: healthDepartment.uuid, // health department uuid
+      iss: pkcs12.fingerprint, // sha1 fingerprint of cert (hex)
+      name: healthDepartment.name, // name of the health department
+      key: healthDepartment.publicHDSKP, // public key (base64)
+      type: 'publicHDSKP', // key type (base64)
+    },
+    pkcs12.privateKey,
+    { algorithm: JWT_ALGORITHM }
+  );
+
+  const signedPublicHDEKP = jwt.sign(
+    {
+      sub: healthDepartment.uuid, // health department uuid
+      iss: pkcs12.fingerprint, // sha1 fingerprint of cert (hex)
+      name: healthDepartment.name, // name of the health department
+      key: healthDepartment.publicHDEKP, // public key (base64)
+      type: 'publicHDEKP', // key type (base64)
+    },
+    pkcs12.privateKey,
+    { algorithm: JWT_ALGORITHM }
+  );
+
+  return {
+    publicCertificate: pkcs12.cert,
+    signedPublicHDSKP,
+    signedPublicHDEKP,
+  };
+};
+
+export const signHealthDepartment = async () => {
+  cy.request('GET', '/api/v3/auth/healthDepartmentEmployee/me')
+    .then(response => response.body.departmentId)
+    .then(departmentId =>
+      cy
+        .request('GET', `/api/v4/healthDepartments/${departmentId}`)
+        .then(response => response.body)
+        .then(healthDepartment =>
+          cy.readFile('./certs/health.pfx', 'binary').then(pkcs12 => {
+            const signedPublicKeys = createSignedPublicKeys(
+              healthDepartment,
+              pkcs12
+            );
+            cy.request(
+              'POST',
+              '/api/internal/end2end/signHealthDepartment',
+              signedPublicKeys
+            );
+          })
+        )
+    );
+};
diff --git a/e2e/specs/health-department/helper/ui/tracking.helper.js b/e2e/specs/health-department/helper/ui/tracking.helper.js
index 5cf74a2..dfa20f2 100644
--- a/e2e/specs/health-department/helper/ui/tracking.helper.js
+++ b/e2e/specs/health-department/helper/ui/tracking.helper.js
@@ -1,17 +1,28 @@
 export const setDatePickerTime = () => {
-    cy.get('.ant-picker-dropdown').not('.ant-picker-dropdown-hidden').should('exist').within(() => {
-        cy.get('.ant-picker-time-panel-cell').eq(0).click().type('{enter}');
+  cy.get('.ant-picker-dropdown')
+    .not('.ant-picker-dropdown-hidden')
+    .should('exist')
+    .within(() => {
+      cy.get('.ant-picker-time-panel-cell').eq(0).click().type('{enter}');
     });
 };
 
-export const setDatePickerStartDate = (startDate) => {
-    cy.get('#startDate').should('exist').should('be.visible').click().type(`${startDate}{enter}`);
-    cy.get('#startTime').should('exist').should('be.visible').click();
-    setDatePickerTime();
+export const setDatePickerStartDate = startDate => {
+  cy.get('#startDate')
+    .should('exist')
+    .should('be.visible')
+    .click()
+    .type(`${startDate}{enter}`);
+  cy.get('#startTime').should('exist').should('be.visible').click();
+  setDatePickerTime();
 };
 
-export const setDatePickerEndDate = (endDate) => {
-    cy.get('#endDate').should('exist').click({ force: true }).type(`${endDate}{enter}`);
-    cy.get('#endTime').should('exist').click();
-    setDatePickerTime();
-};
\ No newline at end of file
+export const setDatePickerEndDate = endDate => {
+  cy.get('#endDate')
+    .should('exist')
+    .should('be.visible')
+    .click({ force: true })
+    .type(`${endDate}{enter}`);
+  cy.get('#endTime').should('exist').should('be.visible').click();
+  setDatePickerTime();
+};
diff --git a/e2e/specs/locations/downloadPDF/downloadPDF.spec.js b/e2e/specs/locations/downloadPDF/downloadPDF.spec.js
index 1751c91..9cb9fee 100644
--- a/e2e/specs/locations/downloadPDF/downloadPDF.spec.js
+++ b/e2e/specs/locations/downloadPDF/downloadPDF.spec.js
@@ -10,7 +10,7 @@ import {
 
 import { removeLocation } from '../location/location.helper';
 
-describe('Download QR Codes PDF', {retries: 3}, () => {
+describe('Download QR Codes PDF', { retries: 3 }, () => {
   beforeEach(() => login());
   after(() => removeLocation(NEW_RESTAURANT_LOCATION));
 
@@ -18,7 +18,7 @@ describe('Download QR Codes PDF', {retries: 3}, () => {
     it('downloads the Group PDF', () => {
       cy.getByCy('createGroup').click();
       cy.getByCy('restaurant').click();
-      cy.get('#groupName', {timeout: 20000}).type(RESTAURANT_NAME);
+      cy.get('#groupName', { timeout: 20000 }).type(RESTAURANT_NAME);
       cy.get('form').submit();
       cy.get('#locationSearch').type(RESTAURANT_ADDRESS);
       cy.get('.pac-container > div:first-of-type').click({ force: true });
@@ -33,10 +33,13 @@ describe('Download QR Codes PDF', {retries: 3}, () => {
       cy.getByCy('download').click();
       cy.get('.ant-message-notice').should('exist');
       cy.get('.ant-message-notice', { timeout: 20000 }).should('not.exist');
-      cy.readFile(
-        './downloads/luca_QRCode_Test Restaurant_Test Restaurant.pdf', {timeout: 30000}
+      cy.readFile('./downloads/luca_QRCode_Test_Restaurant.pdf', {
+        timeout: 30000,
+      });
+      cy.task(
+        'deleteFileIfExists',
+        './downloads/luca_QRCode_Test_Restaurant.pdf'
       );
-      cy.task('deleteFileIfExists', './downloads/luca_QRCode_Test Restaurant_Test Restaurant.pdf');
     });
   });
 
@@ -59,9 +62,12 @@ describe('Download QR Codes PDF', {retries: 3}, () => {
       cy.getByCy('qrCodeDownload').click();
       cy.get('.ant-message-notice', { timeout: 20000 }).should('not.exist');
       cy.readFile(
-        './downloads/luca_QRCode_Nexenio_1 e2e_Nexenio_1 e2e - NEW_RESTAURANT_LOCATION.pdf'
+        './downloads/luca_QRCode_Nexenio_1_e2e_NEW_RESTAURANT_LOCATION.pdf'
+      );
+      cy.task(
+        'deleteFileIfExists',
+        './downloads/luca_QRCode_Nexenio_1_e2e_NEW_RESTAURANT_LOCATION.pdf'
       );
-      cy.task('deleteFileIfExists', './downloads/luca_QRCode_Nexenio_1 e2e_Nexenio_1 e2e - NEW_RESTAURANT_LOCATION.pdf');
     });
   });
 
@@ -74,9 +80,12 @@ describe('Download QR Codes PDF', {retries: 3}, () => {
       cy.getByCy('qrCodeDownload').click();
       cy.get('.ant-message-notice', { timeout: 60000 }).should('not.exist');
       cy.readFile(
-        './downloads/luca_QRCodes_Nexenio_1 e2e_Nexenio_1 e2e - NEW_RESTAURANT_LOCATION_Tables.pdf'
+        './downloads/luca_QRCodes_Nexenio_1_e2e_NEW_RESTAURANT_LOCATION_Tables.pdf'
+      );
+      cy.task(
+        'deleteFileIfExists',
+        './downloads/luca_QRCodes_Nexenio_1_e2e_NEW_RESTAURANT_LOCATION_Tables.pdf'
       );
-      cy.task('deleteFileIfExists', './downloads/luca_QRCodes_Nexenio_1 e2e_Nexenio_1 e2e - NEW_RESTAURANT_LOCATION_Tables.pdf');
     });
 
     it('downloads the Location PDF if tables are configured', () => {
@@ -92,10 +101,13 @@ describe('Download QR Codes PDF', {retries: 3}, () => {
       cy.getByCy('qrCodeDownload').click();
       cy.get('.ant-message-notice', { timeout: 20000 }).should('not.exist');
       cy.readFile(
-        './downloads/luca_QRCode_Nexenio_1 e2e_Nexenio_1 e2e - NEW_RESTAURANT_LOCATION.pdf',
+        './downloads/luca_QRCode_Nexenio_1_e2e_NEW_RESTAURANT_LOCATION.pdf',
         { timeout: 20000 }
       );
-      cy.task('deleteFileIfExists', './downloads/luca_QRCode_Nexenio_1 e2e_Nexenio_1 e2e - NEW_RESTAURANT_LOCATION.pdf');
+      cy.task(
+        'deleteFileIfExists',
+        './downloads/luca_QRCode_Nexenio_1_e2e_NEW_RESTAURANT_LOCATION.pdf'
+      );
     });
   });
 });
diff --git a/e2e/specs/locations/helpers/functions.js b/e2e/specs/locations/helpers/functions.js
index 8aaea3b..caa0e63 100644
--- a/e2e/specs/locations/helpers/functions.js
+++ b/e2e/specs/locations/helpers/functions.js
@@ -31,7 +31,6 @@ export const basicLocationLogin = (
       origin: 'https://localhost',
     },
   });
-  cy.server();
   cy.intercept({ method: 'GET', url: '**/me' }).as('me');
   cy.visit(APP_ROUTE);
   cy.window().then(window => {
@@ -139,7 +138,10 @@ export const undoAccountDeletion = () => {
 };
 
 export const downloadLocationPrivateKeyFile = () => {
-  cy.getByCy('downloadPrivateKey', { timeout: 8000 }).click();
+  cy.getByCy('downloadPrivateKey', { timeout: 8000 })
+    .should('exist')
+    .should('be.visible')
+    .click();
   cy.getByCy('checkPrivateKeyIsDownloaded').click();
   cy.getByCy('next').should('exist').click();
 };
diff --git a/e2e/specs/locations/location/location.helper.js b/e2e/specs/locations/location/location.helper.js
index 4ed0cbd..6edbfe3 100644
--- a/e2e/specs/locations/location/location.helper.js
+++ b/e2e/specs/locations/location/location.helper.js
@@ -7,6 +7,7 @@ export const removeLocation = locationName => {
   cy.getByCy(`location-${locationName}`).click();
   cy.getByCy('openSettings').click();
   cy.getByCy('deleteLocation').click();
+
   cy.get('.ant-popover-buttons .ant-btn-primary').click();
 
   cy.wait('@deleteLocation');
diff --git a/e2e/specs/webapp/checkin/checkinByLinkFlow.spec.js b/e2e/specs/webapp/checkin/checkinByLinkFlow.spec.js
index 7d377ee..5e9ec81 100644
--- a/e2e/specs/webapp/checkin/checkinByLinkFlow.spec.js
+++ b/e2e/specs/webapp/checkin/checkinByLinkFlow.spec.js
@@ -11,16 +11,18 @@ import { registerDevice } from '../helpers/functions';
 import { clean } from '../../workflow/helpers/functions';
 import { addHealthDepartmentPrivateKeyFile } from '../../health-department/helper/ui/login.helper';
 import { loginHealthDepartment } from '../../health-department/helper/api/auth.helper';
+import { signHealthDepartment } from '../../health-department/helper/signHealthDepartment';
 import { WEBAPP_ROUTE, LOCATIONS_ROUTE } from '../helpers/routes';
-import {E2E_EMAIL, E2E_PASSWORD} from "../../locations/helpers/users";
+import { E2E_EMAIL, E2E_PASSWORD } from '../../locations/helpers/users';
 
-describe('WebApp / CheckIn', {retries: 3}, () => {
+describe('WebApp / CheckIn', { retries: 3 }, () => {
   before(() => {
     clean();
     basicLocationLogin();
     createGroup(createGroupPayload, false);
     logout();
     loginHealthDepartment();
+    signHealthDepartment();
     addHealthDepartmentPrivateKeyFile();
     cy.wait(1000);
     registerDevice();
@@ -39,7 +41,7 @@ describe('WebApp / CheckIn', {retries: 3}, () => {
       cy.get('@scannerId').then(scannerId => {
         cy.visit(`${WEBAPP_ROUTE}/${scannerId}`);
       });
-      cy.url({timeout: 4000}).should('contain', '/checkout');
+      cy.url({ timeout: 4000 }).should('contain', '/checkout');
       cy.getByCy('locationName').should('contain', createGroupPayload.name);
 
       // Simulate clock
diff --git a/e2e/specs/workflow/senarios/contactForm.spec.js b/e2e/specs/workflow/senarios/contactForm.spec.js
index de889be..0f4b196 100644
--- a/e2e/specs/workflow/senarios/contactForm.spec.js
+++ b/e2e/specs/workflow/senarios/contactForm.spec.js
@@ -3,6 +3,7 @@ import moment from 'moment';
 import { fillForm } from '../../contact-form/helpers/functions';
 
 import { loginHealthDepartment } from '../../health-department/helper/api/auth.helper';
+import { signHealthDepartment } from '../../health-department/helper/signHealthDepartment';
 
 import { addHealthDepartmentPrivateKeyFile } from '../../health-department/helper/ui/login.helper';
 
@@ -43,6 +44,7 @@ context('Workflow', () => {
       // SETUP Health department
       cy.log('Setup Health department');
       loginHealthDepartment();
+      signHealthDepartment();
       addHealthDepartmentPrivateKeyFile();
       // We need to wait for the key rotation logic
       cy.wait(1000);
@@ -116,7 +118,6 @@ context('Workflow', () => {
         WORKFLOW_LOCATION_PRIVATE_KEY_NAME
       );
       cy.getByCy('next').click();
-      cy.getByCy('finish').click();
 
       // Check requested data in Health department
       cy.log('Check requested data in Health department');
diff --git a/e2e/support/index.js b/e2e/support/index.js
index f1e0865..249a3a3 100644
--- a/e2e/support/index.js
+++ b/e2e/support/index.js
@@ -17,6 +17,7 @@
 import './commands';
 
 import 'cypress-file-upload';
+import "cypress-fail-fast";
 
 beforeEach(() => {
   cy.intercept('/', req => {
diff --git a/e2e/yarn.lock b/e2e/yarn.lock
index 39fa13a..c962e8f 100644
--- a/e2e/yarn.lock
+++ b/e2e/yarn.lock
@@ -255,6 +255,11 @@ buffer-crc32@~0.2.3:
   resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
   integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
 
+buffer-equal-constant-time@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
+  integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
+
 buffer-writer@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04"
@@ -270,6 +275,14 @@ caseless@~0.12.0:
   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
   integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
 
+chalk@4.1.1, chalk@^4.1.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
+  integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
 chalk@^1.0.0, chalk@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
@@ -290,14 +303,6 @@ chalk@^2.4.1:
     escape-string-regexp "^1.0.5"
     supports-color "^5.3.0"
 
-chalk@^4.1.0:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
-  integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==
-  dependencies:
-    ansi-styles "^4.1.0"
-    supports-color "^7.1.0"
-
 check-more-types@^2.24.0:
   version "2.24.0"
   resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
@@ -410,6 +415,13 @@ cross-spawn@^7.0.0:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
+cypress-fail-fast@3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/cypress-fail-fast/-/cypress-fail-fast-3.1.0.tgz#e81c5414d7246d97341a09fe3740f792063318bc"
+  integrity sha512-KvzFyBgwNWQZgrBAP+9fX7AA50mtCEkZi4kEpDbDE9sR7lD/q3L15MtL2R5WUbChAW46Et87dZx2iMMbhUEWIQ==
+  dependencies:
+    chalk "4.1.1"
+
 cypress-file-upload@5.0.7:
   version "5.0.7"
   resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.7.tgz#acf24fe08a92b2d0c892a58b56811fb933d34ea9"
@@ -511,6 +523,13 @@ ecc-jsbn@~0.1.1:
     jsbn "~0.1.0"
     safer-buffer "^2.1.0"
 
+ecdsa-sig-formatter@1.0.11:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
+  integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
+  dependencies:
+    safe-buffer "^5.0.1"
+
 elegant-spinner@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
@@ -934,6 +953,22 @@ jsonfile@^6.0.1:
   optionalDependencies:
     graceful-fs "^4.1.6"
 
+jsonwebtoken@8.5.1:
+  version "8.5.1"
+  resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
+  integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
+  dependencies:
+    jws "^3.2.2"
+    lodash.includes "^4.3.0"
+    lodash.isboolean "^3.0.3"
+    lodash.isinteger "^4.0.4"
+    lodash.isnumber "^3.0.3"
+    lodash.isplainobject "^4.0.6"
+    lodash.isstring "^4.0.1"
+    lodash.once "^4.0.0"
+    ms "^2.1.1"
+    semver "^5.6.0"
+
 jsprim@^1.2.2:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -944,6 +979,23 @@ jsprim@^1.2.2:
     json-schema "0.2.3"
     verror "1.10.0"
 
+jwa@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
+  integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
+  dependencies:
+    buffer-equal-constant-time "1.0.1"
+    ecdsa-sig-formatter "1.0.11"
+    safe-buffer "^5.0.1"
+
+jws@^3.2.2:
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
+  integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
+  dependencies:
+    jwa "^1.4.1"
+    safe-buffer "^5.0.1"
+
 lazy-ass@^1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513"
@@ -993,7 +1045,37 @@ listr@^0.14.3:
     p-map "^2.0.0"
     rxjs "^6.3.3"
 
-lodash.once@^4.1.1:
+lodash.includes@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
+  integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=
+
+lodash.isboolean@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
+  integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=
+
+lodash.isinteger@^4.0.4:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
+  integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=
+
+lodash.isnumber@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
+  integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=
+
+lodash.isplainobject@^4.0.6:
+  version "4.0.6"
+  resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
+  integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
+
+lodash.isstring@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
+  integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
+
+lodash.once@^4.0.0, lodash.once@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
   integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
@@ -1382,6 +1464,11 @@ semver@4.3.2:
   resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7"
   integrity sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c=
 
+semver@^5.6.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
 shebang-command@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
diff --git a/package.json b/package.json
index e86519b..2ecaba7 100644
--- a/package.json
+++ b/package.json
@@ -1,46 +1,49 @@
 {
-  "name": "@lucaapp/web",
-  "version": "1.9.2",
-  "private": true,
-  "license": "Apache-2.0",
-  "author": "Culture4Life <hello@luca-app.de> (https://www.luca-app.de/)",
-  "scripts": {
-    "latest": ". ./scripts/getGITEnvironmentVariables.sh && docker-compose -f docker-compose.yml",
-    "prod": ". ./scripts/getGITEnvironmentVariables.sh && IMAGE_TAG=prod docker-compose -f docker-compose.yml",
-    "dev": ". ./scripts/getGITEnvironmentVariables.sh && IMAGE_TAG=dev docker-compose -f docker-compose.yml -f docker-compose.dev.yml",
-    "test": ". ./scripts/getGITEnvironmentVariables.sh && IMAGE_TAG=test docker-compose -f docker-compose.yml -f docker-compose.test.yml",
-    "all": "./scripts/yarnAll.sh",
-    "prepare": "git lfs install && husky install && yarn generate:certs",
-    "generate:certs": "./scripts/generateCertificates.sh",
-    "lint:backend": "npm run lint --prefix services/backend",
-    "lint:contact-form": "npm run lint --prefix services/contact-form",
-    "lint:health-department": "npm run lint --prefix services/health-department",
-    "lint:locations": "npm run lint --prefix services/locations",
-    "lint:scanner": "npm run lint --prefix services/scanner",
-    "lint:webapp": "npm run lint --prefix services/webapp"
-  },
-  "lint-staged": {
-    "services/backend/**/*.{js,jsx}": [
-      "npm run --silent lint:backend"
-    ],
-    "services/contact-form/**/*.{js,jsx}": [
-      "npm run --silent lint:contact-form"
-    ],
-    "services/health-department/**/*.{js,jsx}": [
-      "npm run --silent lint:health-department"
-    ],
-    "services/locations/**/*.{js,jsx}": [
-      "npm run --silent lint:locations"
-    ],
-    "services/scanner/**/*.{js,jsx}": [
-      "npm run --silent lint:scanner"
-    ],
-    "services/webapp/**/*.{js,jsx}": [
-      "npm run --silent lint:webapp"
-    ]
-  },
-  "devDependencies": {
-    "husky": "6.0.0",
-    "lint-staged": "11.0.0"
-  }
+    "name": "@lucaapp/web",
+    "version": "2.0.0",
+    "private": true,
+    "license": "Apache-2.0",
+    "author": "Culture4Life <hello@luca-app.de> (https://www.luca-app.de/)",
+    "scripts": {
+        "latest": ". ./scripts/getGITEnvironmentVariables.sh && docker-compose -f docker-compose.yml",
+        "prod": ". ./scripts/getGITEnvironmentVariables.sh && IMAGE_TAG=prod docker-compose -f docker-compose.yml",
+        "dev": ". ./scripts/getGITEnvironmentVariables.sh && IMAGE_TAG=dev docker-compose -f docker-compose.yml -f docker-compose.dev.yml",
+        "test": ". ./scripts/getGITEnvironmentVariables.sh && IMAGE_TAG=test docker-compose -f docker-compose.yml -f docker-compose.test.yml",
+        "all": "./scripts/yarnAll.sh",
+        "prepare": "git lfs install && husky install && yarn generate:certs",
+        "generate:certs": "./scripts/generateCertificates.sh",
+        "lint:backend": "npm run lint --prefix services/backend",
+        "lint:contact-form": "npm run lint --prefix services/contact-form",
+        "lint:health-department": "npm run lint --prefix services/health-department",
+        "lint:locations": "npm run lint --prefix services/locations",
+        "lint:scanner": "npm run lint --prefix services/scanner",
+        "lint:webapp": "npm run lint --prefix services/webapp"
+    },
+    "lint-staged": {
+        "services/backend/**/*.{js,jsx}": [
+            "npm run --silent lint:backend"
+        ],
+        "services/contact-form/**/*.{js,jsx}": [
+            "npm run --silent lint:contact-form"
+        ],
+        "services/health-department/**/*.{js,jsx}": [
+            "npm run --silent lint:health-department"
+        ],
+        "services/locations/**/*.{js,jsx}": [
+            "npm run --silent lint:locations"
+        ],
+        "services/scanner/**/*.{js,jsx}": [
+            "npm run --silent lint:scanner"
+        ],
+        "services/webapp/**/*.{js,jsx}": [
+            "npm run --silent lint:webapp"
+        ]
+    },
+    "devDependencies": {
+        "husky": "6.0.0",
+        "lint-staged": "11.0.0"
+    },
+    "dependencies": {
+        "@types/lodash": "4.14.172"
+    }
 }
diff --git a/scripts/generateCertificates.sh b/scripts/generateCertificates.sh
index 42439eb..aa07910 100755
--- a/scripts/generateCertificates.sh
+++ b/scripts/generateCertificates.sh
@@ -8,6 +8,7 @@ PFX_PASS="testing"
 if [ -f "$CERTS_DIR/ca_root.pem" ]; then
   echo "Root CA already exists."
 else
+
   mkdir -p $CERTS_DIR
 
   # generate self-signed root ca
@@ -25,10 +26,10 @@ else
   cfssl genkey $CA_DIR/ssl.json | cfssljson -bare $CERTS_DIR/ssl
   cfssl sign -config $CA_DIR/config.json -profile server -ca $CERTS_DIR/ca_basic.pem -ca-key $CERTS_DIR/ca_basic-key.pem $CERTS_DIR/ssl.csr | cfssljson -bare $CERTS_DIR/ssl
 
-  # generate client certificates
+ # generate client certificates
   openssl pkcs12 -export -inkey $CERTS_DIR/health-key.pem  -in $CERTS_DIR/health.pem -name health -passout pass:$PFX_PASS -out $CERTS_DIR/health.pfx
+
 fi
-echo "Copying certs..."
 # copy certificates to services
 # nginx
 cp $CERTS_DIR/ssl.pem services/elb/ssl/ssl.crt.pem
@@ -42,3 +43,7 @@ chmod 644 services/elb/ssl/*.pem
 cp $CERTS_DIR/ca_root.pem services/backend/certs/root.pem
 cp $CERTS_DIR/ca_basic.pem services/backend/certs/basic.pem
 chmod 644 services/backend/certs/*.pem
+
+# e2e
+cp $CERTS_DIR/health.pfx e2e/certs/health.pfx
+chmod 644  e2e/certs/health.pfx
\ No newline at end of file
diff --git a/services/backend/config/custom-environment-variables.js b/services/backend/config/custom-environment-variables.js
index e6be7a2..bf1558c 100644
--- a/services/backend/config/custom-environment-variables.js
+++ b/services/backend/config/custom-environment-variables.js
@@ -22,6 +22,7 @@ module.exports = {
   redis: {
     hostname: 'REDIS_HOSTNAME',
     password: 'REDIS_PASSWORD',
+    database: 'REDIS_DATABASE',
   },
   mailer: {
     apiUrl: 'MAILER_API_URL',
@@ -103,6 +104,22 @@ module.exports = {
     dummy_max_tracings: 'DUMMY_MAX_TRACINGS',
     dummy_max_traces: 'DUMMY_MAX_TRACES',
     badges_post_ratelimit_hour: 'BADGES_POST_RATELIMIT_HOUR',
+    badges_bloomfilter_get_ratelimit_hour:
+      'BADGES_BLOOMFILTER_GET_RATELIMIT_HOUR',
     operator_email_patch_ratelimit_hour: 'OPERATOR_EMAIL_PATCH_RATELIMIT_HOUR',
+    keys_daily_rotate_post_ratelimit_hour:
+      'KEYS_DAILY_ROTATE_POST_RATELIMIT_HOUR',
+    location_transfer_post_ratelimit_hour:
+      'LOCATION_TRANSFER_POST_RATELIMIT_HOUR',
+    notifications_traces_get_ratelimit_hour:
+      'NOTIFICATIONS_TRACES_GET_RATELIMIT_HOUR',
+    notifications_v4_health_departments_get_ratelimit_hour:
+      'NOTIFICATIONS_V4_HEALTH_DEPARTMENTS_GET_RATELIMIT_HOUR',
+    notifications_v4_traces_active_chunk_get_ratelimit_hour:
+      'NOTIFICATIONS_V4_TRACES_ACTIVE_CHUNK_GET_RATELIMIT_HOUR',
+    notifications_v4_traces_archived_chunk_get_ratelimit_hour:
+      'NOTIFICATIONS_V4_TRACES_ARCHIVED_CHUNK_GET_RATELIMIT_HOUR',
+    notifications_v4_config_get_ratelimit_hour:
+      'NOTIFICATIONS_V4_CONFIG_GET_RATELIMIT_HOUR',
   },
 };
diff --git a/services/backend/config/default.js b/services/backend/config/default.js
index 281b880..ef7c8d1 100644
--- a/services/backend/config/default.js
+++ b/services/backend/config/default.js
@@ -42,6 +42,7 @@ module.exports = {
   },
   redis: {
     hostname: 'redis',
+    database: 0,
     password:
       // DEV ONLY TOKEN
       'ConqsCqWd]eaR82wv%C.iDdRybor8Ms2bM*h=m?V3@x2w^UxKA9pEjMjHn^y7?78',
@@ -93,11 +94,23 @@ module.exports = {
     },
     tracingProcess: {
       maxAge: moment.duration(28, 'days').as('hours'),
+      maxRiskLevel: 2,
     },
     testRedeems: {
       defaultMaxAge: moment.duration(72, 'hours').as('hours'),
       maxAge: moment.duration(1, 'years').as('hours'),
     },
+    notificationChunks: {
+      initialChunkCoverage: moment.duration(14, 'days').as('hours'),
+      maxAge: moment.duration(14, 'days').as('hours'),
+      cacheTTL: moment.duration(2, 'hours').as('seconds'),
+    },
+    auditLogs: {
+      maxAge: moment.duration(1, 'years').as('hours'),
+    },
+    healthDepartments: {
+      maxAmount: 450,
+    },
   },
   emails: {
     expiry: moment.duration(24, 'hours').as('hours'),
@@ -174,6 +187,14 @@ module.exports = {
     tests_redeem_post_ratelimit_minute: 50,
     tests_redeem_delete_ratelimit_minute: 50,
     badges_post_ratelimit_hour: 10,
+    badges_bloomfilter_get_ratelimit_hour: 10,
     operator_email_patch_ratelimit_hour: 5,
+    keys_daily_rotate_post_ratelimit_hour: 5,
+    location_transfer_post_ratelimit_hour: 5,
+    notifications_traces_get_ratelimit_hour: 5,
+    notifications_v4_health_departments_get_ratelimit_hour: 1000,
+    notifications_v4_traces_active_chunk_get_ratelimit_hour: 1000,
+    notifications_v4_traces_archived_chunk_get_ratelimit_hour: 7500,
+    notifications_v4_config_get_ratelimit_hour: 1000,
   },
 };
diff --git a/services/backend/package.json b/services/backend/package.json
index 70bc60c..927b022 100644
--- a/services/backend/package.json
+++ b/services/backend/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@lucaapp/backend",
-  "version": "1.9.2",
+  "version": "2.0.0",
   "private": true,
   "license": "Apache-2.0",
   "author": "Culture4Life <hello@luca-app.de> (https://www.luca-app.de/)",
@@ -30,6 +30,7 @@
     "@types/config": "0.0.39",
     "@types/etag": "1.8.1",
     "@types/express": "4.17.13",
+    "@types/lodash": "4.14.172",
     "@types/validator": "13.6.3",
     "axios": "0.21.1",
     "bloomit": "1.1.1",
@@ -57,6 +58,7 @@
     "libphonenumber-js": "1.9.13",
     "lodash": "4.17.21",
     "moment": "2.29.1",
+    "node-cache": "5.1.2",
     "node-forge": "0.10.0",
     "passport": "0.4.1",
     "passport-custom": "1.1.1",
diff --git a/services/backend/src/@types/express.d.ts b/services/backend/src/@types/express.d.ts
index 94bf4ba..39dff8f 100644
--- a/services/backend/src/@types/express.d.ts
+++ b/services/backend/src/@types/express.d.ts
@@ -1,8 +1,19 @@
+interface IUser {
+  uuid: string;
+}
+
+interface IOperator extends IUser {
+  type: 'Operator';
+}
+
+interface IHealthDepartmentEmployee extends IUser {
+  departmentId: string;
+  isAdmin: boolean;
+  type: 'HealthDepartmentEmployeee';
+}
+
 declare namespace Express {
   export interface Request {
-    user?: {
-      // for now just as a basic check until the models are correctly types
-      uuid: string;
-    };
+    user?: IUser | IOperator | IHealthDepartmentEmployee;
   }
 }
diff --git a/services/backend/src/app.js b/services/backend/src/app.js
index eba8b4f..e059f99 100644
--- a/services/backend/src/app.js
+++ b/services/backend/src/app.js
@@ -57,18 +57,16 @@ const configureApp = () => {
   const requestLogger = expressWinston.logger({
     winstonInstance: logger,
     statusLevels: true,
-    ignoredRoutes: [
-      '/api/v3/health',
-      '/api/v3/health/ready',
-      '/api/internal/metrics',
-    ],
+    ignoredRoutes: ['/api/v3/health', '/api/v3/health/ready'],
   });
   const errorLogger = expressWinston.errorLogger({
     winstonInstance: logger,
   });
 
   app.disable('x-powered-by');
+  app.disable('etag');
   app.enable('strict routing');
+
   app.set('trust proxy', 2);
 
   // Global Middlewares
diff --git a/services/backend/src/constants/auditLog.ts b/services/backend/src/constants/auditLog.ts
new file mode 100644
index 0000000..f535d7a
--- /dev/null
+++ b/services/backend/src/constants/auditLog.ts
@@ -0,0 +1,42 @@
+// eslint-disable-next-line no-shadow
+export enum AuditLogEvents {
+  LOGIN = 'LOGIN',
+  LOGOUT = 'LOGOUT',
+  CHANGE_PASSWORD = 'CHANGE_PASSWORD',
+  RESET_PASSWORD = 'RESET_PASSWORD',
+
+  CREATE_EMPLOYEE = 'CREATE_EMPLOYEE',
+  UPDATE_EMPLOYEE = 'UPDATE_EMPLOYEE',
+  DELETE_EMPLOYEE = 'DELETE_EMPLOYEE',
+  REACTIVATE_EMPLOYEE = 'REACTIVATE_EMPLOYEE',
+  CHANGE_ROLE = 'CHANGE_ROLE',
+
+  CREATE_TRACING_PROCESS = 'CREATE_TRACING_PROCESS',
+  REQUEST_DATA = 'REQUEST_DATA',
+  RECEIVE_DATA = 'RECEIVE_DATA',
+  VIEW_DATA = 'VIEW_DATA',
+  DOWNLOAD_AUDITLOG = 'DOWNLOAD_AUDITLOG',
+
+  ISSUE_DAILY_KEYPAIR = 'ISSUE_DAILY_KEYPAIR',
+  ISSUE_BADGE_KEYPAIR = 'ISSUE_BADGE_KEYPAIR',
+  REKEY_DAILY_KEYPAIR = 'REKEY_DAILY_KEYPAIR',
+  REKEY_BADGE_KEYPAIR = 'REKEY_BADGE_KEYPAIR',
+}
+
+// eslint-disable-next-line no-shadow
+export enum AuditStatusType {
+  SUCCESS = 'SUCCESS',
+  ERROR_INVALID_USER = 'ERROR_INVALID_USER',
+  ERROR_TARGET_NOT_FOUND = 'ERROR_TARGET_NOT_FOUND',
+  ERROR_INVALID_PASSWORD = 'ERROR_INVALID_PASSWORD',
+  ERROR_INSECURE_PASSWORD = 'ERROR_INSECURE_PASSWORD',
+  ERROR_INVALID_EMAIL = 'ERROR_INVALID_EMAIL',
+  ERROR_INVALID_SIGNATURE = 'ERROR_INVALID_SIGNATURE',
+  ERROR_INVALID_KEYID = 'ERROR_INVALID_KEYID',
+  ERROR_INVALID_TAN = 'ERROR_INVALID_TAN',
+  ERROR_CONFLICT_KEY = 'ERROR_CONFLICT_KEY',
+  ERROR_DUPLICATE_EMAIL = 'ERROR_DUPLICATE_EMAIL',
+  ERROR_LIMIT_EXCEEDED = 'ERROR_LIMIT_EXCEEDED',
+  ERROR_TIMEFRAME = 'ERROR_TIMEFRAME',
+  ERROR_UNKNOWN_SERVER_ERROR = 'ERROR_UNKNOWN_SERVER_ERROR',
+}
diff --git a/services/backend/src/database/migrations/2021.08.06-0710-auditLogs.js b/services/backend/src/database/migrations/2021.08.06-0710-auditLogs.js
new file mode 100644
index 0000000..7d84022
--- /dev/null
+++ b/services/backend/src/database/migrations/2021.08.06-0710-auditLogs.js
@@ -0,0 +1,74 @@
+module.exports = {
+  up: async (queryInterface, DataTypes) => {
+    return queryInterface.sequelize.transaction(async transaction => {
+      await queryInterface.createTable('HealthDepartmentAuditLogs', {
+        uuid: {
+          type: DataTypes.UUID,
+          allowNull: false,
+          primaryKey: true,
+          defaultValue: DataTypes.UUIDV4,
+        },
+        departmentId: {
+          type: DataTypes.UUID,
+          allowNull: false,
+        },
+        employeeId: {
+          type: DataTypes.UUID,
+          allowNull: false,
+        },
+        type: {
+          type: DataTypes.STRING,
+          allowNull: false,
+        },
+        status: {
+          type: DataTypes.STRING,
+          allowNull: false,
+        },
+        meta: {
+          type: DataTypes.JSONB,
+          allowNull: true,
+          defaultValue: null,
+        },
+        createdAt: {
+          allowNull: false,
+          type: DataTypes.DATE,
+          defaultValue: DataTypes.literal('CURRENT_TIMESTAMP'),
+        },
+        updatedAt: {
+          allowNull: false,
+          type: DataTypes.DATE,
+          defaultValue: DataTypes.literal('CURRENT_TIMESTAMP'),
+        },
+      });
+
+      await queryInterface.addConstraint(
+        'HealthDepartmentAuditLogs',
+        {
+          fields: ['departmentId'],
+          type: 'FOREIGN KEY',
+          references: {
+            table: 'HealthDepartments',
+            field: 'uuid',
+          },
+        },
+        { transaction }
+      );
+
+      await queryInterface.addConstraint(
+        'HealthDepartmentAuditLogs',
+        {
+          fields: ['employeeId'],
+          type: 'FOREIGN KEY',
+          references: {
+            table: 'HealthDepartmentEmployees',
+            field: 'uuid',
+          },
+        },
+        { transaction }
+      );
+    });
+  },
+  down: async queryInterface => {
+    return queryInterface.dropTable('HealthDepartmentAuditLogs');
+  },
+};
diff --git a/services/backend/src/database/migrations/2021.08.06-1411-addApprovedAtAttributeToLocationTransfers.js b/services/backend/src/database/migrations/2021.08.06-1411-addApprovedAtAttributeToLocationTransfers.js
new file mode 100644
index 0000000..91516d1
--- /dev/null
+++ b/services/backend/src/database/migrations/2021.08.06-1411-addApprovedAtAttributeToLocationTransfers.js
@@ -0,0 +1,25 @@
+module.exports = {
+  up: async (queryInterface, Sequelize) => {
+    await queryInterface.sequelize.transaction(async transaction => {
+      await queryInterface.addColumn(
+        'LocationTransfers',
+        'approvedAt',
+        {
+          allowNull: true,
+          type: Sequelize.DATE,
+        },
+        {
+          transaction,
+        }
+      );
+    });
+  },
+
+  down: async queryInterface => {
+    await queryInterface.sequelize.transaction(async transaction => {
+      await queryInterface.removeColumn('LocationTransfers', 'approvedAt', {
+        transaction,
+      });
+    });
+  },
+};
diff --git a/services/backend/src/database/migrations/2021.08.12-1307-createNotificationChunks.js b/services/backend/src/database/migrations/2021.08.12-1307-createNotificationChunks.js
new file mode 100644
index 0000000..93ff0be
--- /dev/null
+++ b/services/backend/src/database/migrations/2021.08.12-1307-createNotificationChunks.js
@@ -0,0 +1,25 @@
+module.exports = {
+  up: (queryInterface, DataTypes) =>
+    queryInterface.createTable('NotificationChunks', {
+      chunk: {
+        type: DataTypes.BLOB,
+        allowNull: false,
+      },
+      hash: {
+        type: DataTypes.STRING(24),
+        allowNull: false,
+        primaryKey: true,
+      },
+      createdAt: {
+        allowNull: false,
+        type: DataTypes.DATE,
+        defaultValue: DataTypes.literal('CURRENT_TIMESTAMP'),
+      },
+      updatedAt: {
+        allowNull: false,
+        type: DataTypes.DATE,
+        defaultValue: DataTypes.literal('CURRENT_TIMESTAMP'),
+      },
+    }),
+  down: queryInterface => queryInterface.dropTable('NotificationChunks'),
+};
diff --git a/services/backend/src/database/migrations/2021.08.13-1321-addContactInfosToHealthDepartment.js b/services/backend/src/database/migrations/2021.08.13-1321-addContactInfosToHealthDepartment.js
new file mode 100644
index 0000000..f70452b
--- /dev/null
+++ b/services/backend/src/database/migrations/2021.08.13-1321-addContactInfosToHealthDepartment.js
@@ -0,0 +1,17 @@
+module.exports = {
+  up: async (queryInterface, DataTypes) => {
+    await queryInterface.addColumn('HealthDepartments', 'email', {
+      allowNull: true,
+      type: DataTypes.CITEXT,
+    });
+    await queryInterface.addColumn('HealthDepartments', 'phone', {
+      allowNull: true,
+      type: DataTypes.STRING,
+    });
+  },
+
+  down: async queryInterface => {
+    await queryInterface.removeColumn('HealthDepartments', 'email');
+    await queryInterface.removeColumn('HealthDepartments', 'phone');
+  },
+};
diff --git a/services/backend/src/database/migrations/2021.08.16-1926-createRiskLevels.js b/services/backend/src/database/migrations/2021.08.16-1926-createRiskLevels.js
new file mode 100644
index 0000000..e9a98fa
--- /dev/null
+++ b/services/backend/src/database/migrations/2021.08.16-1926-createRiskLevels.js
@@ -0,0 +1,37 @@
+module.exports = {
+  up: async (queryInterface, DataTypes) => {
+    await queryInterface.createTable('RiskLevels', {
+      locationTransferTraceId: {
+        type: DataTypes.UUID,
+        allowNull: false,
+        primaryKey: true,
+      },
+      level: {
+        type: DataTypes.INTEGER,
+        allowNull: false,
+        primaryKey: true,
+      },
+      createdAt: {
+        allowNull: false,
+        type: DataTypes.DATE,
+        defaultValue: DataTypes.literal('CURRENT_TIMESTAMP'),
+      },
+      updatedAt: {
+        allowNull: false,
+        type: DataTypes.DATE,
+        defaultValue: DataTypes.literal('CURRENT_TIMESTAMP'),
+      },
+    });
+    await queryInterface.addConstraint('RiskLevels', {
+      fields: ['locationTransferTraceId'],
+      type: 'foreign key',
+      references: {
+        table: 'LocationTransferTraces',
+        field: 'uuid',
+      },
+      onDelete: 'cascade',
+      onUpdate: 'cascade',
+    });
+  },
+  down: queryInterface => queryInterface.dropTable('RiskLevels'),
+};
diff --git a/services/backend/src/database/migrations/2021.08.30-1100-addNotificationsEnabledColumnToHealthDepartments.js b/services/backend/src/database/migrations/2021.08.30-1100-addNotificationsEnabledColumnToHealthDepartments.js
new file mode 100644
index 0000000..885cd0e
--- /dev/null
+++ b/services/backend/src/database/migrations/2021.08.30-1100-addNotificationsEnabledColumnToHealthDepartments.js
@@ -0,0 +1,10 @@
+module.exports = {
+  up: async (queryInterface, DataTypes) =>
+    queryInterface.addColumn('HealthDepartments', 'notificationsEnabled', {
+      allowNull: false,
+      type: DataTypes.BOOLEAN,
+      defaultValue: false,
+    }),
+  down: async queryInterface =>
+    queryInterface.removeColumn('HealthDepartments', 'notificationsEnabled'),
+};
diff --git a/services/backend/src/database/migrations/2021.09.02-1530-employeeNonRequiredFields.js b/services/backend/src/database/migrations/2021.09.02-1530-employeeNonRequiredFields.js
new file mode 100644
index 0000000..6db7b8f
--- /dev/null
+++ b/services/backend/src/database/migrations/2021.09.02-1530-employeeNonRequiredFields.js
@@ -0,0 +1,69 @@
+module.exports = {
+  up: async (queryInterface, DataTypes) => {
+    return queryInterface.sequelize.transaction(async transaction => {
+      await Promise.all([
+        queryInterface.changeColumn(
+          'HealthDepartmentEmployees',
+          'firstName',
+          {
+            type: DataTypes.STRING,
+            allowNull: true,
+          },
+          { transaction }
+        ),
+        queryInterface.changeColumn(
+          'HealthDepartmentEmployees',
+          'lastName',
+          {
+            type: DataTypes.STRING,
+            allowNull: true,
+          },
+          { transaction }
+        ),
+        queryInterface.changeColumn(
+          'HealthDepartmentEmployees',
+          'phone',
+          {
+            type: DataTypes.STRING,
+            allowNull: true,
+          },
+          { transaction }
+        ),
+      ]);
+    });
+  },
+
+  down: (queryInterface, DataTypes) => {
+    return queryInterface.sequelize.transaction(async transaction => {
+      await Promise.all([
+        queryInterface.changeColumn(
+          'HealthDepartmentEmployees',
+          'firstName',
+          {
+            type: DataTypes.STRING,
+            allowNull: false,
+          },
+          { transaction }
+        ),
+        queryInterface.changeColumn(
+          'HealthDepartmentEmployees',
+          'lastName',
+          {
+            type: DataTypes.STRING,
+            allowNull: false,
+          },
+          { transaction }
+        ),
+        queryInterface.changeColumn(
+          'HealthDepartmentEmployees',
+          'phone',
+          {
+            type: DataTypes.STRING,
+            allowNull: false,
+          },
+          { transaction }
+        ),
+      ]);
+    });
+  },
+};
diff --git a/services/backend/src/database/migrations/2021.09.03-1900-addNotificationMessages.js b/services/backend/src/database/migrations/2021.09.03-1900-addNotificationMessages.js
new file mode 100644
index 0000000..e294984
--- /dev/null
+++ b/services/backend/src/database/migrations/2021.09.03-1900-addNotificationMessages.js
@@ -0,0 +1,70 @@
+module.exports = {
+  up: async (queryInterface, DataTypes) => {
+    await queryInterface.sequelize.transaction(async transaction => {
+      await queryInterface.createTable(
+        'NotificationMessages',
+        {
+          uuid: {
+            type: DataTypes.UUID,
+            allowNull: false,
+            primaryKey: true,
+            defaultValue: DataTypes.UUIDV4,
+          },
+          departmentId: {
+            type: DataTypes.UUID,
+            allowNull: true,
+            references: {
+              model: 'HealthDepartments',
+              key: 'uuid',
+            },
+            onUpdate: 'CASCADE',
+            onDelete: 'CASCADE',
+          },
+          level: {
+            type: DataTypes.INTEGER,
+            allowNull: false,
+          },
+          language: {
+            type: DataTypes.STRING,
+            allowNull: false,
+          },
+          key: {
+            type: DataTypes.STRING,
+            allowNull: false,
+          },
+          content: {
+            allowNull: false,
+            type: DataTypes.TEXT,
+          },
+          createdAt: {
+            allowNull: false,
+            type: DataTypes.DATE,
+            defaultValue: DataTypes.literal('CURRENT_TIMESTAMP'),
+          },
+          updatedAt: {
+            allowNull: false,
+            type: DataTypes.DATE,
+            defaultValue: DataTypes.literal('CURRENT_TIMESTAMP'),
+          },
+        },
+        { transaction }
+      );
+
+      await queryInterface.addConstraint('NotificationMessages', {
+        fields: ['departmentId', 'level', 'language'],
+        type: 'unique',
+        name: 'departmentId_level_language_unique',
+        transaction,
+      });
+
+      await queryInterface.addIndex('NotificationMessages', {
+        fields: ['departmentId'],
+        name: 'departmentId_index',
+        transaction,
+      });
+    });
+  },
+  down: async queryInterface => {
+    await queryInterface.dropTable('NotificationMessages');
+  },
+};
diff --git a/services/backend/src/database/models/healthDepartment.js b/services/backend/src/database/models/healthDepartment.js
index fd9777f..d556aa7 100644
--- a/services/backend/src/database/models/healthDepartment.js
+++ b/services/backend/src/database/models/healthDepartment.js
@@ -17,6 +17,14 @@ module.exports = (Sequelize, DataTypes) => {
         allowNull: false,
         defaultValue: null,
       },
+      email: {
+        type: DataTypes.CITEXT,
+        allowNull: true,
+      },
+      phone: {
+        type: DataTypes.STRING,
+        allowNull: true,
+      },
       publicHDEKP: {
         type: DataTypes.STRING(88),
       },
@@ -35,6 +43,11 @@ module.exports = (Sequelize, DataTypes) => {
       signedPublicHDSKP: {
         type: DataTypes.STRING(2048),
       },
+      notificationsEnabled: {
+        allowNull: false,
+        defaultValue: false,
+        type: DataTypes.BOOLEAN,
+      },
     },
     {
       paranoid: true,
@@ -53,6 +66,14 @@ module.exports = (Sequelize, DataTypes) => {
     HealthDepartment.hasMany(models.HealthDepartmentEmployee, {
       foreignKey: 'departmentId',
     });
+
+    HealthDepartment.hasMany(models.HealthDepartmentAuditLog, {
+      foreignKey: 'departmentId',
+    });
+
+    HealthDepartment.hasMany(models.NotificationMessage, {
+      foreignKey: 'departmentId',
+    });
   };
 
   return HealthDepartment;
diff --git a/services/backend/src/database/models/healthDepartmentAuditLog.js b/services/backend/src/database/models/healthDepartmentAuditLog.js
new file mode 100644
index 0000000..3c3cf5a
--- /dev/null
+++ b/services/backend/src/database/models/healthDepartmentAuditLog.js
@@ -0,0 +1,46 @@
+module.exports = (Sequelize, DataTypes) => {
+  const HealthDepartmentAuditLog = Sequelize.define(
+    'HealthDepartmentAuditLog',
+    {
+      uuid: {
+        type: DataTypes.UUID,
+        allowNull: false,
+        primaryKey: true,
+        defaultValue: DataTypes.UUIDV4,
+      },
+      departmentId: {
+        type: DataTypes.UUID,
+        allowNull: false,
+      },
+      employeeId: {
+        type: DataTypes.UUID,
+        allowNull: false,
+      },
+      type: {
+        type: DataTypes.STRING,
+        allowNull: false,
+      },
+      status: {
+        type: DataTypes.STRING,
+        allowNull: false,
+      },
+      meta: {
+        type: DataTypes.JSONB,
+        allowNull: true,
+        defaultValue: null,
+      },
+    }
+  );
+
+  HealthDepartmentAuditLog.associate = models => {
+    HealthDepartmentAuditLog.belongsTo(models.HealthDepartment, {
+      foreignKey: 'departmentId',
+    });
+
+    HealthDepartmentAuditLog.belongsTo(models.HealthDepartmentEmployee, {
+      foreignKey: 'employeeId',
+    });
+  };
+
+  return HealthDepartmentAuditLog;
+};
diff --git a/services/backend/src/database/models/healthDepartmentEmployee.js b/services/backend/src/database/models/healthDepartmentEmployee.js
index a599d07..ec87820 100644
--- a/services/backend/src/database/models/healthDepartmentEmployee.js
+++ b/services/backend/src/database/models/healthDepartmentEmployee.js
@@ -23,11 +23,11 @@ module.exports = (Sequelize, DataTypes) => {
       },
       firstName: {
         type: DataTypes.STRING,
-        allowNull: false,
+        allowNull: true,
       },
       lastName: {
         type: DataTypes.STRING,
-        allowNull: false,
+        allowNull: true,
       },
       email: {
         type: DataTypes.CITEXT,
@@ -36,7 +36,7 @@ module.exports = (Sequelize, DataTypes) => {
       },
       phone: {
         type: DataTypes.STRING,
-        allowNull: false,
+        allowNull: true,
       },
       departmentId: {
         type: DataTypes.UUID,
@@ -72,6 +72,10 @@ module.exports = (Sequelize, DataTypes) => {
     HealthDepartmentEmployee.hasMany(models.TracingProcess, {
       foreignKey: 'assigneeId',
     });
+
+    HealthDepartmentEmployee.hasMany(models.HealthDepartmentAuditLog, {
+      foreignKey: 'employeeId',
+    });
   };
 
   HealthDepartmentEmployee.prototype.checkPassword = async function checkPassword(
diff --git a/services/backend/src/database/models/locationTransfer.js b/services/backend/src/database/models/locationTransfer.js
index 27f36bc..90dd320 100644
--- a/services/backend/src/database/models/locationTransfer.js
+++ b/services/backend/src/database/models/locationTransfer.js
@@ -34,6 +34,11 @@ module.exports = (Sequelize, DataTypes) => {
         allowNull: true,
         defaultValue: null,
       },
+      approvedAt: {
+        type: DataTypes.DATE,
+        allowNull: true,
+        defaultValue: null,
+      },
     },
     {
       paranoid: true,
diff --git a/services/backend/src/database/models/locationTransferTrace.js b/services/backend/src/database/models/locationTransferTrace.js
index adc7e95..2c8b953 100644
--- a/services/backend/src/database/models/locationTransferTrace.js
+++ b/services/backend/src/database/models/locationTransferTrace.js
@@ -73,6 +73,10 @@ module.exports = (Sequelize, DataTypes) => {
       sourceKey: 'traceId',
       foreignKey: 'traceId',
     });
+
+    LocationTransferTrace.hasMany(models.RiskLevel, {
+      foreignKey: 'locationTransferTraceId',
+    });
   };
 
   return LocationTransferTrace;
diff --git a/services/backend/src/database/models/notificationChunk.js b/services/backend/src/database/models/notificationChunk.js
new file mode 100644
index 0000000..a7be0fb
--- /dev/null
+++ b/services/backend/src/database/models/notificationChunk.js
@@ -0,0 +1,13 @@
+module.exports = (Sequelize, DataTypes) => {
+  return Sequelize.define('NotificationChunk', {
+    chunk: {
+      type: DataTypes.BLOB,
+      allowNull: false,
+    },
+    hash: {
+      type: DataTypes.STRING(24),
+      allowNull: false,
+      primaryKey: true,
+    },
+  });
+};
diff --git a/services/backend/src/database/models/notificationMessage.js b/services/backend/src/database/models/notificationMessage.js
new file mode 100644
index 0000000..a3b9eea
--- /dev/null
+++ b/services/backend/src/database/models/notificationMessage.js
@@ -0,0 +1,35 @@
+module.exports = (Sequelize, DataTypes) => {
+  const NotificationMessage = Sequelize.define('NotificationMessage', {
+    departmentId: {
+      type: DataTypes.UUID,
+      primaryKey: true,
+    },
+    level: {
+      type: DataTypes.INTEGER,
+      allowNull: false,
+      primaryKey: true,
+    },
+    language: {
+      type: DataTypes.STRING,
+      allowNull: false,
+      primaryKey: true,
+    },
+    key: {
+      type: DataTypes.STRING,
+      allowNull: false,
+      primaryKey: true,
+    },
+    content: {
+      allowNull: false,
+      type: DataTypes.TEXT,
+    },
+  });
+
+  NotificationMessage.associate = models => {
+    NotificationMessage.belongsTo(models.HealthDepartment, {
+      foreignKey: 'departmentId',
+    });
+  };
+
+  return NotificationMessage;
+};
diff --git a/services/backend/src/database/models/riskLevel.js b/services/backend/src/database/models/riskLevel.js
new file mode 100644
index 0000000..1a895f0
--- /dev/null
+++ b/services/backend/src/database/models/riskLevel.js
@@ -0,0 +1,24 @@
+module.exports = (Sequelize, DataTypes) => {
+  const RiskLevel = Sequelize.define('RiskLevel', {
+    locationTransferTraceId: {
+      type: DataTypes.UUID,
+      allowNull: false,
+      primaryKey: true,
+    },
+    level: {
+      type: DataTypes.INTEGER,
+      allowNull: false,
+      primaryKey: true,
+    },
+  });
+
+  RiskLevel.associate = models => {
+    RiskLevel.belongsTo(models.LocationTransferTrace, {
+      foreignKey: 'locationTransferTraceId',
+      onDelete: 'CASCADE',
+      onUpdate: 'CASCADE',
+    });
+  };
+
+  return RiskLevel;
+};
diff --git a/services/backend/src/database/seeds/createDefaultNotificationMessages.js b/services/backend/src/database/seeds/createDefaultNotificationMessages.js
new file mode 100644
index 0000000..c13cd82
--- /dev/null
+++ b/services/backend/src/database/seeds/createDefaultNotificationMessages.js
@@ -0,0 +1,261 @@
+const { v4: uuid } = require('uuid');
+
+const MESSAGE_KEY_TITLE = 'title';
+const MESSAGE_KEY_BANNER = 'banner';
+const MESSAGE_KEY_SHORT_MESSAGE = 'shortMessage';
+const MESSAGE_KEY_MESSAGE = 'message';
+const LANG_EN = 'en';
+const LANG_DE = 'de';
+
+const defaultMessages = [
+  // WARNING LEVEL 1
+  {
+    key: MESSAGE_KEY_TITLE,
+    language: LANG_DE,
+    content: 'Hinweis: Mögliches Infektionsrisiko',
+    level: 1,
+  },
+  {
+    key: MESSAGE_KEY_BANNER,
+    language: LANG_DE,
+    content: 'Du hast eine neue Datenanfrage.',
+    level: 1,
+  },
+  {
+    key: MESSAGE_KEY_SHORT_MESSAGE,
+    language: LANG_DE,
+    content:
+      'Ein oder mehrere Gesundheitsämter haben im Rahmen einer Datenanfrage deine Kontaktdaten erhalten.',
+    level: 1,
+  },
+  {
+    key: MESSAGE_KEY_MESSAGE,
+    language: LANG_DE,
+    content: `Im Rahmen einer Kontaktnachverfolgung wurden deine Kontaktdaten von einem oder mehreren Gesundheitsämtern entschlüsselt. Diese werten gerade aus, ob ein Risiko besteht und werden sich möglicherweise bei dir melden.
+
+Bitte handle verantwortungsvoll.`,
+    level: 1,
+  },
+  {
+    key: MESSAGE_KEY_TITLE,
+    language: LANG_EN,
+    content: 'New data request',
+    level: 1,
+  },
+  {
+    key: MESSAGE_KEY_BANNER,
+    language: LANG_EN,
+    content: 'You have a new data request.',
+    level: 1,
+  },
+  {
+    key: MESSAGE_KEY_SHORT_MESSAGE,
+    language: LANG_EN,
+    content: 'One or more health departments have requested your contact data.',
+    level: 1,
+  },
+  {
+    key: MESSAGE_KEY_MESSAGE,
+    language: LANG_EN,
+    content: `While contact tracing, one or more health departments decrypted your contact data. They are currently evaluating whether there is a risk and will contact you if necessary.
+
+Please act responsibly.`,
+    level: 1,
+  },
+  // WARNING LEVEL 2
+  {
+    key: MESSAGE_KEY_TITLE,
+    language: LANG_DE,
+    content: 'Mögliches Infektionsrisiko',
+    level: 2,
+  },
+  {
+    key: MESSAGE_KEY_BANNER,
+    language: LANG_DE,
+    content: 'Achtung! Es besteht ein mögliches Infektionsrisiko.',
+    level: 2,
+  },
+  {
+    key: MESSAGE_KEY_SHORT_MESSAGE,
+    language: LANG_DE,
+    content:
+      'Du warst zeitgleich mit einer Person in einem luca-Standort eingecheckt, die später positiv auf das Coronavirus (SARS-CoV-2) getestet wurde.',
+    level: 2,
+  },
+  {
+    key: MESSAGE_KEY_MESSAGE,
+    language: LANG_DE,
+    content: `Bitte handle verantwortungsvoll, reduziere deine Kontakte und melde dich gegebenenfalls beim Gesundheitsamt – vor allem, wenn bei dir Symptome auftreten. Das Gesundheitsamt empfiehlt dir, einen Schnelltest zu machen.
+
+Dieser Hinweis wurde vom ((name)) ausgelöst.
+
+E-Mail-Adresse: ((email))
+Telefonnummer: ((phone))`,
+    level: 2,
+  },
+  {
+    key: MESSAGE_KEY_TITLE,
+    language: LANG_EN,
+    content: 'Potential infection risk',
+    level: 2,
+  },
+  {
+    key: MESSAGE_KEY_BANNER,
+    language: LANG_EN,
+    content: 'Attention! There is a potential risk of infection.',
+    level: 2,
+  },
+  {
+    key: MESSAGE_KEY_SHORT_MESSAGE,
+    language: LANG_EN,
+    content:
+      'You were checked into a luca place at the same time as a person who tested positive for coronavirus (SARS-CoV-2).',
+    level: 2,
+  },
+  {
+    key: MESSAGE_KEY_MESSAGE,
+    language: LANG_EN,
+    content: `Please act responsibly, reduce your contacts and contact your local health department – especially, if you experience any symptoms. The health department also advises doing a rapid test.
+
+This notification has been triggered by the health department ((name)).
+
+Email: ((email))
+Phone: ((phone))`,
+    level: 2,
+  },
+  // WARNING LEVEL 3
+  {
+    key: MESSAGE_KEY_TITLE,
+    language: LANG_DE,
+    content: 'Erhöhtes Infektionsrisiko',
+    level: 3,
+  },
+  {
+    key: MESSAGE_KEY_BANNER,
+    language: LANG_DE,
+    content: 'Achtung! Es besteht ein erhöhtes Infektionsrisiko.',
+    level: 3,
+  },
+  {
+    key: MESSAGE_KEY_SHORT_MESSAGE,
+    language: LANG_DE,
+    content:
+      'Du warst zeitgleich mit einer Person in einem luca-Standort eingecheckt, die später positiv auf das Coronavirus (SARS-CoV-2) getestet wurde. Ein Gesundheitsamt hat die Situation vor Ort ausgewertet und geht von einem erhöhten Infektionsrisiko für dich aus.',
+    level: 3,
+  },
+  {
+    key: MESSAGE_KEY_MESSAGE,
+    language: LANG_DE,
+    content: `Bitte handle verantwortungsvoll, reduziere deine Kontakte und melde dich gegebenenfalls beim Gesundheitsamt – vor allem, wenn bei dir Symptome auftreten. Das Gesundheitsamt empfiehlt dir, einen Schnelltest zu machen.
+
+Dieser Hinweis wurde vom ((name)) ausgelöst.
+
+E-Mail-Adresse: ((email))
+Telefonnummer: ((phone))`,
+    level: 3,
+  },
+  {
+    key: MESSAGE_KEY_TITLE,
+    language: LANG_EN,
+    content: 'Elevated infection risk',
+    level: 3,
+  },
+  {
+    key: MESSAGE_KEY_BANNER,
+    language: LANG_EN,
+    content: 'Attention! There is an elevated risk of infection.',
+    level: 3,
+  },
+  {
+    key: MESSAGE_KEY_SHORT_MESSAGE,
+    language: LANG_EN,
+    content:
+      'You were checked into a luca place at the same time as a person who tested positive for coronavirus (SARS-CoV-2). A health department has assessed the situation at this location and deems you at an elevated risk of infection.',
+    level: 3,
+  },
+  {
+    key: MESSAGE_KEY_MESSAGE,
+    language: LANG_EN,
+    content: `Please act responsibly, reduce your contacts and contact your local health department – especially, if you experience any symptoms. The health department also advises doing a rapid test.
+
+This notification has been triggered by the health department ((name)).
+
+Email: ((email))
+Phone: ((phone))`,
+    level: 3,
+  },
+  // WARNING LEVEL 4
+  {
+    key: MESSAGE_KEY_TITLE,
+    language: LANG_DE,
+    content: 'Mehrfache Datenanfrage',
+    level: 4,
+  },
+  {
+    key: MESSAGE_KEY_BANNER,
+    language: LANG_DE,
+    content:
+      'Achtung! Du warst in einen luca-Standort eingecheckt, in dem mehr als ein Gesundheitsamt Daten entschlüsselt hat.',
+    level: 4,
+  },
+  {
+    key: MESSAGE_KEY_SHORT_MESSAGE,
+    language: LANG_DE,
+    content:
+      'Du warst in einem luca-Standort eingecheckt, in dem mehr als ein Gesundheitsamt Daten entschlüsselt hat. Das bedeutet möglicherweise, dass sich dort mehrere Personen aufgehalten haben, die später positiv auf das Coronavirus (SARS-CoV-2) getestet wurden. Es besteht das Risiko, dass es sich um ein erhöhtes Infektionsgeschehen handelt.',
+    level: 4,
+  },
+  {
+    key: MESSAGE_KEY_MESSAGE,
+    language: LANG_DE,
+    content: `Bitte handle verantwortungsvoll, reduziere deine Kontakte und melde dich gegebenenfalls beim für dich zuständigen Gesundheitsamt – vor allem, wenn bei dir Symptome auftreten. Vielleicht hast du auch die Möglichkeit, einen Test zu machen.
+
+Dieser Hinweis wurde automatisiert von luca ausgelöst.`,
+    level: 4,
+  },
+  {
+    key: MESSAGE_KEY_TITLE,
+    language: LANG_EN,
+    content: 'Multiple data accesses',
+    level: 4,
+  },
+  {
+    key: MESSAGE_KEY_BANNER,
+    language: LANG_EN,
+    content:
+      'Attention! You were checked into a luca location for which multiple health departments have decrypted data.',
+    level: 4,
+  },
+  {
+    key: MESSAGE_KEY_SHORT_MESSAGE,
+    language: LANG_EN,
+    content:
+      'You were checked into a luca place for which multiple health departments have decrypted data. This could potentially mean that there were multiple people present who tested positive for coronavirus (SARS-CoV-2). Therefore there is a risk that this is an aggregation of infections.',
+    level: 4,
+  },
+  {
+    key: MESSAGE_KEY_MESSAGE,
+    language: LANG_EN,
+    content: `Please act responsibly, reduce your contacts and contact your local health department – especially, if you experience any symptoms. You should also get tested, if possible.
+
+This notification has been triggered automatically by luca.`,
+    level: 4,
+  },
+];
+
+module.exports = {
+  up: async queryInterface => {
+    try {
+      await queryInterface.bulkInsert(
+        'NotificationMessages',
+        defaultMessages.map(message => ({ uuid: uuid(), ...message }))
+      );
+    } catch (error) {
+      console.log(error, error.stack);
+      throw error;
+    }
+  },
+  down: () => {
+    console.warn('Not implemented.');
+  },
+};
diff --git a/services/backend/src/middlewares/rateLimit.js b/services/backend/src/middlewares/rateLimit.js
index d71e206..41578d3 100644
--- a/services/backend/src/middlewares/rateLimit.js
+++ b/services/backend/src/middlewares/rateLimit.js
@@ -129,10 +129,25 @@ const limitRequestsByFixedLinePhoneNumberPerDay = key =>
     }
   );
 
+const limitRequestsByUserPerHour = key =>
+  limitRequestsByFeatureFlag(
+    key || DEFAULT_RATE_LIMIT_HOUR,
+    {},
+    {
+      store: hourStore,
+      windowMs: hourDuration.as('ms'),
+      keyGenerator: request => {
+        const { user } = request;
+        return !user ? ipKeyGenerator(request) : user.uuid;
+      },
+    }
+  );
+
 module.exports = {
   limitRequestsPerMinute,
   limitRequestsPerHour,
   limitRequestsPerDay,
   limitRequestsByPhoneNumberPerDay,
   limitRequestsByFixedLinePhoneNumberPerDay,
+  limitRequestsByUserPerHour,
 };
diff --git a/services/backend/src/middlewares/requestMetrics.js b/services/backend/src/middlewares/requestMetrics.js
index 7f12f9d..c53c9a4 100644
--- a/services/backend/src/middlewares/requestMetrics.js
+++ b/services/backend/src/middlewares/requestMetrics.js
@@ -2,50 +2,41 @@ const responseTime = require('response-time');
 
 const { client } = require('../utils/metrics');
 
-const labelNames = ['method', 'statusCode', 'path'];
+const labelNames = ['method', 'statusCode', 'path', 'host'];
 
-const histogram = new client.Histogram({
+const requestDuration = new client.Histogram({
   name: 'http_request_duration_seconds',
   help: 'A histogram of the HTTP request durations in seconds.',
   buckets: [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10],
   labelNames,
 });
 
-const summary = new client.Summary({
-  name: 'summary_http_request_duration_seconds',
-  help: 'A summary of the HTTP request durations in seconds.',
-  percentiles: [0.5, 0.75, 0.95, 0.98, 0.99, 0.999],
-  labelNames,
-});
-
-const counter = new client.Counter({
-  name: 'http_requests_total',
-  help: 'The total number of handled HTTP requests.',
+const responseSize = new client.Histogram({
+  name: 'http_response_size_bytes',
+  help: 'A histogram of the HTTP response size in bytes.',
+  buckets: [100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000],
   labelNames,
 });
 
 module.exports = responseTime((request, response, time) => {
-  const { method, route, baseUrl } = request;
-  const { statusCode } = response;
+  const {
+    method,
+    route,
+    baseUrl,
+    headers: { host },
+  } = request;
+  const { statusCode, _contentLength } = response;
 
   if (!route) return;
   const path = `${baseUrl}${route.path}`.toLowerCase();
 
-  histogram.observe(
-    {
-      method,
-      path,
-      statusCode,
-    },
-    time / 1000
-  );
-  summary.observe(
-    {
-      method,
-      path,
-      statusCode,
-    },
-    time / 1000
-  );
-  counter.inc({ method, path, statusCode });
+  const labels = {
+    method,
+    path,
+    statusCode,
+    host,
+  };
+
+  requestDuration.observe(labels, time / 1000);
+  responseSize.observe(labels, _contentLength || 0);
 });
diff --git a/services/backend/src/middlewares/requireUser.js b/services/backend/src/middlewares/requireUser.js
index c164804..28b7696 100644
--- a/services/backend/src/middlewares/requireUser.js
+++ b/services/backend/src/middlewares/requireUser.js
@@ -4,6 +4,11 @@ const config = require('config');
 const { combineMiddlewares } = require('../utils/middlewares');
 const { verifyCertificateAgainstDTrustChain } = require('../utils/signedKeys');
 
+const UserTypes = {
+  HD_EMPLOYEE: 'HealthDepartmentEmployee',
+  OPERATOR: 'Operator',
+};
+
 const isUserOfType = (type, request) =>
   request.user && request.user.type === type;
 
@@ -18,7 +23,7 @@ const hasValidClientCertificate = (user, request) => {
 };
 
 const requireOperator = (request, response, next) => {
-  if (isUserOfType('Operator', request)) {
+  if (isUserOfType(UserTypes.OPERATOR, request)) {
     return next();
   }
   return response.sendStatus(status.UNAUTHORIZED);
@@ -26,7 +31,7 @@ const requireOperator = (request, response, next) => {
 
 const requireHealthDepartmentEmployee = (request, response, next) => {
   if (
-    isUserOfType('HealthDepartmentEmployee', request) &&
+    isUserOfType(UserTypes.HD_EMPLOYEE, request) &&
     (hasValidClientCertificate(request.user, request) || config.get('e2e'))
   ) {
     return next();
@@ -61,4 +66,6 @@ module.exports = {
   requireHealthDepartmentEmployee,
   requireHealthDepartmentAdmin,
   requireNonDeletedUser,
+  isUserOfType,
+  UserTypes,
 };
diff --git a/services/backend/src/passport/localHealthDepartmentEmployee.js b/services/backend/src/passport/localHealthDepartmentEmployee.js
index 0f633fb..e26b5c6 100644
--- a/services/backend/src/passport/localHealthDepartmentEmployee.js
+++ b/services/backend/src/passport/localHealthDepartmentEmployee.js
@@ -3,6 +3,9 @@
 
 const LocalStrategy = require('passport-local').Strategy;
 const database = require('../database');
+const { logEvent } = require('../utils/hdAuditLog');
+const { AuditLogEvents, AuditStatusType } = require('../constants/auditLog');
+const { UserTypes } = require('../middlewares/requireUser');
 
 const localStrategy = new LocalStrategy(
   {
@@ -21,10 +24,21 @@ const localStrategy = new LocalStrategy(
     const isValidPassword = await user.checkPassword(password);
 
     if (!isValidPassword) {
+      logEvent(user, {
+        type: AuditLogEvents.LOGIN,
+        status: AuditStatusType.ERROR_INVALID_PASSWORD,
+      });
+
       return done(null, false);
     }
 
-    user.type = 'HealthDepartmentEmployee';
+    user.type = UserTypes.HD_EMPLOYEE;
+
+    logEvent(user, {
+      type: AuditLogEvents.LOGIN,
+      status: AuditStatusType.SUCCESS,
+    });
+
     return done(null, user);
   }
 );
diff --git a/services/backend/src/passport/localOperator.js b/services/backend/src/passport/localOperator.js
index f4141f9..fbe3cab 100644
--- a/services/backend/src/passport/localOperator.js
+++ b/services/backend/src/passport/localOperator.js
@@ -3,6 +3,7 @@
 
 const LocalStrategy = require('passport-local').Strategy;
 const database = require('../database');
+const { UserTypes } = require('../middlewares/requireUser');
 
 const localStrategy = new LocalStrategy(
   {
@@ -26,7 +27,7 @@ const localStrategy = new LocalStrategy(
     if (!user.activated) {
       return done({ errorType: 'UNACTIVATED' }, null);
     }
-    user.type = 'Operator';
+    user.type = UserTypes.OPERATOR;
     return done(null, user);
   }
 );
diff --git a/services/backend/src/routes/internal/en2end.schema.js b/services/backend/src/routes/internal/en2end.schema.js
new file mode 100644
index 0000000..7665f3b
--- /dev/null
+++ b/services/backend/src/routes/internal/en2end.schema.js
@@ -0,0 +1,11 @@
+const { z } = require('../../utils/validation');
+
+const storeKeysSchema = z.object({
+  publicCertificate: z.string(),
+  signedPublicHDEKP: z.string(),
+  signedPublicHDSKP: z.string(),
+});
+
+module.exports = {
+  storeKeysSchema,
+};
diff --git a/services/backend/src/routes/internal/end2end.js b/services/backend/src/routes/internal/end2end.js
index 92bf769..c4d675f 100644
--- a/services/backend/src/routes/internal/end2end.js
+++ b/services/backend/src/routes/internal/end2end.js
@@ -1,8 +1,31 @@
 const router = require('express').Router();
+const status = require('http-status');
 const { performance } = require('perf_hooks');
-
+const { validateSchema } = require('../../middlewares/validateSchema');
 const database = require('../../database');
 
+const { storeKeysSchema } = require('./en2end.schema');
+
+router.post(
+  '/signHealthDepartment',
+  validateSchema(storeKeysSchema),
+  async (request, response) => {
+    const healthDepartment = await database.HealthDepartment.findOne({
+      where: {
+        name: 'neXenio Testing',
+      },
+    });
+
+    await healthDepartment.update({
+      publicCertificate: request.body.publicCertificate,
+      signedPublicHDEKP: request.body.signedPublicHDEKP,
+      signedPublicHDSKP: request.body.signedPublicHDSKP,
+    });
+
+    response.sendStatus(status.NO_CONTENT);
+  }
+);
+
 router.post('/clean', async (request, response) => {
   const t0 = performance.now();
 
@@ -37,6 +60,9 @@ router.post('/clean', async (request, response) => {
       {
         publicHDEKP: null,
         publicHDSKP: null,
+        publicCertificate: null,
+        signedPublicHDEKP: null,
+        signedPublicHDSKP: null,
       },
       { where: {} }
     ),
diff --git a/services/backend/src/routes/internal/jobs.js b/services/backend/src/routes/internal/jobs.js
index c01582e..b0b3048 100644
--- a/services/backend/src/routes/internal/jobs.js
+++ b/services/backend/src/routes/internal/jobs.js
@@ -11,8 +11,14 @@ const { GET_RANDOM_BYTES, hexToBase64 } = require('@lucaapp/crypto');
 
 const database = require('../../database');
 const featureFlag = require('../../utils/featureFlag');
-const { generateNotifications } = require('../../utils/notifications.js');
-const { updateBloomFilter } = require('../../utils/bloomFilter.js');
+const {
+  generateNotifications,
+} = require('../../utils/notifications/notificationsV3');
+const {
+  generateActiveChunk,
+  generateArchiveChunk,
+} = require('../../utils/notifications/notificationsV4');
+const { updateBloomFilter } = require('../../utils/bloomFilter');
 
 router.post('/deleteOldTraces', async (request, response) => {
   const t0 = performance.now();
@@ -263,9 +269,69 @@ router.post('/regenerateNotifications', async (request, response) => {
   });
 });
 
+router.post(
+  '/regenerateV4NotificationsActiveChunk',
+  async (request, response) => {
+    const t0 = performance.now();
+    await generateActiveChunk();
+
+    response.send({
+      time: performance.now() - t0,
+    });
+  }
+);
+
+router.post(
+  '/generateV4NotificationsArchiveChunk',
+  async (request, response) => {
+    const t0 = performance.now();
+    await generateArchiveChunk();
+
+    response.send({
+      time: performance.now() - t0,
+    });
+  }
+);
+
+router.post('/deleteOldV4NotificationChunks', async (request, response) => {
+  const t0 = performance.now();
+  const earliestTimeToKeep = moment().subtract(
+    config.get('luca.notificationChunks.maxAge'),
+    'hours'
+  );
+  const affectedRows = await database.NotificationsChunk.destroy({
+    where: {
+      createdAt: {
+        [Op.lt]: earliestTimeToKeep,
+      },
+    },
+    paranoid: false,
+    force: true,
+  });
+
+  response.send({ affectedRows, time: performance.now() - t0 });
+});
+
 router.post('/regenerateBloomFilter', async (request, response) => {
   updateBloomFilter();
   response.sendStatus(status.NO_CONTENT);
 });
 
+router.post('/deleteOldAuditLogs', async (request, response) => {
+  const t0 = performance.now();
+  const maxAge = config.get('luca.auditLogs.maxAge');
+  const affectedRows = await database.HealthDepartmentAuditLog.destroy({
+    where: {
+      createdAt: {
+        [Op.lt]: moment().subtract(maxAge, 'hours'),
+      },
+    },
+  });
+
+  response.send({
+    affectedRows,
+    time: performance.now() - t0,
+  });
+});
+
 module.exports = router;
diff --git a/services/backend/src/routes/v3/auth.js b/services/backend/src/routes/v3/auth.js
index a129b15..bd9ede1 100644
--- a/services/backend/src/routes/v3/auth.js
+++ b/services/backend/src/routes/v3/auth.js
@@ -3,15 +3,21 @@ const status = require('http-status');
 const passport = require('passport');
 const moment = require('moment');
 
+const { AuditLogEvents, AuditStatusType } = require('../../constants/auditLog');
+
 const { validateSchema } = require('../../middlewares/validateSchema');
 const { restrictOrigin } = require('../../middlewares/restrictOrigin');
 const {
   requireOperator,
   requireHealthDepartmentEmployee,
+  isUserOfType,
+  UserTypes,
 } = require('../../middlewares/requireUser');
 
 const { limitRequestsPerMinute } = require('../../middlewares/rateLimit');
 
+const { logEvent } = require('../../utils/hdAuditLog');
+
 const { authSchema } = require('./auth.schemas');
 
 router.post(
@@ -65,16 +71,36 @@ router.get(
       email: request.user.email,
       departmentId: request.user.departmentId,
       isAdmin: request.user.isAdmin,
+      isSigned: !!request.user.HealthDepartment.signedPublicHDSKP,
+      notificationsEnabled: request.user.HealthDepartment.notificationsEnabled,
     });
   }
 );
 
 router.post('/logout', restrictOrigin, (request, response) => {
-  request.logout();
-  request.session.destroy(error => {
+  const { user, logout, session } = request;
+  const isHDUser = isUserOfType(UserTypes.HD_EMPLOYEE, request);
+
+  logout();
+  session.destroy(error => {
     if (error) {
+      if (isHDUser) {
+        logEvent(user, {
+          type: AuditLogEvents.LOGOUT,
+          status: AuditStatusType.ERROR_UNKNOWN_SERVER_ERROR,
+        });
+      }
+
       throw error;
     }
+
+    if (isHDUser) {
+      logEvent(user, {
+        type: AuditLogEvents.LOGOUT,
+        status: AuditStatusType.SUCCESS,
+      });
+    }
+
     response.clearCookie('connect.sid');
     response.sendStatus(status.NO_CONTENT);
   });
diff --git a/services/backend/src/routes/v3/badges.js b/services/backend/src/routes/v3/badges.js
index 86a3f53..3eccbed 100644
--- a/services/backend/src/routes/v3/badges.js
+++ b/services/backend/src/routes/v3/badges.js
@@ -66,18 +66,22 @@ router.post(
   }
 );
 
-router.get('/bloomFilter', async (request, response) => {
-  const [bloomFilter, bloomFilterEtag] = await getBloomFilterAndEtag();
-  if (bloomFilterEtag === request.headers['If-None-Match']) {
-    return response.sendStatus(status.NOT_MODIFIED);
-  }
+router.get(
+  '/bloomFilter',
+  limitRequestsPerHour('badges_bloomfilter_get_ratelimit_hour'),
+  async (request, response) => {
+    const [bloomFilter, bloomFilterEtag] = await getBloomFilterAndEtag();
+    if (bloomFilterEtag === request.headers['If-None-Match']) {
+      return response.sendStatus(status.NOT_MODIFIED);
+    }
 
-  if (!bloomFilter) {
-    return response.sendStatus(status.NOT_FOUND);
-  }
+    if (!bloomFilter) {
+      return response.sendStatus(status.NOT_FOUND);
+    }
 
-  response.setHeader('ETag', bloomFilterEtag);
-  return response.send(bloomFilter);
-});
+    response.setHeader('ETag', bloomFilterEtag);
+    return response.send(bloomFilter);
+  }
+);
 
 module.exports = router;
diff --git a/services/backend/src/routes/v3/healthDepartmentEmployees.js b/services/backend/src/routes/v3/healthDepartmentEmployees.js
index 2b72a68..d43244d 100644
--- a/services/backend/src/routes/v3/healthDepartmentEmployees.js
+++ b/services/backend/src/routes/v3/healthDepartmentEmployees.js
@@ -1,11 +1,15 @@
 const router = require('express').Router();
 const status = require('http-status');
 const crypto = require('crypto');
+
+const { AuditLogEvents, AuditStatusType } = require('../../constants/auditLog');
 const { generatePassword } = require('../../utils/generators');
+const { logEvent } = require('../../utils/hdAuditLog');
 
 const database = require('../../database');
 const {
   validateSchema,
+  validateQuerySchema,
   validateParametersSchema,
 } = require('../../middlewares/validateSchema');
 const {
@@ -16,32 +20,41 @@ const passwordRouter = require('./healthDepartmentEmployees/password');
 const locationsRouter = require('./healthDepartmentEmployees/locations');
 
 const {
+  getSchema,
   createSchema,
   updateSchema,
   employeeIdParametersSchema,
 } = require('./healthDepartmentEmployees.schemas');
 
 // HD get all employees
-router.get('/', requireHealthDepartmentEmployee, async (request, response) => {
-  const healthDepartmentEmployees = await database.HealthDepartmentEmployee.findAll(
-    {
-      where: {
-        departmentId: request.user.departmentId,
-      },
-    }
-  );
-
-  return response.send(
-    healthDepartmentEmployees.map(employee => ({
-      uuid: employee.uuid,
-      email: employee.email,
-      phone: employee.phone,
-      firstName: employee.firstName,
-      lastName: employee.lastName,
-      isAdmin: employee.isAdmin,
-    }))
-  );
-});
+router.get(
+  '/',
+  requireHealthDepartmentEmployee,
+  validateQuerySchema(getSchema),
+  async (request, response) => {
+    const healthDepartmentEmployees = await database.HealthDepartmentEmployee.findAll(
+      {
+        where: {
+          departmentId: request.user.departmentId,
+        },
+        paranoid:
+          request.query.includeDeleted === undefined ||
+          request.query.includeDeleted === 'false',
+      }
+    );
+
+    return response.send(
+      healthDepartmentEmployees.map(employee => ({
+        uuid: employee.uuid,
+        email: employee.email,
+        phone: employee.phone,
+        firstName: employee.firstName,
+        lastName: employee.lastName,
+        isAdmin: employee.isAdmin,
+      }))
+    );
+  }
+);
 
 // delete employees
 router.delete(
@@ -57,10 +70,33 @@ router.delete(
     });
 
     if (!employee) {
+      logEvent(request.user, {
+        type: AuditLogEvents.DELETE_EMPLOYEE,
+        status: AuditStatusType.ERROR_TARGET_NOT_FOUND,
+      });
       return response.sendStatus(status.NOT_FOUND);
     }
 
-    await employee.destroy({ force: true });
+    await employee.update({
+      firstName: null,
+      lastName: null,
+      phone: null,
+    });
+    await database.TracingProcess.update(
+      { assigneeId: null },
+      { where: { assigneeId: request.params.employeeId } }
+    );
+
+    await employee.destroy();
+
+    logEvent(request.user, {
+      type: AuditLogEvents.DELETE_EMPLOYEE,
+      status: AuditStatusType.SUCCESS,
+      meta: {
+        target: employee.uuid,
+      },
+    });
+
     return response.sendStatus(status.NO_CONTENT);
   }
 );
@@ -80,15 +116,49 @@ router.patch(
     });
 
     if (!employee) {
+      logEvent(request.user, {
+        type: AuditLogEvents.UPDATE_EMPLOYEE,
+        status: AuditStatusType.ERROR_TARGET_NOT_FOUND,
+      });
+
       return response.sendStatus(status.NOT_FOUND);
     }
 
+    if (
+      typeof request.body.isAdmin !== 'undefined' &&
+      employee.isAdmin !== request.body.isAdmin
+    ) {
+      logEvent(request.user, {
+        type: AuditLogEvents.CHANGE_ROLE,
+        status: AuditStatusType.SUCCESS,
+        meta: {
+          target: employee.uuid,
+          isAdmin: request.body.isAdmin,
+        },
+      });
+    }
+
     await employee.update({
       isAdmin: request.body.isAdmin,
       firstName: request.body.firstName,
       lastName: request.body.lastName,
       phone: request.body.phone,
     });
+
+    logEvent(request.user, {
+      type: AuditLogEvents.UPDATE_EMPLOYEE,
+      status: AuditStatusType.SUCCESS,
+      meta: {
+        target: employee.uuid,
+        attributes: {
+          firstName: request.body.firstName !== undefined,
+          lastName: request.body.lastName !== undefined,
+          phone: request.body.phone !== undefined,
+          isAdmin: request.body.isAdmin !== undefined,
+        },
+      },
+    });
+
     return response.sendStatus(status.NO_CONTENT);
   }
 );
@@ -99,29 +169,61 @@ router.post(
   requireHealthDepartmentAdmin,
   validateSchema(createSchema),
   async (request, response) => {
-    const healthDepartment = await database.HealthDepartment.findOne({
+    const initialPassword = generatePassword(8);
+
+    const existingEmployee = await database.HealthDepartmentEmployee.findOne({
       where: {
-        uuid: request.user.departmentId,
+        email: request.body.email,
+        departmentId: request.user.departmentId,
       },
+      paranoid: false,
     });
 
-    if (!healthDepartment) {
-      return response.sendStatus(status.NOT_FOUND);
+    if (existingEmployee) {
+      const isDeletedEmployee = !!existingEmployee.deletedAt;
+
+      if (isDeletedEmployee) {
+        await existingEmployee.restore();
+        await existingEmployee.update({
+          firstName: request.body.firstName,
+          lastName: request.body.lastName,
+          phone: request.body.phone,
+          password: initialPassword,
+          isAdmin: false,
+          salt: crypto.randomBytes(16).toString('base64'),
+        });
+
+        logEvent(request.user, {
+          type: AuditLogEvents.REACTIVATE_EMPLOYEE,
+          status: AuditStatusType.SUCCESS,
+          meta: {
+            target: existingEmployee.uuid,
+          },
+        });
+      } else {
+        return response.sendStatus(status.CONFLICT);
+      }
+    } else {
+      const employee = await database.HealthDepartmentEmployee.create({
+        email: request.body.email,
+        firstName: request.body.firstName,
+        lastName: request.body.lastName,
+        phone: request.body.phone,
+        isAdmin: false,
+        departmentId: request.user.departmentId,
+        password: initialPassword,
+        salt: crypto.randomBytes(16).toString('base64'),
+      });
+
+      logEvent(request.user, {
+        type: AuditLogEvents.CREATE_EMPLOYEE,
+        status: AuditStatusType.SUCCESS,
+        meta: {
+          target: employee.uuid,
+        },
+      });
     }
 
-    const initialPassword = generatePassword(8);
-
-    await database.HealthDepartmentEmployee.create({
-      email: request.body.email,
-      firstName: request.body.firstName,
-      lastName: request.body.lastName,
-      phone: request.body.phone,
-      isAdmin: false,
-      departmentId: request.user.departmentId,
-      password: initialPassword,
-      salt: crypto.randomBytes(16).toString('base64'),
-    });
-
     response.status(status.CREATED);
     return response.send({ password: initialPassword });
   }
diff --git a/services/backend/src/routes/v3/healthDepartmentEmployees.openapi.yaml b/services/backend/src/routes/v3/healthDepartmentEmployees.openapi.yaml
index 30691f1..5616c58 100644
--- a/services/backend/src/routes/v3/healthDepartmentEmployees.openapi.yaml
+++ b/services/backend/src/routes/v3/healthDepartmentEmployees.openapi.yaml
@@ -46,7 +46,7 @@ paths:
       summary: Create new employee
       operationId: post-healthDepartmentEmployees
       responses:
-        '201':
+        201:
           description: Success
           content:
             application/json:
@@ -59,11 +59,13 @@ paths:
                     minLength: 1
                 required:
                   - password
-        '400':
+        400:
           $ref: '#/components/schemas/BadRequest'
-        '401':
+        401:
           $ref: '#/components/schemas/RequiredUserOfTypeHealthDepartmentEmployeeAdmin'
-        '404':
+        409:
+          description: Employee already exists
+        404:
           description: Health Department not found
       description: Create a new employee in the current user's department.
       requestBody:
@@ -147,3 +149,36 @@ paths:
         'Delete a [health
         department](https://luca-app.de/securityconcept/properties/actors.html#term-Health-Department)
         employee from the HealthDepartmentEmployee database table.'
+  /healthDepartmentEmployees/auditmapping:
+    get:
+      summary: Health department employee list for Audit Log mapping
+      tags:
+        - HealthDepartmentEmployees
+      responses:
+        200:
+          description: OK
+          content:
+            application/json:
+              schema:
+                type: array
+                description: ''
+                minItems: 1
+                uniqueItems: true
+                items:
+                  type: object
+                  properties:
+                    uuid:
+                      type: string
+                      minLength: 1
+                    email:
+                      type: string
+                      minLength: 1
+                  required:
+                    - uuid
+        401:
+          $ref: '#/components/schemas/RequiredUserOfTypeHealthDepartmentEmployee'
+      description:
+        "Returns mapping of employee UUID´s and email of the current user's
+        [health
+        department](https://luca-app.de/securityconcept/properties/actors.html#term-Health-Department),
+        including the current user."
diff --git a/services/backend/src/routes/v3/healthDepartmentEmployees.schemas.js b/services/backend/src/routes/v3/healthDepartmentEmployees.schemas.js
index bf78de4..9c01f96 100644
--- a/services/backend/src/routes/v3/healthDepartmentEmployees.schemas.js
+++ b/services/backend/src/routes/v3/healthDepartmentEmployees.schemas.js
@@ -1,5 +1,9 @@
 const { z } = require('../../utils/validation');
 
+const getSchema = z.object({
+  includeDeleted: z.enum(['true', 'false']).optional(),
+});
+
 const createSchema = z.object({
   email: z.email(),
   firstName: z.safeString().max(255),
@@ -19,6 +23,7 @@ const employeeIdParametersSchema = z.object({
 });
 
 module.exports = {
+  getSchema,
   createSchema,
   updateSchema,
   employeeIdParametersSchema,
diff --git a/services/backend/src/routes/v3/healthDepartmentEmployees/locations.js b/services/backend/src/routes/v3/healthDepartmentEmployees/locations.js
index 94859ae..b1751d9 100644
--- a/services/backend/src/routes/v3/healthDepartmentEmployees/locations.js
+++ b/services/backend/src/routes/v3/healthDepartmentEmployees/locations.js
@@ -10,6 +10,7 @@ const {
 const {
   validateParametersSchema,
 } = require('../../../middlewares/validateSchema');
+
 const { locationIdParametersSchema } = require('./locations.schemas');
 
 router.get(
diff --git a/services/backend/src/routes/v3/healthDepartmentEmployees/password.js b/services/backend/src/routes/v3/healthDepartmentEmployees/password.js
index 50454a3..6bf53c8 100644
--- a/services/backend/src/routes/v3/healthDepartmentEmployees/password.js
+++ b/services/backend/src/routes/v3/healthDepartmentEmployees/password.js
@@ -13,6 +13,11 @@ const { generatePassword } = require('../../../utils/generators');
 const { limitRequestsPerHour } = require('../../../middlewares/rateLimit');
 
 const { changePasswordSchema, renewSchema } = require('./password.schemas');
+const {
+  AuditLogEvents,
+  AuditStatusType,
+} = require('../../../constants/auditLog');
+const { logEvent } = require('../../../utils/hdAuditLog');
 
 // change password
 router.post(
@@ -31,6 +36,11 @@ router.post(
     );
 
     if (!isCurrentPasswordCorrect) {
+      logEvent(employee, {
+        type: AuditLogEvents.CHANGE_PASSWORD,
+        status: AuditStatusType.ERROR_INVALID_PASSWORD,
+      });
+
       return response.sendStatus(status.FORBIDDEN);
     }
 
@@ -47,6 +57,14 @@ router.post(
       }
     );
 
+    logEvent(employee, {
+      type: AuditLogEvents.CHANGE_PASSWORD,
+      status: AuditStatusType.SUCCESS,
+      meta: {
+        target: employee.uuid,
+      },
+    });
+
     return response.sendStatus(status.NO_CONTENT);
   }
 );
@@ -64,6 +82,14 @@ router.patch(
     );
 
     if (employee.departmentId !== request.user.departmentId) {
+      logEvent(request.user, {
+        type: AuditLogEvents.RESET_PASSWORD,
+        status: AuditStatusType.ERROR_TARGET_NOT_FOUND,
+        meta: {
+          target: employee.uuid,
+        },
+      });
+
       return response.sendStatus(status.FORBIDDEN);
     }
 
@@ -71,6 +97,14 @@ router.patch(
 
     employee.update({ password: newPassword });
 
+    logEvent(request.user, {
+      type: AuditLogEvents.RESET_PASSWORD,
+      status: AuditStatusType.SUCCESS,
+      meta: {
+        target: employee.uuid,
+      },
+    });
+
     response.status(status.OK);
     return response.send({ password: newPassword });
   }
diff --git a/services/backend/src/routes/v3/healthDepartments.js b/services/backend/src/routes/v3/healthDepartments.js
index 415273d..d6b8e7d 100644
--- a/services/backend/src/routes/v3/healthDepartments.js
+++ b/services/backend/src/routes/v3/healthDepartments.js
@@ -14,11 +14,13 @@ const {
 } = require('../../middlewares/validateSchema');
 const {
   requireHealthDepartmentEmployee,
+  requireHealthDepartmentAdmin,
 } = require('../../middlewares/requireUser');
 
 const {
   storeKeysSchema,
   departmentIdParametersSchema,
+  contactParametersSchema,
 } = require('./healthDepartments.schemas');
 
 /**
@@ -113,8 +115,31 @@ router.get(
       name: department.name,
       publicHDEKP: department.publicHDEKP,
       publicHDSKP: department.publicHDSKP,
+      email: department.email,
+      phone: department.phone,
     });
   }
 );
 
+router.patch(
+  '/contact',
+  requireHealthDepartmentAdmin,
+  validateSchema(contactParametersSchema),
+  async (request, response) => {
+    const department = await database.HealthDepartment.findByPk(
+      request.user.departmentId
+    );
+
+    if (!department) {
+      return response.send(status.NOT_FOUND);
+    }
+    const {
+      body: { email = '', phone = '' },
+    } = request;
+
+    await department.update({ email, phone });
+    return response.sendStatus(status.NO_CONTENT);
+  }
+);
+
 module.exports = router;
diff --git a/services/backend/src/routes/v3/healthDepartments.openapi.yaml b/services/backend/src/routes/v3/healthDepartments.openapi.yaml
index 7f9c98b..022a5ac 100644
--- a/services/backend/src/routes/v3/healthDepartments.openapi.yaml
+++ b/services/backend/src/routes/v3/healthDepartments.openapi.yaml
@@ -2,7 +2,10 @@ paths:
   /healthDepartments/keys:
     get:
       summary: Retrieve keys
-      description: 'Get the current user''s [health department](https://luca-app.de/securityconcept/properties/actors.html#term-Health-Department)''s public keys.'
+      description:
+        "Get the current user's [health
+        department](https://luca-app.de/securityconcept/properties/actors.html#term-Health-Department)'s
+        public keys."
       tags:
         - HealthDepartments
       responses:
@@ -96,6 +99,10 @@ paths:
                     type: string
                   name:
                     type: string
+                  email:
+                    type: string
+                  phone:
+                    type: string
                   publicHDEKP:
                     type: string
                     nullable: true
@@ -113,10 +120,33 @@ paths:
         name: departmentId
         in: path
         required: true
+  '/healthDepartments/contact':
+    patch:
+      summary: Update contact information department
+      tags:
+        - HealthDepartments
+      responses:
+        '204':
+          description: No Content
+        '400':
+          $ref: '#/components/schemas/BadRequest'
+        '404':
+          description: Department not found
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                email:
+                  type: string
+                phone:
+                  type: string
   /healthDepartments/privateKeySecret:
     get:
       summary: Get privateKeySecret
-      description: Get privateKeySecret for the current user's health department.
+      description:
+        Get privateKeySecret for the current user's health department.
       tags:
         - HealthDepartments
       responses:
@@ -132,4 +162,4 @@ paths:
         '401':
           $ref: '#/components/schemas/RequiredUserOfTypeHealthDepartmentEmployee'
         '404':
-          description: Department not found
\ No newline at end of file
+          description: Department not found
diff --git a/services/backend/src/routes/v3/healthDepartments.schemas.js b/services/backend/src/routes/v3/healthDepartments.schemas.js
index 8125ef5..fd0acdc 100644
--- a/services/backend/src/routes/v3/healthDepartments.schemas.js
+++ b/services/backend/src/routes/v3/healthDepartments.schemas.js
@@ -9,7 +9,13 @@ const departmentIdParametersSchema = z.object({
   departmentId: z.uuid(),
 });
 
+const contactParametersSchema = z.object({
+  email: z.email().optional(),
+  phone: z.phoneNumber().optional(),
+});
+
 module.exports = {
   storeKeysSchema,
   departmentIdParametersSchema,
+  contactParametersSchema,
 };
diff --git a/services/backend/src/routes/v3/keys/badges.js b/services/backend/src/routes/v3/keys/badges.js
index 25d8824..3a80b9e 100644
--- a/services/backend/src/routes/v3/keys/badges.js
+++ b/services/backend/src/routes/v3/keys/badges.js
@@ -31,6 +31,11 @@ const {
   rotateSchema,
   rekeySchema,
 } = require('./badges.schemas');
+const {
+  AuditLogEvents,
+  AuditStatusType,
+} = require('../../../constants/auditLog');
+const { logEvent } = require('../../../utils/hdAuditLog');
 
 const UNABLE_TO_SERIALIZE_ERROR_CODE = '40001';
 
@@ -171,20 +176,19 @@ router.post(
   requireHealthDepartmentEmployee,
   validateSchema(rekeySchema, '600kb'),
   async (request, response) => {
-    const healthDepartment = await database.HealthDepartment.findByPk(
-      request.user.departmentId
-    );
+    const healthDepartment = request.user.HealthDepartment;
+    const { encryptedBadgePrivateKeys, keyId, createdAt } = request.body;
+    const auditLogMeta = { keyId };
 
-    if (!healthDepartment) {
-      return response.sendStatus(status.NOT_FOUND);
+    if (!healthDepartment.signedPublicHDSKP) {
+      return response.sendStatus(status.FORBIDDEN);
     }
 
     // verify signatures of encryptedKeys
-    for (const encryptedBadgePrivateKey of request.body
-      .encryptedBadgePrivateKeys) {
+    for (const encryptedBadgePrivateKey of encryptedBadgePrivateKeys) {
       const signedData =
-        int32ToHex(request.body.keyId) +
-        int32ToHex(request.body.createdAt) +
+        int32ToHex(keyId) +
+        int32ToHex(createdAt) +
         base64ToHex(encryptedBadgePrivateKey.publicKey);
       const isValidSignature = VERIFY_EC_SHA256_DER_SIGNATURE(
         base64ToHex(healthDepartment.publicHDSKP),
@@ -193,36 +197,79 @@ router.post(
       );
 
       if (!isValidSignature) {
+        logEvent(request.user, {
+          type: AuditLogEvents.REKEY_BADGE_KEYPAIR,
+          status: AuditStatusType.ERROR_INVALID_SIGNATURE,
+          meta: auditLogMeta,
+        });
+
         return response.sendStatus(status.FORBIDDEN);
       }
     }
 
-    const encryptedBadgePrivateKeys = request.body.encryptedBadgePrivateKeys.map(
-      key => ({
-        keyId: request.body.keyId,
-        createdAt: moment.unix(request.body.createdAt),
-        issuerId: request.user.departmentId,
-        healthDepartmentId: key.healthDepartmentId,
-        data: key.data,
-        iv: key.iv,
-        mac: key.mac,
-        publicKey: key.publicKey,
-        signature: key.signature,
-      })
-    );
+    const badgePublicKey = await database.BadgePublicKey.findOne({
+      where: { keyId, createdAt: moment.unix(createdAt) },
+    });
 
-    await Promise.all(
-      encryptedBadgePrivateKeys.map(key =>
-        database.EncryptedBadgePrivateKey.upsert(key)
-      )
-    );
+    if (!badgePublicKey) {
+      logEvent(request.user, {
+        type: AuditLogEvents.REKEY_BADGE_KEYPAIR,
+        status: AuditStatusType.ERROR_TARGET_NOT_FOUND,
+        meta: auditLogMeta,
+      });
+
+      return response.sendStatus(status.CONFLICT);
+    }
+
+    for (const encryptedBadgePrivateKey of encryptedBadgePrivateKeys) {
+      const newKey = {
+        keyId,
+        createdAt: moment.unix(createdAt),
+        issuerId: request.user.departmentId,
+        healthDepartmentId: encryptedBadgePrivateKey.healthDepartmentId,
+        data: encryptedBadgePrivateKey.data,
+        iv: encryptedBadgePrivateKey.iv,
+        mac: encryptedBadgePrivateKey.mac,
+        publicKey: encryptedBadgePrivateKey.publicKey,
+        signature: encryptedBadgePrivateKey.signature,
+      };
+
+      const oldKey = await database.EncryptedBadgePrivateKey.findOne({
+        where: {
+          keyId,
+          healthDepartmentId: encryptedBadgePrivateKey.healthDepartmentId,
+        },
+      });
+
+      if (!oldKey) {
+        await database.EncryptedBadgePrivateKey.create(newKey);
+      } else if (oldKey.createdAt === badgePublicKey.createdAt) {
+        logEvent(request.user, {
+          type: AuditLogEvents.REKEY_BADGE_KEYPAIR,
+          status: AuditStatusType.ERROR_CONFLICT_KEY,
+          meta: auditLogMeta,
+        });
+        logger.warn('key already current.');
+      } else {
+        await oldKey.update(newKey);
+        logEvent(request.user, {
+          type: AuditLogEvents.REKEY_BADGE_KEYPAIR,
+          status: AuditStatusType.SUCCESS,
+          meta: {
+            oldKeyHd: oldKey.healthDepartmentId,
+            newKeyId: keyId,
+            oldKeyId: oldKey.keyId,
+          },
+        });
+      }
+    }
 
     return response.sendStatus(status.OK);
   }
 );
 
 /**
- * Rotate the badge keypair, similarly to the daily keypair, ensuring future
+ * Rotate the badge keypair, similarly to the badge keypair, ensuring future
  * badges will be generated using this new key
  * @see https://www.luca-app.de/securityoverview/properties/secrets.html#term-badge-keypair
  * @see https://www.luca-app.de/securityoverview/badge/badge_generation.html
@@ -233,15 +280,16 @@ router.post(
   validateSchema(rotateSchema, '600kb'),
   // eslint-disable-next-line sonarjs/cognitive-complexity
   async (request, response) => {
-    const healthDepartment = await database.HealthDepartment.findByPk(
-      request.user.departmentId
-    );
+    const healthDepartment = request.user.HealthDepartment;
+    const auditLogMeta = {
+      keyId: request.body.keyId,
+    };
 
-    if (!healthDepartment) {
-      return response.sendStatus(status.NOT_FOUND);
+    if (!healthDepartment.signedPublicHDSKP) {
+      return response.sendStatus(status.FORBIDDEN);
     }
 
-    // verify signature of daily key
+    // verify signature of badge key
     const signedBadgeKeyData =
       int32ToHex(request.body.keyId) +
       int32ToHex(request.body.createdAt) +
@@ -253,6 +301,12 @@ router.post(
     );
 
     if (!isValidBadgeKeySignature) {
+      logEvent(request.user, {
+        type: AuditLogEvents.ISSUE_BADGE_KEYPAIR,
+        status: AuditStatusType.ERROR_INVALID_SIGNATURE,
+        meta: auditLogMeta,
+      });
+
       return response.sendStatus(status.FORBIDDEN);
     }
 
@@ -270,6 +324,12 @@ router.post(
       );
 
       if (!isValidSignature) {
+        logEvent(request.user, {
+          type: AuditLogEvents.ISSUE_BADGE_KEYPAIR,
+          status: AuditStatusType.ERROR_INVALID_SIGNATURE,
+          meta: auditLogMeta,
+        });
+
         return response.sendStatus(status.FORBIDDEN);
       }
     }
@@ -278,6 +338,12 @@ router.post(
     const now = moment();
     const createdAt = moment.unix(request.body.createdAt);
     if (moment.duration(now.diff(createdAt)).as('minutes') > 5) {
+      logEvent(request.user, {
+        type: AuditLogEvents.ISSUE_BADGE_KEYPAIR,
+        status: AuditStatusType.ERROR_TIMEFRAME,
+        meta: auditLogMeta,
+      });
+
       return response.sendStatus(status.CONFLICT);
     }
 
@@ -296,11 +362,23 @@ router.post(
       // initial keyId should be 0
       if (!badgePublicKey && request.body.keyId !== 0) {
         await transaction.rollback();
+
+        logEvent(request.user, {
+          type: AuditLogEvents.ISSUE_BADGE_KEYPAIR,
+          status: AuditStatusType.ERROR_INVALID_KEYID,
+          meta: auditLogMeta,
+        });
+
         return response.sendStatus(status.CONFLICT);
       }
 
       // new keyId should +1 the old keyId
       if (badgePublicKey && badgePublicKey.keyId + 1 !== request.body.keyId) {
+        logEvent(request.user, {
+          type: AuditLogEvents.ISSUE_BADGE_KEYPAIR,
+          status: AuditStatusType.ERROR_INVALID_KEYID,
+          meta: auditLogMeta,
+        });
         await transaction.rollback();
         return response.sendStatus(status.CONFLICT);
       }
@@ -310,6 +388,12 @@ router.post(
         badgePublicKey &&
         request.body.keyId > config.get('keys.badge.targetKeyId')
       ) {
+        logEvent(request.user, {
+          type: AuditLogEvents.ISSUE_BADGE_KEYPAIR,
+          status: AuditStatusType.ERROR_INVALID_KEYID,
+          meta: auditLogMeta,
+        });
+
         await transaction.rollback();
         return response.sendStatus(status.CONFLICT);
       }
@@ -347,11 +431,24 @@ router.post(
       );
 
       await transaction.commit();
+
+      logEvent(request.user, {
+        type: AuditLogEvents.ISSUE_BADGE_KEYPAIR,
+        status: AuditStatusType.SUCCESS,
+        meta: auditLogMeta,
+      });
+
       return response.sendStatus(status.OK);
     } catch (error) {
       await transaction.rollback();
       logger.error(error);
 
+      logEvent(request.user, {
+        type: AuditLogEvents.ISSUE_BADGE_KEYPAIR,
+        status: AuditStatusType.ERROR_UNKNOWN_SERVER_ERROR,
+        meta: auditLogMeta,
+      });
+
       // Transaction error
       if (
         error &&
diff --git a/services/backend/src/routes/v3/keys/badges.schemas.js b/services/backend/src/routes/v3/keys/badges.schemas.js
index fde3b09..ac883ee 100644
--- a/services/backend/src/routes/v3/keys/badges.schemas.js
+++ b/services/backend/src/routes/v3/keys/badges.schemas.js
@@ -1,3 +1,4 @@
+const config = require('config');
 const { z } = require('../../../utils/validation');
 
 const keyIdParametersSchema = z.object({
@@ -9,31 +10,35 @@ const rotateSchema = z.object({
   signature: z.ecSignature(),
   createdAt: z.unixTimestamp(),
   keyId: z.number().int().min(0),
-  encryptedBadgePrivateKeys: z.array(
-    z.object({
-      healthDepartmentId: z.uuid(),
-      data: z.base64({ rawLength: 32 }),
-      iv: z.iv(),
-      mac: z.mac(),
-      publicKey: z.ecPublicKey(),
-      signature: z.ecSignature(),
-    })
-  ),
+  encryptedBadgePrivateKeys: z
+    .array(
+      z.object({
+        healthDepartmentId: z.uuid(),
+        data: z.base64({ rawLength: 32 }),
+        iv: z.iv(),
+        mac: z.mac(),
+        publicKey: z.ecPublicKey(),
+        signature: z.ecSignature(),
+      })
+    )
+    .max(config.get('luca.healthDepartments.maxAmount')),
 });
 
 const rekeySchema = z.object({
   keyId: z.badgeKeyId(),
   createdAt: z.unixTimestamp(),
-  encryptedBadgePrivateKeys: z.array(
-    z.object({
-      healthDepartmentId: z.uuid(),
-      data: z.base64({ rawLength: 32 }),
-      iv: z.iv(),
-      mac: z.mac(),
-      publicKey: z.ecPublicKey(),
-      signature: z.ecSignature(),
-    })
-  ),
+  encryptedBadgePrivateKeys: z
+    .array(
+      z.object({
+        healthDepartmentId: z.uuid(),
+        data: z.base64({ rawLength: 32 }),
+        iv: z.iv(),
+        mac: z.mac(),
+        publicKey: z.ecPublicKey(),
+        signature: z.ecSignature(),
+      })
+    )
+    .max(config.get('luca.healthDepartments.maxAmount')),
 });
 
 module.exports = {
diff --git a/services/backend/src/routes/v3/keys/daily.js b/services/backend/src/routes/v3/keys/daily.js
index d1f9f30..8a0884e 100644
--- a/services/backend/src/routes/v3/keys/daily.js
+++ b/services/backend/src/routes/v3/keys/daily.js
@@ -17,6 +17,12 @@ const {
   VERIFY_EC_SHA256_DER_SIGNATURE,
 } = require('@lucaapp/crypto');
 
+const {
+  AuditLogEvents,
+  AuditStatusType,
+} = require('../../../constants/auditLog');
+const { logEvent } = require('../../../utils/hdAuditLog');
+
 const {
   validateSchema,
   validateParametersSchema,
@@ -27,6 +33,9 @@ const logger = require('../../../utils/logger');
 const {
   requireHealthDepartmentEmployee,
 } = require('../../../middlewares/requireUser');
+const {
+  limitRequestsByUserPerHour,
+} = require('../../../middlewares/rateLimit');
 
 const {
   keyIdParametersSchema,
@@ -171,15 +180,17 @@ router.get(
 router.post(
   '/rotate',
   requireHealthDepartmentEmployee,
+  limitRequestsByUserPerHour('keys_daily_rotate_post_ratelimit_hour'),
   validateSchema(rotateSchema, '600kb'),
   // eslint-disable-next-line sonarjs/cognitive-complexity
   async (request, response) => {
-    const healthDepartment = await database.HealthDepartment.findByPk(
-      request.user.departmentId
-    );
+    const healthDepartment = request.user.HealthDepartment;
+    const auditLogMeta = {
+      keyId: request.body.keyId,
+    };
 
-    if (!healthDepartment) {
-      return response.sendStatus(status.NOT_FOUND);
+    if (!healthDepartment.signedPublicHDSKP) {
+      return response.sendStatus(status.FORBIDDEN);
     }
 
     // verify signature of daily key
@@ -194,6 +205,12 @@ router.post(
     );
 
     if (!isValidDailyKeySignature) {
+      logEvent(request.user, {
+        type: AuditLogEvents.ISSUE_DAILY_KEYPAIR,
+        status: AuditStatusType.ERROR_INVALID_SIGNATURE,
+        meta: auditLogMeta,
+      });
+
       return response.sendStatus(status.FORBIDDEN);
     }
 
@@ -211,6 +228,12 @@ router.post(
       );
 
       if (!isValidSignature) {
+        logEvent(request.user, {
+          type: AuditLogEvents.ISSUE_DAILY_KEYPAIR,
+          status: AuditStatusType.ERROR_INVALID_SIGNATURE,
+          meta: auditLogMeta,
+        });
+
         return response.sendStatus(status.FORBIDDEN);
       }
     }
@@ -219,6 +242,12 @@ router.post(
     const now = moment();
     const createdAt = moment.unix(request.body.createdAt);
     if (moment.duration(now.diff(createdAt)).as('minutes') > 5) {
+      logEvent(request.user, {
+        type: AuditLogEvents.ISSUE_DAILY_KEYPAIR,
+        status: AuditStatusType.ERROR_TIMEFRAME,
+        meta: auditLogMeta,
+      });
+
       return response.sendStatus(status.CONFLICT);
     }
 
@@ -236,6 +265,12 @@ router.post(
 
       // initial keyId should be 0
       if (!dailyPublicKey && request.body.keyId !== 0) {
+        logEvent(request.user, {
+          type: AuditLogEvents.ISSUE_DAILY_KEYPAIR,
+          status: AuditStatusType.ERROR_INVALID_KEYID,
+          meta: auditLogMeta,
+        });
+
         await transaction.rollback();
         return response.sendStatus(status.CONFLICT);
       }
@@ -247,6 +282,13 @@ router.post(
           request.body.keyId
       ) {
         await transaction.rollback();
+
+        logEvent(request.user, {
+          type: AuditLogEvents.ISSUE_DAILY_KEYPAIR,
+          status: AuditStatusType.ERROR_LIMIT_EXCEEDED,
+          meta: auditLogMeta,
+        });
+
         return response.sendStatus(status.CONFLICT);
       }
 
@@ -255,6 +297,11 @@ router.post(
 
         // old key should be at least 1 day old before rotation
         if (keyAge.asHours() < config.get('keys.daily.minKeyAge')) {
+          logEvent(request.user, {
+            type: AuditLogEvents.ISSUE_DAILY_KEYPAIR,
+            status: AuditStatusType.ERROR_LIMIT_EXCEEDED,
+            meta: auditLogMeta,
+          });
           await transaction.rollback();
           return response.sendStatus(status.CONFLICT);
         }
@@ -292,11 +339,24 @@ router.post(
       );
 
       await transaction.commit();
+
+      logEvent(request.user, {
+        type: AuditLogEvents.ISSUE_DAILY_KEYPAIR,
+        status: AuditStatusType.SUCCESS,
+        meta: auditLogMeta,
+      });
+
       return response.sendStatus(status.OK);
     } catch (error) {
       await transaction.rollback();
       logger.error(error);
 
+      logEvent(request.user, {
+        type: AuditLogEvents.ISSUE_DAILY_KEYPAIR,
+        status: AuditStatusType.ERROR_UNKNOWN_SERVER_ERROR,
+        meta: auditLogMeta,
+      });
+
       // Transaction error
       if (
         error &&
@@ -322,20 +382,20 @@ router.post(
   requireHealthDepartmentEmployee,
   validateSchema(rekeySchema, '600kb'),
   async (request, response) => {
-    const healthDepartment = await database.HealthDepartment.findByPk(
-      request.user.departmentId
-    );
+    const healthDepartment = request.user.HealthDepartment;
+    const { encryptedDailyPrivateKeys, keyId, createdAt } = request.body;
 
-    if (!healthDepartment) {
-      return response.sendStatus(status.NOT_FOUND);
+    const auditLogMeta = { keyId };
+
+    if (!healthDepartment.signedPublicHDSKP) {
+      return response.sendStatus(status.FORBIDDEN);
     }
 
     // verify signatures of encryptedKeys
-    for (const encryptedDailyPrivateKey of request.body
-      .encryptedDailyPrivateKeys) {
+    for (const encryptedDailyPrivateKey of encryptedDailyPrivateKeys) {
       const signedData =
-        int32ToHex(request.body.keyId) +
-        int32ToHex(request.body.createdAt) +
+        int32ToHex(keyId) +
+        int32ToHex(createdAt) +
         base64ToHex(encryptedDailyPrivateKey.publicKey);
       const isValidSignature = VERIFY_EC_SHA256_DER_SIGNATURE(
         base64ToHex(healthDepartment.publicHDSKP),
@@ -344,30 +404,73 @@ router.post(
       );
 
       if (!isValidSignature) {
+        logEvent(request.user, {
+          type: AuditLogEvents.REKEY_DAILY_KEYPAIR,
+          status: AuditStatusType.ERROR_INVALID_SIGNATURE,
+          meta: auditLogMeta,
+        });
+
         return response.sendStatus(status.FORBIDDEN);
       }
     }
 
-    const encryptedDailyPrivateKeys = request.body.encryptedDailyPrivateKeys.map(
-      // eslint-disable-next-line sonarjs/no-identical-functions
-      key => ({
-        keyId: request.body.keyId,
-        createdAt: moment.unix(request.body.createdAt),
-        issuerId: request.user.departmentId,
-        healthDepartmentId: key.healthDepartmentId,
-        data: key.data,
-        iv: key.iv,
-        mac: key.mac,
-        publicKey: key.publicKey,
-        signature: key.signature,
-      })
-    );
+    const dailyPublicKey = await database.DailyPublicKey.findOne({
+      where: { keyId, createdAt: moment.unix(createdAt) },
+    });
 
-    await Promise.all(
-      encryptedDailyPrivateKeys.map(key =>
-        database.EncryptedDailyPrivateKey.upsert(key)
-      )
-    );
+    if (!dailyPublicKey) {
+      logEvent(request.user, {
+        type: AuditLogEvents.REKEY_DAILY_KEYPAIR,
+        status: AuditStatusType.ERROR_TARGET_NOT_FOUND,
+        meta: auditLogMeta,
+      });
+
+      return response.sendStatus(status.CONFLICT);
+    }
+
+    for (const encryptedDailyPrivateKey of encryptedDailyPrivateKeys) {
+      const newKey = {
+        keyId,
+        createdAt: moment.unix(createdAt),
+        issuerId: request.user.departmentId,
+        healthDepartmentId: encryptedDailyPrivateKey.healthDepartmentId,
+        data: encryptedDailyPrivateKey.data,
+        iv: encryptedDailyPrivateKey.iv,
+        mac: encryptedDailyPrivateKey.mac,
+        publicKey: encryptedDailyPrivateKey.publicKey,
+        signature: encryptedDailyPrivateKey.signature,
+      };
+      const oldKey = await database.EncryptedDailyPrivateKey.findOne({
+        where: {
+          keyId,
+          healthDepartmentId: encryptedDailyPrivateKey.healthDepartmentId,
+        },
+      });
+
+      if (!oldKey) {
+        await database.EncryptedDailyPrivateKey.create(newKey);
+      } else if (oldKey.createdAt === dailyPublicKey.createdAt) {
+        logEvent(request.user, {
+          type: AuditLogEvents.REKEY_DAILY_KEYPAIR,
+          status: AuditStatusType.ERROR_CONFLICT_KEY,
+          meta: auditLogMeta,
+        });
+
+        logger.warn('key already current.');
+      } else {
+        logEvent(request.user, {
+          type: AuditLogEvents.REKEY_DAILY_KEYPAIR,
+          status: AuditStatusType.SUCCESS,
+          meta: {
+            oldKeyHd: oldKey.healthDepartmentId,
+            newKeyId: keyId,
+            oldKeyId: oldKey.keyId,
+          },
+        });
+
+        oldKey.update(newKey);
+      }
+    }
 
     return response.sendStatus(status.OK);
   }
diff --git a/services/backend/src/routes/v3/keys/daily.schemas.js b/services/backend/src/routes/v3/keys/daily.schemas.js
index 3ab18ea..77df73d 100644
--- a/services/backend/src/routes/v3/keys/daily.schemas.js
+++ b/services/backend/src/routes/v3/keys/daily.schemas.js
@@ -1,3 +1,4 @@
+const config = require('config');
 const { z } = require('../../../utils/validation');
 
 const keyIdParametersSchema = z.object({
@@ -9,31 +10,34 @@ const rotateSchema = z.object({
   signature: z.ecSignature(),
   createdAt: z.unixTimestamp(),
   keyId: z.dailyKeyId(),
-  encryptedDailyPrivateKeys: z.array(
-    z.object({
-      healthDepartmentId: z.uuid(),
-      data: z.base64({ rawLength: 32 }),
-      iv: z.iv(),
-      mac: z.mac(),
-      publicKey: z.ecPublicKey(),
-      signature: z.ecSignature(),
-    })
-  ),
+  encryptedDailyPrivateKeys: z
+    .array(
+      z.object({
+        healthDepartmentId: z.uuid(),
+        data: z.base64({ rawLength: 32 }),
+        iv: z.iv(),
+        mac: z.mac(),
+        publicKey: z.ecPublicKey(),
+        signature: z.ecSignature(),
+      })
+    )
+    .max(config.get('luca.healthDepartments.maxAmount')),
 });
-
 const rekeySchema = z.object({
   keyId: z.dailyKeyId(),
   createdAt: z.unixTimestamp(),
-  encryptedDailyPrivateKeys: z.array(
-    z.object({
-      healthDepartmentId: z.uuid(),
-      data: z.base64({ rawLength: 32 }),
-      iv: z.iv(),
-      mac: z.mac(),
-      publicKey: z.ecPublicKey(),
-      signature: z.ecSignature(),
-    })
-  ),
+  encryptedDailyPrivateKeys: z
+    .array(
+      z.object({
+        healthDepartmentId: z.uuid(),
+        data: z.base64({ rawLength: 32 }),
+        iv: z.iv(),
+        mac: z.mac(),
+        publicKey: z.ecPublicKey(),
+        signature: z.ecSignature(),
+      })
+    )
+    .max(config.get('luca.healthDepartments.maxAmount')),
 });
 
 module.exports = {
diff --git a/services/backend/src/routes/v3/locationTransfers.js b/services/backend/src/routes/v3/locationTransfers.js
index c8c9637..09107ea 100644
--- a/services/backend/src/routes/v3/locationTransfers.js
+++ b/services/backend/src/routes/v3/locationTransfers.js
@@ -19,9 +19,14 @@ const { requireOperator } = require('../../middlewares/requireUser');
 
 const {
   requireHealthDepartmentEmployee,
+  isUserOfType,
+  UserTypes,
 } = require('../../middlewares/requireUser');
+const { limitRequestsByUserPerHour } = require('../../middlewares/rateLimit');
 const logger = require('../../utils/logger');
 const { formatLocationName } = require('../../utils/format');
+const { AuditLogEvents, AuditStatusType } = require('../../constants/auditLog');
+const { logEvent } = require('../../utils/hdAuditLog');
 
 const {
   createSchema,
@@ -52,12 +57,25 @@ const mapTraceEncryptedData = trace => ({
 router.post(
   '/',
   requireHealthDepartmentEmployee,
+  limitRequestsByUserPerHour('location_transfer_post_ratelimit_hour'),
   validateSchema(createSchema),
   async (request, response) => {
     const transaction = await database.transaction();
     const maxLocations = config.get('luca.locationTransfers.maxLocations');
-    if (request.body.locations.length > maxLocations)
+
+    const isUserTransfer = !!request.body.userTransferId;
+
+    if (request.body.locations.length > maxLocations) {
+      logEvent(request.user, {
+        type: AuditLogEvents.CREATE_TRACING_PROCESS,
+        status: AuditStatusType.ERROR_LIMIT_EXCEEDED,
+        meta: {
+          viaTan: isUserTransfer,
+        },
+      });
+
       return response.sendStatus(status.REQUEST_ENTITY_TOO_LARGE);
+    }
 
     try {
       const tracingProcess = await database.TracingProcess.create(
@@ -68,7 +86,7 @@ router.post(
         { transaction }
       );
 
-      if (request.body.userTransferId) {
+      if (isUserTransfer) {
         const userTransfer = await database.UserTransfer.findByPk(
           request.body.userTransferId,
           { transaction }
@@ -76,6 +94,15 @@ router.post(
 
         if (!userTransfer) {
           await transaction.rollback();
+
+          logEvent(request.user, {
+            type: AuditLogEvents.CREATE_TRACING_PROCESS,
+            status: AuditStatusType.ERROR_INVALID_USER,
+            meta: {
+              viaTan: isUserTransfer,
+            },
+          });
+
           return response.sendStatus(status.NOT_FOUND);
         }
 
@@ -107,6 +134,15 @@ router.post(
               message: 'Missing location for location transfer',
               locations: request.body.locations,
             });
+
+            logEvent(request.user, {
+              type: AuditLogEvents.CREATE_TRACING_PROCESS,
+              status: AuditStatusType.ERROR_TARGET_NOT_FOUND,
+              meta: {
+                locationId: locationRequest?.locationId || location?.uuid,
+                viaTan: isUserTransfer,
+              },
+            });
             return null;
           }
 
@@ -145,6 +181,15 @@ router.post(
             { transaction }
           );
 
+          logEvent(request.user, {
+            type: AuditLogEvents.CREATE_TRACING_PROCESS,
+            status: AuditStatusType.SUCCESS,
+            meta: {
+              transferId: locationTransfer.uuid,
+              viaTan: isUserTransfer,
+            },
+          });
+
           return { location, locationTransfer };
         })
       );
@@ -154,6 +199,15 @@ router.post(
       return response.send({ tracingProcessId: tracingProcess.uuid });
     } catch (error) {
       await transaction.rollback();
+
+      logEvent(request.user, {
+        type: AuditLogEvents.CREATE_TRACING_PROCESS,
+        status: AuditStatusType.ERROR_UNKNOWN_SERVER_ERROR,
+        meta: {
+          viaTan: isUserTransfer,
+        },
+      });
+
       throw error;
     }
   }
@@ -232,6 +286,7 @@ router.get(
         contactedAt: transfer.contactedAt,
         createdAt: moment(transfer.createdAt).unix(),
         deletedAt: transfer.deletedAt && moment(transfer.deletedAt).unix(),
+        approvedAt: transfer.approvedAt && moment(transfer.approvedAt).unix(),
       }))
     );
   }
@@ -337,13 +392,25 @@ router.get(
   '/:transferId',
   validateParametersSchema(transferIdParametersSchema),
   async (request, response) => {
+    if (!request.user) {
+      return response.sendStatus(status.NOT_FOUND);
+    }
+
     const transfer = await database.LocationTransfer.findByPk(
       request.params.transferId
     );
+
     if (!transfer) {
       return response.sendStatus(status.NOT_FOUND);
     }
 
+    if (
+      isUserOfType(UserTypes.HD_EMPLOYEE, request) &&
+      transfer.departmentId !== request.user.departmentId
+    ) {
+      return response.sendStatus(status.FORBIDDEN);
+    }
+
     if (transfer.isCompleted) {
       return response.sendStatus(status.GONE);
     }
@@ -365,6 +432,13 @@ router.get(
       paranoid: false,
     });
 
+    if (
+      isUserOfType(UserTypes.OPERATOR, request) &&
+      location.operator !== request.user.uuid
+    ) {
+      return response.sendStatus(status.FORBIDDEN);
+    }
+
     const transferTraces = await database.LocationTransferTrace.findAll({
       where: {
         locationTransferId: transfer.uuid,
@@ -462,6 +536,12 @@ router.post(
       ],
     });
 
+    const amount = await database.LocationTransferTrace.count({
+      where: {
+        locationTransferId: transfer.uuid,
+      },
+    });
+
     if (!transfer) {
       return response.sendStatus(status.NOT_FOUND);
     }
@@ -470,6 +550,18 @@ router.post(
       return response.sendStatus(status.GONE);
     }
 
+    logEvent(request.user, {
+      type: AuditLogEvents.REQUEST_DATA,
+      status: AuditStatusType.SUCCESS,
+      meta: {
+        processId: transfer.tracingProcessId,
+        transferId: transfer.uuid,
+        locationId: transfer.locationId,
+        timeframe: transfer.time,
+        amountOfTraces: amount,
+      },
+    });
+
     try {
       sendShareDataRequestNotification(
         transfer.Location.Operator.email,
@@ -505,10 +597,17 @@ router.get(
       where: {
         uuid: request.params.transferId,
         departmentId: request.user.departmentId,
-        isCompleted: true,
       },
     });
     if (!transfer) {
+      logEvent(request.user, {
+        type: AuditLogEvents.VIEW_DATA,
+        status: AuditStatusType.ERROR_TARGET_NOT_FOUND,
+        meta: {
+          transferId: request.params.transferId,
+        },
+      });
+
       return response.sendStatus(status.NOT_FOUND);
     }
 
@@ -518,6 +617,18 @@ router.get(
       },
     });
 
+    logEvent(request.user, {
+      type: AuditLogEvents.VIEW_DATA,
+      status: AuditStatusType.SUCCESS,
+      meta: {
+        processId: transfer.tracingProcessId,
+        transferId: request.params.transferId,
+        locationId: transfer.locationId,
+        timeframe: transfer.time,
+        amountOfTraces: traces.length,
+      },
+    });
+
     return response.send({
       traces: traces.map(trace => ({
         traceId: trace.traceId,
@@ -554,6 +665,9 @@ const validateTransferId = async (request, response, next) => {
         {
           required: true,
           model: database.Location,
+          where: {
+            operator: request.user.uuid,
+          },
           attributes: ['name'],
           include: [
             {
@@ -595,6 +709,7 @@ const validateTransferId = async (request, response, next) => {
 router.post(
   '/:transferId',
   validateParametersSchema(transferIdParametersSchema),
+  requireOperator,
   validateTransferId,
   validateSchema(sendSchema, '20mb'),
   async (request, response) => {
@@ -677,12 +792,29 @@ router.post(
             transfer.Location.name || transfer.Location.LocationGroup.name,
         }
       );
-      await transfer.update({
-        isCompleted: true,
-      });
     } catch (error) {
       logger.error({ message: 'failed to send email', error });
     }
+    await transfer.update({
+      isCompleted: true,
+      approvedAt: moment(),
+    });
+
+    logEvent(
+      {
+        uuid: transfer.HealthDepartment.uuid,
+        departmentId: transfer.HealthDepartment.uuid,
+      },
+      {
+        type: AuditLogEvents.RECEIVE_DATA,
+        status: AuditStatusType.SUCCESS,
+        meta: {
+          transferId: transfer.uuid,
+          locationId: transfer.Location.uuid,
+        },
+      }
+    );
+
     return response.sendStatus(status.NO_CONTENT);
   }
 );
diff --git a/services/backend/src/routes/v3/locationTransfers.openapi.yaml b/services/backend/src/routes/v3/locationTransfers.openapi.yaml
index d3396ec..5e1e3c8 100644
--- a/services/backend/src/routes/v3/locationTransfers.openapi.yaml
+++ b/services/backend/src/routes/v3/locationTransfers.openapi.yaml
@@ -119,6 +119,8 @@ paths:
                       type: integer
                     deletedAt:
                       type: integer
+                    approvedAt:
+                      type: integer
                   required:
                     - uuid
                     - groupName
@@ -243,6 +245,8 @@ paths:
                   - traces
         '400':
           $ref: '#/components/schemas/BadRequest'
+        '403':
+          description: Requesting user is not owner
         '404':
           description: Transfer or Department not found
         '410':
@@ -257,6 +261,8 @@ paths:
       responses:
         '200':
           description: OK
+        '403':
+          description: Requesting user is not owner
         '400':
           $ref: '#/components/schemas/BadRequest'
       requestBody:
diff --git a/services/backend/src/routes/v3/notifications.js b/services/backend/src/routes/v3/notifications.js
index 26af0e1..158a215 100644
--- a/services/backend/src/routes/v3/notifications.js
+++ b/services/backend/src/routes/v3/notifications.js
@@ -1,16 +1,23 @@
 const router = require('express').Router();
-const { getNotifications } = require('../../utils/notifications');
+const {
+  getNotifications,
+} = require('../../utils/notifications/notificationsV3');
+const { limitRequestsPerHour } = require('../../middlewares/rateLimit');
 
 /**
  * Provides hashed trace IDs allowing users to be notified if their data was accessed by a health department.
  * @see https://www.luca-app.de/securityoverview/processes/tracing_find_contacts.html#notifying-guests-about-data-access
  */
-router.get('/traces', async (request, response) => {
-  const cachedResponse = await getNotifications();
-  if (cachedResponse) {
-    return response.send(JSON.parse(cachedResponse));
+router.get(
+  '/traces',
+  limitRequestsPerHour('notifications_traces_get_ratelimit_hour'),
+  async (request, response) => {
+    const cachedResponse = await getNotifications();
+    if (cachedResponse) {
+      return response.send(JSON.parse(cachedResponse));
+    }
+    return response.send([]);
   }
-  return response.send([]);
-});
+);
 
 module.exports = router;
diff --git a/services/backend/src/routes/v3/tracingProcesses.js b/services/backend/src/routes/v3/tracingProcesses.js
index 8b35f08..292c674 100644
--- a/services/backend/src/routes/v3/tracingProcesses.js
+++ b/services/backend/src/routes/v3/tracingProcesses.js
@@ -194,7 +194,7 @@ router
         didRequestLocations: body.didRequestLocations,
         isCompleted: body.isCompleted,
         assigneeId: body.assigneeId,
-        ...(body.note && {
+        ...(body.note !== undefined && {
           note: body.note,
           noteIV: body.noteIV,
           noteMAC: body.noteMAC,
diff --git a/services/backend/src/routes/v3/tracingProcesses.schemas.js b/services/backend/src/routes/v3/tracingProcesses.schemas.js
index 34d85ad..f5b98f6 100644
--- a/services/backend/src/routes/v3/tracingProcesses.schemas.js
+++ b/services/backend/src/routes/v3/tracingProcesses.schemas.js
@@ -5,11 +5,11 @@ const patchSchema = z
     didRequestLocations: z.boolean().optional(),
     isCompleted: z.boolean().optional(),
     assigneeId: z.uuid().nullable().optional(),
-    note: z.base64({ max: 1000 }).optional(),
-    noteIV: z.iv().optional(),
-    noteMAC: z.mac().optional(),
-    noteSignature: z.ecSignature().optional(),
-    notePublicKey: z.ecPublicKey().optional(),
+    note: z.base64({ max: 1000 }).nullable().optional(),
+    noteIV: z.iv().nullable().optional(),
+    noteMAC: z.mac().nullable().optional(),
+    noteSignature: z.ecSignature().nullable().optional(),
+    notePublicKey: z.ecPublicKey().nullable().optional(),
   })
   .refine(value => {
     // check if note property exists all other required properties also exist
@@ -19,7 +19,9 @@ const patchSchema = z
           'noteMAC',
           'noteSignature',
           'notePublicKey',
-        ].every(property => Object.hasOwn(value, property))
+        ].every(property =>
+          Object.prototype.hasOwnProperty.call(value, property)
+        )
       : true;
   });
 
diff --git a/services/backend/src/routes/v4.js b/services/backend/src/routes/v4.js
index a262233..2e30621 100644
--- a/services/backend/src/routes/v4.js
+++ b/services/backend/src/routes/v4.js
@@ -6,6 +6,8 @@ const timeRouter = require('./v4/time');
 const healthDepartmentsRouter = require('./v4/healthDepartments');
 const healthDepartmentEmployeesRouter = require('./v4/healthDepartmentEmployees');
 const signingToolRouter = require('./v4/signingToolDownload');
+const notificationsRouter = require('./v4/notifications');
+const riskLevelsRouter = require('./v4/riskLevels');
 
 router.use('/auth', authRouter);
 router.use('/keys', keysRouter);
@@ -13,5 +15,7 @@ router.use('/time', timeRouter);
 router.use('/healthDepartments', healthDepartmentsRouter);
 router.use('/healthDepartmentEmployees', healthDepartmentEmployeesRouter);
 router.use('/signingTool', signingToolRouter);
+router.use('/notifications', notificationsRouter);
+router.use('/riskLevels', riskLevelsRouter);
 
 module.exports = router;
diff --git a/services/backend/src/routes/v4/auth.js b/services/backend/src/routes/v4/auth.js
index a29d62a..acf7ffb 100644
--- a/services/backend/src/routes/v4/auth.js
+++ b/services/backend/src/routes/v4/auth.js
@@ -7,6 +7,9 @@ const { restrictOrigin } = require('../../middlewares/restrictOrigin');
 const { limitRequestsPerMinute } = require('../../middlewares/rateLimit');
 
 const { authSchema } = require('./auth.schemas');
+const { AuditLogEvents, AuditStatusType } = require('../../constants/auditLog');
+const { logEvent } = require('../../utils/hdAuditLog');
+const { isUserOfType, UserTypes } = require('../../middlewares/requireUser');
 
 // Certificate check is done in Load Balancer
 router.post(
@@ -23,11 +26,29 @@ router.post(
 );
 
 router.post('/logout', restrictOrigin, (request, response) => {
-  request.logout();
-  request.session.destroy(error => {
+  const { user, logout, session } = request;
+  const isHDUser = isUserOfType(UserTypes.HD_EMPLOYEE, request);
+
+  logout();
+  session.destroy(error => {
     if (error) {
+      if (isHDUser) {
+        logEvent(user, {
+          type: AuditLogEvents.LOGOUT,
+          status: AuditStatusType.ERROR_UNKNOWN_SERVER_ERROR,
+        });
+      }
+
       throw error;
     }
+
+    if (isHDUser) {
+      logEvent(user, {
+        type: AuditLogEvents.LOGOUT,
+        status: AuditStatusType.SUCCESS,
+      });
+    }
+
     response.clearCookie('connect.sid');
     response.sendStatus(status.NO_CONTENT);
   });
diff --git a/services/backend/src/routes/v4/healthDepartments.js b/services/backend/src/routes/v4/healthDepartments.js
index c144c64..c528a13 100644
--- a/services/backend/src/routes/v4/healthDepartments.js
+++ b/services/backend/src/routes/v4/healthDepartments.js
@@ -1,5 +1,7 @@
 const router = require('express').Router();
+const moment = require('moment');
 const status = require('http-status');
+const { Op } = require('sequelize');
 
 const database = require('../../database');
 const ApiError = require('../../utils/apiError');
@@ -7,18 +9,23 @@ const ApiError = require('../../utils/apiError');
 const {
   validateSchema,
   validateParametersSchema,
+  validateQuerySchema,
 } = require('../../middlewares/validateSchema');
 const {
   requireHealthDepartmentEmployee,
   requireHealthDepartmentAdmin,
 } = require('../../middlewares/requireUser');
+
+const { verifySignedPublicKeys } = require('../../utils/signedKeys');
+const { AuditLogEvents, AuditStatusType } = require('../../constants/auditLog');
+const { logEvent, entriesToPlainText } = require('../../utils/hdAuditLog');
+
 const {
   storeSignedKeysSchema,
   departmentIdParametersSchema,
+  auditLogDownloadQuerySchema,
 } = require('./healthDepartments.schemas');
 
-const { verifySignedPublicKeys } = require('../../utils/signedKeys');
-
 // set signed keys
 router.post(
   '/signedKeys',
@@ -82,8 +89,40 @@ router.get(
       publicCertificate: department.publicCertificate,
       signedPublicHDEKP: department.signedPublicHDEKP,
       signedPublicHDSKP: department.signedPublicHDSKP,
+      email: department.email,
+      phone: department.phone,
     });
   }
 );
 
+router.get(
+  '/auditlog/download',
+  requireHealthDepartmentAdmin,
+  validateQuerySchema(auditLogDownloadQuerySchema),
+  async (request, response) => {
+    const entries = await database.HealthDepartmentAuditLog.findAll({
+      where: {
+        departmentId: request.user.departmentId,
+        createdAt: {
+          [Op.lt]: moment.unix(request.query.timeframe[0]),
+          [Op.gt]: moment.unix(request.query.timeframe[1]),
+        },
+      },
+    });
+
+    logEvent(request.user, {
+      type: AuditLogEvents.DOWNLOAD_AUDITLOG,
+      status: AuditStatusType.SUCCESS,
+      meta: {
+        timeframe: request.query.timeframe.map(time =>
+          moment.unix(time).format()
+        ),
+      },
+    });
+
+    response.set('Content-Type', 'text/plain');
+    return response.send(entriesToPlainText(entries));
+  }
+);
+
 module.exports = router;
diff --git a/services/backend/src/routes/v4/healthDepartments.schemas.js b/services/backend/src/routes/v4/healthDepartments.schemas.js
index 9ccf3c2..a85dc8e 100644
--- a/services/backend/src/routes/v4/healthDepartments.schemas.js
+++ b/services/backend/src/routes/v4/healthDepartments.schemas.js
@@ -10,7 +10,12 @@ const departmentIdParametersSchema = z.object({
   departmentId: z.uuid(),
 });
 
+const auditLogDownloadQuerySchema = z.object({
+  timeframe: z.array(z.string()).length(2),
+});
+
 module.exports = {
   storeSignedKeysSchema,
   departmentIdParametersSchema,
+  auditLogDownloadQuerySchema,
 };
diff --git a/services/backend/src/routes/v4/notifications.schemas.ts b/services/backend/src/routes/v4/notifications.schemas.ts
new file mode 100644
index 0000000..1685105
--- /dev/null
+++ b/services/backend/src/routes/v4/notifications.schemas.ts
@@ -0,0 +1,5 @@
+import { z } from 'utils/validation';
+
+export const chunkIdParametersSchema = z.object({
+  chunkId: z.base64({ rawLength: 16 }),
+});
diff --git a/services/backend/src/routes/v4/notifications.ts b/services/backend/src/routes/v4/notifications.ts
new file mode 100644
index 0000000..c9d09fe
--- /dev/null
+++ b/services/backend/src/routes/v4/notifications.ts
@@ -0,0 +1,59 @@
+import { Router } from 'express';
+import status from 'http-status';
+import {
+  getActiveChunk,
+  getArchivedChunk,
+} from 'utils/notifications/notificationsV4';
+import { getNotificationConfig } from 'utils/notifications/notificationsConfig';
+import { validateParametersSchema } from 'middlewares/validateSchema';
+import { limitRequestsPerHour } from 'middlewares/rateLimit';
+import { chunkIdParametersSchema } from './notifications.schemas';
+
+const router = Router();
+
+/**
+ * Provides hashed trace IDs allowing users to be notified if their data was accessed by a health department.
+ * @see https://www.luca-app.de/securityoverview/processes/tracing_find_contacts.html#notifying-guests-about-data-access
+ */
+router.get(
+  '/traces',
+  limitRequestsPerHour(
+    'notifications_v4_traces_active_chunk_get_ratelimit_hour'
+  ),
+  async (request, response) => {
+    const notificationChunk = await getActiveChunk();
+
+    if (notificationChunk) {
+      return response.send(notificationChunk);
+    }
+    return response.send(status.NOT_FOUND);
+  }
+);
+
+router.get(
+  '/traces/:chunkId',
+  limitRequestsPerHour(
+    'notifications_v4_traces_archived_chunk_get_ratelimit_hour'
+  ),
+  validateParametersSchema(chunkIdParametersSchema),
+  async (request, response) => {
+    const notificationChunk = await getArchivedChunk(request.params.chunkId);
+    if (notificationChunk) {
+      return response.send(notificationChunk);
+    }
+    return response.send(status.NOT_FOUND);
+  }
+);
+router.get(
+  '/config',
+  limitRequestsPerHour('notifications_v4_config_get_ratelimit_hour'),
+  async (request, response) => {
+    const notificationConfig = await getNotificationConfig();
+    if (notificationConfig) {
+      return response.send(notificationConfig);
+    }
+    return response.send(status.NOT_FOUND);
+  }
+);
+
+module.exports = router;
diff --git a/services/backend/src/routes/v4/riskLevels.schemas.ts b/services/backend/src/routes/v4/riskLevels.schemas.ts
new file mode 100644
index 0000000..d251b8d
--- /dev/null
+++ b/services/backend/src/routes/v4/riskLevels.schemas.ts
@@ -0,0 +1,14 @@
+import { z } from 'utils/validation';
+
+export const addRiskLevelSchema = z.object({
+  locationTransferId: z.uuid(),
+});
+
+export const getRiskLevelParameterSchema = z.object({
+  locationTransferId: z.uuid(),
+});
+
+export const addRiskLevelTracesSchema = z.object({
+  locationTransferId: z.uuid(),
+  traceIds: z.array(z.traceId()),
+});
diff --git a/services/backend/src/routes/v4/riskLevels.ts b/services/backend/src/routes/v4/riskLevels.ts
new file mode 100644
index 0000000..98f94fc
--- /dev/null
+++ b/services/backend/src/routes/v4/riskLevels.ts
@@ -0,0 +1,159 @@
+import { Router } from 'express';
+import { Op } from 'sequelize';
+import status from 'http-status';
+import database from 'database/models';
+import {
+  validateSchema,
+  validateParametersSchema,
+} from 'middlewares/validateSchema';
+import { requireHealthDepartmentEmployee } from 'middlewares/requireUser';
+import {
+  addRiskLevelSchema,
+  getRiskLevelParameterSchema,
+  addRiskLevelTracesSchema,
+} from './riskLevels.schemas';
+
+const RISK_LEVEL_2 = 2;
+const RISK_LEVEL_3 = 3;
+
+const router = Router();
+
+router.post(
+  '/',
+  requireHealthDepartmentEmployee,
+  validateSchema(addRiskLevelSchema),
+  async (request, response) => {
+    const user = request.user as IHealthDepartmentEmployee;
+
+    const locationTransfer = await database.LocationTransfer.findOne({
+      where: {
+        uuid: request.body.locationTransferId,
+        departmentId: user.departmentId,
+      },
+    });
+
+    if (!locationTransfer) return response.sendStatus(status.NOT_FOUND);
+
+    const locationTransferTraces = await database.LocationTransferTrace.findAll(
+      {
+        where: { locationTransferId: locationTransfer.uuid },
+        include: {
+          model: database.RiskLevel,
+        },
+      }
+    );
+
+    const duplicateRiskLevel = locationTransferTraces.some(
+      // @ts-ignore - any until models are typed
+      locationTransferTrace =>
+        locationTransferTrace.RiskLevels.some(
+          // @ts-ignore - any until models are typed
+          riskLevel => riskLevel.level === RISK_LEVEL_2
+        )
+    );
+
+    if (duplicateRiskLevel) return response.sendStatus(status.CONFLICT);
+
+    // @ts-ignore - any until models are typed
+    const riskLevels = locationTransferTraces.map(locationTransferTrace => ({
+      level: RISK_LEVEL_2,
+      locationTransferTraceId: locationTransferTrace.uuid,
+    }));
+
+    await database.RiskLevel.bulkCreate(riskLevels);
+
+    return response.sendStatus(status.NO_CONTENT);
+  }
+);
+
+router.get(
+  '/:locationTransferId',
+  requireHealthDepartmentEmployee,
+  validateParametersSchema(getRiskLevelParameterSchema),
+  async (request, response) => {
+    const user = request.user as IHealthDepartmentEmployee;
+    const locationTransfer = await database.LocationTransfer.findOne({
+      where: {
+        uuid: request.params.locationTransferId,
+        departmentId: user.departmentId,
+      },
+    });
+
+    if (!locationTransfer) return response.sendStatus(status.NOT_FOUND);
+
+    const locationTransferTraces = await database.LocationTransferTrace.findAll(
+      {
+        where: {
+          locationTransferId: request.params.locationTransferId,
+        },
+        include: {
+          model: database.RiskLevel,
+        },
+      }
+    );
+
+    const traceIdsWithRiskLevels = locationTransferTraces.map(
+      // @ts-ignore - any until models are typed
+      locationTransferTrace => {
+        const riskLevels = locationTransferTrace.RiskLevels.map(
+          // @ts-ignore - any until models are typed
+          riskLevel => riskLevel.level
+        );
+        return { traceId: locationTransferTrace.traceId, riskLevels };
+      }
+    );
+    return response.send(traceIdsWithRiskLevels);
+  }
+);
+
+router.post(
+  '/traces',
+  requireHealthDepartmentEmployee,
+  validateSchema(addRiskLevelTracesSchema),
+  async (request, response) => {
+    const user = request.user as IHealthDepartmentEmployee;
+    const locationTransfer = await database.LocationTransfer.findOne({
+      where: {
+        uuid: request.body.locationTransferId,
+        departmentId: user.departmentId,
+      },
+    });
+
+    if (!locationTransfer) return response.sendStatus(status.NOT_FOUND);
+
+    const locationTransferTraces = await database.LocationTransferTrace.findAll(
+      {
+        where: {
+          locationTransferId: locationTransfer.uuid,
+          traceId: { [Op.in]: request.body.traceIds },
+        },
+        include: {
+          model: database.RiskLevel,
+        },
+      }
+    );
+
+    const duplicateRiskLevel = locationTransferTraces.some(
+      // @ts-ignore - any until models are typed
+      locationTransferTrace =>
+        locationTransferTrace.RiskLevels.some(
+          // @ts-ignore - any until models are typed
+          riskLevel => riskLevel.level === RISK_LEVEL_3
+        )
+    );
+
+    if (duplicateRiskLevel) return response.sendStatus(status.CONFLICT);
+
+    // @ts-ignore - any until models are typed
+    const riskLevels = locationTransferTraces.map(locationTransferTrace => ({
+      level: RISK_LEVEL_3,
+      locationTransferTraceId: locationTransferTrace.uuid,
+    }));
+
+    database.RiskLevel.bulkCreate(riskLevels);
+
+    return response.sendStatus(status.NO_CONTENT);
+  }
+);
+
+module.exports = router;
diff --git a/services/backend/src/utils/bloomFilter.js b/services/backend/src/utils/bloomFilter.js
index a8ce456..5c6f72a 100644
--- a/services/backend/src/utils/bloomFilter.js
+++ b/services/backend/src/utils/bloomFilter.js
@@ -1,3 +1,5 @@
+import cache from 'utils/redisCache';
+
 const moment = require('moment');
 const { Worker } = require('worker_threads');
 const { Op } = require('sequelize');
@@ -9,7 +11,6 @@ const {
   set: redisSet,
   get: redisGet,
 } = require('./redis');
-const cache = require('./redisCache');
 const database = require('../database');
 
 const BLOOM_FILTER_BUFFER_KEY = 'BadgeBloomFilterBuffer';
diff --git a/services/backend/src/utils/generators.js b/services/backend/src/utils/generators.js
index 1bd6518..dea2311 100644
--- a/services/backend/src/utils/generators.js
+++ b/services/backend/src/utils/generators.js
@@ -24,9 +24,9 @@ const generateSupportCode = () => {
   return supportCode.join('');
 };
 
-const generatePassword = () => {
+const generatePassword = (length = 12) => {
   const password = [];
-  for (let count = 0; count < 12; count += 1) {
+  for (let count = 0; count < length; count += 1) {
     password.push(PASSWORD_CHARSET[crypto.randomInt(PASSWORD_CHARSET.length)]);
   }
   return password.join('');
diff --git a/services/backend/src/utils/hdAuditLog.ts b/services/backend/src/utils/hdAuditLog.ts
new file mode 100644
index 0000000..611bebd
--- /dev/null
+++ b/services/backend/src/utils/hdAuditLog.ts
@@ -0,0 +1,307 @@
+/* eslint-disable max-lines */
+
+import database from 'database';
+import moment from 'moment';
+import { AuditLogEvents, AuditStatusType } from 'constants/auditLog';
+import logger from './logger';
+
+interface GenericEvent {
+  type: AuditLogEvents;
+  status: AuditStatusType;
+  meta?: {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    [key: string]: any;
+  };
+}
+
+interface ResetPasswordEvent extends GenericEvent {
+  type: AuditLogEvents.RESET_PASSWORD;
+  meta: {
+    target: string; // employee UUID
+  };
+}
+interface CreateEmployeeEvent extends GenericEvent {
+  type: AuditLogEvents.CREATE_EMPLOYEE;
+  meta: {
+    target: string; // employee UUID
+  };
+}
+interface ReactivateEmployeeEvent extends GenericEvent {
+  type: AuditLogEvents.REACTIVATE_EMPLOYEE;
+  meta: {
+    target: string; // employee UUID
+  };
+}
+interface UpdateEmployeeEvent extends GenericEvent {
+  type: AuditLogEvents.UPDATE_EMPLOYEE;
+  meta: {
+    target: string; // employee UUID
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    attributes: any; // changed Attributes
+  };
+}
+interface DeleteEmployeeEvent extends GenericEvent {
+  type: AuditLogEvents.DELETE_EMPLOYEE;
+  meta: {
+    target: string; // employee UUID
+  };
+}
+interface ChangeRoleEvent extends GenericEvent {
+  type: AuditLogEvents.CHANGE_ROLE;
+  meta: {
+    target: string; // employee UUID
+    isAdmin: boolean;
+  };
+}
+interface CreateTracingProcessEvent extends GenericEvent {
+  type: AuditLogEvents.CREATE_TRACING_PROCESS;
+  meta: {
+    transferId: string;
+    locationId?: string;
+    viaTan?: boolean;
+  };
+}
+
+interface RequestDataEvent extends GenericEvent {
+  type: AuditLogEvents.REQUEST_DATA;
+  meta: {
+    processId: string;
+    transferId: string;
+    locationId: string;
+    timeframe: string[];
+    amountOfTraces: number;
+  };
+}
+
+interface ReceiveDataEvent extends GenericEvent {
+  type: AuditLogEvents.RECEIVE_DATA;
+  meta: {
+    processId: string;
+    transferId: string;
+    locationId: string;
+    timeframe: string[];
+    amountOfTraces: number;
+  };
+}
+
+interface ViewDataEvent extends GenericEvent {
+  type: AuditLogEvents.VIEW_DATA;
+  meta: {
+    processId: string;
+    transferId: string;
+    locationId: string;
+    timeframe: string[];
+    amountOfTraces: number;
+  };
+}
+
+interface IssueDailyKeypairEvent extends GenericEvent {
+  type: AuditLogEvents.ISSUE_DAILY_KEYPAIR;
+  meta: {
+    keyId: string;
+  };
+}
+interface IssueBadgeKeypairEvent extends GenericEvent {
+  type: AuditLogEvents.ISSUE_BADGE_KEYPAIR;
+  meta: {
+    keyId: string;
+  };
+}
+
+interface RekeyDailyKeypairEvent extends GenericEvent {
+  type: AuditLogEvents.REKEY_DAILY_KEYPAIR;
+  meta: {
+    newKeyId: string;
+    oldKeyId?: string;
+    oldKeyHd?: string;
+  };
+}
+
+interface RekeyBadgeKeypairEvent extends GenericEvent {
+  type: AuditLogEvents.REKEY_BADGE_KEYPAIR;
+  meta: {
+    newKeyId: string;
+    oldKeyId?: string;
+    oldKeyHd?: string;
+  };
+}
+
+interface DownloadAuditLogEvent extends GenericEvent {
+  type: AuditLogEvents.DOWNLOAD_AUDITLOG;
+  meta: {
+    timeframe: string[];
+  };
+}
+
+type LogEvent =
+  | GenericEvent
+  | RekeyBadgeKeypairEvent
+  | RekeyDailyKeypairEvent
+  | IssueBadgeKeypairEvent
+  | IssueDailyKeypairEvent
+  | ViewDataEvent
+  | ReceiveDataEvent
+  | RequestDataEvent
+  | CreateTracingProcessEvent
+  | DeleteEmployeeEvent
+  | UpdateEmployeeEvent
+  | CreateEmployeeEvent
+  | ResetPasswordEvent
+  | ChangeRoleEvent
+  | DownloadAuditLogEvent;
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function logEvent(employee: any, event: LogEvent) {
+  try {
+    await database.HealthDepartmentAuditLog.create({
+      departmentId: employee.departmentId,
+      employeeId: employee.uuid,
+      type: event.type,
+      status: event.status,
+      meta: event.meta,
+    });
+  } catch (error) {
+    logger.error(error);
+  }
+}
+
+export const toReadableDate = (date: string) => {
+  return moment(date).format('YYYY-MM-DD HH:mm:ss');
+};
+
+const MESSAGES = {
+  [AuditLogEvents.LOGIN]: (_: GenericEvent) => ({
+    [AuditStatusType.SUCCESS]: `hat sich angemeldet`,
+    [AuditStatusType.ERROR_INVALID_PASSWORD]: `hat versucht sich anzumelden (Fehlgeschlagen: Passwort falsch)`,
+  }),
+  [AuditLogEvents.LOGOUT]: (_: GenericEvent) => ({
+    [AuditStatusType.SUCCESS]: `hat sich abgemeldet`,
+  }),
+  [AuditLogEvents.CHANGE_PASSWORD]: (_: GenericEvent) => ({
+    [AuditStatusType.SUCCESS]: `hat sein Password geändert`,
+    [AuditStatusType.ERROR_INVALID_PASSWORD]: `hat versucht sein Passwort zu ändern (Fehlgeschlagen: Passwort falsch)`,
+  }),
+  [AuditLogEvents.RESET_PASSWORD]: (event: ResetPasswordEvent) => ({
+    [AuditStatusType.SUCCESS]: `hat ein neues Passwort für ${event.meta.target} angefordert`,
+    [AuditStatusType.ERROR_TARGET_NOT_FOUND]: `hat versucht ein neues Passwort für ${event.meta.target} anzufordern (Fehlgeschlagen: Nicht zugehörig zum gleichen GesundheitsGesundheitsamt)`,
+  }),
+  [AuditLogEvents.CREATE_EMPLOYEE]: (event: CreateEmployeeEvent) => ({
+    [AuditStatusType.SUCCESS]: `hat ein neues Konto für ${event.meta.target} angelegt`,
+  }),
+  [AuditLogEvents.REACTIVATE_EMPLOYEE]: (event: ReactivateEmployeeEvent) => ({
+    [AuditStatusType.SUCCESS]: `hat das Konto für ${event.meta.target} reaktiviert`,
+  }),
+  [AuditLogEvents.UPDATE_EMPLOYEE]: (event: UpdateEmployeeEvent) => ({
+    [AuditStatusType.SUCCESS]: `hat die Attribute ${Object.keys(
+      event.meta.attributes
+    )} für das Mitarbeiterkonto ${event.meta.target} geändert.`,
+    [AuditStatusType.ERROR_TARGET_NOT_FOUND]: `hat versucht Attribute für ein Mitarbeiterkonto zu ändern (Fehlgeschlagen: Mitarbeiterkonto nicht gefunden)`,
+  }),
+  [AuditLogEvents.DELETE_EMPLOYEE]: (event: DeleteEmployeeEvent) => ({
+    [AuditStatusType.SUCCESS]: `hat das Mitarbeiterkonto ${event.meta.target} gelöscht.`,
+    [AuditStatusType.ERROR_TARGET_NOT_FOUND]: `hat versucht ein Mitarbeiterkonto zu löschen (Fehlgeschlagen: Mitarbeiterkonto nicht gefunden)`,
+  }),
+  [AuditLogEvents.CHANGE_ROLE]: (event: ChangeRoleEvent) => ({
+    [AuditStatusType.SUCCESS]: `hat die Rolle Administrator für ${
+      event.meta.target
+    } ${event.meta.isAdmin ? 'hinzugefügt' : 'entfernt'}.`,
+  }),
+  [AuditLogEvents.CREATE_TRACING_PROCESS]: (
+    event: CreateTracingProcessEvent
+  ) => ({
+    [AuditStatusType.SUCCESS]: `hat einen Nachverfolgungsprozess ${
+      event.meta?.viaTan ? 'via TAN ' : ''
+    }erstellt: ${event.meta.transferId}`,
+    [AuditStatusType.ERROR_TARGET_NOT_FOUND]: `hat versucht einen Nachverfolgungsprozess  ${
+      event.meta?.viaTan ? 'via TAN ' : ''
+    }zu erstellen (Fehlgeschlagen: Location ${
+      event.meta.locationId
+    } des Locations Transfers wurde nicht gefunden)`,
+    [AuditStatusType.ERROR_LIMIT_EXCEEDED]: `hat versucht einen Nachverfolgungsprozess  ${
+      event.meta?.viaTan ? 'via TAN ' : ''
+    }zu erstellen (Fehlgeschlagen: Zu viele Locations)`,
+    [AuditStatusType.ERROR_INVALID_USER]: `hat versucht einen Nachverfolgungsprozess via TAN zu erstellen (Fehlgeschlagen: Initiierter User Transfer nicht vorhanden)`,
+  }),
+  [AuditLogEvents.REQUEST_DATA]: (event: RequestDataEvent) => ({
+    [AuditStatusType.SUCCESS]: `hat die Anfrage ${event.meta.transferId} an die Location ${event.meta.locationId} gestellt`,
+  }),
+  [AuditLogEvents.RECEIVE_DATA]: (event: ReceiveDataEvent) => ({
+    [AuditStatusType.SUCCESS]: `hat die Freigabe ${event.meta.transferId} von der Location ${event.meta.locationId} erhalten`,
+  }),
+  [AuditLogEvents.VIEW_DATA]: (event: ViewDataEvent) => ({
+    [AuditStatusType.SUCCESS]: `hat den Nachverfolgungsprozess ${event.meta.transferId} angesehen`,
+    [AuditStatusType.ERROR_TARGET_NOT_FOUND]: `hat versucht den Nachverfolgungsprozess ${event.meta.transferId} anzusehen (Fehlgeschlagen: Prozess nicht gefunden)`,
+  }),
+  [AuditLogEvents.DOWNLOAD_AUDITLOG]: (event: DownloadAuditLogEvent) => ({
+    [AuditStatusType.SUCCESS]: `hat einen Log heruntergeladen für den Zeitraum ${toReadableDate(
+      event.meta.timeframe[1]
+    )} - ${toReadableDate(event.meta.timeframe[0])}`,
+  }),
+  [AuditLogEvents.ISSUE_DAILY_KEYPAIR]: (event: IssueDailyKeypairEvent) => ({
+    [AuditStatusType.SUCCESS]: `hat einen neuen daily Key ausgestellt ${event.meta.keyId}`,
+    [AuditStatusType.ERROR_TIMEFRAME]: `hat versucht einen neuen daily Key auszustellen (Fehlgeschlagen: Differenz in der Zeit der Erstellung zu hoch)`,
+    [AuditStatusType.ERROR_INVALID_SIGNATURE]: `hat versucht einen neuen daily Key auszustellen (Fehlgeschlagen: Signatur nicht korrekt)`,
+    [AuditStatusType.ERROR_INVALID_KEYID]: `hat versucht einen neuen daily Key auszustellen (Fehlgeschlagen: KeyID nicht korrekt)`,
+    [AuditStatusType.ERROR_LIMIT_EXCEEDED]: `hat versucht einen neuen daily Key auszustellen (Fehlgeschlagen: maximale Anzahl an täglichen Keys erreicht)`,
+  }),
+  [AuditLogEvents.ISSUE_BADGE_KEYPAIR]: (event: IssueBadgeKeypairEvent) => ({
+    [AuditStatusType.SUCCESS]: `hat einen neuen Badge Key ausgestellt ${event.meta.keyId}`,
+    [AuditStatusType.ERROR_TIMEFRAME]: `hat versucht einen neuen Badge Key auszustellen (Fehlgeschlagen: Differenz in der Zeit der Erstellung zu hoch)`,
+    [AuditStatusType.ERROR_INVALID_SIGNATURE]: `hat versucht einen neuen Badge Key auszustellen (Fehlgeschlagen: Signatur nicht korrekt)`,
+    [AuditStatusType.ERROR_INVALID_KEYID]: `hat versucht einen neuen Badge Key auszustellen (Fehlgeschlagen: KeyID nicht korrekt)`,
+    [AuditStatusType.ERROR_LIMIT_EXCEEDED]: `hat versucht einen neuen Badge Key auszustellen (Fehlgeschlagen: maximale Anzahl an täglichen Badges erreicht)`,
+  }),
+  [AuditLogEvents.REKEY_DAILY_KEYPAIR]: (event: RekeyDailyKeypairEvent) => ({
+    [AuditStatusType.SUCCESS]: `hat den Daily Key ${event.meta.oldKeyId} des Gesundheitsamtes ${event.meta.oldKeyHd} synchronisiert. (Neuer Daily Key: ${event.meta.newKeyId})`,
+    [AuditStatusType.ERROR_CONFLICT_KEY]: `hat versucht die Daily Keys zu synchronisieren (Fehlgeschlagen: Key ist bereits der aktuelle)`,
+    [AuditStatusType.ERROR_TARGET_NOT_FOUND]: `hat versucht die Daily Keys zu synchronisieren (Fehlgeschlagen: Daily Public Key nicht gefunden)`,
+    [AuditStatusType.ERROR_INVALID_SIGNATURE]: `hat versucht die Daily Keys zu synchronisieren (Fehlgeschlagen: Signatur nicht korrekt)`,
+  }),
+  [AuditLogEvents.REKEY_BADGE_KEYPAIR]: (event: RekeyBadgeKeypairEvent) => ({
+    [AuditStatusType.SUCCESS]: `hat den Badge Key ${event.meta.oldKeyId} des Gesundheitsamtes ${event.meta.oldKeyHd} synchronisiert. (Neuer Badge Key: ${event.meta.newKeyId})`,
+    [AuditStatusType.ERROR_CONFLICT_KEY]: `hat versucht die Badge Keys zu synchronisieren (Fehlgeschlagen: Key ist bereits der aktuelle)`,
+    [AuditStatusType.ERROR_TARGET_NOT_FOUND]: `hat versucht die Badge Keys zu synchronisieren (Fehlgeschlagen: Badge Public Key nicht gefunden)`,
+    [AuditStatusType.ERROR_INVALID_SIGNATURE]: `hat versucht die Badge Keys zu synchronisieren (Fehlgeschlagen: Signatur nicht korrekt)`,
+  }),
+};
+
+export function toReadableEvent(event: LogEvent) {
+  try {
+    const eventLogTransformer = MESSAGES[event.type as keyof typeof MESSAGES];
+
+    if (eventLogTransformer !== undefined) {
+      const transformed = eventLogTransformer(event as never);
+
+      const foundStatusTranslation =
+        transformed[event.status as keyof typeof transformed];
+
+      if (foundStatusTranslation) {
+        return foundStatusTranslation;
+      }
+    }
+  } catch (error) {
+    logger.error(`Event not processable: ${error.message}`);
+    return null;
+  }
+
+  return `Event: ${event.type} - Status ${event.status}`;
+}
+
+// any until models are typed
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function entriesToPlainText(entries: any[]) {
+  return `${entries
+    .map(entry => {
+      const readableEvent = toReadableEvent(entry);
+
+      if (!readableEvent) {
+        return null;
+      }
+
+      return `[${toReadableDate(entry.createdAt)}] ${
+        entry.employeeId
+      } ${readableEvent}`;
+    })
+    .filter(event => !!event)
+    .join('\n')}`;
+}
diff --git a/services/backend/src/utils/ipChecks.js b/services/backend/src/utils/ipChecks.js
index df738ed..aa9e4f8 100644
--- a/services/backend/src/utils/ipChecks.js
+++ b/services/backend/src/utils/ipChecks.js
@@ -7,9 +7,9 @@ const database = require('../database');
 const CLASS_A_CIDR = new IPCIDR('10.0.0.0/8');
 const CLASS_B_CIDR = new IPCIDR('172.16.0.0/12');
 const CLASS_C_CIDR = new IPCIDR('192.168.0.0/16');
-const CLASS_A_IPV6_CIDR = new IPCIDR('::ffff:10.0.0.0/8');
-const CLASS_B_IPV6_CIDR = new IPCIDR('::ffff:172.16.0.0/12');
-const CLASS_C_IPV6_CIDR = new IPCIDR('::ffff:192.168.0.0/16');
+const CLASS_A_IPV6_CIDR = new IPCIDR('::ffff:10.0.0.0/104');
+const CLASS_B_IPV6_CIDR = new IPCIDR('::ffff:172.16.0.0/108');
+const CLASS_C_IPV6_CIDR = new IPCIDR('::ffff:192.168.0.0/112');
 
 const isInternalIp = ipAddress => {
   return (
diff --git a/services/backend/src/utils/logger.js b/services/backend/src/utils/logger.js
index 9b406b2..f4f4424 100644
--- a/services/backend/src/utils/logger.js
+++ b/services/backend/src/utils/logger.js
@@ -37,6 +37,7 @@ const filterHeaders = format(info => {
       'accept',
       'referer',
       'accept-encoding',
+      'ssl-client-subject-dn',
     ]);
   }
   return info;
diff --git a/services/backend/src/utils/notifications/notificationsConfig.js b/services/backend/src/utils/notifications/notificationsConfig.js
new file mode 100644
index 0000000..fa87b33
--- /dev/null
+++ b/services/backend/src/utils/notifications/notificationsConfig.js
@@ -0,0 +1,50 @@
+const database = require('../../database');
+
+const getConfig = messages => {
+  const config = {};
+
+  messages.forEach(message => {
+    if (!config[message.level]) {
+      config[message.level] = {};
+    }
+
+    if (!config[message.level].messages) {
+      config[message.level].messages = {};
+    }
+
+    if (!config[message.level].messages[message.language]) {
+      config[message.level].messages[message.language] = {};
+    }
+
+    config[message.level].messages[message.language][message.key] =
+      message.content;
+  });
+
+  return config;
+};
+
+const getNotificationConfig = async () => {
+  const healthDepartments = await database.HealthDepartment.findAll({
+    attributes: ['uuid', 'name', 'email', 'phone'],
+    include: { model: database.NotificationMessage },
+  });
+
+  const defaultMessages = await database.NotificationMessage.findAll({
+    where: {
+      departmentId: null,
+    },
+  });
+
+  return {
+    default: getConfig(defaultMessages),
+    departments: healthDepartments.map(department => ({
+      uuid: department.uuid,
+      name: department.name,
+      phone: department.phone,
+      email: department.email,
+      config: getConfig(department.NotificationMessages),
+    })),
+  };
+};
+
+module.exports = { getNotificationConfig };
diff --git a/services/backend/src/utils/notifications.js b/services/backend/src/utils/notifications/notificationsV3.js
similarity index 95%
rename from services/backend/src/utils/notifications.js
rename to services/backend/src/utils/notifications/notificationsV3.js
index dcba511..aefe551 100644
--- a/services/backend/src/utils/notifications.js
+++ b/services/backend/src/utils/notifications/notificationsV3.js
@@ -1,3 +1,5 @@
+import cache from 'utils/redisCache';
+
 const { Op } = require('sequelize');
 const moment = require('moment');
 const uniq = require('lodash/uniq');
@@ -10,15 +12,14 @@ const {
   HMAC_SHA256,
 } = require('@lucaapp/crypto');
 
-const cache = require('./redisCache');
-const database = require('../database');
+const database = require('../../database');
 
 const NOTIFICATIONS_CACHE_KEY = 'notifications';
 const {
   DEVICE_TYPE_IOS,
   DEVICE_TYPE_ANDROID,
   DEVICE_TYPE_WEBAPP,
-} = require('../constants/deviceTypes');
+} = require('../../constants/deviceTypes');
 
 const generateNotifications = async () => {
   const twoWeeksAgo = moment().subtract(2, 'weeks');
diff --git a/services/backend/src/utils/notifications/notificationsV4.test.js b/services/backend/src/utils/notifications/notificationsV4.test.js
new file mode 100644
index 0000000..224717c
--- /dev/null
+++ b/services/backend/src/utils/notifications/notificationsV4.test.js
@@ -0,0 +1,26 @@
+const { expect } = require('chai');
+const moment = require('moment');
+const { getBinaryHeaderBuffer } = require('./notificationsV4');
+
+const NO_HASH_INPUT_HASH_BUFFER = Buffer.alloc(16);
+
+describe('notifications', () => {
+  describe('build header', () => {
+    it('can build a header without a hash', async () => {
+      const chunkHeader = getBinaryHeaderBuffer(undefined);
+      expect(chunkHeader.length).to.equal(32);
+
+      const schemaVersion = chunkHeader.slice(0, 1).readInt8(0);
+      const algorithmType = chunkHeader.slice(1, 2).readInt8(0);
+      const hashLength = chunkHeader.slice(2, 3).readInt8(0);
+      const createdAt = chunkHeader.slice(3, 11).readBigInt64BE(0);
+      const lastSliceHash = chunkHeader.slice(16, 32);
+
+      expect(schemaVersion).to.equal(1);
+      expect(algorithmType).to.equal(0);
+      expect(hashLength).to.equal(12);
+      expect(Number(createdAt)).to.be.lessThan(moment().valueOf());
+      expect(lastSliceHash).to.deep.equal(NO_HASH_INPUT_HASH_BUFFER);
+    });
+  });
+});
diff --git a/services/backend/src/utils/notifications/notificationsV4.ts b/services/backend/src/utils/notifications/notificationsV4.ts
new file mode 100644
index 0000000..4c360d2
--- /dev/null
+++ b/services/backend/src/utils/notifications/notificationsV4.ts
@@ -0,0 +1,230 @@
+import { Op } from 'sequelize';
+import {
+  uuidToHex,
+  base64ToHex,
+  hexToBase64,
+  HMAC_SHA256,
+  SHA256,
+  int8ToHex,
+} from '@lucaapp/crypto';
+import database from 'database/models';
+import cache from 'utils/redisCache';
+import {
+  DEVICE_TYPE_IOS,
+  DEVICE_TYPE_ANDROID,
+  DEVICE_TYPE_WEBAPP,
+} from 'constants/deviceTypes';
+import moment from 'moment';
+import config from 'config';
+import NodeCache from 'node-cache';
+import uniq from 'lodash/uniq';
+
+const databaseCache = new NodeCache({
+  stdTTL: config.get('luca.notificationChunks.cacheTTL'),
+});
+
+// Hex length 32 = 16 bytes
+const LAST_SLICE_ID_HASH_HEX_LENGTH = 32;
+const NOTIFICATIONS_V4_CACHE_KEY = 'notifications:v4';
+const HASH_LENGTH = 12;
+
+const IS_COMPLETED_WARN_LEVEL = 1;
+
+const SCHEMA_VERSION_BYTE = Buffer.alloc(1);
+SCHEMA_VERSION_BYTE.writeInt8(1);
+
+const ALGORITHM_TYPE_BYTE = Buffer.alloc(1);
+ALGORITHM_TYPE_BYTE.writeInt8(0);
+
+const HASH_LENGTH_BYTE = Buffer.alloc(1);
+HASH_LENGTH_BYTE.writeInt8(HASH_LENGTH);
+
+const PADDING_BYTES = Buffer.alloc(5);
+
+export const getBinaryHeaderBuffer = (hash?: string): Buffer => {
+  const createdAt = Buffer.allocUnsafe(8);
+  createdAt.writeBigUInt64BE(BigInt(moment.now()), 0);
+  const lastSlice = hash
+    ? Buffer.from(base64ToHex(hash), 'hex')
+    : Buffer.alloc(16);
+
+  return Buffer.concat([
+    SCHEMA_VERSION_BYTE,
+    ALGORITHM_TYPE_BYTE,
+    HASH_LENGTH_BYTE,
+    createdAt,
+    PADDING_BYTES,
+    lastSlice,
+  ]);
+};
+
+const hashWithRiskLevels = (
+  traceId: string,
+  healthDepartmentId: string,
+  riskLevels: number[]
+) =>
+  riskLevels.map((riskLevelToHash: number) =>
+    HMAC_SHA256(
+      uuidToHex(healthDepartmentId) + int8ToHex(riskLevelToHash),
+      base64ToHex(traceId)
+    )
+  );
+
+export const generateChunk = async () => {
+  const lastChunk = await database.NotificationChunk.findOne({
+    order: [['createdAt', 'DESC']],
+  });
+
+  const startTime = lastChunk
+    ? lastChunk.createdAt
+    : moment().subtract(
+        config.get('luca.notificationChunks.initialChunkCoverage'),
+        'hours'
+      );
+
+  const completedLocationTransfers = await database.LocationTransfer.findAll({
+    attributes: ['departmentId'],
+    include: [
+      {
+        required: false,
+        attributes: ['traceId'],
+        model: database.LocationTransferTrace,
+        where: {
+          traceId: {
+            [Op.not]: null,
+          },
+          deviceType: [
+            DEVICE_TYPE_IOS,
+            DEVICE_TYPE_ANDROID,
+            DEVICE_TYPE_WEBAPP,
+          ],
+        },
+      },
+    ],
+    where: {
+      approvedAt: { [Op.gt]: startTime },
+    },
+  });
+
+  const locationTransfersWithRiskLevels = await database.LocationTransfer.findAll(
+    {
+      attributes: ['departmentId'],
+      include: [
+        {
+          required: false,
+          attributes: ['traceId'],
+          model: database.LocationTransferTrace,
+          where: {
+            traceId: {
+              [Op.not]: null,
+            },
+            deviceType: [
+              DEVICE_TYPE_IOS,
+              DEVICE_TYPE_ANDROID,
+              DEVICE_TYPE_WEBAPP,
+            ],
+          },
+          include: {
+            required: true,
+            model: database.RiskLevel,
+            where: {
+              createdAt: { [Op.gt]: startTime },
+            },
+          },
+        },
+      ],
+    }
+  );
+
+  const dummyTraces = await database.DummyTrace.findAll({
+    attributes: ['healthDepartmentId', 'traceId'],
+    where: { createdAt: { [Op.gt]: startTime } },
+  });
+
+  const completedHashHexStrings: string[] = completedLocationTransfers.flatMap(
+    // @ts-ignore - any until models are typed
+    completedLocationTransfer =>
+      completedLocationTransfer.LocationTransferTraces.flatMap(
+        // @ts-ignore - any until models are typed
+        completedLocationTransferTrace =>
+          hashWithRiskLevels(
+            completedLocationTransferTrace.traceId,
+            completedLocationTransfer.departmentId,
+            [IS_COMPLETED_WARN_LEVEL]
+          )
+      )
+  );
+
+  const riskLevelHashHexStrings: string[] = locationTransfersWithRiskLevels.flatMap(
+    // @ts-ignore - any until models are typed
+    locationTransfer =>
+      // @ts-ignore - any until models are typed
+      locationTransfer.LocationTransferTraces.flatMap(locationTransferTrace => {
+        const riskLevelsToHash = locationTransferTrace.RiskLevels.map(
+          // @ts-ignore - any until models are typed
+          riskLevel => riskLevel.level
+        );
+        return hashWithRiskLevels(
+          locationTransferTrace.traceId,
+          locationTransfer.departmentId,
+          riskLevelsToHash
+        );
+      })
+  );
+
+  // @ts-ignore - any until models are typed
+  const dummyHashHexStrings: string[] = dummyTraces.flatMap(dummyTrace =>
+    hashWithRiskLevels(dummyTrace.traceId, dummyTrace.healthDepartmentId, [
+      IS_COMPLETED_WARN_LEVEL,
+    ])
+  );
+
+  /* eslint-disable unicorn/prefer-spread */
+  const hashStrings = completedHashHexStrings
+    .concat(riskLevelHashHexStrings)
+    .concat(dummyHashHexStrings);
+  /* eslint-enable unicorn/prefer-spread */
+
+  hashStrings.sort((a, b) => b.localeCompare(a));
+  const uniqueHashStrings = uniq(hashStrings);
+  const hashesAsBuffers = uniqueHashStrings.map(hashString =>
+    Buffer.from(hashString, 'hex').slice(0, HASH_LENGTH)
+  );
+
+  const headerBuffer = getBinaryHeaderBuffer(lastChunk?.hash);
+  const hashListBuffer = Buffer.concat(hashesAsBuffers);
+
+  return Buffer.concat([headerBuffer, hashListBuffer]);
+};
+
+export const generateActiveChunk = async () => {
+  const chunk = await generateChunk();
+  cache.set(NOTIFICATIONS_V4_CACHE_KEY, chunk);
+};
+
+export const generateArchiveChunk = async () => {
+  const chunk = await generateChunk();
+  const rawHash = SHA256(chunk.toString('hex'));
+  const trimmedBase64Hash = hexToBase64(
+    rawHash.slice(0, LAST_SLICE_ID_HASH_HEX_LENGTH)
+  );
+  await database.NotificationChunk.create({ chunk, hash: trimmedBase64Hash });
+};
+
+export const getActiveChunk = () =>
+  cache
+    .get(NOTIFICATIONS_V4_CACHE_KEY, true)
+    .then(cachedResponse => cachedResponse[0]);
+
+export const getArchivedChunk = async (chunkId: string) => {
+  let chunk = databaseCache.get(chunkId);
+  if (!chunk) {
+    const notificationChunk = await database.NotificationChunk.findOne({
+      where: { hash: chunkId },
+      attributes: ['chunk'],
+    });
+    chunk = notificationChunk?.chunk;
+    if (chunk) databaseCache.set(chunkId, chunk);
+  }
+  return chunk;
+};
diff --git a/services/backend/src/utils/redis.js b/services/backend/src/utils/redis.js
index fd97c9c..c269837 100644
--- a/services/backend/src/utils/redis.js
+++ b/services/backend/src/utils/redis.js
@@ -16,6 +16,7 @@ const client = redis.createClient({
   password: config.get('redis.password'),
   enable_offline_queue: true,
   detect_buffers: true,
+  db: config.get('redis.database'),
   retry_strategy: retryStrategy,
 });
 
diff --git a/services/backend/src/utils/redisCache.ts b/services/backend/src/utils/redisCache.ts
index 1624c68..d235881 100644
--- a/services/backend/src/utils/redisCache.ts
+++ b/services/backend/src/utils/redisCache.ts
@@ -34,5 +34,4 @@ const set = (key: string, value: string | Buffer) => {
 };
 
 const cache = { get, set };
-
-module.exports = cache;
+export default cache;
diff --git a/services/backend/src/utils/signedKeys.js b/services/backend/src/utils/signedKeys.js
index 91e8891..96b50b2 100644
--- a/services/backend/src/utils/signedKeys.js
+++ b/services/backend/src/utils/signedKeys.js
@@ -19,14 +19,16 @@ const loadCertificates = () => {
   ROOT_CA_STORE = forge.pki.createCaStore([D_TRUST_ROOT_CA]);
 };
 
-const jwtSchema = z.object({
-  sub: z.uuid(),
-  iss: z.string().length(40),
-  name: z.string().max(255),
-  key: z.ecPublicKey(),
-  type: z.enum(['publicHDEKP', 'publicHDSKP']),
-  iat: z.unixTimestamp(),
-});
+const jwtSchema = z
+  .object({
+    sub: z.uuid(),
+    iss: z.string().length(40),
+    name: z.string().max(255),
+    key: z.ecPublicKey(),
+    type: z.enum(['publicHDEKP', 'publicHDSKP']),
+    iat: z.unixTimestamp(),
+  })
+  .strict();
 
 const getFingerprint = certificate => {
   const certDer = forge.asn1.toDer(forge.pki.certificateToAsn1(certificate));
diff --git a/services/backend/yarn.lock b/services/backend/yarn.lock
index 6cf651f..b07d9c7 100644
--- a/services/backend/yarn.lock
+++ b/services/backend/yarn.lock
@@ -481,6 +481,11 @@
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
   integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==
 
+"@types/lodash@4.14.172":
+  version "4.14.172"
+  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.172.tgz#aad774c28e7bfd7a67de25408e03ee5a8c3d028a"
+  integrity sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==
+
 "@types/mime@^1":
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
@@ -1068,9 +1073,9 @@ camelcase@^6.0.0:
   integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
 
 caniuse-lite@^1.0.30001248:
-  version "1.0.30001251"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz#6853a606ec50893115db660f82c094d18f096d85"
-  integrity sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A==
+  version "1.0.30001255"
+  resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001255.tgz"
+  integrity sha512-F+A3N9jTZL882f/fg/WWVnKSu6IOo3ueLz4zwaOPbPYHNmM/ZaDUyzyJwS1mZhX7Ex5jqTyW599Gdelh5PDYLQ==
 
 caseless@~0.12.0:
   version "0.12.0"
@@ -1227,6 +1232,11 @@ cliui@^7.0.2:
     strip-ansi "^6.0.0"
     wrap-ansi "^7.0.0"
 
+clone@2.x:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
+  integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
+
 clone@^1.0.2:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
@@ -3785,6 +3795,13 @@ node-addon-api@^3.0.0:
   resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
   integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==
 
+node-cache@5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d"
+  integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==
+  dependencies:
+    clone "2.x"
+
 node-forge@0.10.0:
   version "0.10.0"
   resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
diff --git a/services/contact-form/package.json b/services/contact-form/package.json
index 86e2521..bd429e1 100644
--- a/services/contact-form/package.json
+++ b/services/contact-form/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@lucaapp/contact-form",
-  "version": "1.9.2",
+  "version": "2.0.0",
   "private": true,
   "license": "Apache-2.0",
   "author": "Culture4Life <hello@luca-app.de> (https://www.luca-app.de/)",
diff --git a/services/contact-form/yarn.lock b/services/contact-form/yarn.lock
index 20647d4..de0c5fe 100644
--- a/services/contact-form/yarn.lock
+++ b/services/contact-form/yarn.lock
@@ -3697,9 +3697,9 @@ caniuse-api@^3.0.0:
     lodash.uniq "^4.5.0"
 
 caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001219:
-  version "1.0.30001249"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001249.tgz#90a330057f8ff75bfe97a94d047d5e14fabb2ee8"
-  integrity sha512-vcX4U8lwVXPdqzPWi6cAJ3FnQaqXbBqy/GZseKNQzRj37J7qZdGcBtxq/QLFNLLlfsoXLUdHw8Iwenri86Tagw==
+  version "1.0.30001255"
+  resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001255.tgz"
+  integrity sha512-F+A3N9jTZL882f/fg/WWVnKSu6IOo3ueLz4zwaOPbPYHNmM/ZaDUyzyJwS1mZhX7Ex5jqTyW599Gdelh5PDYLQ==
 
 capture-exit@^2.0.0:
   version "2.0.0"
diff --git a/services/elb/shared/proxy.nginx.conf b/services/elb/shared/proxy.nginx.conf
index 632cca7..f8bb46b 100644
--- a/services/elb/shared/proxy.nginx.conf
+++ b/services/elb/shared/proxy.nginx.conf
@@ -11,3 +11,6 @@ proxy_set_header X-Forwarded-Host  $host;
 proxy_set_header X-Forwarded-Port  $server_port;
 proxy_set_header X-Scheme          $scheme;
 proxy_set_header ssl-client-cert   $ssl_client_escaped_cert;
+proxy_set_header ssl-client-verify      $ssl_client_verify;
+proxy_set_header ssl-client-subject-dn  $ssl_client_s_dn;
+proxy_set_header ssl-client-issuer-dn   $ssl_client_i_dn;
diff --git a/services/health-department/.iyarc b/services/health-department/.iyarc
index 58d5b66..0c37c42 100644
--- a/services/health-department/.iyarc
+++ b/services/health-department/.iyarc
@@ -18,6 +18,12 @@
 # This is only used by react-scripts for development purposes.
 1755
 
+# jszip
+# https://nvd.nist.gov/vuln/detail/CVE-2021-23413
+# Crafting a new zip file with filenames set to Object prototype values (e.g proto,
+# toString, etc) results in a returned object with a modified prototype instance.
+1774
+
 # tar
 # https://nvd.nist.gov/vuln/detail/CVE-2021-32804
 # Arbitrary File Creation/Overwrite due to insufficient absolute path sanitization
diff --git a/services/health-department/package.json b/services/health-department/package.json
index 76e1ed0..d7e81bb 100644
--- a/services/health-department/package.json
+++ b/services/health-department/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@lucaapp/health-department",
-  "version": "1.9.2",
+  "version": "2.0.0",
   "private": true,
   "license": "Apache-2.0",
   "author": "Culture4Life <hello@luca-app.de> (https://www.luca-app.de/)",
diff --git a/services/health-department/src/ant.css b/services/health-department/src/ant.css
index b36d60a..e1c774d 100644
--- a/services/health-department/src/ant.css
+++ b/services/health-department/src/ant.css
@@ -75,6 +75,10 @@ input:-webkit-autofill:active {
   background-color: #f5f5f5 !important;
 }
 
+.ant-switch-checked {
+  background-color: #b2c596;
+}
+
 .filter.ant-select {
   width: 180px;
 }
@@ -390,3 +394,7 @@ input:-webkit-autofill:active {
   font-weight: bold;
   font-size: 32px;
 }
+
+.ant-picker-dropdown .ant-picker-time-panel + .ant-picker-footer {
+  display: none;
+}
diff --git a/services/health-department/src/assets/Bell.svg b/services/health-department/src/assets/Bell.svg
new file mode 100644
index 0000000..b9d375a
--- /dev/null
+++ b/services/health-department/src/assets/Bell.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <g id="Gesundheitsamt" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Level-2-/-Benachrichtigung" transform="translate(-1336.000000, -1020.000000)">
+            <g id="Group" transform="translate(1336.000000, 1020.000000)">
+                <circle id="Oval-Copy" fill="#C3CED9" cx="16" cy="16" r="16"></circle>
+                <g transform="translate(8.000000, 8.000000)">
+                    <path d="M7.19216122,12.1708643 C8.20284419,11.7250788 9.3833502,12.1814783 9.82913568,13.1921612 C10.2761005,14.2016649 9.81852174,15.3821709 8.80783878,15.8291357 C7.79833514,16.2749212 6.61782913,15.8185217 6.17086432,14.8078388 C5.72507883,13.7983351 6.18147826,12.6166498 7.19216122,12.1708643" id="Fill-4" fill="#020203"></path>
+                    <path d="M2,13 C1.44771525,13 1,12.5522847 1,12 C1,11.4871642 1.38604019,11.0644928 1.88337887,11.0067277 L2,11 L3,11 L3,4.64285714 C3,2.14798009 5.11920732,0.112715609 7.77728041,0.0045237217 L8,0 C10.7614237,0 13,2.07867795 13,4.64285714 L13,4.64285714 L13,11 L14,11 C14.5522847,11 15,11.4477153 15,12 C15,12.5128358 14.6139598,12.9355072 14.1166211,12.9932723 L14,13 L2,13 Z M8,2 C6.34314575,2 5,3.34314575 5,5 L5,5 L5,11 L11,11 L11,5 C11,3.46149248 9.84187943,2.19347852 8.34986338,2.02018319 L8.17627279,2.00509269 Z" id="Combined-Shape" fill="#000000" fill-rule="nonzero"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>
diff --git a/services/health-department/src/assets/BellIcon.svg b/services/health-department/src/assets/BellIcon.svg
new file mode 100644
index 0000000..84b5715
--- /dev/null
+++ b/services/health-department/src/assets/BellIcon.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <g id="Gesundheitsamt" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Level-2-/-Benachrichtigung" transform="translate(-1336.000000, -1020.000000)">
+            <g id="Group" transform="translate(1336.000000, 1020.000000)">
+                <g transform="translate(8.000000, 8.000000)">
+                    <path d="M7.19216122,12.1708643 C8.20284419,11.7250788 9.3833502,12.1814783 9.82913568,13.1921612 C10.2761005,14.2016649 9.81852174,15.3821709 8.80783878,15.8291357 C7.79833514,16.2749212 6.61782913,15.8185217 6.17086432,14.8078388 C5.72507883,13.7983351 6.18147826,12.6166498 7.19216122,12.1708643" id="Fill-4" fill="#020203"></path>
+                    <path d="M2,13 C1.44771525,13 1,12.5522847 1,12 C1,11.4871642 1.38604019,11.0644928 1.88337887,11.0067277 L2,11 L3,11 L3,4.64285714 C3,2.14798009 5.11920732,0.112715609 7.77728041,0.0045237217 L8,0 C10.7614237,0 13,2.07867795 13,4.64285714 L13,4.64285714 L13,11 L14,11 C14.5522847,11 15,11.4477153 15,12 C15,12.5128358 14.6139598,12.9355072 14.1166211,12.9932723 L14,13 L2,13 Z M8,2 C6.34314575,2 5,3.34314575 5,5 L5,5 L5,11 L11,11 L11,5 C11,3.46149248 9.84187943,2.19347852 8.34986338,2.02018319 L8.17627279,2.00509269 Z" id="Combined-Shape" fill="#000000" fill-rule="nonzero"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>
diff --git a/services/health-department/src/components/App/App.styled.js b/services/health-department/src/components/App/App.styled.js
index 680320a..dd11af3 100644
--- a/services/health-department/src/components/App/App.styled.js
+++ b/services/health-department/src/components/App/App.styled.js
@@ -3,7 +3,6 @@ import styled from 'styled-components';
 export const AppWrapper = styled.div`
   display: flex;
   flex-direction: column;
-  height: 100vh;
   width: 100vw;
   padding-top: 40px;
   background-color: #f5f5f4;
diff --git a/services/health-department/src/components/App/ProcessDetails/HeaderRow/HeaderRow.react.js b/services/health-department/src/components/App/ProcessDetails/HeaderRow/HeaderRow.react.js
index dd1261e..ca2d4bb 100644
--- a/services/health-department/src/components/App/ProcessDetails/HeaderRow/HeaderRow.react.js
+++ b/services/health-department/src/components/App/ProcessDetails/HeaderRow/HeaderRow.react.js
@@ -4,14 +4,14 @@ import { useIntl } from 'react-intl';
 
 import { decryptUserTransfer } from 'utils/cryptoOperations';
 import { IncompleteDataError } from 'errors/incompleteDataError';
-import { useLocationWithTransfers } from 'components/hooks/useLocationWithTransfers';
+import { useLocationTransfers } from 'components/hooks/useLocationTransfers';
 
 import { ToggleCompleted } from './ToggleCompleted';
 import { Wrapper, ProcessName, ButtonRow } from './HeaderRow.styled';
 
 export const HeaderRow = ({ process }) => {
   const intl = useIntl();
-  const locations = useLocationWithTransfers(process.uuid);
+  const locations = useLocationTransfers(process.uuid);
 
   const {
     isLoading: isUserLoading,
@@ -29,7 +29,7 @@ export const HeaderRow = ({ process }) => {
 
   const processName = (() => {
     if (!process.userTransferId) {
-      return locations?.[0]?.name || '–';
+      return locations?.[0]?.groupName || '–';
     }
     if (userData) {
       return `${userData.fn} ${userData.ln}`;
diff --git a/services/health-department/src/components/App/ProcessDetails/History/Header/Header.react.js b/services/health-department/src/components/App/ProcessDetails/History/Header/Header.react.js
index ffd739c..15523db 100644
--- a/services/health-department/src/components/App/ProcessDetails/History/Header/Header.react.js
+++ b/services/health-department/src/components/App/ProcessDetails/History/Header/Header.react.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import { useIntl } from 'react-intl';
 
-import { useLocationWithTransfers } from 'components/hooks/useLocationWithTransfers';
+import { useLocationTransfers } from 'components/hooks/useLocationTransfers';
 
 import {
   Wrapper,
@@ -12,7 +12,7 @@ import {
 
 export const Header = ({ process }) => {
   const intl = useIntl();
-  const locations = useLocationWithTransfers(process.uuid);
+  const locations = useLocationTransfers(process.uuid);
 
   if (!locations) return null;
 
diff --git a/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/ContactConfirmationButton.react.js b/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/ContactConfirmationButton.react.js
index ec2dce0..1011632 100644
--- a/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/ContactConfirmationButton.react.js
+++ b/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/ContactConfirmationButton.react.js
@@ -4,11 +4,17 @@ import { Popconfirm } from 'antd';
 import moment from 'moment';
 
 import { PrimaryButton, SuccessButton } from 'components/general';
-import { Expiry, ButtonWrapper } from './ContactConfirmationButton.styled';
+import { FlexWrapper } from 'components/App/modals/ContactPersonsModal/ContactPersons/ContactPersons.styled';
+import { NotificationTrigger } from './NotificationTrigger';
+import {
+  Expiry,
+  ButtonWrapper,
+  Wrapper,
+} from './ContactConfirmationButton.styled';
 
 export const ContactConfirmationButton = ({ location, callback }) => {
   const intl = useIntl();
-  const { contactedAt, isCompleted, time } = location;
+  const { contactedAt, isCompleted, time, name: locationName } = location;
 
   const renderExpiration = () => (
     <Expiry>{`${intl.formatMessage({
@@ -18,59 +24,68 @@ export const ContactConfirmationButton = ({ location, callback }) => {
 
   if (!isCompleted && !!contactedAt) {
     return (
-      <ButtonWrapper>
-        <PrimaryButton disabled>
-          {intl.formatMessage({ id: 'history.contacted' })}
-        </PrimaryButton>
-        {renderExpiration()}
-      </ButtonWrapper>
+      <FlexWrapper>
+        <ButtonWrapper>
+          <PrimaryButton disabled>
+            {intl.formatMessage({ id: 'history.contacted' })}
+          </PrimaryButton>
+          {renderExpiration()}
+        </ButtonWrapper>
+        <NotificationTrigger location={location} />
+      </FlexWrapper>
     );
   }
 
   if (isCompleted) {
     return (
-      <ButtonWrapper>
-        <SuccessButton
-          data-cy={`confirmedLocation_${location.name}`}
-          onClick={() => callback(location)}
-        >
-          {intl.formatMessage({ id: 'history.confirmed' })}
-        </SuccessButton>
-        {renderExpiration()}
-      </ButtonWrapper>
+      <FlexWrapper>
+        <ButtonWrapper>
+          <SuccessButton
+            data-cy={`confirmedLocation_${locationName}`}
+            onClick={() => callback(location)}
+          >
+            {intl.formatMessage({ id: 'history.confirmed' })}
+          </SuccessButton>
+          {renderExpiration()}
+        </ButtonWrapper>
+        <NotificationTrigger location={location} />
+      </FlexWrapper>
     );
   }
 
   return (
-    <Popconfirm
-      placement="top"
-      disabled={isCompleted}
-      onConfirm={() => callback(location)}
-      title={intl.formatMessage(
-        {
-          id: 'modal.dataRequest.confirmation',
-        },
-        { venue: location.name }
-      )}
-      okText={intl.formatMessage({
-        id: 'modal.dataRequest.confirmButton',
-      })}
-      cancelText={intl.formatMessage({
-        id: 'modal.dataRequest.declineButton',
-      })}
-    >
-      <ButtonWrapper>
-        <PrimaryButton
-          disabled={!isCompleted && !!contactedAt}
-          data-cy={`contactLocation_${location.name}`}
-          onClick={() => {
-            if (contactedAt) callback(location);
-          }}
-        >
-          {intl.formatMessage({ id: 'history.contact' })}
-        </PrimaryButton>
-        {renderExpiration()}
-      </ButtonWrapper>
-    </Popconfirm>
+    <Wrapper>
+      <Popconfirm
+        placement="top"
+        disabled={isCompleted}
+        onConfirm={() => callback(location)}
+        title={intl.formatMessage(
+          {
+            id: 'modal.dataRequest.confirmation',
+          },
+          { venue: locationName }
+        )}
+        okText={intl.formatMessage({
+          id: 'modal.dataRequest.confirmButton',
+        })}
+        cancelText={intl.formatMessage({
+          id: 'modal.dataRequest.declineButton',
+        })}
+      >
+        <ButtonWrapper>
+          <PrimaryButton
+            disabled={!isCompleted && !!contactedAt}
+            data-cy={`contactLocation_${locationName}`}
+            onClick={() => {
+              if (contactedAt) callback(location);
+            }}
+          >
+            {intl.formatMessage({ id: 'history.contact' })}
+          </PrimaryButton>
+          {renderExpiration()}
+        </ButtonWrapper>
+      </Popconfirm>
+      <NotificationTrigger location={location} />
+    </Wrapper>
   );
 };
diff --git a/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/ContactConfirmationButton.styled.js b/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/ContactConfirmationButton.styled.js
index fb4febe..f6dedff 100644
--- a/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/ContactConfirmationButton.styled.js
+++ b/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/ContactConfirmationButton.styled.js
@@ -11,3 +11,7 @@ export const Expiry = styled.div`
 export const ButtonWrapper = styled.div`
   text-align: left;
 `;
+
+export const Wrapper = styled.div`
+  display: flex;
+`;
diff --git a/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/NotificationTrigger/NotificationTrigger.react.js b/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/NotificationTrigger/NotificationTrigger.react.js
new file mode 100644
index 0000000..4668ec1
--- /dev/null
+++ b/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/NotificationTrigger/NotificationTrigger.react.js
@@ -0,0 +1,87 @@
+import React from 'react';
+import { useIntl } from 'react-intl';
+import { Tooltip } from 'antd';
+import { useQuery } from 'react-query';
+
+import { RISK_LEVEL_2 } from 'constants/riskLevels';
+
+import {
+  getWarningLevelsForLocationTransfer,
+  getContactPersons,
+  getMe,
+} from 'network/api';
+
+import { useModal } from 'components/hooks/useModal';
+import { NotificationModal } from 'components/App/modals/NotificationModal';
+
+import { BellIcon, ButtonWrapper } from './NotificationTrigger.styled';
+
+export const NotificationTrigger = ({ location }) => {
+  const intl = useIntl();
+  const [openModal] = useModal();
+  const {
+    uuid: locationId,
+    name: locationName,
+    transferId: locationTransferId,
+    time,
+  } = location;
+
+  const {
+    data: riskLevels,
+  } = useQuery(
+    `getWarningLevelsForLocationTransfer${locationTransferId}`,
+    () => getWarningLevelsForLocationTransfer(locationTransferId),
+    { refetchOnWindowFocus: false }
+  );
+
+  const {
+    data: contactPersons,
+  } = useQuery(
+    `contactPersons${locationTransferId}`,
+    () => getContactPersons(locationTransferId),
+    { refetchOnWindowFocus: false }
+  );
+
+  const { data: healthDepartmentEmployee } = useQuery('me', getMe, {
+    refetchOnWindowFocus: false,
+  });
+
+  const openNotificationModal = () =>
+    openModal({
+      title: intl.formatMessage({
+        id: 'modal.notification.title',
+      }),
+      content: (
+        <NotificationModal
+          locationId={locationId}
+          locationName={locationName}
+          locationTransferId={locationTransferId}
+          time={time}
+          departmentId={healthDepartmentEmployee.departmentId}
+        />
+      ),
+      wide: true,
+    });
+
+  if (
+    !riskLevels ||
+    !contactPersons ||
+    !healthDepartmentEmployee ||
+    !healthDepartmentEmployee.notificationsEnabled
+  )
+    return null;
+
+  const isNotificationDisabled =
+    contactPersons.traces.length === 0 ||
+    riskLevels.some(traceRisk => traceRisk.riskLevels.includes(RISK_LEVEL_2));
+
+  return (
+    <ButtonWrapper
+      onClick={isNotificationDisabled ? () => {} : openNotificationModal}
+    >
+      <Tooltip title={intl.formatMessage({ id: 'modal.notification.button' })}>
+        <BellIcon disabled={isNotificationDisabled} />
+      </Tooltip>
+    </ButtonWrapper>
+  );
+};
diff --git a/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/NotificationTrigger/NotificationTrigger.styled.js b/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/NotificationTrigger/NotificationTrigger.styled.js
new file mode 100644
index 0000000..b85f339
--- /dev/null
+++ b/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/NotificationTrigger/NotificationTrigger.styled.js
@@ -0,0 +1,16 @@
+import styled from 'styled-components';
+import Icon from '@ant-design/icons';
+
+import { ReactComponent as BellSvg } from 'assets/Bell.svg';
+
+export const ButtonWrapper = styled.div`
+  text-align: left;
+`;
+
+export const BellIcon = styled(Icon).attrs({ component: BellSvg })`
+  color: black;
+  font-size: 32px;
+  margin-left: 32px;
+  cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
+  opacity: ${({ disabled }) => (disabled ? '50%' : '100%')};
+`;
diff --git a/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/NotificationTrigger/index.js b/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/NotificationTrigger/index.js
new file mode 100644
index 0000000..2738bd3
--- /dev/null
+++ b/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/ContactConfirmationButton/NotificationTrigger/index.js
@@ -0,0 +1 @@
+export { NotificationTrigger } from './NotificationTrigger.react';
diff --git a/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/HistoryTable.react.js b/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/HistoryTable.react.js
index 519d689..2584517 100644
--- a/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/HistoryTable.react.js
+++ b/services/health-department/src/components/App/ProcessDetails/History/HistoryTable/HistoryTable.react.js
@@ -8,7 +8,7 @@ import { sortByNameAsc } from 'utils/string';
 import { contactLocation } from 'network/api';
 
 import { useModal } from 'components/hooks/useModal';
-import { useLocationWithTransfers } from 'components/hooks/useLocationWithTransfers';
+import { useLocationTransfers } from 'components/hooks/useLocationTransfers';
 import { ContactPersonsModal } from 'components/App/modals/ContactPersonsModal';
 
 import { Time, Contact } from './HistoryTable.styled';
@@ -18,22 +18,24 @@ export const HistoryTable = ({ process }) => {
   const intl = useIntl();
   const [openModal] = useModal();
   const queryClient = useQueryClient();
-  const locations = useLocationWithTransfers(process.uuid);
+  const locationTransfers = useLocationTransfers(process.uuid);
 
-  if (!locations) return null;
+  if (!locationTransfers) return null;
 
   const formattedContactInfo = (firstName, lastName) =>
     `${firstName} ${lastName}`;
 
-  const handleAction = location => {
-    if (location.isCompleted) {
+  const handleAction = locationTransfer => {
+    if (locationTransfer.isCompleted) {
       openModal({
-        content: <ContactPersonsModal location={location} process={process} />,
+        content: (
+          <ContactPersonsModal location={locationTransfer} process={process} />
+        ),
         wide: true,
       });
     }
-    if (!location.contactedAt) {
-      contactLocation(location.transferId)
+    if (!locationTransfer.contactedAt) {
+      contactLocation(locationTransfer.transferId)
         .then(() => {
           notification.success({
             message: intl.formatMessage({
@@ -76,13 +78,16 @@ export const HistoryTable = ({ process }) => {
       title: intl.formatMessage({ id: 'history.label.contactInfo' }),
 
       key: 'contactInfo',
-      render: function renderContact(location) {
+      render: function renderContact(locationTransfer) {
         return (
           <>
             <Contact>
-              {formattedContactInfo(location.firstName, location.lastName)}
+              {formattedContactInfo(
+                locationTransfer.firstName,
+                locationTransfer.lastName
+              )}
             </Contact>
-            <Contact>{location.phone}</Contact>
+            <Contact>{locationTransfer.phone}</Contact>
           </>
         );
       },
@@ -117,10 +122,10 @@ export const HistoryTable = ({ process }) => {
     {
       title: intl.formatMessage({ id: 'history.label.confirmStatus' }),
       key: 'confirmStatus',
-      render: function renderConfirmStatus(location) {
+      render: function renderConfirmStatus(locationTransfer) {
         return (
           <ContactConfirmationButton
-            location={location}
+            location={locationTransfer}
             callback={handleAction}
           />
         );
@@ -134,8 +139,8 @@ export const HistoryTable = ({ process }) => {
       columns={columns}
       dataSource={
         process.userTransferId
-          ? sortByTimeAsc(locations)
-          : sortByNameAsc(locations)
+          ? sortByTimeAsc(locationTransfers)
+          : sortByNameAsc(locationTransfers)
       }
       pagination={false}
       rowKey={record => record.transferId}
diff --git a/services/health-department/src/components/App/ProcessDetails/InfoRow/InfoRow.react.js b/services/health-department/src/components/App/ProcessDetails/InfoRow/InfoRow.react.js
index 5cb02c5..8c73389 100644
--- a/services/health-department/src/components/App/ProcessDetails/InfoRow/InfoRow.react.js
+++ b/services/health-department/src/components/App/ProcessDetails/InfoRow/InfoRow.react.js
@@ -1,9 +1,9 @@
 import React from 'react';
 import { useIntl } from 'react-intl';
 
-import { CreationDate } from '../../Tracking/TrackingList/Table/Entry/CreationDate';
-import { SelectAssignee } from '../../Tracking/TrackingList/Table/Entry/SelectAssignee';
-import { CheckDone } from '../../Tracking/TrackingList/Table/Entry/CheckDone';
+import { CreationDate } from 'components/App/Tracking/TrackingList/Table/Entry/CreationDate';
+import { SelectAssignee } from 'components/App/Tracking/TrackingList/Table/Entry/SelectAssignee';
+import { CheckDone } from 'components/App/Tracking/TrackingList/Table/Entry/CheckDone';
 import { Wrapper, Title, Value, AttributeWrapper } from './InfoRow.styled';
 
 export const InfoRow = ({ process }) => {
diff --git a/services/health-department/src/components/App/ProcessDetails/Note/EmptyNote/EmptyNote.react.js b/services/health-department/src/components/App/ProcessDetails/Note/EmptyNote/EmptyNote.react.js
new file mode 100644
index 0000000..04ad3a1
--- /dev/null
+++ b/services/health-department/src/components/App/ProcessDetails/Note/EmptyNote/EmptyNote.react.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import { useIntl } from 'react-intl';
+import { EmptyNoteWrapper, PlusCircleNote, Title } from './EmptyNote.styled';
+
+export const EmptyNote = ({ setEditMode }) => {
+  const intl = useIntl();
+  return (
+    <EmptyNoteWrapper onClick={() => setEditMode(true)}>
+      <PlusCircleNote />
+      <Title>
+        {intl.formatMessage({
+          id: 'processDetails.note.addNote',
+        })}
+      </Title>
+    </EmptyNoteWrapper>
+  );
+};
diff --git a/services/health-department/src/components/App/ProcessDetails/Note/EmptyNote/EmptyNote.styled.js b/services/health-department/src/components/App/ProcessDetails/Note/EmptyNote/EmptyNote.styled.js
new file mode 100644
index 0000000..81f71ee
--- /dev/null
+++ b/services/health-department/src/components/App/ProcessDetails/Note/EmptyNote/EmptyNote.styled.js
@@ -0,0 +1,30 @@
+import styled from 'styled-components';
+import { PlusCircleOutlined } from '@ant-design/icons';
+
+export const Title = styled.div`
+  color: rgb(80, 102, 124);
+  font-size: 16px;
+  font-weight: bold;
+  text-transform: uppercase;
+  margin-top: 2px;
+`;
+
+export const EmptyNoteWrapper = styled.div`
+  display: flex;
+  cursor: pointer;
+  margin-top: 32px;
+  width: fit-content;
+
+  &:hover {
+    & div,
+    span {
+      color: black;
+    }
+  }
+`;
+
+export const PlusCircleNote = styled(PlusCircleOutlined)`
+  margin-right: 16px;
+  font-size: 26px;
+  color: rgb(80, 102, 124);
+`;
diff --git a/services/health-department/src/components/App/ProcessDetails/Note/EmptyNote/index.js b/services/health-department/src/components/App/ProcessDetails/Note/EmptyNote/index.js
new file mode 100644
index 0000000..122057b
--- /dev/null
+++ b/services/health-department/src/components/App/ProcessDetails/Note/EmptyNote/index.js
@@ -0,0 +1 @@
+export { EmptyNote } from './EmptyNote.react';
diff --git a/services/health-department/src/components/App/ProcessDetails/Note/Note.react.js b/services/health-department/src/components/App/ProcessDetails/Note/Note.react.js
new file mode 100644
index 0000000..46351d4
--- /dev/null
+++ b/services/health-department/src/components/App/ProcessDetails/Note/Note.react.js
@@ -0,0 +1,23 @@
+import React, { useState } from 'react';
+import { NoteProcess } from './NoteProcess';
+import { EmptyNote } from './EmptyNote';
+
+export const Note = ({ process, keysData, processId }) => {
+  const [editMode, setEditMode] = useState(false);
+
+  return (
+    <>
+      {process.note || editMode ? (
+        <NoteProcess
+          process={process}
+          keys={keysData}
+          processId={processId}
+          setEditMode={setEditMode}
+          editMode={editMode}
+        />
+      ) : (
+        <EmptyNote setEditMode={setEditMode} />
+      )}
+    </>
+  );
+};
diff --git a/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteButtonWrapper/NoteButtonWrapper.react.js b/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteButtonWrapper/NoteButtonWrapper.react.js
new file mode 100644
index 0000000..1a28549
--- /dev/null
+++ b/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteButtonWrapper/NoteButtonWrapper.react.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import { useIntl } from 'react-intl';
+import { ButtonWrapper, LinkButton } from './NoteButtonWrapper.styled';
+
+export const NoteButtonWrapper = ({
+  editMode,
+  setEditMode,
+  handleNoteProcess,
+  isButtonDisabled,
+}) => {
+  const intl = useIntl();
+  return (
+    <ButtonWrapper>
+      {editMode && (
+        <LinkButton onClick={() => setEditMode(false)} hasMarginRight>
+          {intl.formatMessage({
+            id: 'processDetails.cancelProcessNote',
+          })}
+        </LinkButton>
+      )}
+      <LinkButton
+        onClick={handleNoteProcess}
+        disabled={editMode ? isButtonDisabled : false}
+      >
+        {intl.formatMessage({
+          id: editMode
+            ? 'processDetails.saveProcessNote'
+            : 'processDetails.updateProcessNote',
+        })}
+      </LinkButton>
+    </ButtonWrapper>
+  );
+};
diff --git a/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteButtonWrapper/NoteButtonWrapper.styled.js b/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteButtonWrapper/NoteButtonWrapper.styled.js
new file mode 100644
index 0000000..28bfb3a
--- /dev/null
+++ b/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteButtonWrapper/NoteButtonWrapper.styled.js
@@ -0,0 +1,16 @@
+import styled from 'styled-components';
+
+export const ButtonWrapper = styled.div`
+  display: flex;
+  justify-content: flex-end;
+  margin-left: 32px;
+`;
+
+export const LinkButton = styled.a`
+  color: rgb(80, 102, 124);
+  font-size: 16px;
+  font-weight: bold;
+  text-transform: uppercase;
+  text-decoration: none;
+  ${({ hasMarginRight }) => (hasMarginRight ? 'margin-right: 24px;' : '')};
+`;
diff --git a/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteButtonWrapper/index.js b/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteButtonWrapper/index.js
new file mode 100644
index 0000000..e86c714
--- /dev/null
+++ b/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteButtonWrapper/index.js
@@ -0,0 +1 @@
+export { NoteButtonWrapper } from './NoteButtonWrapper.react';
diff --git a/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteProcess.helper.js b/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteProcess.helper.js
new file mode 100644
index 0000000..7fadfa4
--- /dev/null
+++ b/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteProcess.helper.js
@@ -0,0 +1,70 @@
+import {
+  ENCRYPT_DLIES,
+  hexToBase64,
+  base64ToHex,
+  bytesToHex,
+  encodeUtf8,
+  decodeUtf8,
+  hexToBytes,
+} from '@lucaapp/crypto';
+import { updateProcess } from 'network/api';
+import {
+  decryptNote,
+  generateSignature,
+  verifyNoteSignature,
+} from 'utils/cryptoKeyOperations';
+
+function updateProcessHandler(keys, note) {
+  if (!note || note.trim().length === 0) {
+    return {
+      note: null,
+      noteIV: null,
+      noteMAC: null,
+      notePublicKey: null,
+      noteSignature: null,
+    };
+  }
+
+  const encodedNote = bytesToHex(encodeUtf8(note));
+  const { publicKey, data: encryptedData, iv, mac } = ENCRYPT_DLIES(
+    base64ToHex(keys.publicHDEKP),
+    encodedNote
+  );
+  const signature = generateSignature(encryptedData + mac + iv);
+
+  return {
+    note: hexToBase64(encryptedData),
+    noteIV: hexToBase64(iv),
+    noteMAC: hexToBase64(mac),
+    notePublicKey: hexToBase64(publicKey),
+    noteSignature: hexToBase64(signature),
+  };
+}
+
+export async function updateProcessRequest(processID, keys, note) {
+  const noteHandler = updateProcessHandler(keys, note);
+
+  await updateProcess(processID, noteHandler).then(response => {
+    if (response.status >= 400 && response.status <= 500)
+      throw new Error('Error');
+  });
+}
+
+export function getDecryptedNote(process) {
+  const { notePublicKey, noteIV, noteMAC, note, noteSignature } = process;
+
+  if (!note) {
+    return '';
+  }
+
+  verifyNoteSignature(
+    base64ToHex(note),
+    base64ToHex(noteIV),
+    base64ToHex(noteMAC),
+    base64ToHex(noteSignature)
+  );
+
+  return note
+    ? hexToBytes(decodeUtf8(decryptNote(notePublicKey, noteIV, noteMAC, note)))
+    : '';
+}
diff --git a/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteProcess.react.js b/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteProcess.react.js
new file mode 100644
index 0000000..f0aaabf
--- /dev/null
+++ b/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteProcess.react.js
@@ -0,0 +1,122 @@
+import React, { useMemo, useState } from 'react';
+import { useIntl } from 'react-intl';
+import { useQueryClient } from 'react-query';
+import { Input, notification, Form } from 'antd';
+
+import { MAX_PROCESS_NOTE_LENGTH } from 'constants/valueLength';
+import { InvalidNoteSignatureError } from 'errors/InvalidNoteSignatureError';
+
+import { updateProcessRequest, getDecryptedNote } from './NoteProcess.helper';
+
+import { NoteButtonWrapper } from './NoteButtonWrapper';
+import { NoteWrapper, Title, TextNote, NoteHeader } from './NoteProcess.styled';
+
+export const NoteProcess = ({
+  process,
+  processId,
+  keys,
+  editMode,
+  setEditMode,
+}) => {
+  const intl = useIntl();
+  const queryClient = useQueryClient();
+  const [form] = Form.useForm();
+  const [isButtonDisabled, setIsButtonDisabled] = useState(true);
+  const processNote = useMemo(() => {
+    try {
+      return getDecryptedNote(process);
+    } catch (error) {
+      console.error(error);
+
+      if (error instanceof InvalidNoteSignatureError) {
+        notification.error({
+          message: intl.formatMessage({
+            id: 'processDetails.note.invalidSignature',
+          }),
+        });
+      }
+
+      return '';
+    }
+  }, [intl, process]);
+
+  const onEditModeChanged = changedEditMode => {
+    if (!changedEditMode) {
+      form.resetFields();
+    }
+
+    setEditMode(changedEditMode);
+  };
+
+  const addNoteToProcess = () => {
+    const note = form.getFieldValue('note');
+    if (note === processNote) {
+      setEditMode(false);
+      return;
+    }
+
+    updateProcessRequest(process.uuid, keys, note)
+      .then(() => {
+        setEditMode(false);
+        queryClient.invalidateQueries(`process${processId}`);
+        notification.success({
+          message: intl.formatMessage({
+            id: 'processDetails.note.success',
+          }),
+        });
+      })
+      .catch(() => {
+        notification.error({
+          message: intl.formatMessage({
+            id: 'processDetails.note.error',
+          }),
+        });
+      });
+  };
+
+  const onValueUpdate = (_, values) => {
+    return values.note === processNote
+      ? setIsButtonDisabled(true)
+      : setIsButtonDisabled(false);
+  };
+
+  const handleNoteProcess = () =>
+    !editMode ? setEditMode(true) : addNoteToProcess();
+
+  return (
+    <NoteWrapper>
+      <NoteHeader>
+        <Title>{intl.formatMessage({ id: 'processDetails.labelNote' })}</Title>
+        <NoteButtonWrapper
+          editMode={editMode}
+          setEditMode={onEditModeChanged}
+          handleNoteProcess={handleNoteProcess}
+          isButtonDisabled={isButtonDisabled}
+        />
+      </NoteHeader>
+      {editMode ? (
+        <Form
+          form={form}
+          onFinish={addNoteToProcess}
+          onValuesChange={onValueUpdate}
+          initialValues={{ note: processNote }}
+        >
+          <Form.Item name="note">
+            <Input.TextArea
+              showCount
+              value={processNote}
+              maxLength={MAX_PROCESS_NOTE_LENGTH}
+            />
+          </Form.Item>
+        </Form>
+      ) : (
+        <TextNote>
+          {processNote ||
+            intl.formatMessage({
+              id: 'processDetails.labelNote.empty',
+            })}
+        </TextNote>
+      )}
+    </NoteWrapper>
+  );
+};
diff --git a/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteProcess.styled.js b/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteProcess.styled.js
new file mode 100644
index 0000000..9400c5b
--- /dev/null
+++ b/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/NoteProcess.styled.js
@@ -0,0 +1,24 @@
+import styled from 'styled-components';
+
+export const NoteWrapper = styled.div`
+  margin-top: 32px;
+  width: 50%;
+`;
+
+export const NoteHeader = styled.div`
+  display: flex;
+  flex-direction: row;
+  margin-bottom: 16px;
+`;
+
+export const Title = styled.div`
+  font-size: 16px;
+  font-weight: bold;
+`;
+
+export const TextNote = styled.p`
+  font-size: 14px;
+  font-weight: 500;
+  letter-spacing: 0;
+  word-break: break-all;
+`;
diff --git a/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/index.js b/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/index.js
new file mode 100644
index 0000000..7351354
--- /dev/null
+++ b/services/health-department/src/components/App/ProcessDetails/Note/NoteProcess/index.js
@@ -0,0 +1 @@
+export { NoteProcess } from './NoteProcess.react';
diff --git a/services/health-department/src/components/App/ProcessDetails/Note/index.js b/services/health-department/src/components/App/ProcessDetails/Note/index.js
new file mode 100644
index 0000000..51994cf
--- /dev/null
+++ b/services/health-department/src/components/App/ProcessDetails/Note/index.js
@@ -0,0 +1 @@
+export { Note } from './Note.react';
diff --git a/services/health-department/src/components/App/ProcessDetails/ProcessDetails.react.js b/services/health-department/src/components/App/ProcessDetails/ProcessDetails.react.js
index e33d46d..cc82bbe 100644
--- a/services/health-department/src/components/App/ProcessDetails/ProcessDetails.react.js
+++ b/services/health-department/src/components/App/ProcessDetails/ProcessDetails.react.js
@@ -9,19 +9,21 @@ import { useKeyLoader } from 'components/hooks/useKeyLoader';
 import { BackButton } from './BackButton';
 import { HeaderRow } from './HeaderRow';
 import { InfoRow } from './InfoRow';
+import { Note } from './Note';
 import { History } from './History';
 import { PageWrapper, ContentWrapper } from './ProcessDetails.styled';
 
 export const ProcessDetails = () => {
   const { processId } = useParams();
-  const { isLoading: isLoadingKey, error: keyError } = useKeyLoader();
-
+  const { keysData, isLoading: isLoadingKey, error: keyError } = useKeyLoader();
   const {
     isLoading: isLoadingProcess,
     error: processError,
     data: process,
   } = useQuery(`process${processId}`, () => getProcess(processId), {
     cacheTime: 0,
+    staleTime: 60000,
+    refetchInterval: 60000,
     retry: false,
   });
 
@@ -34,6 +36,7 @@ export const ProcessDetails = () => {
         <BackButton />
         <HeaderRow process={process} />
         <InfoRow process={process} />
+        <Note process={process} keysData={keysData} processId={processId} />
         <History process={process} />
       </ContentWrapper>
     </PageWrapper>
diff --git a/services/health-department/src/components/App/Profile/AuditLogsDownloads/AuditLogsDownloads.react.js b/services/health-department/src/components/App/Profile/AuditLogsDownloads/AuditLogsDownloads.react.js
new file mode 100644
index 0000000..52100fd
--- /dev/null
+++ b/services/health-department/src/components/App/Profile/AuditLogsDownloads/AuditLogsDownloads.react.js
@@ -0,0 +1,76 @@
+import React from 'react';
+import moment from 'moment';
+import { useIntl } from 'react-intl';
+import { useQuery } from 'react-query';
+import { notification } from 'antd';
+
+// Api
+import { getAuditLog, getEmployees } from 'network/api';
+
+import { SecondaryButton } from 'components/general';
+import { FILES } from './DownloadFiles';
+
+import {
+  Wrapper,
+  StyledHeadline,
+  StyledInfo,
+  ButtonWrapper,
+} from './AuditLogsDownloads.styled';
+
+export const AuditLogsDownloads = () => {
+  const intl = useIntl();
+
+  const { isLoading, error, data: employees } = useQuery(
+    'getAllEmployees',
+    () => getEmployees(true),
+    { refetchOnWindowFocus: false }
+  );
+
+  const downloadLogFile = logData => {
+    const blob = new Blob([logData], { type: 'text' });
+    const element = window.document.createElement('a');
+    element.href = window.URL.createObjectURL(blob);
+    element.download = `${intl.formatMessage({
+      id: 'auditlogDownload.filename',
+    })}.log-file`;
+    document.body.append(element);
+    element.click();
+    element.remove();
+  };
+
+  const getLogs = () => {
+    const startTime = moment().unix();
+    const endTime = moment().subtract(1, 'years').unix();
+
+    getAuditLog({ timeframe: [startTime, endTime] })
+      .then(response => {
+        downloadLogFile(response);
+      })
+      .catch(() => {
+        notification.error({
+          message: intl.formatMessage({
+            id: 'notification.auditlogDownload.error',
+          }),
+        });
+      });
+  };
+
+  if (isLoading || error) return null;
+
+  return (
+    <Wrapper>
+      <StyledHeadline>
+        {intl.formatMessage({ id: 'profile.auditLogs.headline' })}
+      </StyledHeadline>
+      <StyledInfo>
+        {intl.formatMessage({ id: 'profile.auditLogs.info' })}
+      </StyledInfo>
+      <ButtonWrapper>
+        <SecondaryButton onClick={getLogs} style={{ marginBottom: 24 }}>
+          {intl.formatMessage({ id: 'profile.auditLogs.downloadAuditLog' })}
+        </SecondaryButton>
+        <FILES.EmailToUuidListDownload employees={employees} />
+      </ButtonWrapper>
+    </Wrapper>
+  );
+};
diff --git a/services/health-department/src/components/App/Profile/AuditLogsDownloads/AuditLogsDownloads.styled.js b/services/health-department/src/components/App/Profile/AuditLogsDownloads/AuditLogsDownloads.styled.js
new file mode 100644
index 0000000..e6bd7f1
--- /dev/null
+++ b/services/health-department/src/components/App/Profile/AuditLogsDownloads/AuditLogsDownloads.styled.js
@@ -0,0 +1,25 @@
+import styled from 'styled-components';
+
+export const Wrapper = styled.div`
+  width: 65%;
+`;
+
+export const ButtonWrapper = styled.div`
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+`;
+
+export const StyledHeadline = styled.div`
+  margin-bottom: 16px;
+  color: rgb(0, 0, 0);
+  font-family: Montserrat-Bold, sans-serif;
+  font-size: 16px;
+  font-weight: bold;
+`;
+
+export const StyledInfo = styled.div`
+  margin-bottom: 16px;
+  color: rgb(0, 0, 0);
+  font-size: 14px;
+`;
diff --git a/services/health-department/src/components/App/Profile/AuditLogsDownloads/DownloadFiles/EmailToUuidListDownload.js b/services/health-department/src/components/App/Profile/AuditLogsDownloads/DownloadFiles/EmailToUuidListDownload.js
new file mode 100644
index 0000000..fb6ea2c
--- /dev/null
+++ b/services/health-department/src/components/App/Profile/AuditLogsDownloads/DownloadFiles/EmailToUuidListDownload.js
@@ -0,0 +1,42 @@
+import React from 'react';
+import { useIntl } from 'react-intl';
+import ReactExport from 'react-data-export';
+import { sanitizeForCSV } from 'utils/sanitizer';
+
+import { SecondaryButton } from 'components/general';
+
+const {
+  ExcelFile,
+  ExcelFile: { ExcelSheet, ExcelColumn },
+} = ReactExport;
+
+export const EmailToUuidListDownload = ({ employees }) => {
+  const intl = useIntl();
+
+  return (
+    <ExcelFile
+      filename={intl.formatMessage({ id: 'profile.auditLog.downloadFileName' })}
+      element={
+        <SecondaryButton>
+          {intl.formatMessage({
+            id: 'profile.auditLogs.downloadUserList',
+          })}
+        </SecondaryButton>
+      }
+    >
+      <ExcelSheet
+        data={employees}
+        name={intl.formatMessage({ id: 'profile.auditLog.downloadFileName' })}
+      >
+        <ExcelColumn
+          label={intl.formatMessage({ id: 'profile.auditLog.email' })}
+          value={col => sanitizeForCSV(col.email)}
+        />
+        <ExcelColumn
+          label={intl.formatMessage({ id: 'profile.auditLog.uuid' })}
+          value={col => sanitizeForCSV(col.uuid)}
+        />
+      </ExcelSheet>
+    </ExcelFile>
+  );
+};
diff --git a/services/health-department/src/components/App/Profile/AuditLogsDownloads/DownloadFiles/index.js b/services/health-department/src/components/App/Profile/AuditLogsDownloads/DownloadFiles/index.js
new file mode 100644
index 0000000..c03c05b
--- /dev/null
+++ b/services/health-department/src/components/App/Profile/AuditLogsDownloads/DownloadFiles/index.js
@@ -0,0 +1,5 @@
+import { EmailToUuidListDownload } from './EmailToUuidListDownload';
+
+export const FILES = {
+  EmailToUuidListDownload,
+};
diff --git a/services/health-department/src/components/App/Profile/AuditLogsDownloads/index.js b/services/health-department/src/components/App/Profile/AuditLogsDownloads/index.js
new file mode 100644
index 0000000..cd9b3a7
--- /dev/null
+++ b/services/health-department/src/components/App/Profile/AuditLogsDownloads/index.js
@@ -0,0 +1 @@
+export { AuditLogsDownloads } from './AuditLogsDownloads.react';
diff --git a/services/health-department/src/components/App/Profile/ChangePasswordView/ChangePasswordView.react.js b/services/health-department/src/components/App/Profile/ChangePasswordView/ChangePasswordView.react.js
index 2cbcd6e..042bb26 100644
--- a/services/health-department/src/components/App/Profile/ChangePasswordView/ChangePasswordView.react.js
+++ b/services/health-department/src/components/App/Profile/ChangePasswordView/ChangePasswordView.react.js
@@ -1,12 +1,12 @@
 import React from 'react';
 import { useIntl } from 'react-intl';
 import { Input, Form, notification } from 'antd';
-import { PrimaryButton } from 'components/general';
+import { SecondaryButton } from 'components/general';
 import { changePassword } from 'network/api';
 import { passwordMeetsCriteria } from 'utils/passwordCheck';
 import { handleResponse } from './ChangePasswordView.helper';
 import { Wrapper, StyledHeadline } from './ChangePasswordView.styled';
-import { inputStyle, StyledButtonRow } from '../Profile.styled';
+import { StyledInput, StyledButtonRow } from '../Profile.styled';
 
 export const ChangePasswordView = () => {
   const intl = useIntl();
@@ -52,7 +52,7 @@ export const ChangePasswordView = () => {
             },
           ]}
         >
-          <Input.Password style={inputStyle} />
+          <StyledInput as={Input.Password} />
         </Form.Item>
         <Form.Item
           colon={false}
@@ -82,7 +82,7 @@ export const ChangePasswordView = () => {
             }),
           ]}
         >
-          <Input.Password style={inputStyle} />
+          <StyledInput as={Input.Password} />
         </Form.Item>
         <Form.Item
           colon={false}
@@ -113,15 +113,15 @@ export const ChangePasswordView = () => {
             }),
           ]}
         >
-          <Input.Password style={inputStyle} />
+          <StyledInput as={Input.Password} />
         </Form.Item>
         <StyledButtonRow>
           <Form.Item>
-            <PrimaryButton htmlType="submit">
+            <SecondaryButton htmlType="submit">
               {intl.formatMessage({
                 id: 'profile.changePassword',
               })}
-            </PrimaryButton>
+            </SecondaryButton>
           </Form.Item>
         </StyledButtonRow>
       </Form>
diff --git a/services/health-department/src/components/App/Profile/ContactInformation/ContactInformation.react.js b/services/health-department/src/components/App/Profile/ContactInformation/ContactInformation.react.js
new file mode 100644
index 0000000..f9e59ec
--- /dev/null
+++ b/services/health-department/src/components/App/Profile/ContactInformation/ContactInformation.react.js
@@ -0,0 +1,164 @@
+import React, { useEffect, useState } from 'react';
+import { Form, notification } from 'antd';
+import { useQueryClient } from 'react-query';
+import { useIntl } from 'react-intl';
+import { setContactInformation } from 'network/api';
+import { SecondaryButton } from 'components/general/Buttons.styled';
+
+import {
+  getEmailRule,
+  getMaxLengthRule,
+  getPhoneRule,
+} from 'utils/validatorRules';
+import { getFormattedPhoneNumber } from 'utils/checkPhoneNumber';
+import { MAX_EMAIL_LENGTH, MAX_PHONE_LENGTH } from 'constants/valueLength';
+
+import { StyledHeadline, Wrapper } from './ContactInformation.styled';
+import { StyledButtonRow, Description, StyledInput } from '../Profile.styled';
+
+export const ContactInformation = ({ department }) => {
+  const intl = useIntl();
+  const [form] = Form.useForm();
+  const [isButtonDisabled, setIsButtonDisabled] = useState(true);
+  const [isRequired, setIsRequired] = useState(false);
+  const useQuery = useQueryClient();
+
+  const handleError = () =>
+    notification.error({
+      message: intl.formatMessage({
+        id: 'notification.profile.contactInformation.error',
+      }),
+    });
+
+  const onFinish = ({ email, phone }) => {
+    const formattedPhone = phone?.trim()
+      ? getFormattedPhoneNumber(phone)
+      : undefined;
+    setContactInformation({
+      email: email?.trim() || undefined,
+      phone: formattedPhone,
+    })
+      .then(response => {
+        if (response.ok) {
+          useQuery.invalidateQueries('healthDepartment');
+          form.setFieldsValue({ phone: formattedPhone });
+          notification.success({
+            message: intl.formatMessage({
+              id: 'notification.profile.contactInformation.success',
+            }),
+          });
+          setIsButtonDisabled(true);
+        } else {
+          handleError();
+        }
+      })
+      .catch(() => handleError());
+  };
+
+  useEffect(() => {
+    form.setFields([
+      {
+        name: 'email',
+        value: department.email || '',
+        errors: [],
+      },
+      {
+        name: 'phone',
+        value: department.phone || '',
+        errors: [],
+      },
+    ]);
+  }, [form, department.email, department.phone]);
+
+  const onValuesUpdate = (_, values) => {
+    if (!values.email && !values.phone) {
+      setIsRequired(true);
+      setIsButtonDisabled(true);
+    }
+    if (values.email || values.phone) {
+      setIsRequired(false);
+      setIsButtonDisabled(false);
+    }
+    /**
+     * Clear error message of fields
+     * Source: https://github.com/ant-design/ant-design/issues/24599
+     */
+    const updatedFields = Object.keys(values)
+      .filter(name => form.getFieldError(name).length)
+      .map(name => ({ name, errors: [] }));
+    form.setFields(updatedFields);
+  };
+
+  const validateAtLeastOneInput = () => {
+    if (
+      !form.getFieldValue('email').trim() &&
+      !form.getFieldValue('phone').trim()
+    ) {
+      return Promise.reject();
+    }
+    return Promise.resolve();
+  };
+
+  const requireOneFieldRule = {
+    required: isRequired,
+    validator: () => validateAtLeastOneInput(),
+    message: intl.formatMessage({
+      id: 'profile.contactInformation.required',
+    }),
+  };
+
+  return (
+    <Wrapper>
+      <StyledHeadline>
+        {intl.formatMessage({ id: 'profile.contactInformation.headline' })}
+      </StyledHeadline>
+      <Description>
+        {intl.formatMessage({ id: 'profile.contactInformation.description' })}
+      </Description>
+      <Form
+        form={form}
+        onFinish={onFinish}
+        onValuesChange={onValuesUpdate}
+        initialValues={{}}
+      >
+        <Form.Item
+          colon={false}
+          name="email"
+          label={intl.formatMessage({
+            id: 'profile.contactInformation.email',
+          })}
+          rules={[
+            getEmailRule(intl),
+            getMaxLengthRule(intl, MAX_EMAIL_LENGTH),
+            requireOneFieldRule,
+          ]}
+        >
+          <StyledInput />
+        </Form.Item>
+        <Form.Item
+          colon={false}
+          name="phone"
+          label={intl.formatMessage({
+            id: 'profile.contactInformation.phone',
+          })}
+          rules={[
+            getPhoneRule(intl),
+            getMaxLengthRule(intl, MAX_PHONE_LENGTH),
+            requireOneFieldRule,
+          ]}
+        >
+          <StyledInput />
+        </Form.Item>
+        <StyledButtonRow>
+          <Form.Item>
+            <SecondaryButton htmlType="submit" disabled={isButtonDisabled}>
+              {intl.formatMessage({
+                id: 'save',
+              })}
+            </SecondaryButton>
+          </Form.Item>
+        </StyledButtonRow>
+      </Form>
+    </Wrapper>
+  );
+};
diff --git a/services/health-department/src/components/App/Profile/ContactInformation/ContactInformation.styled.js b/services/health-department/src/components/App/Profile/ContactInformation/ContactInformation.styled.js
new file mode 100644
index 0000000..e152450
--- /dev/null
+++ b/services/health-department/src/components/App/Profile/ContactInformation/ContactInformation.styled.js
@@ -0,0 +1,13 @@
+import styled from 'styled-components';
+
+export const StyledHeadline = styled.div`
+  margin-bottom: 16px;
+  color: rgb(0, 0, 0);
+  font-family: Montserrat-Bold, sans-serif;
+  font-size: 16px;
+  font-weight: bold;
+`;
+
+export const Wrapper = styled.div`
+  width: 65%;
+`;
diff --git a/services/health-department/src/components/App/Profile/ContactInformation/index.js b/services/health-department/src/components/App/Profile/ContactInformation/index.js
new file mode 100644
index 0000000..af72e9e
--- /dev/null
+++ b/services/health-department/src/components/App/Profile/ContactInformation/index.js
@@ -0,0 +1 @@
+export { ContactInformation } from './ContactInformation.react';
diff --git a/services/health-department/src/components/App/Profile/DownloadSigningTool/DownloadSigningTool.react.js b/services/health-department/src/components/App/Profile/DownloadSigningTool/DownloadSigningTool.react.js
index 9cd6762..fb2bccf 100644
--- a/services/health-department/src/components/App/Profile/DownloadSigningTool/DownloadSigningTool.react.js
+++ b/services/health-department/src/components/App/Profile/DownloadSigningTool/DownloadSigningTool.react.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import { useIntl } from 'react-intl';
 import { useQuery } from 'react-query';
-import { PrimaryButton } from 'components/general';
+import { PrimaryButton, Divider } from 'components/general';
 
 // Api
 import { getSigningTool } from 'network/api';
@@ -31,32 +31,38 @@ export const DownloadSigningTool = ({ department }) => {
     return null;
 
   return (
-    <Wrapper>
-      <ChildWrapper>
-        <StyledHeadline>
-          {intl.formatMessage({ id: 'profile.signingTool.download.title' })}
-        </StyledHeadline>
-        <StyledText>
-          {intl.formatMessage(
-            { id: 'profile.signingTool.download.info' },
-            { br: <br /> }
-          )}
-        </StyledText>
-        <ButtonWrapper>
-          <PrimaryButton href={`${signingTool[0].downloadUrl}`} target="_blank">
+    <>
+      <Wrapper>
+        <ChildWrapper>
+          <StyledHeadline>
+            {intl.formatMessage({ id: 'profile.signingTool.download.title' })}
+          </StyledHeadline>
+          <StyledText>
             {intl.formatMessage(
-              { id: 'profile.signingTool.download.button' },
-              { version: `v${signingTool[0].version}` }
+              { id: 'profile.signingTool.download.info' },
+              { br: <br /> }
             )}
-          </PrimaryButton>
-          <VersionTag>
-            {intl.formatMessage(
-              { id: 'profile.signingTool.download.hash' },
-              { hash: signingTool[0].hash }
-            )}
-          </VersionTag>
-        </ButtonWrapper>
-      </ChildWrapper>
-    </Wrapper>
+          </StyledText>
+          <ButtonWrapper>
+            <PrimaryButton
+              href={`${signingTool[0].downloadUrl}`}
+              target="_blank"
+            >
+              {intl.formatMessage(
+                { id: 'profile.signingTool.download.button' },
+                { version: `v${signingTool[0].version}` }
+              )}
+            </PrimaryButton>
+            <VersionTag>
+              {intl.formatMessage(
+                { id: 'profile.signingTool.download.hash' },
+                { hash: signingTool[0].hash }
+              )}
+            </VersionTag>
+          </ButtonWrapper>
+        </ChildWrapper>
+      </Wrapper>
+      <Divider />
+    </>
   );
 };
diff --git a/services/health-department/src/components/App/Profile/DownloadSigningTool/DownloadSigningTool.styled.js b/services/health-department/src/components/App/Profile/DownloadSigningTool/DownloadSigningTool.styled.js
index 15a4332..a9997d3 100644
--- a/services/health-department/src/components/App/Profile/DownloadSigningTool/DownloadSigningTool.styled.js
+++ b/services/health-department/src/components/App/Profile/DownloadSigningTool/DownloadSigningTool.styled.js
@@ -3,7 +3,6 @@ import styled from 'styled-components';
 export const Wrapper = styled.div`
   padding-bottom: 32px;
   margin-bottom: 24px;
-  border-bottom: 1px solid black;
 `;
 
 export const ChildWrapper = styled.div`
diff --git a/services/health-department/src/components/App/Profile/Profile.react.js b/services/health-department/src/components/App/Profile/Profile.react.js
index 3e5e114..6050e1b 100644
--- a/services/health-department/src/components/App/Profile/Profile.react.js
+++ b/services/health-department/src/components/App/Profile/Profile.react.js
@@ -7,17 +7,21 @@ import { getHealthDepartment } from 'network/api';
 // Components
 import { VerificationTag } from 'components/App/VerificationTag';
 import { VersionFooter } from 'components/App/VersionFooter';
+import { Divider } from 'components/general';
 import { BackButton } from './BackButton';
 import { ProfileOverview } from './ProfileOverview';
 import { DownloadSigningTool } from './DownloadSigningTool';
+import { AuditLogsDownloads } from './AuditLogsDownloads';
 import { ChangePasswordView } from './ChangePasswordView';
 import {
   ProfileWrapper,
   StyledChildWrapper,
-  ProfileHeader,
+  Header,
   VerificationTagWrapper,
   VersionFooterWrapper,
+  InformationWrapper,
 } from './Profile.styled';
+import { ContactInformation } from './ContactInformation';
 
 export const Profile = ({ profileData }) => {
   const intl = useIntl();
@@ -39,22 +43,39 @@ export const Profile = ({ profileData }) => {
       <ProfileWrapper>
         <StyledChildWrapper>
           <BackButton />
-          <ProfileHeader>
+          <Header>
             {intl.formatMessage({ id: 'navigation.profile' })}
             <VerificationTagWrapper>
               <VerificationTag />
             </VerificationTagWrapper>
-          </ProfileHeader>
+          </Header>
           <DownloadSigningTool department={department} />
           <ProfileOverview me={profileData} department={department} />
         </StyledChildWrapper>
+        <Divider />
         <StyledChildWrapper>
           <ChangePasswordView />
         </StyledChildWrapper>
+      </ProfileWrapper>
+
+      <InformationWrapper>
+        {profileData.isAdmin && (
+          <>
+            <Header>
+              {intl.formatMessage({
+                id: 'navigation.comprehensiveInformation',
+              })}
+            </Header>
+            <ContactInformation department={department} />
+            <StyledChildWrapper>
+              <AuditLogsDownloads />
+            </StyledChildWrapper>
+          </>
+        )}
         <VersionFooterWrapper>
           <VersionFooter />
         </VersionFooterWrapper>
-      </ProfileWrapper>
+      </InformationWrapper>
     </>
   );
 };
diff --git a/services/health-department/src/components/App/Profile/Profile.styled.js b/services/health-department/src/components/App/Profile/Profile.styled.js
index e728d4c..925a467 100644
--- a/services/health-department/src/components/App/Profile/Profile.styled.js
+++ b/services/health-department/src/components/App/Profile/Profile.styled.js
@@ -1,4 +1,5 @@
 import styled from 'styled-components';
+import { Input } from 'antd';
 
 export const ProfileWrapper = styled.div`
   padding: 30px 32px;
@@ -6,7 +7,13 @@ export const ProfileWrapper = styled.div`
   margin: 40px;
 `;
 
-export const ProfileHeader = styled.div`
+export const InformationWrapper = styled.div`
+  padding: 30px 32px;
+  background-color: white;
+  margin: 0px 40px 40px 40px;
+`;
+
+export const Header = styled.div`
   color: rgba(0, 0, 0, 0.87);
   font-family: Montserrat-SemiBold, sans-serif;
   font-size: 34px;
@@ -15,8 +22,13 @@ export const ProfileHeader = styled.div`
   display: flex;
 `;
 
+export const Description = styled.p`
+  margin-bottom: 36px;
+  display: flex;
+`;
+
 export const StyledChildWrapper = styled.div`
-  margin-bottom: 24px;
+  margin-bottom: 64px;
 `;
 
 export const VerificationTagWrapper = styled.div`
@@ -24,19 +36,17 @@ export const VerificationTagWrapper = styled.div`
 `;
 
 export const VersionFooterWrapper = styled.div`
-  padding-top: 42px;
   text-align: right;
 `;
 
-export const inputStyle = {
-  border: '1px solid #696969',
-  backgroundColor: 'transparent',
-};
-
-export const disabledInputStyle = {
-  ...inputStyle,
-  backgroundColor: 'rgba(0,0,0,0.1) !important',
-};
+export const StyledInput = styled(Input)`
+  border: 1px solid #696969;
+  background-color: transparent;
+  &:hover,
+  &:focus {
+    border: 1px solid #696969;
+  }
+`;
 
 export const StyledButtonRow = styled.div`
   width: 100%;
diff --git a/services/health-department/src/components/App/Tracking/TrackingList/Table/Entry/Entry.react.js b/services/health-department/src/components/App/Tracking/TrackingList/Table/Entry/Entry.react.js
index e46efbb..ecaf851 100644
--- a/services/health-department/src/components/App/Tracking/TrackingList/Table/Entry/Entry.react.js
+++ b/services/health-department/src/components/App/Tracking/TrackingList/Table/Entry/Entry.react.js
@@ -1,10 +1,13 @@
 import React from 'react';
 import { useIntl } from 'react-intl';
 import { useHistory } from 'react-router-dom';
+import { useQuery } from 'react-query';
 
 import { PROCESS_DETAILS_BASE_ROUTE } from 'constants/routes';
+import { getProcess } from 'network/api';
 
 // Components
+import { FileOutlined } from '@ant-design/icons';
 import { Row, Column } from '../Table.styled';
 import { CheckDone } from './CheckDone';
 import { ManualSearchNameDisplay } from './ManualSearchNameDisplay';
@@ -20,6 +23,13 @@ export const Entry = ({ process, onProcessName }) => {
     history.push(`${PROCESS_DETAILS_BASE_ROUTE}${process.uuid}`);
   };
 
+  const {
+    isLoading,
+    error,
+    data: processDetails,
+  } = useQuery(`process${process.uuid}`, () => getProcess(process.uuid));
+
+  if (isLoading || error) return null;
   return (
     <Row data-cy="processEntry" onClick={openDetails}>
       <Column flex="10%">
@@ -40,7 +50,7 @@ export const Entry = ({ process, onProcessName }) => {
           />
         )}
       </Column>
-      <Column flex="15%">
+      <Column flex="10%">
         <CreationDate createdAt={process.createdAt} />
       </Column>
       <Column flex="20%">
@@ -49,6 +59,11 @@ export const Entry = ({ process, onProcessName }) => {
       <Column flex="10%">
         <CheckDone status={process.status} />
       </Column>
+      <Column flex="5%">
+        {!!processDetails.note && (
+          <FileOutlined style={{ marginLeft: 'auto' }} />
+        )}
+      </Column>
     </Row>
   );
 };
diff --git a/services/health-department/src/components/App/Tracking/TrackingList/Table/Entry/ManualSearchNameDisplay/ManualSearchNameDisplay.react.js b/services/health-department/src/components/App/Tracking/TrackingList/Table/Entry/ManualSearchNameDisplay/ManualSearchNameDisplay.react.js
index 194b220..acc6c32 100644
--- a/services/health-department/src/components/App/Tracking/TrackingList/Table/Entry/ManualSearchNameDisplay/ManualSearchNameDisplay.react.js
+++ b/services/health-department/src/components/App/Tracking/TrackingList/Table/Entry/ManualSearchNameDisplay/ManualSearchNameDisplay.react.js
@@ -1,10 +1,10 @@
 import React from 'react';
 import moment from 'moment';
 
-import { useLocationWithTransfers } from 'components/hooks/useLocationWithTransfers';
+import { useLocationTransfers } from 'components/hooks/useLocationTransfers';
 
 export const ManualSearchNameDisplay = ({ processId, onProcessName }) => {
-  const locations = useLocationWithTransfers(processId);
+  const locations = useLocationTransfers(processId);
 
   if (!locations.length) return null;
   const baseLocation = locations.find(location => !location.locationName);
diff --git a/services/health-department/src/components/App/Tracking/TrackingList/Table/Entry/SelectAssignee/SelectAssignee.react.js b/services/health-department/src/components/App/Tracking/TrackingList/Table/Entry/SelectAssignee/SelectAssignee.react.js
index d081e88..3c0ddd9 100644
--- a/services/health-department/src/components/App/Tracking/TrackingList/Table/Entry/SelectAssignee/SelectAssignee.react.js
+++ b/services/health-department/src/components/App/Tracking/TrackingList/Table/Entry/SelectAssignee/SelectAssignee.react.js
@@ -19,9 +19,8 @@ export const SelectAssignee = ({ process }) => {
     setCurrentAssignee(process?.assignee?.uuid || null);
   }, [process, setCurrentAssignee]);
 
-  const { isLoading, error, data: employees } = useQuery(
-    'employees',
-    getEmployees
+  const { isLoading, error, data: employees } = useQuery('employees', () =>
+    getEmployees()
   );
 
   const handleSelectAssignee = useCallback(
diff --git a/services/health-department/src/components/App/Tracking/TrackingList/Table/Header/Header.react.js b/services/health-department/src/components/App/Tracking/TrackingList/Table/Header/Header.react.js
index 08021d1..6ff212d 100644
--- a/services/health-department/src/components/App/Tracking/TrackingList/Table/Header/Header.react.js
+++ b/services/health-department/src/components/App/Tracking/TrackingList/Table/Header/Header.react.js
@@ -28,7 +28,7 @@ export const Header = ({ setSorting, sorting }) => {
         {intl.formatMessage({ id: 'processTable.name' })}
         <SortingSelector onClick={() => toggleNameSorting()} />
       </Column>
-      <Column flex="15%">
+      <Column flex="10%">
         {intl.formatMessage({ id: 'processTable.createdAt' })}
         <SortingSelector onClick={() => toggleTimeSorting()} />
       </Column>
@@ -38,6 +38,7 @@ export const Header = ({ setSorting, sorting }) => {
       <Column flex="10%">
         {intl.formatMessage({ id: 'processTable.status' })}
       </Column>
+      <Column flex="5%" />
     </TableHeader>
   );
 };
diff --git a/services/health-department/src/components/App/Tracking/TrackingList/Table/Table.styled.js b/services/health-department/src/components/App/Tracking/TrackingList/Table/Table.styled.js
index 50c9c64..3ea134b 100644
--- a/services/health-department/src/components/App/Tracking/TrackingList/Table/Table.styled.js
+++ b/services/health-department/src/components/App/Tracking/TrackingList/Table/Table.styled.js
@@ -22,7 +22,7 @@ export const Row = styled.div`
 
 export const RowWrapper = styled.div`
   display: block;
-  overflow-y: scroll;
+  overflow-y: auto;
   overflow-x: hidden;
   max-height: 550px;
 `;
diff --git a/services/health-department/src/components/App/UserManagement/EmployeeList/EmployeeList.styled.js b/services/health-department/src/components/App/UserManagement/EmployeeList/EmployeeList.styled.js
index 110f056..146d6b3 100644
--- a/services/health-department/src/components/App/UserManagement/EmployeeList/EmployeeList.styled.js
+++ b/services/health-department/src/components/App/UserManagement/EmployeeList/EmployeeList.styled.js
@@ -4,7 +4,7 @@ export const TableWrapper = styled.div`
   display: flex;
   flex-direction: column;
   background-color: white;
-  padding: 0 32px;
+  padding: 0 32px 32px 32px;
   height: 100%;
   overflow: auto;
 `;
diff --git a/services/health-department/src/components/App/VerificationTag/VerificationTag.react.js b/services/health-department/src/components/App/VerificationTag/VerificationTag.react.js
index 0391ca5..a58cb4b 100644
--- a/services/health-department/src/components/App/VerificationTag/VerificationTag.react.js
+++ b/services/health-department/src/components/App/VerificationTag/VerificationTag.react.js
@@ -5,7 +5,7 @@ import { Tooltip } from 'antd';
 import Icon from '@ant-design/icons';
 
 // Api
-import { getHealthDepartment, getMe } from 'network/api';
+import { getMe } from 'network/api';
 
 // Assets
 import { ReactComponent as VerificationSVG } from 'assets/verification.svg';
@@ -19,21 +19,13 @@ const VerificationIcon = () => (
 export const VerificationTag = () => {
   const intl = useIntl();
 
-  const { isLoading, error, data: healthDepartment } = useQuery(
-    'healthDepartment',
-    () =>
-      getMe().then(response => {
-        return getHealthDepartment(response.departmentId);
-      })
-  );
+  const {
+    isLoading,
+    error,
+    data: healthDepartmentUser,
+  } = useQuery('healthDepartmentUser', () => getMe());
 
-  if (
-    isLoading ||
-    error ||
-    !healthDepartment.signedPublicHDEKP ||
-    !healthDepartment.signedPublicHDSKP
-  )
-    return null;
+  if (isLoading || error || !healthDepartmentUser.isSigned) return null;
 
   return (
     <Tooltip title={intl.formatMessage({ id: 'verificationTag.info' })}>
diff --git a/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/ContactPersons.react.js b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/ContactPersons.react.js
index 99ed56f..1ab4481 100644
--- a/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/ContactPersons.react.js
+++ b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/ContactPersons.react.js
@@ -8,10 +8,12 @@ import { Overview } from './Overview';
 import { GuestCount } from './GuestCount';
 import { GuestList } from './GuestList';
 import { Export } from './Export';
+import { Notify } from './Notify';
 import { ContactPersonsWrapper, FlexWrapper } from './ContactPersons.styled';
 
 export const ContactPersons = ({ location, indexPersonData }) => {
   const [selectedTraces, setSelectedTraces] = useState(null);
+  const [decryptedTraces, setDecryptedTraces] = useState([]);
   const {
     isLoading,
     error,
@@ -28,15 +30,17 @@ export const ContactPersons = ({ location, indexPersonData }) => {
     <ContactPersonsWrapper>
       <FlexWrapper>
         <Header location={location} indexPersonData={indexPersonData} />
+        <Notify traces={selectedTraces} location={location} />
         <Export traces={selectedTraces} location={location} />
       </FlexWrapper>
       <Overview location={location} />
-      <GuestCount guestCount={contactPersons.traces.length} />
+      <GuestCount guestCount={decryptedTraces.length} />
       <GuestList
-        encryptedTraces={contactPersons.traces}
         location={location}
+        encryptedTraces={contactPersons.traces}
         indexPersonData={indexPersonData}
         setSelectedTraces={setSelectedTraces}
+        setDecryptedTraces={setDecryptedTraces}
       />
     </ContactPersonsWrapper>
   );
diff --git a/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Export/Export.styled.js b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Export/Export.styled.js
index 3756fc3..8ba4c0d 100644
--- a/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Export/Export.styled.js
+++ b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Export/Export.styled.js
@@ -3,7 +3,7 @@ import styled from 'styled-components';
 
 export const Wrapper = styled.div`
   display: flex;
-  margin-left: auto;
+  margin-left: 40px;
 `;
 
 export const StyledLink = styled.a`
diff --git a/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/GuestList/GuestList.react.js b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/GuestList/GuestList.react.js
index 6af6d78..deafd1e 100644
--- a/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/GuestList/GuestList.react.js
+++ b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/GuestList/GuestList.react.js
@@ -14,6 +14,8 @@ export const GuestList = ({
   encryptedTraces,
   indexPersonData,
   setSelectedTraces,
+  setDecryptedTraces,
+  location,
 }) => {
   const intl = useIntl();
   const [traces, setTraces] = useState(null);
@@ -42,11 +44,12 @@ export const GuestList = ({
       );
 
       setTraces(validDecrypedTraces);
+      setDecryptedTraces(validDecrypedTraces);
       hideMessage();
     }
 
     decryptTraces();
-  }, [encryptedTraces, intl]);
+  }, [encryptedTraces, intl, setDecryptedTraces]);
 
   useEffect(() => {
     if (!traces) return;
@@ -67,6 +70,7 @@ export const GuestList = ({
         <OverlappingTime setMinTimeOverlap={setMinTimeOverlap} />
       )}
       <GuestListTable
+        location={location}
         traces={filteredTraces}
         indexPersonData={indexPersonData}
         setSelectedTraces={setSelectedTraces}
diff --git a/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/GuestList/GuestListTable/GuestListTable.react.js b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/GuestList/GuestListTable/GuestListTable.react.js
index 68155c5..6f3ef53 100644
--- a/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/GuestList/GuestListTable/GuestListTable.react.js
+++ b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/GuestList/GuestListTable/GuestListTable.react.js
@@ -1,8 +1,13 @@
 import React, { useState, useCallback, useEffect } from 'react';
 import { useIntl } from 'react-intl';
 import { Table } from 'antd';
+import { getWarningLevelsForLocationTransfer, getMe } from 'network/api';
 
+import { useQuery } from 'react-query';
+
+import { DEVICE_TYPES } from '../../Notify/Notify.helper';
 import { AdditionalData } from './AdditionalData';
+import { Notified } from './Notified';
 import { Name } from './Name';
 import { Address } from './Address';
 import { Phone } from './Phone';
@@ -14,9 +19,23 @@ export const GuestListTable = ({
   traces,
   indexPersonData,
   setSelectedTraces,
+  location,
 }) => {
   const intl = useIntl();
 
+  const { transferId } = location;
+  const {
+    data: riskLevels,
+  } = useQuery(
+    `getWarningLevelsForLocationTransfer${transferId}`,
+    () => getWarningLevelsForLocationTransfer(transferId),
+    { refetchOnWindowFocus: false }
+  );
+
+  const { data: healthDepartmentEmployee } = useQuery('me', getMe, {
+    refetchOnWindowFocus: false,
+  });
+
   const updateAllCheckboxes = useCallback(
     checked =>
       traces.map(trace => {
@@ -27,6 +46,7 @@ export const GuestListTable = ({
       }),
     [traces]
   );
+
   const [tracesChecked, updateTracesChecked] = useState(
     updateAllCheckboxes(true)
   );
@@ -70,6 +90,30 @@ export const GuestListTable = ({
   };
 
   const columns = [
+    {
+      key: 'Notified',
+      render: function renderBell(notificationTrace) {
+        if (
+          !riskLevels ||
+          !healthDepartmentEmployee ||
+          !healthDepartmentEmployee.notificationsEnabled ||
+          !(
+            notificationTrace.deviceType === DEVICE_TYPES.IOS ||
+            notificationTrace.deviceType === DEVICE_TYPES.ANDROID
+          )
+        )
+          return null;
+
+        const traceRiskLevels = riskLevels.find(
+          riskLevelObject =>
+            riskLevelObject.traceId === notificationTrace.traceId
+        )?.riskLevels;
+
+        if (!traceRiskLevels?.length) return null;
+
+        return <Notified />;
+      },
+    },
     {
       title: intl.formatMessage({ id: 'contactPersonTable.name' }),
       key: 'name',
diff --git a/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/GuestList/GuestListTable/Notified/Notified.react.js b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/GuestList/GuestListTable/Notified/Notified.react.js
new file mode 100644
index 0000000..271a9e8
--- /dev/null
+++ b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/GuestList/GuestListTable/Notified/Notified.react.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import { useIntl } from 'react-intl';
+import { Tooltip } from 'antd';
+import Icon from '@ant-design/icons';
+import { ReactComponent as BellSvg } from 'assets/BellIcon.svg';
+
+export const Notified = () => {
+  const intl = useIntl();
+  return (
+    <Tooltip title={intl.formatMessage({ id: 'contactperson.notified' })}>
+      <Icon
+        component={BellSvg}
+        style={{
+          color: 'black',
+          fontSize: 32,
+          marginLeft: 32,
+        }}
+      />
+    </Tooltip>
+  );
+};
diff --git a/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/GuestList/GuestListTable/Notified/index.js b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/GuestList/GuestListTable/Notified/index.js
new file mode 100644
index 0000000..a30d046
--- /dev/null
+++ b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/GuestList/GuestListTable/Notified/index.js
@@ -0,0 +1 @@
+export { Notified } from './Notified.react';
diff --git a/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Notify/Notify.helper.js b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Notify/Notify.helper.js
new file mode 100644
index 0000000..88ce93f
--- /dev/null
+++ b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Notify/Notify.helper.js
@@ -0,0 +1,11 @@
+export const DEVICE_TYPES = {
+  IOS: 0,
+  ANDROID: 1,
+};
+
+export const filterByDeviceType = contactPersons =>
+  contactPersons.filter(
+    contact =>
+      contact.deviceType === DEVICE_TYPES.IOS ||
+      contact.deviceType === DEVICE_TYPES.ANDROID
+  );
diff --git a/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Notify/Notify.react.js b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Notify/Notify.react.js
new file mode 100644
index 0000000..4763f6e
--- /dev/null
+++ b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Notify/Notify.react.js
@@ -0,0 +1,61 @@
+import React from 'react';
+import { useIntl } from 'react-intl';
+import { useQuery } from 'react-query';
+import { useModal } from 'components/hooks/useModal';
+import { getMe } from 'network/api';
+import { NotificationModal } from 'components/App/modals/NotificationModal';
+import { StyledLink, Wrapper, BellOutlinedIcon } from './Notify.styled';
+import { filterByDeviceType } from './Notify.helper';
+
+export const Notify = ({ traces, location }) => {
+  const intl = useIntl();
+  const [openModal] = useModal();
+  const { data: healthDepartmentEmployee } = useQuery('me', getMe, {
+    refetchOnWindowFocus: false,
+  });
+
+  const {
+    uuid: locationId,
+    name: locationName,
+    transferId: locationTransferId,
+    time,
+  } = location;
+
+  if (!traces || traces.length === 0) return null;
+
+  const selectedFilteredTraces = filterByDeviceType(traces);
+  const selectedTracesIds = selectedFilteredTraces.map(trace => trace.traceId);
+
+  const openNotificationModal = () => {
+    openModal({
+      title: intl.formatMessage({
+        id: 'modal.notification.title',
+      }),
+      content: (
+        <NotificationModal
+          locationId={locationId}
+          locationName={locationName}
+          locationTransferId={locationTransferId}
+          traceIds={selectedTracesIds}
+          time={time}
+          departmentId={healthDepartmentEmployee.departmentId}
+        />
+      ),
+      wide: true,
+    });
+  };
+
+  return (
+    <Wrapper>
+      {healthDepartmentEmployee &&
+      healthDepartmentEmployee.notificationsEnabled ? (
+        <StyledLink onClick={openNotificationModal}>
+          <BellOutlinedIcon />
+          {intl.formatMessage({
+            id: 'ContactPerson.notify.title',
+          })}
+        </StyledLink>
+      ) : null}
+    </Wrapper>
+  );
+};
diff --git a/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Notify/Notify.styled.js b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Notify/Notify.styled.js
new file mode 100644
index 0000000..d1e93b4
--- /dev/null
+++ b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Notify/Notify.styled.js
@@ -0,0 +1,19 @@
+import styled from 'styled-components';
+import { BellOutlined } from '@ant-design/icons';
+
+export const Wrapper = styled.div`
+  display: flex;
+  margin-left: auto;
+`;
+
+export const StyledLink = styled.a`
+  color: rgb(80, 102, 124);
+  font-size: 16px;
+  font-weight: bold;
+  text-transform: uppercase;
+  text-decoration: none;
+`;
+
+export const BellOutlinedIcon = styled(BellOutlined)`
+  margin-right: 9px;
+`;
diff --git a/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Notify/index.js b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Notify/index.js
new file mode 100644
index 0000000..051a6a8
--- /dev/null
+++ b/services/health-department/src/components/App/modals/ContactPersonsModal/ContactPersons/Notify/index.js
@@ -0,0 +1 @@
+export { Notify } from './Notify.react';
diff --git a/services/health-department/src/components/App/modals/GroupSearchModal/DataRequestModal/DataRequestModal.react.js b/services/health-department/src/components/App/modals/GroupSearchModal/DataRequestModal/DataRequestModal.react.js
index 205d12e..d455a08 100644
--- a/services/health-department/src/components/App/modals/GroupSearchModal/DataRequestModal/DataRequestModal.react.js
+++ b/services/health-department/src/components/App/modals/GroupSearchModal/DataRequestModal/DataRequestModal.react.js
@@ -18,12 +18,14 @@ import {
   DatePickerRow,
 } from './DataRequestModal.styled';
 
+const START_TIME = 'START_TIME';
+
 export const DataRequestModal = ({ group, back }) => {
   const intl = useIntl();
   const queryClient = useQueryClient();
   const [, closeModal] = useModal();
   const { streetName, streetNr, zipCode, city } = group.baseLocation;
-
+  const [form] = Form.useForm();
   const requiredFieldMessage = intl.formatMessage({
     id: 'modal.dataRequest.form.error.required',
   });
@@ -75,6 +77,18 @@ export const DataRequestModal = ({ group, back }) => {
       });
   };
 
+  const setTime = (time, field) => {
+    form.setFieldsValue(
+      field === START_TIME
+        ? {
+            startTime: time,
+          }
+        : {
+            endTime: time,
+          }
+    );
+  };
+
   return (
     <>
       <GroupText>{group.name}</GroupText>
@@ -82,7 +96,7 @@ export const DataRequestModal = ({ group, back }) => {
       <InfoText>
         {intl.formatMessage({ id: 'modal.dataRequest.info.timeframe' })}
       </InfoText>
-      <Form onFinish={onFinish}>
+      <Form onFinish={onFinish} form={form}>
         <DateSelectorWrapper>
           <DateText>
             {intl.formatMessage({ id: 'modal.dataRequest.from' })}
@@ -126,6 +140,9 @@ export const DataRequestModal = ({ group, back }) => {
                   id: 'modal.dataRequest.time.placeholder',
                 })}
                 showNow={false}
+                onSelect={time => {
+                  setTime(time, START_TIME);
+                }}
                 id="startTime"
               />
             </Form.Item>
@@ -138,7 +155,7 @@ export const DataRequestModal = ({ group, back }) => {
           <DatePickerRow>
             <Form.Item
               name="endDate"
-              style={{ paddingRight: '24px' }}
+              style={{ paddingRight: 24 }}
               rules={[
                 {
                   required: true,
@@ -173,6 +190,9 @@ export const DataRequestModal = ({ group, back }) => {
                 placeholder={intl.formatMessage({
                   id: 'modal.dataRequest.time.placeholder',
                 })}
+                onSelect={time => {
+                  setTime(time);
+                }}
                 showNow={false}
                 id="endTime"
               />
diff --git a/services/health-department/src/components/App/modals/NotificationModal/NotificationModal.helper.js b/services/health-department/src/components/App/modals/NotificationModal/NotificationModal.helper.js
new file mode 100644
index 0000000..54348b7
--- /dev/null
+++ b/services/health-department/src/components/App/modals/NotificationModal/NotificationModal.helper.js
@@ -0,0 +1,37 @@
+import { RISK_LEVEL_3 } from 'constants/riskLevels';
+
+export const filterLevel3RiskLevels = (traceIdsToFilter, riskLevels) =>
+  traceIdsToFilter.filter(traceId =>
+    riskLevels.some(
+      riskLevel =>
+        riskLevel.traceId === traceId &&
+        !riskLevel.riskLevels.includes(RISK_LEVEL_3)
+    )
+  );
+
+export const getLocaleObject = (localeConfig, departmentId, level, intl) => {
+  const departmentInfo = localeConfig.departments.find(
+    departmentMessageObject => departmentMessageObject.uuid === departmentId
+  );
+
+  const departmentMessages = departmentInfo.config[level]
+    ? departmentInfo.config[level].messages
+    : localeConfig.default[level].messages;
+
+  const localizeddepartmentMessages =
+    intl.locale === 'de' ? departmentMessages.de : departmentMessages.en;
+
+  for (const [key, value] of Object.entries(localizeddepartmentMessages)) {
+    localizeddepartmentMessages[key] = value
+      .replaceAll('((', '{')
+      .replaceAll('))', '}')
+      .replaceAll(/^\s*\n/gm, '{br}')
+      .replaceAll('\n', '{br}');
+  }
+  return {
+    messages: localizeddepartmentMessages,
+    healthDepartmentName: departmentInfo.name,
+    email: departmentInfo.email,
+    phone: departmentInfo.phone,
+  };
+};
diff --git a/services/health-department/src/components/App/modals/NotificationModal/NotificationModal.react.js b/services/health-department/src/components/App/modals/NotificationModal/NotificationModal.react.js
new file mode 100644
index 0000000..d483e3e
--- /dev/null
+++ b/services/health-department/src/components/App/modals/NotificationModal/NotificationModal.react.js
@@ -0,0 +1,273 @@
+import React, { useState } from 'react';
+import { useIntl } from 'react-intl';
+import { Popconfirm, notification } from 'antd';
+import { useQuery, useQueryClient } from 'react-query';
+import {
+  notifyLocationGuests,
+  notifyLocationTracesGuests,
+  getNotificationConfig,
+  getContactPersons,
+  getWarningLevelsForLocationTransfer,
+} from 'network/api';
+import { formattedTimeLabel } from 'utils/time';
+import { useModal } from 'components/hooks/useModal';
+import { PrimaryButton } from 'components/general';
+import { RISK_LEVEL_2, RISK_LEVEL_3 } from 'constants/riskLevels';
+import { filterByDeviceType } from '../ContactPersonsModal/ContactPersons/Notify/Notify.helper';
+import {
+  Wrapper,
+  SectionTitle,
+  Section,
+  ButtonWrapper,
+  SwitchDescription,
+  StyledSwitch,
+  SwitchWrapper,
+  Warning,
+} from './NotificationModal.styled';
+import {
+  filterLevel3RiskLevels,
+  getLocaleObject,
+} from './NotificationModal.helper';
+
+// eslint-disable-next-line complexity
+export const NotificationModal = ({
+  locationName,
+  locationTransferId,
+  traceIds,
+  time,
+  departmentId,
+}) => {
+  const intl = useIntl();
+  const queryClient = useQueryClient();
+  const [, closeModal] = useModal();
+  const [level, setLevel] = useState(traceIds ? RISK_LEVEL_3 : RISK_LEVEL_2);
+
+  const setRiskLevel2 = checked =>
+    checked ? setLevel(RISK_LEVEL_2) : setLevel(RISK_LEVEL_3);
+
+  const setRiskLevel3 = checked =>
+    checked ? setLevel(RISK_LEVEL_3) : setLevel(RISK_LEVEL_2);
+
+  const { data: config } = useQuery(
+    'notificationConfig',
+    getNotificationConfig,
+    {
+      refetchOnWindowFocus: false,
+      staleTime: Number.POSITIVE_INFINITY,
+    }
+  );
+
+  const {
+    data: contactPersons,
+  } = useQuery(
+    `contactPersons${locationTransferId}`,
+    () => getContactPersons(locationTransferId),
+    { refetchOnWindowFocus: false }
+  );
+
+  const {
+    data: riskLevels,
+  } = useQuery(
+    `getWarningLevelsForLocationTransfer${locationTransferId}`,
+    () => getWarningLevelsForLocationTransfer(locationTransferId),
+    { refetchOnWindowFocus: false }
+  );
+
+  const triggerNotificationError = () =>
+    notification.error({
+      message: intl.formatMessage({
+        id: 'notification.notification.error',
+      }),
+    });
+
+  if (!config || !contactPersons || !riskLevels) return null;
+
+  const contactPersonFiltered = filterByDeviceType(contactPersons.traces);
+
+  const notify = () => {
+    const notificationRequest =
+      level === RISK_LEVEL_2
+        ? notifyLocationGuests(locationTransferId)
+        : notifyLocationTracesGuests({
+            traceIds:
+              traceIds || contactPersonFiltered.map(trace => trace.traceId),
+            locationTransferId,
+          });
+
+    notificationRequest
+      .then(response => {
+        if (response.status === 204) {
+          notification.success({
+            message: intl.formatMessage({
+              id: 'notification.notification.success',
+            }),
+          });
+          queryClient.invalidateQueries(
+            `getWarningLevelsForLocationTransfer${locationTransferId}`
+          );
+          closeModal();
+          return;
+        }
+        triggerNotificationError();
+      })
+      .catch(() => triggerNotificationError());
+  };
+
+  const localeObject = getLocaleObject(config, departmentId, level, intl);
+  const level3TraceIds = filterLevel3RiskLevels(
+    traceIds || contactPersonFiltered.map(trace => trace.traceId),
+    riskLevels
+  );
+  const wasLevel2Triggered = riskLevels.some(traceRisk =>
+    traceRisk.riskLevels.includes(RISK_LEVEL_2)
+  );
+  const isButtonDisabled =
+    level3TraceIds.length === 0 && level === RISK_LEVEL_3;
+
+  return (
+    <Wrapper>
+      <SectionTitle>
+        {intl.formatMessage(
+          { id: 'modal.notification.section1.title' },
+          { locationName }
+        )}
+      </SectionTitle>
+      <Section>
+        {intl.formatMessage({ id: 'modal.notification.section1' })}
+      </Section>
+
+      {!wasLevel2Triggered && (
+        <Section>
+          <SwitchWrapper>
+            <StyledSwitch
+              checked={level === RISK_LEVEL_2}
+              onChange={setRiskLevel2}
+            />
+            <SwitchDescription>
+              {intl.formatMessage({
+                id: 'modal.notification.selection.potentialInfectionRisk',
+              })}
+            </SwitchDescription>
+          </SwitchWrapper>
+          <SwitchWrapper>
+            <StyledSwitch
+              checked={level === RISK_LEVEL_3}
+              onChange={setRiskLevel3}
+            />
+            <SwitchDescription>
+              {intl.formatMessage({
+                id: 'modal.notification.selection.elevatedInfectionRisk',
+              })}
+            </SwitchDescription>
+          </SwitchWrapper>
+        </Section>
+      )}
+
+      {traceIds && level === RISK_LEVEL_2 && (
+        <Warning>
+          {intl.formatMessage({ id: 'modal.notification.selectionWarning' })}
+        </Warning>
+      )}
+
+      <SectionTitle>
+        {intl.formatMessage({ id: 'modal.notification.section2.title' })}
+      </SectionTitle>
+      <Section>
+        {intl.formatMessage(
+          {
+            id: 'localeObject.messages.title',
+            defaultMessage: localeObject.messages.title,
+          },
+          { br: <br /> }
+        )}
+      </Section>
+      <Section>
+        {intl.formatMessage(
+          {
+            id: 'localeObject.messages.message',
+            defaultMessage: localeObject.messages.message,
+          },
+          {
+            br: <br />,
+            name: localeObject.healthDepartmentName,
+            phone:
+              localeObject.phone ||
+              intl.formatMessage({ id: 'modal.notification.notSpecified' }),
+            email:
+              localeObject.email ||
+              intl.formatMessage({ id: 'modal.notification.notSpecified' }),
+          }
+        )}
+      </Section>
+
+      <SectionTitle>
+        {intl.formatMessage({ id: 'modal.notification.section3.title' })}
+      </SectionTitle>
+      <Section>
+        {intl.formatMessage(
+          { id: 'modal.notification.section3' },
+          {
+            br: <br />,
+            locationName,
+            from: formattedTimeLabel(time[0]),
+            till: formattedTimeLabel(time[1]),
+          }
+        )}
+      </Section>
+      <SectionTitle>
+        {intl.formatMessage(
+          { id: 'modal.notification.section4.title' },
+          {
+            guestCount:
+              level === RISK_LEVEL_2
+                ? contactPersonFiltered.length
+                : level3TraceIds.length,
+          }
+        )}
+      </SectionTitle>
+      <SectionTitle>
+        {intl.formatMessage(
+          { id: 'modal.notification.selection.countNoNotification' },
+          {
+            amount: contactPersons.traces.length - contactPersonFiltered.length,
+          }
+        )}
+      </SectionTitle>
+      <ButtonWrapper>
+        {isButtonDisabled ? (
+          <PrimaryButton disabled>
+            {intl.formatMessage({
+              id: 'modal.notification.button.alreadyNotified',
+            })}
+          </PrimaryButton>
+        ) : (
+          <Popconfirm
+            placement="top"
+            onConfirm={notify}
+            title={intl.formatMessage(
+              {
+                id: 'modal.notification.confirmation',
+              },
+              {
+                guestCount:
+                  level === RISK_LEVEL_2
+                    ? contactPersonFiltered.length
+                    : level3TraceIds.length,
+              }
+            )}
+            okText={intl.formatMessage({
+              id: 'modal.notification.confirmButton',
+            })}
+            cancelText={intl.formatMessage({
+              id: 'modal.dataRequest.declineButton',
+            })}
+          >
+            <PrimaryButton>
+              {intl.formatMessage({ id: 'modal.notification.button' })}
+            </PrimaryButton>
+          </Popconfirm>
+        )}
+      </ButtonWrapper>
+    </Wrapper>
+  );
+};
diff --git a/services/health-department/src/components/App/modals/NotificationModal/NotificationModal.styled.js b/services/health-department/src/components/App/modals/NotificationModal/NotificationModal.styled.js
new file mode 100644
index 0000000..cd1054b
--- /dev/null
+++ b/services/health-department/src/components/App/modals/NotificationModal/NotificationModal.styled.js
@@ -0,0 +1,51 @@
+import styled from 'styled-components';
+import { Switch } from 'antd';
+
+export const Wrapper = styled.div`
+  display: flex;
+  flex-direction: column;
+`;
+
+export const Warning = styled.div`
+  color: rgb(241, 103, 4);
+  font-family: Montserrat-Medium, sans-serif;
+  font-size: 16px;
+  font-weight: 500;
+  margin-bottom: 24px;
+`;
+
+export const SectionTitle = styled.div`
+  font-family: Montserrat-Bold, sans-serif;
+  font-size: 16px;
+  font-weight: bold;
+  margin-bottom: 8px;
+`;
+
+export const SwitchDescription = styled.div`
+  font-family: Montserrat-Bold, sans-serif;
+  font-size: 16px;
+  font-weight: bold;
+  margin-bottom: 8px;
+  padding-left: 8px;
+`;
+
+export const Section = styled.div`
+  font-family: Montserrat-Medium, sans-serif;
+  font-size: 16px;
+  font-weight: 500;
+  margin-bottom: 32px;
+`;
+
+export const ButtonWrapper = styled.div`
+  display: flex;
+  justify-content: flex-end;
+`;
+
+export const StyledSwitch = styled(Switch)`
+  width: 40px;
+`;
+
+export const SwitchWrapper = styled.div`
+  display: flex;
+  flex-direction: row;
+`;
diff --git a/services/health-department/src/components/App/modals/NotificationModal/index.js b/services/health-department/src/components/App/modals/NotificationModal/index.js
new file mode 100644
index 0000000..6556767
--- /dev/null
+++ b/services/health-department/src/components/App/modals/NotificationModal/index.js
@@ -0,0 +1 @@
+export { NotificationModal } from './NotificationModal.react';
diff --git a/services/health-department/src/components/general/Divider.styled.js b/services/health-department/src/components/general/Divider.styled.js
new file mode 100644
index 0000000..64abcf1
--- /dev/null
+++ b/services/health-department/src/components/general/Divider.styled.js
@@ -0,0 +1,8 @@
+import styled from 'styled-components';
+
+export const Divider = styled.div`
+  border: 1px solid rgb(151, 151, 151);
+  height: 1px;
+  width: 100%;
+  margin-bottom: 24px;
+`;
diff --git a/services/health-department/src/components/general/index.js b/services/health-department/src/components/general/index.js
index 0ffeb4b..d4c2b0a 100644
--- a/services/health-department/src/components/general/index.js
+++ b/services/health-department/src/components/general/index.js
@@ -1,5 +1,9 @@
-export {
+import {
   PrimaryButton,
   SecondaryButton,
   SuccessButton,
 } from './Buttons.styled';
+
+import { Divider } from './Divider.styled';
+
+export { PrimaryButton, SecondaryButton, SuccessButton, Divider };
diff --git a/services/health-department/src/components/hooks/useLocationTransfers.js b/services/health-department/src/components/hooks/useLocationTransfers.js
new file mode 100644
index 0000000..bd36b05
--- /dev/null
+++ b/services/health-department/src/components/hooks/useLocationTransfers.js
@@ -0,0 +1,60 @@
+import { useState, useEffect } from 'react';
+import { useQuery, useQueries } from 'react-query';
+
+import { getLocation, getLocationTransfers } from 'network/api';
+
+/**
+ * Enriches location transfers with location data
+ * @param transfers to be enriched
+ * @param locations to search from
+ * @returns array of enriched location transfers
+ */
+const enrichTransfersWithLocations = (transfers, locations) => {
+  return transfers?.map(transfer => {
+    // get matching location for current location transfer
+    const matchingLocation = locations.find(
+      locationQuery => locationQuery.data.locationId === transfer.locationId
+    );
+    return {
+      ...matchingLocation.data,
+      time: transfer.time,
+      transferId: transfer.uuid,
+      isCompleted: transfer.isCompleted,
+      contactedAt: transfer.contactedAt,
+    };
+  });
+};
+
+/**
+ * Queries backend to retrieve all locationTransfers for the given process id
+ * and enriches the location transfers with the location information.
+ * @param processUuid
+ * @returns array of location transfers (incl. location data)
+ */
+export const useLocationTransfers = processUuid => {
+  const [locationTransfers, setLocationTransfers] = useState([]);
+
+  const { data: transfers } = useQuery(`transfers${processUuid}`, () =>
+    getLocationTransfers(processUuid)
+  );
+
+  // enrich locationTransfers with location data
+  const locationIds = transfers?.map(transfer => transfer.locationId);
+  const locationQueries = locationIds?.map(locationId => ({
+    queryKey: `location${locationId}`,
+    queryFn: () => getLocation(locationId),
+    staleTime: Number.POSITIVE_INFINITY,
+  }));
+
+  const locations = useQueries(locationQueries || []);
+  const locationsLoaded = locations.every(query => !!query?.isSuccess);
+
+  useEffect(() => {
+    if (locationsLoaded) {
+      setLocationTransfers(enrichTransfersWithLocations(transfers, locations));
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [transfers, locationsLoaded]);
+
+  return locationTransfers || [];
+};
diff --git a/services/health-department/src/components/hooks/useLocationWithTransfers.js b/services/health-department/src/components/hooks/useLocationWithTransfers.js
deleted file mode 100644
index b5979ab..0000000
--- a/services/health-department/src/components/hooks/useLocationWithTransfers.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import { useState, useEffect } from 'react';
-import { useQuery, useQueries } from 'react-query';
-
-import { getLocation, getLocationTransfers } from 'network/api';
-
-export const useLocationWithTransfers = processUuid => {
-  const [locations, setLocations] = useState([]);
-
-  const { data: transfers } = useQuery(
-    `transfers${processUuid}`,
-    () => getLocationTransfers(processUuid),
-    {
-      staleTime: 60000,
-      refetchInterval: 60000,
-    }
-  );
-
-  const queries =
-    transfers?.map(transfer => ({
-      queryKey: `location${transfer.locationId}`,
-      queryFn: () => getLocation(transfer.locationId),
-      staleTime: Number.POSITIVE_INFINITY,
-    })) || [];
-
-  const locationTransferQueries = useQueries(queries);
-  const locationTransferQueriesLoaded = locationTransferQueries.every(
-    query => !!query?.isSuccess
-  );
-
-  useEffect(() => {
-    if (locationTransferQueriesLoaded) {
-      setLocations(
-        locationTransferQueries.map(query => {
-          const matchingTransfer = transfers.find(
-            transfer => transfer.locationId === query.data.locationId
-          );
-          return {
-            ...query.data,
-            time: matchingTransfer.time,
-            transferId: matchingTransfer.uuid,
-            isCompleted: matchingTransfer.isCompleted,
-            contactedAt: matchingTransfer.contactedAt,
-          };
-        })
-      );
-    }
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [transfers, locationTransferQueriesLoaded]);
-
-  if (!transfers) return [];
-  return locations;
-};
diff --git a/services/health-department/src/components/hooks/useValidators.js b/services/health-department/src/components/hooks/useValidators.js
new file mode 100644
index 0000000..e5b9215
--- /dev/null
+++ b/services/health-department/src/components/hooks/useValidators.js
@@ -0,0 +1,46 @@
+import { useIntl } from 'react-intl';
+
+import { MAX_NAME_LENGTH, MAX_EMAIL_LENGTH } from 'constants/valueLength';
+
+import {
+  getRequiredRule,
+  getPhoneRule,
+  getEmailRule,
+  getSafeStringRule,
+  getMaxLengthRule,
+  getNoNumericRule,
+} from 'utils/validatorRules';
+import { useMemo } from 'react';
+
+export const usePersonNameValidator = fieldName => {
+  const intl = useIntl();
+  return useMemo(
+    () => [
+      getRequiredRule(intl, fieldName),
+      getSafeStringRule(intl, fieldName),
+      getNoNumericRule(intl, fieldName),
+      getMaxLengthRule(intl, MAX_NAME_LENGTH),
+    ],
+    [intl, fieldName]
+  );
+};
+
+export const usePhoneValidator = fieldName => {
+  const intl = useIntl();
+  return useMemo(() => [getRequiredRule(intl, fieldName), getPhoneRule(intl)], [
+    intl,
+    fieldName,
+  ]);
+};
+
+export const useEmailValidator = fieldName => {
+  const intl = useIntl();
+  return useMemo(
+    () => [
+      getRequiredRule(intl, fieldName),
+      getEmailRule(intl),
+      getMaxLengthRule(intl, MAX_EMAIL_LENGTH),
+    ],
+    [intl, fieldName]
+  );
+};
diff --git a/services/health-department/src/constants/riskLevels.js b/services/health-department/src/constants/riskLevels.js
new file mode 100644
index 0000000..3e95c67
--- /dev/null
+++ b/services/health-department/src/constants/riskLevels.js
@@ -0,0 +1,2 @@
+export const RISK_LEVEL_2 = 2;
+export const RISK_LEVEL_3 = 3;
diff --git a/services/health-department/src/constants/sormas.js b/services/health-department/src/constants/sormas.js
index eb66f68..4be7d07 100644
--- a/services/health-department/src/constants/sormas.js
+++ b/services/health-department/src/constants/sormas.js
@@ -3,4 +3,5 @@ export const SUPPORTED_SORMAS_VERSIONS = {
   '"1.59.1"': true,
   '"1.59.2"': true,
   '"1.61.1"': true,
+  '"1.62.3"': true,
 };
diff --git a/services/health-department/src/constants/valueLength.js b/services/health-department/src/constants/valueLength.js
index 8bd2593..c4ed78e 100644
--- a/services/health-department/src/constants/valueLength.js
+++ b/services/health-department/src/constants/valueLength.js
@@ -1,7 +1,5 @@
 export const MAX_PRIVATE_KEY_FILE_SIZE = 2000;
 export const MAX_NAME_LENGTH = 120;
-export const MAX_CITY_LENGTH = 120;
+export const MAX_PHONE_LENGTH = 20;
 export const MAX_EMAIL_LENGTH = 255;
-export const MAX_STREET_LENGTH = 120;
-export const MAX_POSTAL_CODE_LENGTH = 20;
-export const MAX_HOUSE_NUMBER_LENGTH = 120;
+export const MAX_PROCESS_NOTE_LENGTH = 500;
diff --git a/services/health-department/src/errors/InvalidNoteSignatureError.js b/services/health-department/src/errors/InvalidNoteSignatureError.js
new file mode 100644
index 0000000..5ce0964
--- /dev/null
+++ b/services/health-department/src/errors/InvalidNoteSignatureError.js
@@ -0,0 +1,7 @@
+export class InvalidNoteSignatureError extends Error {
+  constructor(publicKey, signature, encryptedData, iv, mac) {
+    super(
+      `Invalid note signature (signature "${signature}", publicKey ${publicKey}, encryptedData "${encryptedData}", iv "${iv}", mac ${mac})`
+    );
+  }
+}
diff --git a/services/health-department/src/messages/de.json b/services/health-department/src/messages/de.json
index 18c45e7..b6d6a35 100644
--- a/services/health-department/src/messages/de.json
+++ b/services/health-department/src/messages/de.json
@@ -118,7 +118,7 @@
     "userManagement.error.firstName": "Bitte geben Sie einen Vornamen an.",
     "userManagement.error.lastName": "Bitte geben Sie einen Nachnamen an.",
     "userManagement.error.phone": "Bitte geben Sie eine Telefonnummer an.",
-    "userManagement.delete.success": "Mitarbeiter:in wurde entfernt.",
+    "userManagement.delete.success": "Mitarbeiter:in wurde erfolgreich gelöscht. Falls er/sie Bearbeiter:in von Prozessen war, sind diese jetzt nicht mehr zugewiesen.",
     "userManagement.delete.error": "Das hat nicht geklappt. Mitarbeiter:in konnte nicht entfernt werden.",
     "userManagement.create.success": "Mitarbeiter:in hinzugefügt",
     "userManagement.create.error": "Das hat nicht geklappt. Mitarbeiter:in konnte nicht hinzugefügt werden.",
@@ -360,5 +360,37 @@
     "auditlogDownload.filename": "Aktivitätenprotokoll",
     "browserWarning.headline": "Fehler!",
     "browserWarning.description1": "Dieser Browser wird leider nicht unterstützt.",
-    "browserWarning.description2": "Am besten funktionieren Chrome oder Safari."
+    "browserWarning.description2": "Am besten funktionieren Chrome oder Safari.",
+    "modal.notification.title": "Hinweis auslösen",
+    "modal.notification.section1.title": "Gäste von {locationName} über die luca App benachrichtigen \n\n",
+    "modal.notification.section1": "Wenn Sie einen Hinweis auslösen, werden die luca App-Nutzer:innen, die sich im ausgewählten Zeitraum eingecheckt haben, über ihre luca App informiert, dass sie sich zeitgleich mit einer infizierten Person an einem Ort aufgehalten haben. ",
+    "modal.notification.section2.title": "Der Hinweis enthält den folgenden Text: ",
+    "modal.notification.section2.part1": "Hinweis: Mögliches Infektionsrisiko\n{br}\n{br}\nDu warst zeitgleich mit einer Person in einem luca-Standort eingecheckt, die später positiv auf das Coronavirus (SARS-CoV-2) getestet wurde.\n{br}\n{br}\nBitte handle verantwortungsvoll und melde dich gegebenenfalls beim Gesundheitsamt – vor allem, wenn bei dir Symptome auftreten. Das Gesundheitsamt empfiehlt dir, einen Schnelltest zu machen.",
+    "modal.notification.section4.title": "Es werden {guestCount} Personen benachrichtigt.",
+    "modal.notification.confirmation": "Möchten Sie den Hinweis auslösen? {guestCount} Personen erhalten über ihre luca App eine Benachrichtigung. Die Aktion kann nicht rückgängig gemacht werden. ",
+    "modal.notification.button": "Hinweis auslösen",
+    "modal.notification.section2.part2": "Dieser Hinweis wurde vom {healthDepartment} ausgelöst. \n{br}\n{br}",
+    "modal.notification.confirmButton": "JETZT AUSLÖSEN",
+    "modal.notification.section2.part3": "E-Mail-Adresse: {email}\n{br}\nTelefonnummer: {phone}",
+    "profile.contactInformation.phone": "Telefonnummer:",
+    "profile.contactInformation.email": "E-Mail-Adresse:",
+    "profile.contactInformation.headline": "Öffentliche Telefonnummer und E-Mail-Adresse angeben",
+    "profile.contactInformation.description": "Bitte geben Sie eine Telefonnummer und/oder eine E-Mail-Adresse an, über die Nutzer:innen im luca-System Sie kontaktieren können. Das kann zum Beispiel nötig sein, wenn Sie Kontaktpersonen von Infizierten durch eine Benachrichtigung über die luca App dazu auffordern, aber auch, wenn ein luca-Standort eine ungewöhnliche Anfrage von einem Gesundheitsamt bekommt und diese überprüfen möchte.",
+    "notification.profile.contactInformation.success": "Kontaktangaben erfolgreich gespeichert.",
+    "notification.profile.contactInformation.error": "Fehler beim Anlegen der Kontaktangaben.",
+    "navigation.comprehensiveInformation": "Ãœbergreifende Informationen",
+    "notification.notification.success": "Benachrichtigungen gesendet.",
+    "notification.notification.error": "Benachrichtigungen konnten nicht gesendet werden.",
+    "ContactPerson.notify.title": "AUSWAHL BENACHRICHTIGEN",
+    "profile.contactInformation.required": "Bitte gib wenigstens eine Telefonnummer und/oder eine E-Mail-Adresse an.",
+    "modal.notification.section3": "Alle luca App-Nutzer:innen, die in diesem Zeitraum bei {locationName} eingecheckt waren, erhalten die Benachrichtigung.\n{br}Von: {from}\n{br}Bis: {till}",
+    "modal.notification.section3.title": "Zeitraum",
+    "processDetails.note.invalidSignature": "Die Signatur der Notiz ist falsch!",
+    "modal.notification.notSpecified": "Keine Angabe",
+    "modal.notification.selection.potentialInfectionRisk": "Mögliches Infektionsrisiko",
+    "modal.notification.selection.elevatedInfectionRisk": "Erhöhtes Infektionsrisiko",
+    "modal.notification.button.alreadyNotified": "Bereits informiert",
+    "contactperson.notified": "Hinweis ausgelöst",
+    "modal.notification.selectionWarning": "Diese Warnmeldung kann nur an alle Personen in der Kontaktliste ausgespielt werden.",
+    "modal.notification.selection.countNoNotification": "{amount} Personen werden nicht benachrichtigt, da der Check-in via Schlüsselanhänger, Kontaktformular oder Web App erfolgte"
 }
\ No newline at end of file
diff --git a/services/health-department/src/messages/en.json b/services/health-department/src/messages/en.json
index dbb37c3..990acae 100644
--- a/services/health-department/src/messages/en.json
+++ b/services/health-department/src/messages/en.json
@@ -118,7 +118,7 @@
     "userManagement.error.firstName": "Please provide a firstname.",
     "userManagement.error.lastName": "Please provide a lastname.",
     "userManagement.error.phone": "Please provide a phone number.",
-    "userManagement.delete.success": "Employee removed",
+    "userManagement.delete.success": "Employee has been deleted successfully. If they were assigned to any processes, these have been switched to not assigned.",
     "userManagement.delete.error": "Error while removing the employee",
     "userManagement.create.success": "Employee created",
     "userManagement.create.error": "Error while creating the employee",
@@ -360,5 +360,37 @@
     "auditlogDownload.filename": "Activity_protocol",
     "browserWarning.headline": "Error!",
     "browserWarning.description1": "Unfortunately, your browser is not supported.",
-    "browserWarning.description2": "It will work best with Chrome or Safari."
+    "browserWarning.description2": "It will work best with Chrome or Safari.",
+    "modal.notification.title": "Trigger notification\n\n",
+    "modal.notification.section1.title": "Notify guests of {locationName} via the luca app",
+    "modal.notification.section1": "If you trigger a notification, the luca app users who were checked in during the selected time period will be informed via their luca app, that they attended a location at the same time as an infected person.",
+    "modal.notification.section2.title": "The notification contains the following text:",
+    "modal.notification.section2.part1": "Notification: Potential infection risk\n{br}\n{br}\nYou were checked into a Luca place at the same time as a person who tested positive for coronavirus (SARS-CoV-2).\n{br}\n{br}\nPlease act responsibly, reduce your contacts and contact your local health department – especially, if you experience any symptoms. The health department also advises doing a rapid test.",
+    "modal.notification.section4.title": "{guestCount} persons will receive this notification.",
+    "modal.notification.confirmation": "Do you want to trigger the notification? {guestCount} persons will receive the notification via their luca app. This action cannot be undone.",
+    "modal.notification.button": "Trigger notification",
+    "modal.notification.section2.part2": "This notification has been triggered by the health department {healthDepartment}.\n{br}\n{br}",
+    "modal.notification.confirmButton": "TRIGGER NOW",
+    "modal.notification.section2.part3": "Email: {email}\n{br}\nPhone: {phone}",
+    "profile.contactInformation.phone": "Phone number:",
+    "profile.contactInformation.email": "Email address:",
+    "profile.contactInformation.headline": "Provide public phone number and mail address",
+    "profile.contactInformation.description": "Please provide a phone number and/or an email address that can be used by users in the luca system to get in touch with you. This may be necessary, for example, if you've asked users of the luca app to contact you via notification or when a luca place receives an unusual request from a health department and would like to check its authenticity.",
+    "notification.profile.contactInformation.success": "Saved successfully.",
+    "notification.profile.contactInformation.error": "Error while updating the contact information.",
+    "navigation.comprehensiveInformation": "Comprehensive information",
+    "notification.notification.success": "Notifications sent.",
+    "notification.notification.error": "Notifications could not be sent.",
+    "ContactPerson.notify.title": "NOTIFY SELECTION",
+    "profile.contactInformation.required": "Please provide at least a phone number and/or an email address.",
+    "modal.notification.section3": "All luca app users who were checked into {locationName} during this timeframe will receive this notification.\n{br}From: {from}\n{br}To: {till}",
+    "modal.notification.section3.title": "Timeframe",
+    "processDetails.note.invalidSignature": "Note signature is wrong!",
+    "modal.notification.notSpecified": "Not specified",
+    "modal.notification.selection.potentialInfectionRisk": "Potential infection risk",
+    "modal.notification.selection.elevatedInfectionRisk": "Elevated infection risk",
+    "modal.notification.button.alreadyNotified": "Already notified",
+    "contactperson.notified": "Notification sent",
+    "modal.notification.selectionWarning": "This warning can only be triggered for all contact persons.",
+    "modal.notification.selection.countNoNotification": "{amount} people won't receive this notification as the check-in occured via badge, contact form or web app."
 }
\ No newline at end of file
diff --git a/services/health-department/src/network/api.js b/services/health-department/src/network/api.js
index 3899601..c8085f4 100644
--- a/services/health-department/src/network/api.js
+++ b/services/health-department/src/network/api.js
@@ -163,8 +163,10 @@ export const createLocationTransfer = data => {
 
 // USER MANAGEMENT
 
-export const getEmployees = () => {
-  return getRequest(`${API_PATH}/v3/healthDepartmentEmployees/`);
+export const getEmployees = (includeDeleted = false) => {
+  return getRequest(
+    `${API_PATH}/v3/healthDepartmentEmployees/?includeDeleted=${includeDeleted}`
+  );
 };
 
 export const renewEmployeePassword = data => {
@@ -290,3 +292,40 @@ export const createUserTransfer = payload => {
     headers,
   }).then(response => response.json());
 };
+
+// NOTIFICATIONS
+export const notifyLocationGuests = locationTransferId =>
+  fetch(`${API_PATH}/v4/riskLevels`, {
+    method: 'POST',
+    body: JSON.stringify({
+      locationTransferId,
+    }),
+    headers,
+  });
+
+export const notifyLocationTracesGuests = payload =>
+  fetch(`${API_PATH}/v4/riskLevels/traces`, {
+    method: 'POST',
+    body: JSON.stringify(payload),
+    headers,
+  });
+
+export const getWarningLevelsForLocationTransfer = locationTransferId =>
+  getRequest(`${API_PATH}/v4/riskLevels/${locationTransferId}`);
+
+export const getNotificationConfig = () =>
+  getRequest(`${API_PATH}/v4/notifications/config`);
+
+// AUDITS
+export const getAuditLog = data =>
+  getRequest(
+    `${API_PATH}/v4/healthDepartments/auditlog/download/?timeframe[0]=${data.timeframe[0]}&timeframe[1]=${data.timeframe[1]}`
+  );
+
+// PROFILE
+export const setContactInformation = payload =>
+  fetch(`${API_PATH}/v3/healthDepartments/contact`, {
+    method: 'PATCH',
+    body: JSON.stringify(payload),
+    headers,
+  });
diff --git a/services/health-department/src/utils/cryptoKeyOperations.js b/services/health-department/src/utils/cryptoKeyOperations.js
index fd1ad25..031d7e9 100644
--- a/services/health-department/src/utils/cryptoKeyOperations.js
+++ b/services/health-department/src/utils/cryptoKeyOperations.js
@@ -7,6 +7,7 @@ import {
   getCurrentBadgeKey,
   getBadgeKeyedList,
   getBadgeTargetKeyId,
+  getMe,
   getIssuers,
   sendBadgeKeyRotation,
   sendDailyKeyRotation,
@@ -17,8 +18,10 @@ import {
 } from 'network/api';
 
 import {
-  SIGN_EC_SHA256_DER,
+  VERIFY_EC_SHA256_DER_SIGNATURE,
+  EC_KEYPAIR_FROM_PRIVATE_KEY,
   EC_KEYPAIR_GENERATE,
+  SIGN_EC_SHA256_DER,
   ENCRYPT_DLIES,
   DECRYPT_DLIES,
   int32ToHex,
@@ -26,6 +29,8 @@ import {
   hexToBase64,
 } from '@lucaapp/crypto';
 
+import { InvalidNoteSignatureError } from 'errors/InvalidNoteSignatureError';
+
 const MIN_DAILY_KEY_AGE_BEFORE_ROTATION_DAYS = 1;
 const MAX_DAILY_KEYS = 28;
 
@@ -92,6 +97,12 @@ export const rotateDailyKeypair = async () => {
     dailyKey = { createdAt: 0, keyId: -1 };
   }
 
+  const me = await getMe();
+
+  if (!me.isSigned) {
+    return false;
+  }
+
   const currentDailyKeypairAge = moment.duration(
     moment().diff(moment.unix(dailyKey.createdAt))
   );
@@ -155,6 +166,12 @@ export const rekeyDailyKeypairs = async () => {
   const dailyKeys = await getAllDailyKeys();
   const issuers = await getIssuers();
 
+  const me = await getMe();
+
+  if (!me.isSigned) {
+    return;
+  }
+
   const rekeyPromises = dailyKeys.map(async dailyKey => {
     const keyedList = await getDailyKeyedList(dailyKey.keyId);
 
@@ -338,3 +355,33 @@ export const rekeyBadgeKeypairs = async () => {
 
   sendRekeyBadgeKeys(serverPayload);
 };
+
+export const generateSignature = data => SIGN_EC_SHA256_DER(hdskp, data);
+
+export const verifyNoteSignature = (encryptedData, iv, mac, signature) => {
+  const keyPair = EC_KEYPAIR_FROM_PRIVATE_KEY(hdskp);
+  if (
+    !VERIFY_EC_SHA256_DER_SIGNATURE(
+      keyPair.publicKey,
+      encryptedData + mac + iv,
+      signature
+    )
+  ) {
+    throw new InvalidNoteSignatureError(
+      keyPair.publicKey,
+      signature,
+      encryptedData,
+      iv,
+      mac
+    );
+  }
+};
+
+export const decryptNote = (publicKey, iv, mac, note) =>
+  DECRYPT_DLIES(
+    hdekp,
+    base64ToHex(publicKey),
+    base64ToHex(note),
+    base64ToHex(iv),
+    base64ToHex(mac)
+  );
diff --git a/services/health-department/src/utils/cryptoOperations.js b/services/health-department/src/utils/cryptoOperations.js
index 1cde68a..31e4673 100644
--- a/services/health-department/src/utils/cryptoOperations.js
+++ b/services/health-department/src/utils/cryptoOperations.js
@@ -118,6 +118,7 @@ export const decryptTrace = async encryptedTrace => {
       traceId: encryptedTrace.traceId,
       checkin: encryptedTrace.checkin,
       checkout: encryptedTrace.checkout,
+      deviceType: encryptedTrace.deviceType,
       userData: sanitizeObject(userData),
       additionalData: sanitizeObject(additionalData),
       isInvalid,
diff --git a/services/health-department/src/utils/decryption.js b/services/health-department/src/utils/decryption.js
index 3f52a6c..02c4368 100644
--- a/services/health-department/src/utils/decryption.js
+++ b/services/health-department/src/utils/decryption.js
@@ -108,7 +108,16 @@ export async function decryptStaticDeviceTrace(encryptedTrace) {
     const userId = hexToUuid(traceData.slice(0, 32));
     const userDataKey = traceData.slice(32, 64);
 
-    const encryptedUser = await getEncryptedUserContactData(userId);
+    let encryptedUser;
+    try {
+      encryptedUser = await getEncryptedUserContactData(userId);
+    } catch (error) {
+      console.error(
+        `Error while getting contact data (TraceId: ${encryptedTrace.traceId})`
+      );
+      throw error;
+    }
+
     const { userData, verificationSecret, isInvalid } = decryptUser(
       encryptedUser,
       userDataKey
diff --git a/services/health-department/src/utils/validatorRules.helper.js b/services/health-department/src/utils/validatorRules.helper.js
index 393a5e7..86d66d4 100644
--- a/services/health-department/src/utils/validatorRules.helper.js
+++ b/services/health-department/src/utils/validatorRules.helper.js
@@ -40,7 +40,6 @@ export const validateEmail = (_, value) => {
       allow_utf8_local_part: true,
       require_tld: true,
       allow_ip_domain: false,
-      domain_specific_validation: true,
       blacklisted_chars: "=',\\\\",
     })
   ) {
diff --git a/services/health-department/yarn.lock b/services/health-department/yarn.lock
index 7fd0573..a388e0e 100644
--- a/services/health-department/yarn.lock
+++ b/services/health-department/yarn.lock
@@ -3743,9 +3743,9 @@ caniuse-api@^3.0.0:
     lodash.uniq "^4.5.0"
 
 caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001219:
-  version "1.0.30001249"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001249.tgz#90a330057f8ff75bfe97a94d047d5e14fabb2ee8"
-  integrity sha512-vcX4U8lwVXPdqzPWi6cAJ3FnQaqXbBqy/GZseKNQzRj37J7qZdGcBtxq/QLFNLLlfsoXLUdHw8Iwenri86Tagw==
+  version "1.0.30001255"
+  resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001255.tgz"
+  integrity sha512-F+A3N9jTZL882f/fg/WWVnKSu6IOo3ueLz4zwaOPbPYHNmM/ZaDUyzyJwS1mZhX7Ex5jqTyW599Gdelh5PDYLQ==
 
 capture-exit@^2.0.0:
   version "2.0.0"
diff --git a/services/locations/package.json b/services/locations/package.json
index af20515..6abd860 100644
--- a/services/locations/package.json
+++ b/services/locations/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@lucaapp/locations",
-  "version": "1.9.2",
+  "version": "2.0.0",
   "private": true,
   "license": "Apache-2.0",
   "author": "Culture4Life <hello@luca-app.de> (https://www.luca-app.de/)",
diff --git a/services/locations/src/components/App/Dashboard/Location/GenerateQRCodes/GenerateQRCodes.helper.js b/services/locations/src/components/App/Dashboard/Location/GenerateQRCodes/GenerateQRCodes.helper.js
index fcd9d54..8438690 100644
--- a/services/locations/src/components/App/Dashboard/Location/GenerateQRCodes/GenerateQRCodes.helper.js
+++ b/services/locations/src/components/App/Dashboard/Location/GenerateQRCodes/GenerateQRCodes.helper.js
@@ -79,10 +79,8 @@ const getCSVFileContentFromLocation = (
 
 const getQRCodeCSVFileName = location =>
   location.name === null
-    ? sanitize(`QR_Codes_${location.LocationGroup?.name}_luca.csv`)
-    : sanitize(
-        `QR_Codes_${location.LocationGroup?.name}_${location.name}_luca.csv`
-      );
+    ? sanitize(`QR_Codes_${location.groupName}_luca.csv`)
+    : sanitize(`QR_Codes_${location.groupName}_${location.name}_luca.csv`);
 
 export const QRCodeCSVDownload = ({
   location,
diff --git a/services/locations/src/components/App/DataTransfers/TransferList/CompletedDataRequests/CompletedDataRequests.react.js b/services/locations/src/components/App/DataTransfers/TransferList/CompletedDataRequests/CompletedDataRequests.react.js
index 7a99425..65b9096 100644
--- a/services/locations/src/components/App/DataTransfers/TransferList/CompletedDataRequests/CompletedDataRequests.react.js
+++ b/services/locations/src/components/App/DataTransfers/TransferList/CompletedDataRequests/CompletedDataRequests.react.js
@@ -2,6 +2,7 @@ import React from 'react';
 import moment from 'moment';
 import { useIntl } from 'react-intl';
 
+import { getFormattedDate, getFormattedTime } from 'utils/time';
 import { Wrapper, Header } from '../TransferList.styled';
 import {
   TableHeader,
@@ -15,9 +16,6 @@ export const CompletedDataRequests = ({ tracingProcesses }) => {
 
   const intl = useIntl();
 
-  const formatDate = timestamp =>
-    `${moment.unix(timestamp).format('DD.MM.YYYY')}`;
-
   return (
     <Wrapper>
       <Header>
@@ -29,50 +27,53 @@ export const CompletedDataRequests = ({ tracingProcesses }) => {
         <div>{tracingProcesses.length}</div>
       </Header>
       <TableHeader>
-        <TableHeaderEntry style={{ flex: '15%' }}>
+        <TableHeaderEntry style={{ flex: '14%' }}>
           {intl.formatMessage({
             id: 'dataTransfers.list.complete.table.header.date',
           })}
         </TableHeaderEntry>
-        <TableHeaderEntry style={{ flex: '15%' }}>
+        <TableHeaderEntry style={{ flex: '14%' }}>
           {intl.formatMessage({
             id: 'dataTransfers.list.complete.table.header.timeFrom',
           })}
         </TableHeaderEntry>
-        <TableHeaderEntry style={{ flex: '15%' }}>
+        <TableHeaderEntry style={{ flex: '14%' }}>
           {intl.formatMessage({
             id: 'dataTransfers.list.complete.table.header.timeUntil',
           })}
         </TableHeaderEntry>
-        <TableHeaderEntry style={{ flex: '30%' }}>
+        <TableHeaderEntry style={{ flex: '28%' }}>
           {intl.formatMessage({
             id: 'dataTransfers.list.complete.table.header.location',
           })}
         </TableHeaderEntry>
-        <TableHeaderEntry style={{ flex: '25%' }}>
+        <TableHeaderEntry style={{ flex: '16%' }}>
           {intl.formatMessage({
             id: 'dataTransfers.list.complete.table.header.healthDepartment',
           })}
         </TableHeaderEntry>
+        <TableHeaderEntry style={{ flex: '14%' }}>
+          {intl.formatMessage({
+            id: 'dataTransfers.list.complete.table.header.approvedAt',
+          })}
+        </TableHeaderEntry>
       </TableHeader>
       {tracingProcesses.map(tracingProcess => (
         <TableRow key={tracingProcess.tracingProcessId}>
-          <TableEntry style={{ flex: '15%' }}>
-            {formatDate(tracingProcess.createdAt)}
+          <TableEntry style={{ flex: '14%' }}>
+            {getFormattedDate(tracingProcess.createdAt)}
           </TableEntry>
-          <TableEntry style={{ flex: '15%' }}>
-            <div>{formatDate(tracingProcess.timeSpan[0])}</div>
+          <TableEntry style={{ flex: '14%' }}>
+            <div>{getFormattedDate(tracingProcess.timeSpan[0])}</div>
             <div>
               {`${moment.unix(tracingProcess.timeSpan[0]).format('HH:mm')}`}
             </div>
           </TableEntry>
-          <TableEntry style={{ flex: '15%' }}>
-            <div>{formatDate(tracingProcess.timeSpan[1])}</div>
-            <div>
-              {`${moment.unix(tracingProcess.timeSpan[1]).format('HH:mm')}`}
-            </div>
+          <TableEntry style={{ flex: '14%' }}>
+            <div>{getFormattedDate(tracingProcess.timeSpan[1])}</div>
+            <div>{getFormattedTime(tracingProcess.timeSpan[1])}</div>
           </TableEntry>
-          <TableEntry style={{ flex: '30%' }}>
+          <TableEntry style={{ flex: '28%' }}>
             {tracingProcess.transfers.map(transfer => (
               <div key={transfer.uuid}>
                 {`${transfer.groupName} - ${
@@ -82,9 +83,17 @@ export const CompletedDataRequests = ({ tracingProcesses }) => {
               </div>
             ))}
           </TableEntry>
-          <TableEntry style={{ flex: '25%' }}>
+          <TableEntry style={{ flex: '16%' }}>
             {tracingProcess.healthDepartment}
           </TableEntry>
+          <TableEntry style={{ flex: '14%' }}>
+            <div>
+              {getFormattedDate(tracingProcess.transfers[0].approvedAt)}
+            </div>
+            <div>
+              {getFormattedTime(tracingProcess.transfers[0].approvedAt)}
+            </div>
+          </TableEntry>
         </TableRow>
       ))}
     </Wrapper>
diff --git a/services/locations/src/components/Authentication/Footer/Footer.react.js b/services/locations/src/components/Authentication/Footer/Footer.react.js
index c23d7a5..f323075 100644
--- a/services/locations/src/components/Authentication/Footer/Footer.react.js
+++ b/services/locations/src/components/Authentication/Footer/Footer.react.js
@@ -24,7 +24,7 @@ export const Footer = () => {
       <Link href={TERMS_CONDITIONS_LINK} target="_blank">
         {intl.formatMessage({ id: 'authentication.background.legal.agb' })}
       </Link>
-      <Version>{isSuccess ? 'luca location' : ''}</Version>
+      <Version>{isSuccess ? 'luca Locations' : ''}</Version>
       <Version title={isSuccess ? `(${info.version}#${info.commit})` : ''}>
         {isSuccess ? `(${info.version}#${info.commit})` : ''}
       </Version>
diff --git a/services/locations/src/components/ShareData/FinishStep/FinishStep.react.js b/services/locations/src/components/ShareData/FinishStep/FinishStep.react.js
index c008e5c..45a97b9 100644
--- a/services/locations/src/components/ShareData/FinishStep/FinishStep.react.js
+++ b/services/locations/src/components/ShareData/FinishStep/FinishStep.react.js
@@ -1,13 +1,11 @@
 import React from 'react';
 import { useIntl } from 'react-intl';
 import { Tick } from 'react-crude-animated-tick';
-import { PrimaryButton } from 'components/general';
 
 // Components
-import { FinishButtonWrapper } from '../ShareData.styled';
 import { ContentWrapper, SubTitle, SuccessWrapper } from './FinishStep.styled';
 
-export const FinishStep = ({ done }) => {
+export const FinishStep = () => {
   const intl = useIntl();
 
   return (
@@ -16,11 +14,9 @@ export const FinishStep = ({ done }) => {
         <Tick size={100} />
       </SuccessWrapper>
       <SubTitle>{intl.formatMessage({ id: 'shareData.finish.text' })}</SubTitle>
-      <FinishButtonWrapper align="flex-end">
-        <PrimaryButton onClick={done} data-cy="finish">
-          {intl.formatMessage({ id: 'shareData.finish' })}
-        </PrimaryButton>
-      </FinishButtonWrapper>
+      <SubTitle>
+        {intl.formatMessage({ id: 'shareData.finish.closeTab' })}
+      </SubTitle>
     </ContentWrapper>
   );
 };
diff --git a/services/locations/src/components/ShareData/ShareData.react.js b/services/locations/src/components/ShareData/ShareData.react.js
index 32e5947..9b2d2e1 100644
--- a/services/locations/src/components/ShareData/ShareData.react.js
+++ b/services/locations/src/components/ShareData/ShareData.react.js
@@ -4,11 +4,9 @@ import { Alert } from 'antd';
 import { useIntl } from 'react-intl';
 import { Helmet } from 'react-helmet';
 import { useQuery } from 'react-query';
-import { useHistory } from 'react-router';
 import { useParams } from 'react-router-dom';
 
 import { usePrivateKey } from 'utils/privateKey';
-import { BASE_SHARE_DATA_ROUTE } from 'constants/routes';
 import {
   getAllUncompletedTransfers,
   getLocationTransfer,
@@ -26,7 +24,6 @@ import { LocationFooter } from '../App/LocationFooter';
 
 export const ShareData = () => {
   const intl = useIntl();
-  const history = useHistory();
   const { transferId } = useParams();
   const [privateKey, setPrivateKey] = useState();
   const [isPrivateKeyPreloaded, setIsPrivateKeyPreloaded] = useState(false);
@@ -73,8 +70,6 @@ export const ShareData = () => {
 
   const setKey = key => setPrivateKey(key);
 
-  const onDone = () => history.push(BASE_SHARE_DATA_ROUTE);
-
   if (isLoading || error) return null;
 
   const steps = [
@@ -103,7 +98,7 @@ export const ShareData = () => {
     },
     {
       id: '2',
-      content: <FinishStep done={onDone} />,
+      content: <FinishStep />,
     },
   ];
 
diff --git a/services/locations/src/messages/de.json b/services/locations/src/messages/de.json
index 792404c..9be6913 100644
--- a/services/locations/src/messages/de.json
+++ b/services/locations/src/messages/de.json
@@ -150,7 +150,7 @@
     "shareData.header.title": "Datenfreigabe",
     "shareData.noData": "Es liegen keine Anfragen aus dem Gesundheitsamt vor",
     "shareData.mainTitle": "Daten für das Gesundheitsamt {healthDepartment} freigeben",
-    "shareData.privateKeyStep.title": "Privaten Schlüssel öffnen",
+    "shareData.privateKeyStep.title": "Privaten Schlüssel bereitstellen",
     "shareData.privateKeyStep.info": "Zum Entschlüsseln der Daten wird dein privater Schlüssel benötigt. \u2028Den Schlüssel hast du bei der Registrierung erhalten.\nBitte öffne deine private Schlüsseldatei, um den Datenzugriff durch das Gesundheitsamt zu ermöglichen.{br}{br}\nDu findest deinen Schlüssel nicht? Bitte such doch noch einmal in deinem Downloadordner nach einer Datei mit der Endung \".luca\". Der Schlüssel kann leider nicht noch einmal generiert werden.",
     "shareData.shareDataStep.title": "Daten freigeben",
     "shareData.privateKey": "Privater Schlüssel",
@@ -237,7 +237,7 @@
     "location.edit.tables": "Tische",
     "modal.qrCodeDocument.additionalDataTitle": "QR-Codes für zusätzliche Informationen erstellen",
     "downloadFile.groups.publicKey": "luca_locations_{name}_privaterSchlüssel.luca",
-    "downloadFile.locations.tableQrCodes": "luca_QRCodes_{groupName}_{locationName}_Tische.pdf",
+    "downloadFile.locations.tableQrCodes": "luca_QRCodes_{fileName}_Tische.pdf",
     "activation.alert.410": "Dieser Link ist abgelaufen.",
     "changeMail.alert.410": "Dieser Link ist abgelaufen.",
     "authentication.form.button.next": "WEITER",
@@ -327,7 +327,7 @@
     "modal.createGroup.complete.description": "Die Konfiguration für deinen Standort ist abgeschlossen. Sobald der Standort erstellt wurde, können Gäste bei dir einchecken.\nIn deinem Standortprofil kannst du deine Einstellungen jederzeit bearbeiten und erweitern.",
     "createGroup.button.done": "ERSTELLEN",
     "modal.createGroup.complete.showCodes": "QR-Code(s) jetzt erstellen?",
-    "downloadFile.locations.generalQrCode": "luca_QRCode_{groupName}_{locationName}.pdf",
+    "downloadFile.locations.generalQrCode": "luca_QRCode_{fileName}.pdf",
     "modal.createGroup.singleQRDownload.description": "Das ist der QR-Code für deinen Standort, den deine Gäste für den Check-in einscannen können.",
     "modal.createGroup.tableQRDownload.description": "Hier findest du einen QR-Code für jeden Tisch deines Standorts. Wenn du sie auslegst, können Gäste sich direkt an ihrem Tisch einchecken, indem sie den Code mit ihrer App scannen.",
     "done": "FERTIG",
@@ -494,12 +494,12 @@
     "error.registerBadge.invalidTanVerification": "Bitte überprüfe die TAN.",
     "authentication.registration.legalHint": "Hinweis: Nach der Anmeldung findest in deinem Profil, welches über das Menü in der rechten oberen Ecke zu erreichen ist, die Datenschutzerklärung zwischen dir und deinen Gästen sowie den Auftragsverarbeitungsvertrag (AVV).",
     "profile.services.overview": "Datenschutz und Sicherheit",
-    "profile.services.download.dataPrivacyMandatory": "Dein Standort ist zur Kontakterfassung gesetzlich verpflichtet: Datenschutzerklärung herunterladen",
+    "profile.services.download.dataPrivacyMandatory": "Muster Datenschutzerklärung (gesetzliche Rechtsgrundlage (DSGVO Art. 6 (1) 1 c)) herunterladen",
     "profile.services.download.avv": "Auftragsverarbeitungsvertrag (AVV) herunterladen",
     "profile.services.agb": "AGB anzeigen",
     "downloadFile.profile.avv": "AVV_luca.pdf",
     "downloadFile.profile.privacy": "Datenschutzerklärung_luca.pdf",
-    "profile.services.download.dataPrivacyOptional": "Dein Standort ist zur Kontakterfassung NICHT gesetzlich verpflichtet: Datenschutzerklärung herunterladen",
+    "profile.services.download.dataPrivacyOptional": "Muster Datenschutzerklärung (auf Basis der freiwilligen Nutzung (DSGVO Art. 6 (1) 1 a)) herunterladen",
     "additionalData.information": "Information",
     "additionalData.tooltip": "Standardmäßig werden beim Check-in die verschlüsselten Kontaktdaten der Gäste übermittelt. Nach dem Check-in können weitere Informationen erfragt werden, die für eine mögliche Nachverfolgung relevant sind. Diese Daten werden bei einer Datenfreigabe ebenfalls verschlüsselt übermittelt und können nur vom Gesundheitsamt entschlüsselt werden. Beispiel: Zimmernummer eines besuchten Patienten im Pflegeheim.",
     "general.information": "Information",
@@ -520,7 +520,7 @@
     "settings.location.checkout.automatic.min": "Der Radius kann nicht unter 50 m liegen.",
     "settings.location.indoorToggle.headline": "Bereichsdetails",
     "settings.location.indoorToggle.description.label": "Innen- oder Außenbereich",
-    "settings.location.indoorToggle.description": "Bitte gib an ob es sich bei deinem Berich um einen Innen- oder Außenbereich handelt.",
+    "settings.location.indoorToggle.description": "Bitte gib an, ob es sich bei deinem Bereich um einen Innen- oder Außenbereich handelt.",
     "settings.location.indoorToggle.indoor": "Innenbereich",
     "settings.location.indoorToggle.outdoor": "Außenbereich",
     "settings.location.indoorToggle.tooltip": "Information",
@@ -587,7 +587,7 @@
     "error.locationName.notDefault": "Bitte verwende nicht den Standardnamen \"Allgemein\".",
     "modal.tableAllocation.activeTableCount": "Anzahl besetzter Tische",
     "refresh": "AKTUALISIEREN",
-    "privateKey.modal.title": "Privaten Schlüssel öffnen",
+    "privateKey.modal.title": "Privaten Schlüssel bereitstellen",
     "error.locationName.exist": "Der Bereichsname existiert bereits an diesem Standort. Bitte verwende einen anderen Namen.",
     "table": "Tisch",
     "modal.tableAllocation.checkedInGuests": "Eingecheckte Gäste",
@@ -638,7 +638,7 @@
     "modal.guestList.checkinTime": "Checkin Zeit",
     "modal.guestList.checkoutDate": "Checkout Datum",
     "modal.guestList.checkoutTime": "Checkout Zeit",
-    "shareData.privateKey.keySize": "Die hochgeladene Datei war zu groß.",
+    "shareData.privateKey.keySize": "Die importierte Datei war zu groß.",
     "profile.services.support": "Hilfe",
     "shareData.note": "HINWEIS",
     "shareData.dataRequest": "Location und Anfragezeitraum",
@@ -660,5 +660,78 @@
     "settings.location.editAddressLink": "Addresse ändern",
     "browserWarning.headline": "Fehler!",
     "browserWarning.description1": "Dieser Browser wird leider nicht unterstützt.",
-    "browserWarning.description2": "Am besten funktionieren Chrome oder Safari."
+    "browserWarning.description2": "Am besten funktionieren Chrome oder Safari.",
+    "header.changeGroup.dropdown.searchLocation": "Standort suchen",
+    "device.title": "App auf Smartphone aktivieren",
+    "device.addDevice": "Gerät Aktivieren",
+    "device.list": "Aktivierte Geräte",
+    "device.list.delete": "Löschen",
+    "settings.location.checkout.average.description": "Deine Gäste werden nach einiger Zeit an das Auschecken erinnert. Gib hier die durchschnittliche Aufenthaltsdauer deiner Gäste an.",
+    "settings.location.checkout.average.title": "Check-out Erinnerung",
+    "settings.location.checkout.average.placeholder": "Zeit auswählen",
+    "notification.updateAverageCheckinTime.constraint.error": "Die angegebene durchschnittliche Aufenthaltszeit ist ungültig. Sie muss mindestens 15 Minuten und darf höchstens 24 Stunden betragen.",
+    "notification.updateAverageCheckinTime.error": "Fehler beim bearbeiten der durchschnittlichen Aufenthaltszeit.",
+    "device.noDevices": "Kein Gerät gefunden",
+    "device.noDevices.info": "Bitte laden Sie die \"Location App\" von Ihrem AppStore oder Google PlayStore herunter und folgen Sie der Anleitung.",
+    "modal.createGroup.averageCheckinTime.title": "Check-out Erinnerung",
+    "modal.createGroup.averageCheckinTime.description": "Wie lange halten sich deine Gäste durchschnittlich in deinem Standort auf?{br}\nDeine Gäste werden nach der angegebenen Zeit erinnert, sich auszuchecken, um zu vermeiden, dass sie es vergessen. \n{br}\n{br}\nDurchschnittliche Aufenthaltsdauer, um eine Auschecken-Erinnerung zu erhalten.",
+    "createGroup.averageCheckinTime": "Durchschnittliche Aufenthaltsdauer",
+    "modal.createDevice.selectRole": "Wähle eine Geräterolle aus",
+    "modal.createDevice.selectRole.description": "Sie können dem Geräte eine Rolle zuweisen. So können nur z. B. nur Geräte mit der Rolle \"Manager\" Daten anfragen freigeben.",
+    "modal.createDevice.selectRole.create": "Geräte erstellen",
+    "modal.createDevice.cancel": "Abbrechen",
+    "modal.createDevice.next": "Weiter",
+    "modal.createDevice.privateKeyPIN": "Privater-Schlüssel-PIN für Smartphone",
+    "modal.createDevice.privateKeyPIN.description": "Gebe den zugehörigen PIN ein, um den Privaten schlüssel zu entschlüsseln.",
+    "modal.createDevice.authenticationPIN": "Aktivierungs-PIN für Smartphone",
+    "modal.createDevice.authenticationPIN.description": "Gebe den zugehörigen PIN ein um das Smartphone zu aktivieren.",
+    "modal.createDevice.authenticationQRCode": "Aktivierungs-QR-Code für Smartphone",
+    "modal.createDevice.authenticationQRCode.description": "Scanne mit dem Smartphone das du aktivieren möchtest den untenstehenden QR-Code.",
+    "modal.createDevice.privateKeyQRCode": "Privater-Schlüssel-QR-Code für Smartphone",
+    "modal.createDevice.privateKeyQRCode.description": "Scanne mit dem Smartphone das du aktivieren möchtest den untenstehenden QR-Code, um den privaten Schlüssel zu übertragen.",
+    "modal.createDevice.done": "Los gehts!",
+    "modal.createDevice.selectRole.manager": "Administrator:in",
+    "modal.createDevice.selectRole.employee": "Mitarbeiter:in",
+    "modal.createDevice.back": "Zurück",
+    "notification.device.error": "Geräte konnte nicht erstellt werden!",
+    "notification.device.success": "Geräte erstellt",
+    "helpCenter.title": "Hilfecenter",
+    "device.list.delete.confirmation": "Sind sie sicher {deviceName} zu löschen?",
+    "device.list.delete.confirmButton": "Löschen",
+    "device.list.delete.declineButton": "Abbrechen",
+    "device.unknown": "Unbekannt",
+    "notification.deviceDeletion.error": "{deviceName} konnte nicht gelöscht werden!",
+    "notification.deviceDeletion.success": "{deviceName} wurde erfolgreich gelöscht",
+    "device.list.deviceName": "Gerätename",
+    "device.list.status": "Status",
+    "device.list.lastActive": "Zuletzt aktiv",
+    "device.list.role": "Rolle",
+    "device.active": "Aktiv",
+    "device.unactive": "Unaktiv",
+    "device.role.employee": "Mitarbeiter:in",
+    "device.role.manager": "Administrator:in",
+    "profile.services.videos": "Erklärvideos",
+    "helpCenter.heading": "Du findest die passende Antwort nicht auf unserer Website oder den FAQ´s ?",
+    "helpCenter.mail.heading": "Schreib uns per Kontaktformular",
+    "helpCenter.mail.text": "Wir werden deine Anfrage schnellstmöglich beantworten.",
+    "helpCenter.mail.buttonText": "Schreib uns",
+    "helpCenter.phone.heading": "Ruf uns an",
+    "helpCenter.phone.text": "Wir sind 24/7 für dich erreichbar. Kostenfrei für Deutschland. {br} \u2028Bitte halte deinen Support Code bereit.",
+    "helpCenter.phone.showSupportCode": "Support Code anzeigen",
+    "contactForm.modal.title": "Kontaktformular",
+    "contactForm.modal.text": "Hast du Fragen oder Probleme? Melde dich gerne bei uns. Deine Anfrage wird schnellstmöglich von unserem luca-Team bearbeitet.",
+    "contactForm.modal.supportCode": "Support Code",
+    "contactForm.modal.name": "Name",
+    "contactForm.modal.email": "E-Mail",
+    "contactForm.modal.phone": "Telefonnummer (optional)",
+    "contactForm.modal.request": "*Deine Nachricht",
+    "contactForm.modal.cancel": "Abbrechen",
+    "contactForm.modal.submit": "Absenden",
+    "contactForm.modal.success.heading": "Vielen Dank für deine Nachricht",
+    "contactForm.modal.success.text": "Wir werden diese schnellstmöglich bearbeiten und uns anschließend mit dir in Verbindung setzen.",
+    "contactForm.modal.success.button": "Schließen",
+    "contactForm.modal.notification.error": "Fehler beim senden der Nachricht. Bitte versuche es später erneut.",
+    "profile.services.toolkit": "Betreiber Toolkit",
+    "error.requestText.invalid": "Bitte gib einen gültigen Wert ein.",
+    "error.requestText": "Bitte gib deine Nachricht ein."
 }
\ No newline at end of file
diff --git a/services/locations/src/messages/en.json b/services/locations/src/messages/en.json
index 6c54489..8a6ce45 100644
--- a/services/locations/src/messages/en.json
+++ b/services/locations/src/messages/en.json
@@ -237,7 +237,7 @@
     "location.edit.tables": "Tables",
     "modal.qrCodeDocument.additionalDataTitle": "Generate QR codes for additional information",
     "downloadFile.groups.publicKey": "luca_locations_{name}_privateKey.luca",
-    "downloadFile.locations.tableQrCodes": "luca_QRCodes_{groupName}_{locationName}_Tables.pdf",
+    "downloadFile.locations.tableQrCodes": "luca_QRCodes_{fileName}_Tables.pdf",
     "activation.alert.410": "This link is expired.",
     "changeMail.alert.410": "This link is expired.",
     "authentication.form.button.next": "NEXT",
@@ -327,7 +327,7 @@
     "modal.createGroup.complete.description": "The configuration for your place is complete. Your guests can start checking in as soon as the place is created. In your place's profile you can add or update settings at any time.",
     "createGroup.button.done": "CREATE",
     "modal.createGroup.complete.showCodes": "Generate QR codes now?",
-    "downloadFile.locations.generalQrCode": "luca_QRCode_{groupName}_{locationName}.pdf",
+    "downloadFile.locations.generalQrCode": "luca_QRCode_{fileName}.pdf",
     "modal.createGroup.singleQRDownload.description": "This is the QR code for your place. Your guest can check in by scanning it with their app.",
     "modal.createGroup.tableQRDownload.description": "Here are the QR codes for every table of your place. If you display them, guests can check in right at their table by scanning the code with their app.",
     "done": "DONE",
@@ -494,12 +494,12 @@
     "error.registerBadge.invalidTanVerification": "Please check the provided TAN.",
     "authentication.registration.legalHint": "Note: After you logged in you can find the data processing agreement (DPA) and the data privacy notice between you and your guests. You can find the link to your profile in the top right menu.",
     "profile.services.overview": "Data privacy and security",
-    "profile.services.download.dataPrivacyMandatory": "Your location is enforced by law to collect contact data: Download privacy policy",
+    "profile.services.download.dataPrivacyMandatory": "Download privacy policy template (mandatory by law (GDPR Art. 6 (1) 1 c))",
     "profile.services.download.avv": "Download  data processing agreement (DPA)",
     "profile.services.agb": "Show Terms of use",
     "downloadFile.profile.avv": "DPA_luca.pdf",
     "downloadFile.profile.privacy": "DataPrivacy_luca.pdf",
-    "profile.services.download.dataPrivacyOptional": "Your location is NOT enforced by law to collect contact data: Download privacy policy",
+    "profile.services.download.dataPrivacyOptional": "Download privacy policy template (voluntary (GDPR Art. 6 (1) 1 a))",
     "additionalData.information": "Information",
     "additionalData.tooltip": "By default, guests transfer their encrypted contact details during check-in. If additional information may help during tracing, you may ask guests to provide them manually.",
     "general.information": "Information",
@@ -576,7 +576,7 @@
     "notification.updateLocation.error.exist": "Location could not be updated! Please make sure the area name is unique and not called \"General\".",
     "notification.forgotPassword.inactiveUser": "This user is not activated, please activate your account first.",
     "qrPrint.step1": "1. Download the CSV/PDF file.",
-    "qrPrint.step2": "2. Click on Print QR-codes via {link} on the right side.",
+    "qrPrint.step2": "2. Click on {link}.",
     "qrPrint.step3": "3. Drag and drop CSV/PDF into the QR-PRINT configurator.",
     "profile.services.download.toms": "Download technical and organisational measures (TOMs)",
     "downloadFile.profile.toms": "TOMS_luca.pdf",
@@ -638,7 +638,7 @@
     "modal.guestList.checkinTime": "Check in time",
     "modal.guestList.checkoutDate": "Check out date",
     "modal.guestList.checkoutTime": "Check out time",
-    "shareData.privateKey.keySize": "The uploaded file was too big.",
+    "shareData.privateKey.keySize": "The imported file was too big.",
     "profile.services.support": "Support",
     "shareData.note": "NOTE",
     "shareData.dataRequest": "Location and request time",
@@ -660,5 +660,78 @@
     "settings.location.editAddressLink": "Change address",
     "browserWarning.headline": "Error!",
     "browserWarning.description1": "Unfortunately, your browser is not supported.",
-    "browserWarning.description2": "It will work best with Chrome or Safari."
+    "browserWarning.description2": "It will work best with Chrome or Safari.",
+    "header.changeGroup.dropdown.searchLocation": "Search location",
+    "device.title": "Activate app on smartphone",
+    "device.addDevice": "Add Device",
+    "device.list": "Active devices",
+    "device.list.delete": "Remove",
+    "settings.location.checkout.average.description": "Your guest will be informed after a given time period. You can adjust the average check in duration here.",
+    "settings.location.checkout.average.title": "Check out reminder",
+    "settings.location.checkout.average.placeholder": "Select time",
+    "notification.updateAverageCheckinTime.constraint.error": "The provided average checkin time is invalid. It needs to be between 15 minutes and 24 hours.",
+    "notification.updateAverageCheckinTime.error": "Error while updating the average checkin time.",
+    "device.noDevices": "No device found",
+    "device.noDevices.info": "Please download the \"Location App\" from your AppStore or Google PlayStore and follow the guide.",
+    "modal.createGroup.averageCheckinTime.title": "Check out reminder",
+    "modal.createGroup.averageCheckinTime.description": "What is the average time your guests stay in your location?{br}\nYour guests will be reminded to check out after the specified time to avoid them forgetting. \n{br}\n{br}\nAverage length of stay to receive a check-out reminder.",
+    "createGroup.averageCheckinTime": "Average checkin duration",
+    "modal.createDevice.selectRole": "Select device role",
+    "modal.createDevice.selectRole.description": "You can assign a role to the device. For example, only devices with the role \"Manager\" can request data.",
+    "modal.createDevice.selectRole.create": "Create Device",
+    "modal.createDevice.cancel": "Cancel",
+    "modal.createDevice.next": "Next",
+    "modal.createDevice.privateKeyPIN": "Private key PIN for smartphone",
+    "modal.createDevice.privateKeyPIN.description": "Enter the corresponding PIN to decrypt the private key.",
+    "modal.createDevice.authenticationPIN": "Activation PIN for smartphone",
+    "modal.createDevice.authenticationPIN.description": "Enter the corresponding PIN to activate the smartphone.",
+    "modal.createDevice.authenticationQRCode": "Activation QR code for smartphone",
+    "modal.createDevice.authenticationQRCode.description": "Scan the QR code below with the smartphone you want to activate.",
+    "modal.createDevice.privateKeyQRCode": "Private key QR code for smartphone",
+    "modal.createDevice.privateKeyQRCode.description": "Scan the QR code below with the smartphone you want to activate to transfer the private key.",
+    "modal.createDevice.done": "Done!",
+    "modal.createDevice.selectRole.manager": "Admin",
+    "modal.createDevice.selectRole.employee": "Employee",
+    "modal.createDevice.back": "Back",
+    "notification.device.error": "Device creation failed!",
+    "notification.device.success": "Device created",
+    "helpCenter.title": "Help center",
+    "device.list.delete.confirmation": "Are you sure to delete {deviceName}?",
+    "device.list.delete.confirmButton": "Delete",
+    "device.list.delete.declineButton": "Cancel",
+    "device.unknown": "Unknown",
+    "notification.deviceDeletion.error": "{deviceName} could not be deleted!",
+    "notification.deviceDeletion.success": "{deviceName} was successfully deleted",
+    "device.list.deviceName": "Device name",
+    "device.list.status": "Status",
+    "device.list.lastActive": "Last active",
+    "device.list.role": "Role",
+    "device.active": "Active",
+    "device.unactive": "Unactive",
+    "device.role.employee": "Employee",
+    "device.role.manager": "Admin",
+    "profile.services.videos": "Support videos",
+    "helpCenter.heading": "Your question is not answered on our website or in the faq's?",
+    "helpCenter.mail.heading": "Write us via contact form",
+    "helpCenter.mail.text": "We will answer your questions as soon as possible.",
+    "helpCenter.mail.buttonText": "Write us",
+    "helpCenter.phone.heading": "Call us",
+    "helpCenter.phone.text": "You can reach out to us 24/7. Free for Germany. {br} Please have your support code at hand.",
+    "helpCenter.phone.showSupportCode": "Show support code",
+    "contactForm.modal.title": "Contact form",
+    "contactForm.modal.text": "You have questions or problems? Feel free to contact us. Your request will be answered as soon as possible bx our team.",
+    "contactForm.modal.supportCode": "Support code",
+    "contactForm.modal.name": "Name",
+    "contactForm.modal.email": "Email",
+    "contactForm.modal.phone": "Phone number (optional)",
+    "contactForm.modal.request": "*Your request",
+    "contactForm.modal.cancel": "Cancel",
+    "contactForm.modal.submit": "Send",
+    "contactForm.modal.success.heading": "Thank you for your request",
+    "contactForm.modal.success.text": "We will reply as soon as soon as possible.",
+    "contactForm.modal.success.button": "Close",
+    "contactForm.modal.notification.error": "Error while sending the request. Please try again later.",
+    "profile.services.toolkit": "Operator toolkit",
+    "error.requestText.invalid": "Please enter a valid value.",
+    "error.requestText": "Please provide your Request."
 }
\ No newline at end of file
diff --git a/services/locations/src/utils/downloadPDF.js b/services/locations/src/utils/downloadPDF.js
index 2f16438..edeea93 100644
--- a/services/locations/src/utils/downloadPDF.js
+++ b/services/locations/src/utils/downloadPDF.js
@@ -16,13 +16,14 @@ export const downloadPDF = async ({
   isCWAEventEnabled,
 }) => {
   const messageText = intl.formatMessage({ id: 'message.generatingPDF' });
-  const locationName = `${location.groupName}${
-    location.name ? ` - ${location.name}` : ''
-  }`;
   const showLoadingProgress = percentage =>
     openLoadingMessage(percentage, messageText);
   setIsDownloading(true);
   showLoadingProgress(0);
+  const locationName = `_${location.name}`;
+  const fileName = `${location.groupName.replace(' ', '_')}${
+    location.name ? locationName.replace(' ', '_') : ''
+  }`;
   const { getPDF } = pdfWorkerApiReference.current;
   const pdf = await getPDF(
     location,
@@ -42,11 +43,9 @@ export const downloadPDF = async ({
     : 'downloadFile.locations.generalQrCode';
 
   const filename = sanitize(
-    intl.formatMessage(
-      { id: fileNameLocale },
-      { groupName: location.groupName, locationName }
-    )
+    intl.formatMessage({ id: fileNameLocale }, { fileName })
   );
+
   FileSaver.saveAs(pdf, filename);
   setIsDownloading(false);
   message.destroy(LOADING_MESSAGE);
diff --git a/services/locations/src/utils/qrCodeData.js b/services/locations/src/utils/qrCodeData.js
index 191797f..8f7fc92 100644
--- a/services/locations/src/utils/qrCodeData.js
+++ b/services/locations/src/utils/qrCodeData.js
@@ -3,8 +3,8 @@ import { RESTAURANT_TYPE } from '../components/App/modals/CreateLocationModal/Cr
 
 export const getLocationName = location =>
   location.name === null || location.name === undefined
-    ? `${location.LocationGroup?.name}`
-    : `${location.LocationGroup?.name} ${location.name}`;
+    ? `${location.groupName}`
+    : `${location.groupName} ${location.name}`;
 
 const generateLocationCWAContentPart = location => {
   const address = `${location.streetName} ${location.streetNr}, ${location.zipCode} ${location.city}`;
diff --git a/services/locations/src/utils/time.js b/services/locations/src/utils/time.js
new file mode 100644
index 0000000..4dc4551
--- /dev/null
+++ b/services/locations/src/utils/time.js
@@ -0,0 +1,7 @@
+import moment from 'moment';
+
+export const getFormattedDate = timestamp =>
+  timestamp ? moment.unix(timestamp).format('DD.MM.YYYY') : '';
+
+export const getFormattedTime = timestamp =>
+  timestamp ? moment.unix(timestamp).format('HH:mm') : '';
diff --git a/services/locations/yarn.lock b/services/locations/yarn.lock
index e37a818..cfbecec 100644
--- a/services/locations/yarn.lock
+++ b/services/locations/yarn.lock
@@ -3774,9 +3774,9 @@ caniuse-api@^3.0.0:
     lodash.uniq "^4.5.0"
 
 caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001219:
-  version "1.0.30001249"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001249.tgz#90a330057f8ff75bfe97a94d047d5e14fabb2ee8"
-  integrity sha512-vcX4U8lwVXPdqzPWi6cAJ3FnQaqXbBqy/GZseKNQzRj37J7qZdGcBtxq/QLFNLLlfsoXLUdHw8Iwenri86Tagw==
+  version "1.0.30001255"
+  resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001255.tgz"
+  integrity sha512-F+A3N9jTZL882f/fg/WWVnKSu6IOo3ueLz4zwaOPbPYHNmM/ZaDUyzyJwS1mZhX7Ex5jqTyW599Gdelh5PDYLQ==
 
 canvg@^3.0.6:
   version "3.0.7"
diff --git a/services/scanner/package.json b/services/scanner/package.json
index 14bc40d..890951f 100644
--- a/services/scanner/package.json
+++ b/services/scanner/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@lucaapp/scanner",
-  "version": "1.9.2",
+  "version": "2.0.0",
   "private": true,
   "license": "Apache-2.0",
   "author": "Culture4Life <hello@luca-app.de> (https://www.luca-app.de/)",
diff --git a/services/scanner/yarn.lock b/services/scanner/yarn.lock
index daadc39..2877edc 100644
--- a/services/scanner/yarn.lock
+++ b/services/scanner/yarn.lock
@@ -3436,9 +3436,9 @@ caniuse-api@^3.0.0:
     lodash.uniq "^4.5.0"
 
 caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001219:
-  version "1.0.30001249"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001249.tgz#90a330057f8ff75bfe97a94d047d5e14fabb2ee8"
-  integrity sha512-vcX4U8lwVXPdqzPWi6cAJ3FnQaqXbBqy/GZseKNQzRj37J7qZdGcBtxq/QLFNLLlfsoXLUdHw8Iwenri86Tagw==
+  version "1.0.30001255"
+  resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001255.tgz"
+  integrity sha512-F+A3N9jTZL882f/fg/WWVnKSu6IOo3ueLz4zwaOPbPYHNmM/ZaDUyzyJwS1mZhX7Ex5jqTyW599Gdelh5PDYLQ==
 
 capture-exit@^2.0.0:
   version "2.0.0"
diff --git a/services/webapp/package.json b/services/webapp/package.json
index 3ed6f0b..5d75248 100644
--- a/services/webapp/package.json
+++ b/services/webapp/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@lucaapp/webapp",
-  "version": "1.9.2",
+  "version": "2.0.0",
   "private": true,
   "license": "Apache-2.0",
   "author": "Culture4Life <hello@luca-app.de> (https://www.luca-app.de/)",
@@ -20,7 +20,6 @@
     "@ant-design/icons": "4.5.0",
     "@craco/craco": "6.1.1",
     "@lucaapp/crypto": "2.0.3",
-    "use-interval": "1.3.0",
     "antd": "4.12.3",
     "connected-react-router": "6.9.1",
     "craco-less": "1.17.1",
@@ -51,6 +50,7 @@
     "redux-thunk": "2.3.0",
     "styled-components": "5.2.1",
     "ua-parser-js": "0.7.24",
+    "use-interval": "1.3.0",
     "validator": "13.6.0"
   },
   "devDependencies": {
diff --git a/services/webapp/src/components/ContactInformation/ContactInformation.react.js b/services/webapp/src/components/ContactInformation/ContactInformation.react.js
index 45f788e..90dd4ff 100644
--- a/services/webapp/src/components/ContactInformation/ContactInformation.react.js
+++ b/services/webapp/src/components/ContactInformation/ContactInformation.react.js
@@ -34,7 +34,7 @@ import {
   StyledDescription,
 } from './ContactInformation.styled';
 
-const ChallengeFormContent = (newPhoneNumber, setChallengeId) => {
+const ChallengeFormContent = ({ newPhoneNumber, setChallengeId }) => {
   const { formatMessage } = useIntl();
   return (
     <StyledContent>
@@ -73,7 +73,7 @@ const ChallengeFormContent = (newPhoneNumber, setChallengeId) => {
   );
 };
 
-function UserInformationFormContent(user) {
+function UserInformationFormContent({ user }) {
   const intl = useIntl();
   const nameValidator = useNameValidator();
   const cityValidator = useCityValidator();
@@ -218,27 +218,34 @@ export function ContactInformation() {
       .then(() => history.push(SETTINGS_PATH))
       .catch(() => changeUserInfoError());
 
-  const onSubmit = values => {
+  const onSubmit = async values => {
     if (challengeId) {
-      verifySMSTAN(challengeId, values.tan)
-        .then(() => changeUser(user.userId, temporaryUser))
-        .catch(() => verificationFailedError());
+      try {
+        await verifySMSTAN(challengeId, values.tan);
+        changeUser(user.userId, temporaryUser);
+      } catch {
+        verificationFailedError();
+      }
     } else if (values.phoneNumber !== user.phoneNumber) {
-      const formattedPhoneNumber = parsePhoneNumber(
-        values.phoneNumber,
-        'DE'
-      ).formatInternational();
-      sendSMSTAN(formattedPhoneNumber)
-        .then(id => {
-          setChallengeId(id);
-          setTemporaryUser(values);
-          setNewPhoneNumber(values.phoneNumber);
-        })
-        .catch(() => setChallengeId(null));
+      try {
+        const formattedPhoneNumber = parsePhoneNumber(
+          values.phoneNumber,
+          'DE'
+        ).formatInternational();
+        const id = await sendSMSTAN(formattedPhoneNumber);
+        setChallengeId(id);
+        setTemporaryUser(values);
+        setNewPhoneNumber(values.phoneNumber);
+      } catch {
+        setChallengeId(null);
+      }
     } else {
-      changeUserInformation(user.userId, values)
-        .then(() => history.push(SETTINGS_PATH))
-        .catch(() => changeUserInfoError());
+      try {
+        await changeUserInformation(user.userId, values);
+        history.push(SETTINGS_PATH);
+      } catch {
+        changeUserInfoError();
+      }
     }
   };
 
@@ -277,9 +284,14 @@ export function ContactInformation() {
             </StyledFooter>
           }
         >
-          {challengeId
-            ? ChallengeFormContent(newPhoneNumber, setChallengeId)
-            : UserInformationFormContent(user)}
+          {challengeId ? (
+            <ChallengeFormContent
+              newPhoneNumber={newPhoneNumber}
+              setChallengeId={setChallengeId}
+            />
+          ) : (
+            <UserInformationFormContent user={user} />
+          )}
         </AppLayout>
       </StyledForm>
     </>
diff --git a/services/webapp/src/components/History/History.react.js b/services/webapp/src/components/History/History.react.js
index aa19425..7d6db55 100644
--- a/services/webapp/src/components/History/History.react.js
+++ b/services/webapp/src/components/History/History.react.js
@@ -61,10 +61,17 @@ export function History() {
     setCurrentHistoryShareStep(0);
   };
 
-  const showShareTAN = () => setCurrentHistoryShareStep(SHARE_TAN_MODAL_STEP);
+  const shareTan = () =>
+    reportInfection()
+      .then(tan => {
+        setShareTAN(tan);
+        setCurrentHistoryShareStep(SHARE_TAN_MODAL_STEP);
+      })
+      .catch(() => setShareTAN(null));
+
   const historyShareSteps = [
     <HistoryShareConsentModal
-      next={showShareTAN}
+      next={shareTan}
       key="share-consent"
       onClose={closeModal}
     />,
@@ -130,18 +137,6 @@ export function History() {
       .catch(() => internalIndexedDBError());
   }, [userHistory, internalIndexedDBError]);
 
-  const shareHistory = useCallback(() => {
-    reportInfection()
-      .then(tan => {
-        setShareTAN(tan);
-        setShowShareModal(true);
-      })
-      .catch(() => {
-        setShareTAN(null);
-        setShowShareModal(false);
-      });
-  }, [setShareTAN, setShowShareModal]);
-
   return (
     <>
       <Helmet>
@@ -263,12 +258,17 @@ export function History() {
           </StyledSteps>
         </AppContent>
         <StyledFooter flex="unset">
-          <StyledSecondaryButton tabIndex="4" onClick={shareHistory}>
+          <StyledSecondaryButton
+            tabIndex="4"
+            onClick={() => {
+              setShowShareModal(true);
+            }}
+          >
             {formatMessage({ id: 'History.ShareHistory' })}
           </StyledSecondaryButton>
         </StyledFooter>
       </AppLayout>
-      {showShareModal && shareTAN && historyShareSteps[currentHistoryShareStep]}
+      {showShareModal && historyShareSteps[currentHistoryShareStep]}
       {activePrivateMeeting && (
         <HistoryPrivateMeetingInfoModal
           locationId={activePrivateMeeting}
diff --git a/services/webapp/yarn.lock b/services/webapp/yarn.lock
index b16c50e..0d54d73 100644
--- a/services/webapp/yarn.lock
+++ b/services/webapp/yarn.lock
@@ -3416,9 +3416,9 @@ caniuse-api@^3.0.0:
     lodash.uniq "^4.5.0"
 
 caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001219:
-  version "1.0.30001249"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001249.tgz#90a330057f8ff75bfe97a94d047d5e14fabb2ee8"
-  integrity sha512-vcX4U8lwVXPdqzPWi6cAJ3FnQaqXbBqy/GZseKNQzRj37J7qZdGcBtxq/QLFNLLlfsoXLUdHw8Iwenri86Tagw==
+  version "1.0.30001255"
+  resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001255.tgz"
+  integrity sha512-F+A3N9jTZL882f/fg/WWVnKSu6IOo3ueLz4zwaOPbPYHNmM/ZaDUyzyJwS1mZhX7Ex5jqTyW599Gdelh5PDYLQ==
 
 capture-exit@^2.0.0:
   version "2.0.0"
diff --git a/yarn.lock b/yarn.lock
index e2dbaa5..ab787d1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10,9 +10,9 @@
     "@babel/highlight" "^7.14.5"
 
 "@babel/helper-validator-identifier@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz#d0f0e277c512e0c938277faa85a3968c9a44c0e8"
-  integrity sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==
+  version "7.14.9"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz#6654d171b2024f6d8ee151bf2509699919131d48"
+  integrity sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==
 
 "@babel/highlight@^7.14.5":
   version "7.14.5"
@@ -23,6 +23,11 @@
     chalk "^2.0.0"
     js-tokens "^4.0.0"
 
+"@types/lodash@4.14.172":
+  version "4.14.172"
+  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.172.tgz#aad774c28e7bfd7a67de25408e03ee5a8c3d028a"
+  integrity sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==
+
 "@types/parse-json@^4.0.0":
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@@ -94,9 +99,9 @@ chalk@^2.0.0:
     supports-color "^5.3.0"
 
 chalk@^4.1.0, chalk@^4.1.1:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
-  integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
   dependencies:
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
@@ -146,9 +151,9 @@ color-name@~1.1.4:
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
 colorette@^1.2.2:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
-  integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.3.0.tgz#ff45d2f0edb244069d3b772adeb04fed38d0a0af"
+  integrity sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w==
 
 commander@^7.2.0:
   version "7.2.0"
@@ -302,9 +307,9 @@ is-regexp@^1.0.0:
   integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk=
 
 is-stream@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
-  integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
+  integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
 
 is-unicode-supported@^0.1.0:
   version "0.1.0"
@@ -353,9 +358,9 @@ lint-staged@11.0.0:
     stringify-object "^3.3.0"
 
 listr2@^3.8.2:
-  version "3.10.0"
-  resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.10.0.tgz#58105a53ed7fa1430d1b738c6055ef7bb006160f"
-  integrity sha512-eP40ZHihu70sSmqFNbNy2NL1YwImmlMmPh9WO5sLmPDleurMHt3n+SwEWNu2kzKScexZnkyFtc1VI0z/TGlmpw==
+  version "3.11.0"
+  resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.11.0.tgz#9771b02407875aa78e73d6e0ff6541bbec0aaee9"
+  integrity sha512-XLJVe2JgXCyQTa3FbSv11lkKExYmEyA4jltVo8z4FX10Vt1Yj8IMekBfwim0BSOM9uj1QMTJvDQQpHyuPbB/dQ==
   dependencies:
     cli-truncate "^2.1.0"
     colorette "^1.2.2"
-- 
GitLab