diff --git a/docker-compose.yml b/docker-compose.yml
index 2ec72e2df94567571d90c39e96d86094ff19589f..07f8df8fa9704a08e043d468bf13efaf917b99c2 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -262,6 +262,26 @@ services:
     logging:
       driver: json-file
 
+  fda-user-service:
+    restart: on-failure
+    container_name: dbrepo-user-service
+    hostname: user-service
+    build: ./fda-user-service
+    image: fda-user-service
+    networks:
+      core:
+    ports:
+      - "9098:9098"
+    env_file:
+      - .env
+    depends_on:
+      fda-metadata-db:
+        condition: service_healthy
+      fda-authentication-service:
+        condition: service_healthy
+    logging:
+      driver: json-file
+
   fda-semantics-service:
     restart: on-failure
     container_name: dbrepo-semantics-service
diff --git a/fda-gateway-service/rest-service/src/main/java/at/tuwien/config/GatewayConfig.java b/fda-gateway-service/rest-service/src/main/java/at/tuwien/config/GatewayConfig.java
index 937479e0417e4ffd3f8ed19ec3160d7b9c9ce744..bb1450fde72230d2707facca8592f17f5a18c331 100644
--- a/fda-gateway-service/rest-service/src/main/java/at/tuwien/config/GatewayConfig.java
+++ b/fda-gateway-service/rest-service/src/main/java/at/tuwien/config/GatewayConfig.java
@@ -16,6 +16,11 @@ public class GatewayConfig {
                         .method("POST", "GET", "PUT", "DELETE")
                         .filters(f -> f.rewritePath("/api/auth/(?<segment>.*)", "/${segment}"))
                         .uri("lb://authentication-service"))
+                .route("user-service", r -> r.path("/api/user/**")
+                        .and()
+                        .method("POST", "GET", "PUT", "DELETE")
+                        .and()
+                        .uri("lb://user-service"))
                 .route("broker-service", r -> r.path("/api/broker/**")
                         .and()
                         .method("POST", "GET", "PUT", "DELETE")
diff --git a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java
index f394a018034cd8eaa4d9e44d619934fcb480be25..abd946bdb9231d74479b880e4eb822b6ae14dad3 100644
--- a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java
+++ b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java
@@ -36,6 +36,9 @@ public class User {
     @Column(name = "last_name")
     private String lastname;
 
+    @Column(name = "realm id")
+    private String realmId;
+
     @Column(unique = true, nullable = false)
     private String email;
 
diff --git a/fda-ui/.env.example b/fda-ui/.env.example
index 5a83ca98633b20cb62f0b6c9a2c03bae08a58833..366635afbdd0602eb9736a2ac6dfe47fefb7592e 100644
--- a/fda-ui/.env.example
+++ b/fda-ui/.env.example
@@ -7,4 +7,5 @@ BROKER_USERNAME=fda
 BROKER_PASSWORD=fda
 SANDBOX=false
 SHARED_FILESYSTEM=/tmp
-DBREPO_CLIENT_SECRET=
+DBREPO_CLIENT_SECRET=MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG
+DEFAULT_PID_PUBLISHER=
diff --git a/fda-ui/components/dialogs/Persist.vue b/fda-ui/components/dialogs/Persist.vue
index a8595e18fd1151270c670c256f2509997c799b63..7ad914ef20d90eda49a8f9069efb5f617d44b191 100644
--- a/fda-ui/components/dialogs/Persist.vue
+++ b/fda-ui/components/dialogs/Persist.vue
@@ -77,7 +77,7 @@
                 required />
             </v-col>
           </v-row>
-          <v-row v-for="(creator,i) in identifier.creators" :key="`c-${i}`" dense>
+          <v-row v-for="(creator, i) in identifier.creators" :key="`c-${i}`" dense>
             <v-col cols="3">
               <v-text-field
                 v-model="creator.firstname"
@@ -108,7 +108,7 @@
                 name="orcid"
                 label="ORCID" />
             </v-col>
-            <v-col cols="1" class="mt-5">
+            <v-col v-if="i > 0" cols="1" class="mt-5">
               <v-btn icon x-small @click="deleteCreator(i)">
                 <v-icon>mdi-close</v-icon>
               </v-btn>
@@ -334,6 +334,8 @@ export default {
   },
   mounted () {
     this.loadLicenses()
+    this.addCreator()
+    this.identifier.publisher = this.$config.defaultPublisher
   },
   methods: {
     cancel () {
@@ -356,6 +358,9 @@ export default {
       })
     },
     deleteCreator (index) {
+      if (index === 0) {
+        return
+      }
       this.identifier.creators.splice(index, 1)
     },
     deleteRelatedIdentifier (index) {
diff --git a/fda-ui/config.js b/fda-ui/config.js
index a11e09b5c480b74f978ba2a7d86c5d902cfc40a5..b2ea4ef41e7c38bb63c62df110c71fbfd78cb82d 100644
--- a/fda-ui/config.js
+++ b/fda-ui/config.js
@@ -15,5 +15,6 @@ config.tokenMax = process.env.TOKEN_MAX || 5
 config.elasticPassword = process.env.ELASTIC_PASSWORD || 'elastic'
 config.elasticPassword = process.env.ELASTIC_PASSWORD || 'elastic'
 config.clientSecret = process.env.DBREPO_CLIENT_SECRET
+config.defaultPublisher = process.env.DEFAULT_PID_PUBLISHER
 
 module.exports = config
diff --git a/fda-ui/nuxt.config.js b/fda-ui/nuxt.config.js
index 840e02341c795c2e4969bea69c85c66c65d945f9..9530a9de944e26865ee5bd3eee9d4762e6ca8aaa 100644
--- a/fda-ui/nuxt.config.js
+++ b/fda-ui/nuxt.config.js
@@ -1,6 +1,6 @@
 import path from 'path'
 import colors from 'vuetify/es5/util/colors'
-import { sandbox, title, icon, brokerUsername, brokerPassword, sharedFilesystem, version, logo, mailVerify, tokenMax, elasticPassword, clientSecret, api, search } from './config'
+import { sandbox, title, icon, brokerUsername, brokerPassword, sharedFilesystem, version, logo, mailVerify, tokenMax, elasticPassword, clientSecret, api, search, defaultPublisher } from './config'
 
 if (sandbox) {
   console.info('[FDA] Running in sandbox environment')
@@ -77,7 +77,8 @@ export default {
     mailVerify,
     tokenMax,
     elasticPassword,
-    clientSecret
+    clientSecret,
+    defaultPublisher
   },
 
   proxy: {
diff --git a/fda-ui/pages/container/_container_id/database/_database_id/info.vue b/fda-ui/pages/container/_container_id/database/_database_id/info.vue
index b9224362e62047fb60aa8b81f72f49b67b949b53..0eebace71286ad4c51a7ce7e9173dc11f6fd83e0 100644
--- a/fda-ui/pages/container/_container_id/database/_database_id/info.vue
+++ b/fda-ui/pages/container/_container_id/database/_database_id/info.vue
@@ -4,24 +4,38 @@
     <v-progress-linear v-if="loading" />
     <v-tabs-items v-model="tab">
       <v-tab-item>
-        <v-card v-if="isDataSteward || hasIdentifier || (!hasIdentifier && isCreator && isResearcher)" flat tile>
+        <v-card v-if="showIdentifierCard" flat tile>
           <v-card-title>Identifier</v-card-title>
           <v-card-text v-if="hasIdentifier">
             <v-list dense>
               <v-list-item>
                 <v-list-item-content>
-                  <v-list-item-title v-if="publisher" class="mt-2">
+                  <v-list-item-title class="mt-2">
                     Persistent Identifier
                   </v-list-item-title>
-                  <v-list-item-content v-if="publisher">
+                  <v-list-item-content>
                     <v-skeleton-loader v-if="loading" type="text" class="skeleton-small" />
                     <a v-if="!loading" :href="pid">{{ pid }}</a>
                   </v-list-item-content>
-                  <v-list-item-title v-if="publisher" class="mt-2">
+                  <v-list-item-title class="mt-2">
+                    Database Title
+                  </v-list-item-title>
+                  <v-list-item-content>
+                    <v-skeleton-loader v-if="loading" type="paragraph" width="50%" />
+                    <span v-if="!loading">{{ identifier.title }}</span>
+                  </v-list-item-content>
+                  <v-list-item-title class="mt-2">
+                    Database Description
+                  </v-list-item-title>
+                  <v-list-item-content>
+                    <v-skeleton-loader v-if="loading" type="paragraph" width="50%" />
+                    <span v-if="!loading">{{ identifier.description }}</span>
+                  </v-list-item-content>
+                  <v-list-item-title class="mt-2">
                     Database Publisher
                   </v-list-item-title>
-                  <v-list-item-content v-if="publisher">
-                    {{ publisher }}
+                  <v-list-item-content>
+                    {{ database.identifier.publisher }}
                   </v-list-item-content>
                   <v-list-item-title v-if="identifier.creators.length > 0" class="mt-2">
                     Creators
@@ -93,18 +107,17 @@
               </v-list-item>
             </v-list>
           </v-card-text>
-          <v-card-text>
+          <v-card-text v-if="canCreateIdentifier || canDeleteIdentifier">
             <v-card-actions>
               <v-btn
-                v-if="!hasIdentifier && (isDataSteward || (!hasIdentifier && isCreator && isResearcher))"
+                v-if="canCreateIdentifier"
                 small
                 color="primary"
                 @click="editDbDialog = true">
                 Get Database PID
               </v-btn>
-              <!--                v-if="isDataSteward && hasIdentifier"-->
               <v-btn
-                v-if="false"
+                v-if="canDeleteIdentifier"
                 small
                 :loading="loadingDelete"
                 color="error"
@@ -114,7 +127,7 @@
             </v-card-actions>
           </v-card-text>
         </v-card>
-        <v-divider v-if="isDataSteward || hasIdentifier || (!hasIdentifier && isCreator && isResearcher)" />
+        <v-divider v-if="showIdentifierCard" />
         <v-card flat tile>
           <v-card-title>Database</v-card-title>
           <v-card-text>
@@ -135,13 +148,6 @@
                     <v-skeleton-loader v-if="loading" type="text" class="skeleton-small" />
                     <span v-if="!loading">{{ internal_name }}</span>
                   </v-list-item-content>
-                  <v-list-item-title v-if="description" class="mt-2">
-                    Database Description
-                  </v-list-item-title>
-                  <v-list-item-content v-if="description">
-                    <v-skeleton-loader v-if="loading" type="paragraph" width="50%" />
-                    <span v-if="!loading">{{ description }}</span>
-                  </v-list-item-content>
                   <v-list-item-title class="mt-2">
                     Database Creator
                   </v-list-item-title>
@@ -222,7 +228,7 @@ import DBToolbar from '@/components/DBToolbar'
 import Persist from '@/components/dialogs/Persist'
 import OrcidIcon from '@/components/icons/OrcidIcon'
 import Citation from '@/components/identifier/Citation'
-import { formatTimestampUTCLabel, formatUser, isDataSteward, isResearcher } from '@/utils'
+import { formatTimestampUTCLabel, formatUser } from '@/utils'
 
 export default {
   components: {
@@ -254,18 +260,6 @@ export default {
     baseUrl () {
       return location.protocol + '//' + location.host
     },
-    description () {
-      if (!this.hasIdentifier) {
-        return ''
-      }
-      return this.database.identifier.description
-    },
-    publisher () {
-      if (!this.hasIdentifier) {
-        return ''
-      }
-      return this.database.identifier.publisher
-    },
     token () {
       return this.$store.state.token
     },
@@ -273,10 +267,10 @@ export default {
       return this.$store.state.user
     },
     identifier () {
-      if (this.database) {
-        return this.$store.state.database.identifier
+      if (!this.database) {
+        return null
       }
-      return null
+      return this.$store.state?.database.identifier
     },
     access () {
       return this.$store.state.access
@@ -294,27 +288,12 @@ export default {
         headers: { Authorization: `Bearer ${this.token}`, Accept: 'application/json' }
       }
     },
-    isResearcher () {
-      return isResearcher(this.user)
-    },
-    isDataSteward () {
-      return isDataSteward(this.user)
-    },
     pid () {
       return `${this.baseUrl}/pid/${this.database.identifier.id}`
     },
     createdUTC () {
       return formatTimestampUTCLabel(this.database.created)
     },
-    isCreator () {
-      if (!this.database) {
-        return false
-      }
-      if (!this.database.creator.username || !this.user || !this.user.username) {
-        return false
-      }
-      return this.database.creator.username === this.user.username
-    },
     language () {
       return this.database.identifier.language
     },
@@ -327,6 +306,30 @@ export default {
     container_internal_name () {
       return this.database.container.internal_name
     },
+    showIdentifierCard () {
+      if (this.hasIdentifier) {
+        return true
+      }
+      if (!this.user) {
+        return false
+      }
+      return this.canCreateIdentifier || this.canDeleteIdentifier || this.user.roles.includes('modify-identifier-metadata')
+    },
+    canCreateIdentifier () {
+      if (!this.user) {
+        return false
+      }
+      if (this.hasIdentifier) {
+        return false
+      }
+      return this.user.roles.includes('create-identifier')
+    },
+    canDeleteIdentifier () {
+      if (!this.user) {
+        return false
+      }
+      return this.user.roles.includes('delete-identifier')
+    },
     contact () {
       if (this.database.contact === null || this.database.contact === undefined) {
         return null
diff --git a/fda-ui/pages/container/_container_id/database/_database_id/settings.vue b/fda-ui/pages/container/_container_id/database/_database_id/settings.vue
index 85631642ea9c0b656309f7024109171412c44176..888ba4f681117fd66f97a8df17748ec5b3998450 100644
--- a/fda-ui/pages/container/_container_id/database/_database_id/settings.vue
+++ b/fda-ui/pages/container/_container_id/database/_database_id/settings.vue
@@ -5,8 +5,7 @@
     <v-tabs-items v-model="tab">
       <v-tab-item>
         <v-card v-if="isOwner" flat tile>
-          <v-card-title>Modify database access</v-card-title>
-          <v-card-subtitle>This is a dangerous operation</v-card-subtitle>
+          <v-card-title>Access</v-card-title>
           <v-data-table
             :headers="headers"
             :items="database.accesses"
@@ -34,15 +33,8 @@
         </v-card>
         <v-divider />
         <v-card v-if="canModifyVisibility" flat tile>
-          <v-card-title>Modify database visibility</v-card-title>
-          <v-card-subtitle>This is a dangerous operation</v-card-subtitle>
+          <v-card-title>Visibility</v-card-title>
           <v-card-text>
-            <v-alert
-              v-if="database.is_public !== modifyVisibility.is_public"
-              border="left"
-              color="warning">
-              <strong>Dangerous operation:</strong> you are about to change the visibility of the database. This affects all (sensitive) data held in the database.
-            </v-alert>
             <v-row dense>
               <v-col sm="6">
                 <v-select
@@ -55,7 +47,29 @@
             </v-row>
             <v-btn
               small
-              :disabled="database.is_public === modifyVisibility.is_public"
+              color="warning"
+              class="black--text"
+              @click="updateDatabaseVisibility">
+              Modify Visibility
+            </v-btn>
+          </v-card-text>
+        </v-card>
+        <v-divider />
+        <v-card v-if="canModifyOwnership" flat tile>
+          <v-card-title>Ownership</v-card-title>
+          <v-card-text>
+            <v-row dense>
+              <v-col sm="6">
+                <v-select
+                  id="owner"
+                  v-model="modifyOwner.username"
+                  :items="users"
+                  label="Owner"
+                  name="owner" />
+              </v-col>
+            </v-row>
+            <v-btn
+              small
               color="warning"
               class="black--text"
               @click="updateDatabaseVisibility">
@@ -89,12 +103,17 @@ export default {
       dialogDelete: false,
       confirm: null,
       username: null,
+      users: [],
       loading: false,
+      loadingUsers: false,
       editAccessDialog: false,
       editVisibilityDialog: false,
       modifyVisibility: {
         is_public: null
       },
+      modifyOwner: {
+        username: null
+      },
       visibility: [
         { text: 'Public', value: true },
         { text: 'Private', value: false }
@@ -153,6 +172,12 @@ export default {
         return false
       }
       return this.user.roles.includes('modify-database-visibility')
+    },
+    canModifyOwnership () {
+      if (!this.isOwner) {
+        return false
+      }
+      return this.user.roles.includes('modify-database-owner')
     }
   },
   watch: {
@@ -164,6 +189,7 @@ export default {
     }
   },
   mounted () {
+    this.loadUsers()
     if (!this.database) {
       return
     }
@@ -197,6 +223,19 @@ export default {
       this.username = item.user.username
       this.editAccessDialog = true
     },
+    async loadUsers () {
+      this.loadingUsers = true
+      try {
+        const res = await this.$axios.get('/api/user', this.config)
+        this.users = res.data
+        console.debug('users', this.users)
+      } catch (error) {
+        console.error('Failed to load users', error)
+        const { message } = error.response.data
+        this.$toast.error(`Failed to load users: ${message}`)
+      }
+      this.loadingUsers = false
+    },
     async loadDatabase () {
       if (!this.$route.params.container_id || !this.$route.params.database_id) {
         return
diff --git a/fda-user-service/.gitignore b/fda-user-service/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..d68acdb421ac6a167610221b2792e4dcafa4f935
--- /dev/null
+++ b/fda-user-service/.gitignore
@@ -0,0 +1,44 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+# Documentation
+docs/*.html
+docs/css/
+docs/images/
+
+# Docker
+ready
+
+## JUnit
+.attach_pid*
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
diff --git a/fda-user-service/.mvn/wrapper/MavenWrapperDownloader.java b/fda-user-service/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 0000000000000000000000000000000000000000..a45eb6ba269cd38f8965cef786729790945d9537
--- /dev/null
+++ b/fda-user-service/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2007-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+    private static final String WRAPPER_VERSION = "0.5.6";
+    /**
+     * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+     */
+    private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+            + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+    /**
+     * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+     * use instead of the default one.
+     */
+    private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+            ".mvn/wrapper/maven-wrapper.properties";
+
+    /**
+     * Path where the maven-wrapper.jar will be saved to.
+     */
+    private static final String MAVEN_WRAPPER_JAR_PATH =
+            ".mvn/wrapper/maven-wrapper.jar";
+
+    /**
+     * Name of the property which should be used to override the default download url for the wrapper.
+     */
+    private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+    public static void main(String args[]) {
+        System.out.println("- Downloader started");
+        File baseDirectory = new File(args[0]);
+        System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+        // If the maven-wrapper.properties exists, read it and check if it contains a custom
+        // wrapperUrl parameter.
+        File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+        String url = DEFAULT_DOWNLOAD_URL;
+        if (mavenWrapperPropertyFile.exists()) {
+            FileInputStream mavenWrapperPropertyFileInputStream = null;
+            try {
+                mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+                Properties mavenWrapperProperties = new Properties();
+                mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+                url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+            } catch (IOException e) {
+                System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+            } finally {
+                try {
+                    if (mavenWrapperPropertyFileInputStream != null) {
+                        mavenWrapperPropertyFileInputStream.close();
+                    }
+                } catch (IOException e) {
+                    // Ignore ...
+                }
+            }
+        }
+        System.out.println("- Downloading from: " + url);
+
+        File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+        if (!outputFile.getParentFile().exists()) {
+            if (!outputFile.getParentFile().mkdirs()) {
+                System.out.println(
+                        "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+            }
+        }
+        System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+        try {
+            downloadFileFromURL(url, outputFile);
+            System.out.println("Done");
+            System.exit(0);
+        } catch (Throwable e) {
+            System.out.println("- Error downloading");
+            e.printStackTrace();
+            System.exit(1);
+        }
+    }
+
+    private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+        if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+            String username = System.getenv("MVNW_USERNAME");
+            char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+            Authenticator.setDefault(new Authenticator() {
+                @Override
+                protected PasswordAuthentication getPasswordAuthentication() {
+                    return new PasswordAuthentication(username, password);
+                }
+            });
+        }
+        URL website = new URL(urlString);
+        ReadableByteChannel rbc;
+        rbc = Channels.newChannel(website.openStream());
+        FileOutputStream fos = new FileOutputStream(destination);
+        fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+        fos.close();
+        rbc.close();
+    }
+
+}
diff --git a/fda-user-service/.mvn/wrapper/maven-wrapper.jar b/fda-user-service/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 0000000000000000000000000000000000000000..2cc7d4a55c0cd0092912bf49ae38b3a9e3fd0054
Binary files /dev/null and b/fda-user-service/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/fda-user-service/.mvn/wrapper/maven-wrapper.properties b/fda-user-service/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000000000000000000000000000000000000..642d572ce90e5085986bdd9c9204b9404f028084
--- /dev/null
+++ b/fda-user-service/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
diff --git a/fda-user-service/Dockerfile b/fda-user-service/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..dd9fe0250e12a17d5bfd81ce0070530badd5385a
--- /dev/null
+++ b/fda-user-service/Dockerfile
@@ -0,0 +1,42 @@
+###### FIRST STAGE ######
+FROM fda-metadata-db:latest as dependency
+MAINTAINER Martin Weise <martin.weise@tuwien.ac.at>
+
+###### SECOND STAGE ######
+FROM maven:slim as build
+
+COPY ./pom.xml ./
+
+RUN mvn -fn -B dependency:go-offline > /dev/null
+
+COPY --from=dependency /root/.m2/repository/at/tuwien /root/.m2/repository/at/tuwien
+
+COPY ./rest-service ./rest-service
+COPY ./services ./services
+COPY ./report ./report
+
+# Make sure it compiles
+RUN mvn -q clean package -DskipTests > /dev/null
+
+###### THIRD STAGE ######
+FROM openjdk:11-jre-slim as runtime
+
+ENV METADATA_DB=fda
+ENV METADATA_USERNAME=root
+ENV METADATA_PASSWORD=dbrepo
+ENV LOG_LEVEL=debug
+ENV DBREPO_CLIENT_SECRET=client-secret
+ENV CLIENT_ID=dbrepo-client
+ENV JWT_ISSUER=http://localhost:8080/realms/dbrepo
+ENV JWT_PUBKEY=public-key
+
+COPY ./service_ready /usr/bin
+RUN chmod +x /usr/bin/service_ready
+
+HEALTHCHECK --interval=10s --timeout=5s --retries=12 CMD service_ready
+
+COPY --from=build ./rest-service/target/rest-service-*.jar ./user-service.jar
+
+EXPOSE 9093
+
+ENTRYPOINT ["java", "-Dlog4j2.formatMsgNoLookups=true", "-jar", "./user-service.jar"]
diff --git a/fda-user-service/mvnw b/fda-user-service/mvnw
new file mode 100755
index 0000000000000000000000000000000000000000..a16b5431b4c3cab50323a3f558003fd0abd87dad
--- /dev/null
+++ b/fda-user-service/mvnw
@@ -0,0 +1,310 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+#   JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+#   M2_HOME - location of maven2's installed home dir
+#   MAVEN_OPTS - parameters passed to the Java VM when running Maven
+#     e.g. to debug Maven itself, use
+#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+  if [ -f /etc/mavenrc ] ; then
+    . /etc/mavenrc
+  fi
+
+  if [ -f "$HOME/.mavenrc" ] ; then
+    . "$HOME/.mavenrc"
+  fi
+
+fi
+
+# OS specific support.  $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+  CYGWIN*) cygwin=true ;;
+  MINGW*) mingw=true;;
+  Darwin*) darwin=true
+    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+    if [ -z "$JAVA_HOME" ]; then
+      if [ -x "/usr/libexec/java_home" ]; then
+        export JAVA_HOME="`/usr/libexec/java_home`"
+      else
+        export JAVA_HOME="/Library/Java/Home"
+      fi
+    fi
+    ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+  if [ -r /etc/gentoo-release ] ; then
+    JAVA_HOME=`java-config --jre-home`
+  fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+  ## resolve links - $0 may be a link to maven's home
+  PRG="$0"
+
+  # need this for relative symlinks
+  while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+      PRG="$link"
+    else
+      PRG="`dirname "$PRG"`/$link"
+    fi
+  done
+
+  saveddir=`pwd`
+
+  M2_HOME=`dirname "$PRG"`/..
+
+  # make it fully qualified
+  M2_HOME=`cd "$M2_HOME" && pwd`
+
+  cd "$saveddir"
+  # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --unix "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME="`(cd "$M2_HOME"; pwd)`"
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+  javaExecutable="`which javac`"
+  if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+    # readlink(1) is not available as standard on Solaris 10.
+    readLink=`which readlink`
+    if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+      if $darwin ; then
+        javaHome="`dirname \"$javaExecutable\"`"
+        javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+      else
+        javaExecutable="`readlink -f \"$javaExecutable\"`"
+      fi
+      javaHome="`dirname \"$javaExecutable\"`"
+      javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+      JAVA_HOME="$javaHome"
+      export JAVA_HOME
+    fi
+  fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+  if [ -n "$JAVA_HOME"  ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+      # IBM's JDK on AIX uses strange locations for the executables
+      JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+      JAVACMD="$JAVA_HOME/bin/java"
+    fi
+  else
+    JAVACMD="`which java`"
+  fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+  echo "Error: JAVA_HOME is not defined correctly." >&2
+  echo "  We cannot execute $JAVACMD" >&2
+  exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+  echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+  if [ -z "$1" ]
+  then
+    echo "Path not specified to find_maven_basedir"
+    return 1
+  fi
+
+  basedir="$1"
+  wdir="$1"
+  while [ "$wdir" != '/' ] ; do
+    if [ -d "$wdir"/.mvn ] ; then
+      basedir=$wdir
+      break
+    fi
+    # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+    if [ -d "${wdir}" ]; then
+      wdir=`cd "$wdir/.."; pwd`
+    fi
+    # end of workaround
+  done
+  echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+  if [ -f "$1" ]; then
+    echo "$(tr -s '\n' ' ' < "$1")"
+  fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+  exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Found .mvn/wrapper/maven-wrapper.jar"
+    fi
+else
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+    fi
+    if [ -n "$MVNW_REPOURL" ]; then
+      jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+    else
+      jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+    fi
+    while IFS="=" read key value; do
+      case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+      esac
+    done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Downloading from: $jarUrl"
+    fi
+    wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+    if $cygwin; then
+      wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+    fi
+
+    if command -v wget > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found wget ... using wget"
+        fi
+        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+            wget "$jarUrl" -O "$wrapperJarPath"
+        else
+            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+        fi
+    elif command -v curl > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found curl ... using curl"
+        fi
+        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+            curl -o "$wrapperJarPath" "$jarUrl" -f
+        else
+            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+        fi
+
+    else
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Falling back to using Java to download"
+        fi
+        javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+        # For Cygwin, switch paths to Windows format before running javac
+        if $cygwin; then
+          javaClass=`cygpath --path --windows "$javaClass"`
+        fi
+        if [ -e "$javaClass" ]; then
+            if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Compiling MavenWrapperDownloader.java ..."
+                fi
+                # Compiling the Java class
+                ("$JAVA_HOME/bin/javac" "$javaClass")
+            fi
+            if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                # Running the downloader
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Running MavenWrapperDownloader.java ..."
+                fi
+                ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+            fi
+        fi
+    fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+  echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --path --windows "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+  [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+    MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+  $MAVEN_OPTS \
+  -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+  "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/fda-user-service/mvnw.cmd b/fda-user-service/mvnw.cmd
new file mode 100644
index 0000000000000000000000000000000000000000..c8d43372c986d97911cdc21bd87e0cbe3d83bdda
--- /dev/null
+++ b/fda-user-service/mvnw.cmd
@@ -0,0 +1,182 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements.  See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership.  The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License.  You may obtain a copy of the License at
+@REM
+@REM    https://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied.  See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM     e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on"  echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+    IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Found %WRAPPER_JAR%
+    )
+) else (
+    if not "%MVNW_REPOURL%" == "" (
+        SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+    )
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Couldn't find %WRAPPER_JAR%, downloading it ...
+        echo Downloading from: %DOWNLOAD_URL%
+    )
+
+    powershell -Command "&{"^
+		"$webclient = new-object System.Net.WebClient;"^
+		"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+		"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+		"}"^
+		"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+		"}"
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Finished downloading %WRAPPER_JAR%
+    )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
diff --git a/fda-user-service/pom.xml b/fda-user-service/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f06580d159c91afa43a80c0dcb785fcb21b7f434
--- /dev/null
+++ b/fda-user-service/pom.xml
@@ -0,0 +1,288 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.3.10.RELEASE</version>
+    </parent>
+
+    <groupId>at.tuwien</groupId>
+    <artifactId>fda-user-service</artifactId>
+    <version>1.1.0-alpha</version>
+    <name>fda-user-service</name>
+    <description>
+        The query service provides an interface to insert data into the tables created by the table service. It
+        also allows for view-only (possibly paginated and versioned) query execution to the raw data and consumes
+        messages in the message queue from the Broker Service.
+    </description>
+    <url>https://dbrepo-docs.ossdip.at</url>
+    <developers>
+        <developer>
+            <name>Martin Weise</name>
+            <email>martin.weise@tuwien.ac.at</email>
+            <organization>TU Wien</organization>
+        </developer>
+        <developer>
+            <name>Moritz Staudinger</name>
+            <email>moritz.staudinger@tuwien.ac.at</email>
+            <organization>TU Wien</organization>
+        </developer>
+    </developers>
+
+    <packaging>pom</packaging>
+    <modules>
+        <module>rest-service</module>
+        <module>services</module>
+        <module>report</module>
+    </modules>
+
+    <properties>
+        <java.version>11</java.version>
+        <spring-cloud.version>3.0.1</spring-cloud.version>
+        <mapstruct.version>1.4.2.Final</mapstruct.version>
+        <docker.version>3.2.7</docker.version>
+        <testcontainers.version>1.15.2</testcontainers.version>
+        <swagger.version>2.1.7</swagger.version>
+        <springfox.version>3.0.0</springfox.version>
+        <jacoco.version>0.8.7</jacoco.version>
+        <opencsv.version>5.4</opencsv.version>
+        <hibernate-c3po.version>5.6.3.Final</hibernate-c3po.version>
+        <maven-report.version>3.0.0</maven-report.version>
+        <jwt.version>4.3.0</jwt.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webflux</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-amqp</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-test</artifactId>
+        </dependency>
+        <dependency><!-- https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.3-Release-Notes#validation-starter-no-longer-included-in-web-starters -->
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-bootstrap</artifactId>
+            <version>${spring-cloud.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+        <!-- Authentication -->
+        <dependency>
+            <groupId>com.auth0</groupId>
+            <artifactId>java-jwt</artifactId>
+            <version>${jwt.version}</version>
+        </dependency>
+        <!-- Monitoring -->
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-registry-prometheus</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <!-- Data Source -->
+        <dependency>
+            <groupId>org.mariadb.jdbc</groupId>
+            <artifactId>mariadb-java-client</artifactId>
+            <version>${mariadb.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.data</groupId>
+            <artifactId>spring-data-elasticsearch</artifactId>
+        </dependency>
+        <!-- AMPQ -->
+        <dependency>
+            <groupId>com.rabbitmq</groupId>
+            <artifactId>amqp-client</artifactId>
+            <version>${rabbit-amqp-client.version}</version>
+        </dependency>
+        <!-- Docker -->
+        <dependency>
+            <groupId>com.github.docker-java</groupId>
+            <artifactId>docker-java</artifactId>
+            <version>${docker.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>javax.ws.rs</groupId>
+                    <artifactId>jsr311-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.github.docker-java</groupId>
+            <artifactId>docker-java-transport-httpclient5</artifactId>
+            <version>${docker.version}</version>
+        </dependency>
+        <!-- IDE -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+        <!-- Entity, API, QueryStore -->
+        <dependency>
+            <groupId>at.tuwien</groupId>
+            <artifactId>fda-metadata-db-api</artifactId>
+            <version>${project.version}</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>at.tuwien</groupId>
+            <artifactId>fda-metadata-db-entites</artifactId>
+            <version>${project.version}</version>
+            <scope>compile</scope>
+        </dependency>
+        <!-- Testing -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.junit.jupiter</groupId>
+                    <artifactId>junit-vintage-engine</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.jacoco</groupId>
+            <artifactId>jacoco-maven-plugin</artifactId>
+            <version>${jacoco.version}</version>
+        </dependency>
+        <!-- Mapping -->
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct-processor</artifactId>
+            <version>${mapstruct.version}</version>
+            <optional>true</optional>
+        <!-- IntelliJ -->
+        </dependency>
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct</artifactId>
+            <version>${mapstruct.version}</version>
+        </dependency>
+        <!-- Gateway -->
+        <dependency>
+            <groupId>org.projectreactor</groupId>
+            <artifactId>reactor-spring</artifactId>
+            <version>1.0.1.RELEASE</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.ws.rs</groupId>
+            <artifactId>javax.ws.rs-api</artifactId>
+            <version>2.1.1</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>${basedir}/src/main/resources</directory>
+                <filtering>true</filtering>
+                <includes>
+                    <include>**/application*.yml</include>
+                    <include>**/templates/*.xml</include>
+                </includes>
+            </resource>
+        </resources>
+        <plugins>
+            <plugin>
+                <groupId>org.jacoco</groupId>
+                <artifactId>jacoco-maven-plugin</artifactId>
+                <version>${jacoco.version}</version>
+                <configuration>
+                    <excludes>
+                        <exclude>at/tuwien/utils/**/*</exclude>
+                        <exclude>at/tuwien/mapper/**/*</exclude>
+                        <exclude>at/tuwien/entities/**/*</exclude>
+                        <exclude>at/tuwien/seeder/**/*</exclude>
+                        <exclude>at/tuwien/handlers/**/*</exclude>
+                        <exclude>at/tuwien/exception/**/*</exclude>
+                        <exclude>at/tuwien/config/**/*</exclude>
+                        <exclude>**/RabbitMqServiceImpl.class</exclude>
+                        <exclude>**/ServiceSeeder.class</exclude>
+                        <exclude>**/FdaQueryServiceApplication.class</exclude>
+                    </excludes>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>default-prepare-agent</id>
+                        <goals>
+                            <goal>prepare-agent</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>report</id>
+                        <phase>verify</phase>
+                        <goals>
+                            <goal>report</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-site-plugin</artifactId>
+                <version>3.7.1</version>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-project-info-reports-plugin</artifactId>
+                <version>3.0.0</version>
+<!--                <configuration>-->
+<!--                    <outputDirectory>docs</outputDirectory>-->
+<!--                </configuration>-->
+            </plugin>
+            <plugin>
+                <groupId>com.soebes.maven.plugins</groupId>
+                <artifactId>doxygen-maven-plugin</artifactId>
+                <version>1.1.0</version>
+                <configuration>
+                    <haveDot>false</haveDot>
+                    <quiet>false</quiet>
+                    <projectName>This is a Test Project (basicReportTest)</projectName>
+                    <outputDirectory>docs</outputDirectory>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/fda-user-service/report/pom.xml b/fda-user-service/report/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b93720ffeb6fb8f68ae0c891a7a83afd5dfde333
--- /dev/null
+++ b/fda-user-service/report/pom.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <artifactId>fda-user-service</artifactId>
+        <groupId>at.tuwien</groupId>
+        <version>1.1.0-alpha</version>
+    </parent>
+
+    <artifactId>report</artifactId>
+    <version>1.1.0-alpha</version>
+    <name>fda-user-service-report</name>
+    <description>
+        This module is only intended for the pipeline coverage report. See the detailed report in the
+        respective modules
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>at.tuwien</groupId>
+            <artifactId>rest-service</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>at.tuwien</groupId>
+            <artifactId>services</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+
+    <properties>
+        <jacoco.version>0.8.7</jacoco.version>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.jacoco</groupId>
+                <artifactId>jacoco-maven-plugin</artifactId>
+                <version>${jacoco.version}</version>
+                <executions>
+                    <execution>
+                        <id>report-aggregate</id>
+                        <phase>verify</phase>
+                        <goals>
+                            <goal>report-aggregate</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file
diff --git a/fda-user-service/rest-service/pom.xml b/fda-user-service/rest-service/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6a53b116b31e1bb41ae9bda847e770e83bf836fa
--- /dev/null
+++ b/fda-user-service/rest-service/pom.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <artifactId>fda-user-service</artifactId>
+        <groupId>at.tuwien</groupId>
+        <version>1.1.0-alpha</version>
+    </parent>
+
+    <artifactId>rest-service</artifactId>
+    <version>1.1.0-alpha</version>
+    <name>fda-user-service-rest</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>at.tuwien</groupId>
+            <artifactId>services</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jetbrains</groupId>
+            <artifactId>annotations</artifactId>
+            <version>RELEASE</version>
+            <scope>compile</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal><!-- to make it exuteable with $ java -jar ./app.jar -->
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file
diff --git a/fda-user-service/rest-service/src/main/java/at/tuwien/FdaUserServiceApplication.java b/fda-user-service/rest-service/src/main/java/at/tuwien/FdaUserServiceApplication.java
new file mode 100644
index 0000000000000000000000000000000000000000..4a6c3dff083b1ec4214b2f19a07d3839e6298368
--- /dev/null
+++ b/fda-user-service/rest-service/src/main/java/at/tuwien/FdaUserServiceApplication.java
@@ -0,0 +1,25 @@
+package at.tuwien;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+@EnableJpaAuditing
+@SpringBootApplication
+@EnableTransactionManagement
+@EnableScheduling
+@EntityScan(basePackages = "at.tuwien.entities")
+@EnableElasticsearchRepositories(basePackages = {"at.tuwien.repository.elastic"})
+@EnableJpaRepositories(basePackages = {"at.tuwien.repository.jpa"})
+public class FdaUserServiceApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(FdaUserServiceApplication.class, args);
+    }
+
+}
\ No newline at end of file
diff --git a/fda-user-service/rest-service/src/main/java/at/tuwien/config/SwaggerConfig.java b/fda-user-service/rest-service/src/main/java/at/tuwien/config/SwaggerConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..eb08f946aadc05ad3224133245e771aebf2a2364
--- /dev/null
+++ b/fda-user-service/rest-service/src/main/java/at/tuwien/config/SwaggerConfig.java
@@ -0,0 +1,46 @@
+package at.tuwien.config;
+
+import io.swagger.v3.oas.models.ExternalDocumentation;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.info.License;
+import org.springdoc.core.GroupedOpenApi;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class SwaggerConfig {
+
+    @Value("${app.version:unknown}")
+    private String version;
+
+    @Bean
+    public OpenAPI springShopOpenAPI() {
+        return new OpenAPI()
+                .info(new Info()
+                        .title("Database Repository User Service API")
+                        .contact(new Contact()
+                                .name("Prof. Andreas Rauber")
+                                .email("andreas.rauber@tuwien.ac.at"))
+                        .description("Service that manages the users")
+                        .version(version)
+                        .license(new License()
+                                .name("Apache 2.0")
+                                .url("https://www.apache.org/licenses/LICENSE-2.0")))
+                .externalDocs(new ExternalDocumentation()
+                        .description("Wiki Documentation")
+                        .url("https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/wikis"));
+    }
+
+    @Bean
+    public GroupedOpenApi publicApi() {
+        return GroupedOpenApi.builder()
+                .group("user-service")
+                .pathsToMatch("/api/**")
+                .build();
+    }
+
+}
+
diff --git a/fda-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java b/fda-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java
new file mode 100644
index 0000000000000000000000000000000000000000..7bb8dc31490b8468f1a772fda01ffcb4b242ea9d
--- /dev/null
+++ b/fda-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java
@@ -0,0 +1,58 @@
+package at.tuwien.endpoint;
+
+import at.tuwien.api.auth.SignupRequestDto;
+import at.tuwien.api.user.UserBriefDto;
+import at.tuwien.mapper.UserMapper;
+import at.tuwien.service.UserService;
+import io.micrometer.core.annotation.Timed;
+import io.swagger.v3.oas.annotations.Operation;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Log4j2
+@CrossOrigin(origins = "*")
+@RestController
+@RequestMapping("/api/user")
+public class UserEndpoint {
+
+    private final UserMapper userMapper;
+    private final UserService userService;
+
+    @Autowired
+    public UserEndpoint(UserMapper userMapper, UserService userService) {
+        this.userMapper = userMapper;
+        this.userService = userService;
+    }
+
+    @GetMapping
+    @Transactional(readOnly = true)
+    @Timed(value = "user.list", description = "Time needed to list all users in the metadata database")
+    @Operation(summary = "Find all users")
+    public ResponseEntity<List<UserBriefDto>> findAll() {
+        log.debug("endpoint find all users");
+        final List<UserBriefDto> users = userService.findAll()
+                .stream()
+                .map(userMapper::userToUserBriefDto)
+                .collect(Collectors.toList());
+        log.trace("find all users resulted in users {}", users);
+        return ResponseEntity.ok(users);
+    }
+
+    @PostMapping
+    @Transactional
+    @Timed(value = "user.create", description = "Time needed to create a user in the metadata database")
+    @Operation(summary = "Create a user")
+    public ResponseEntity<UserBriefDto> create(SignupRequestDto data) {
+        log.debug("endpoint create a user, data={}", data);
+        final UserBriefDto dto = userMapper.userToUserBriefDto(userService.create(data));
+        log.trace("create user resulted in dto {}", dto);
+        return ResponseEntity.ok(dto);
+    }
+
+}
diff --git a/fda-user-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java b/fda-user-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..b5d0c1658bf704de3856a2321cdf4d2057412c66
--- /dev/null
+++ b/fda-user-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java
@@ -0,0 +1,249 @@
+package at.tuwien.handlers;
+
+import at.tuwien.api.error.ApiErrorDto;
+import at.tuwien.exception.*;
+import net.sf.jsqlparser.JSQLParserException;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.context.request.WebRequest;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
+
+@ControllerAdvice
+public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
+
+    @ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
+    @ExceptionHandler(AmqpException.class)
+    public ResponseEntity<ApiErrorDto> handle(AmqpException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.NOT_ACCEPTABLE)
+                .message(e.getLocalizedMessage())
+                .code("error.query.amqp")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.EXPECTATION_FAILED)
+    @ExceptionHandler(ColumnParseException.class)
+    public ResponseEntity<ApiErrorDto> handle(ColumnParseException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.EXPECTATION_FAILED)
+                .message(e.getLocalizedMessage())
+                .code("error.query.columnparse")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.NOT_FOUND)
+    @ExceptionHandler(ContainerNotFoundException.class)
+    public ResponseEntity<ApiErrorDto> handle(ContainerNotFoundException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.NOT_FOUND)
+                .message(e.getLocalizedMessage())
+                .code("error.query.containernotfound")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
+    @ExceptionHandler(DatabaseConnectionException.class)
+    public ResponseEntity<ApiErrorDto> handle(DatabaseConnectionException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.METHOD_NOT_ALLOWED)
+                .message(e.getLocalizedMessage())
+                .code("error.query.databaseconnection")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.NOT_FOUND)
+    @ExceptionHandler(DatabaseNotFoundException.class)
+    public ResponseEntity<ApiErrorDto> handle(DatabaseNotFoundException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.NOT_FOUND)
+                .message(e.getLocalizedMessage())
+                .code("error.query.databasenotfound")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    @ExceptionHandler(FileStorageException.class)
+    public ResponseEntity<ApiErrorDto> handle(FileStorageException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.BAD_REQUEST)
+                .message(e.getLocalizedMessage())
+                .code("error.query.filestore")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.NOT_FOUND)
+    @ExceptionHandler(IdentifierNotFoundException.class)
+    public ResponseEntity<ApiErrorDto> handle(IdentifierNotFoundException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.NOT_FOUND)
+                .message(e.getLocalizedMessage())
+                .code("error.query.identifiernotfound")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.CONFLICT)
+    @ExceptionHandler(ImageNotSupportedException.class)
+    public ResponseEntity<ApiErrorDto> handle(ImageNotSupportedException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.CONFLICT)
+                .message(e.getLocalizedMessage())
+                .code("error.query.imagenotsupported")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
+    @ExceptionHandler(NotAllowedException.class)
+    public ResponseEntity<ApiErrorDto> handle(NotAllowedException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.METHOD_NOT_ALLOWED)
+                .message(e.getLocalizedMessage())
+                .code("error.query.permission")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    @ExceptionHandler(PaginationException.class)
+    public ResponseEntity<ApiErrorDto> handle(PaginationException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.BAD_REQUEST)
+                .message(e.getLocalizedMessage())
+                .code("error.query.pagination")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.CONFLICT)
+    @ExceptionHandler(QueryAlreadyPersistedException.class)
+    public ResponseEntity<ApiErrorDto> handle(QueryAlreadyPersistedException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.CONFLICT)
+                .message(e.getLocalizedMessage())
+                .code("error.query.alreadypersisted")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    @ExceptionHandler(QueryMalformedException.class)
+    public ResponseEntity<ApiErrorDto> handle(QueryMalformedException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.BAD_REQUEST)
+                .message(e.getLocalizedMessage())
+                .code("error.query.malformed")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.NOT_FOUND)
+    @ExceptionHandler(QueryNotFoundException.class)
+    public ResponseEntity<ApiErrorDto> handle(QueryNotFoundException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.NOT_FOUND)
+                .message(e.getLocalizedMessage())
+                .code("error.query.notfound")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.GATEWAY_TIMEOUT)
+    @ExceptionHandler(QueryStoreException.class)
+    public ResponseEntity<ApiErrorDto> handle(QueryStoreException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.GATEWAY_TIMEOUT)
+                .message(e.getLocalizedMessage())
+                .code("error.query.store")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    @ExceptionHandler(SortException.class)
+    public ResponseEntity<ApiErrorDto> handle(SortException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.BAD_REQUEST)
+                .message(e.getLocalizedMessage())
+                .code("error.query.sort")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.LOCKED)
+    @ExceptionHandler(TableMalformedException.class)
+    public ResponseEntity<ApiErrorDto> handle(TableMalformedException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.LOCKED)
+                .message(e.getLocalizedMessage())
+                .code("error.query.tablemalformed")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.NOT_FOUND)
+    @ExceptionHandler(TableNotFoundException.class)
+    public ResponseEntity<ApiErrorDto> handle(TableNotFoundException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.NOT_FOUND)
+                .message(e.getLocalizedMessage())
+                .code("error.query.tablenotfound")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.CONFLICT)
+    @ExceptionHandler(TupleDeleteException.class)
+    public ResponseEntity<ApiErrorDto> handle(TupleDeleteException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.CONFLICT)
+                .message(e.getLocalizedMessage())
+                .code("error.query.tupledelete")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.NOT_FOUND)
+    @ExceptionHandler(UserNotFoundException.class)
+    public ResponseEntity<ApiErrorDto> handle(UserNotFoundException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.NOT_FOUND)
+                .message(e.getLocalizedMessage())
+                .code("error.query.usernotfound")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.LOCKED)
+    @ExceptionHandler(ViewMalformedException.class)
+    public ResponseEntity<ApiErrorDto> handle(ViewMalformedException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.LOCKED)
+                .message(e.getLocalizedMessage())
+                .code("error.query.viewmalformed")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+    @ResponseStatus(HttpStatus.NOT_FOUND)
+    @ExceptionHandler(ViewNotFoundException.class)
+    public ResponseEntity<ApiErrorDto> handle(ViewNotFoundException e, WebRequest request) {
+        final ApiErrorDto response = ApiErrorDto.builder()
+                .status(HttpStatus.NOT_FOUND)
+                .message(e.getLocalizedMessage())
+                .code("error.query.viewnotfound")
+                .build();
+        return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus());
+    }
+
+}
diff --git a/fda-user-service/rest-service/src/main/resources/application-docker.yml b/fda-user-service/rest-service/src/main/resources/application-docker.yml
new file mode 100644
index 0000000000000000000000000000000000000000..084cc82986478d9fd376495e9f52bf2c93e2f42c
--- /dev/null
+++ b/fda-user-service/rest-service/src/main/resources/application-docker.yml
@@ -0,0 +1,40 @@
+app.version: '@project.version@'
+spring:
+  main.banner-mode: off
+  datasource:
+    url: jdbc:mariadb://metadata-db:3306/fda
+    driver-class-name: org.mariadb.jdbc.Driver
+    username: "${METADATA_USERNAME}"
+    password: "${METADATA_PASSWORD}"
+  jpa:
+    show-sql: false
+    database-platform: org.hibernate.dialect.MariaDBDialect
+    hibernate:
+      ddl-auto: validate
+      use-new-id-generator-mappings: false
+    open-in-view: false
+    properties:
+      hibernate:
+        default_schema: fda
+        jdbc:
+          time_zone: UTC
+  application:
+    name: user-service
+  cloud:
+    loadbalancer.ribbon.enabled: false
+management.endpoints.web.exposure.include: health,info,prometheus
+server.port: 9098
+logging:
+  pattern.console: "%d %highlight(%-5level) %msg%n"
+  level:
+    root: warn
+    at.tuwien.: "${LOG_LEVEL}"
+    org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver: debug
+eureka:
+  instance.hostname: user-service
+  client.serviceUrl.defaultZone: http://discovery-service:9090/eureka/
+fda:
+  ready.path: /ready
+  jwt:
+    issuer: "${JWT_ISSUER}"
+    public_key: "${JWT_PUBKEY}"
\ No newline at end of file
diff --git a/fda-user-service/rest-service/src/main/resources/application-local.yml b/fda-user-service/rest-service/src/main/resources/application-local.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4b86aa03a823e192a3525861c367164eee7a000c
--- /dev/null
+++ b/fda-user-service/rest-service/src/main/resources/application-local.yml
@@ -0,0 +1,40 @@
+app.version: '@project.version@'
+spring:
+  main.banner-mode: off
+  datasource:
+    url: jdbc:mariadb://localhost:3306/fda
+    driver-class-name: org.mariadb.jdbc.Driver
+    username: root
+    password: dbrepo
+  jpa:
+    show-sql: false
+    database-platform: org.hibernate.dialect.MariaDBDialect
+    hibernate:
+      ddl-auto: validate
+      use-new-id-generator-mappings: false
+    open-in-view: false
+    properties:
+      hibernate:
+        default_schema: fda
+        jdbc:
+          time_zone: UTC
+  application:
+    name: user-service
+  cloud:
+    loadbalancer.ribbon.enabled: false
+management.endpoints.web.exposure.include: health,info,prometheus
+server.port: 9098
+logging:
+  pattern.console: "%d %highlight(%-5level) %msg%n"
+  level:
+    root: warn
+    at.tuwien.: trace
+    org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver: debug
+eureka:
+  instance.hostname: user-service
+  client.serviceUrl.defaultZone: http://discovery-service:9090/eureka/
+fda:
+  ready.path: /ready
+  jwt:
+    issuer: http://localhost:8080/realms/dbrepo
+    public_key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB
\ No newline at end of file
diff --git a/fda-user-service/rest-service/src/main/resources/application.yml b/fda-user-service/rest-service/src/main/resources/application.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9ba4e1547a5795546ef952e72819193e32d670c3
--- /dev/null
+++ b/fda-user-service/rest-service/src/main/resources/application.yml
@@ -0,0 +1,40 @@
+app.version: '@project.version@'
+spring:
+  main.banner-mode: off
+  datasource:
+    url: "jdbc:mariadb://metadata-db:3306/${METADATA_DB}"
+    driver-class-name: org.mariadb.jdbc.Driver
+    username: "${METADATA_USERNAME}"
+    password: "${METADATA_PASSWORD}"
+  jpa:
+    show-sql: false
+    database-platform: org.hibernate.dialect.MariaDBDialect
+    hibernate:
+      ddl-auto: validate
+      use-new-id-generator-mappings: false
+    open-in-view: false
+    properties:
+      hibernate:
+        default_schema: "${METADATA_DB}"
+        jdbc:
+          time_zone: UTC
+  application:
+    name: user-service
+  cloud:
+    loadbalancer.ribbon.enabled: false
+management.endpoints.web.exposure.include: health,info,prometheus
+server.port: 9098
+logging:
+  pattern.console: "%d %highlight(%-5level) %msg%n"
+  level:
+    root: warn
+    at.tuwien.: "${LOG_LEVEL}"
+    org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver: debug
+eureka:
+  instance.hostname: user-service
+  client.serviceUrl.defaultZone: http://discovery-service:9090/eureka/
+fda:
+  ready.path: /ready
+  jwt:
+    issuer: "${JWT_ISSUER}"
+    public_key: "${JWT_PUBKEY}"
diff --git a/fda-user-service/rest-service/src/main/resources/config.properties b/fda-user-service/rest-service/src/main/resources/config.properties
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/fda-user-service/rest-service/src/main/resources/mariadb_hibernate.cfg.xml b/fda-user-service/rest-service/src/main/resources/mariadb_hibernate.cfg.xml
new file mode 100644
index 0000000000000000000000000000000000000000..01f90448caca030263dade2fe9e4d07c20b938f0
--- /dev/null
+++ b/fda-user-service/rest-service/src/main/resources/mariadb_hibernate.cfg.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE hibernate-configuration PUBLIC
+        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
+        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
+<hibernate-configuration>
+    <session-factory>
+        <property name="current_session_context_class">thread</property>
+        <property name="transaction.coordinator_class">jdbc</property>
+        <property name="c3p0.min_size">1</property>
+        <property name="c3p0.max_size">30</property>
+        <property name="c3p0.acquire_increment">1</property>
+        <property name="c3p0.timeout">1800</property>
+        <property name="show_sql">true</property>
+        <property name="format_sql">true</property>
+        <property name="hbm2ddl.auto">update</property>
+        <mapping class="at.tuwien.querystore.Column" />
+        <mapping class="at.tuwien.querystore.Query" />
+        <mapping class="at.tuwien.querystore.Table" />
+    </session-factory>
+</hibernate-configuration>
diff --git a/fda-user-service/rest-service/src/test/resources/application.properties b/fda-user-service/rest-service/src/test/resources/application.properties
new file mode 100644
index 0000000000000000000000000000000000000000..4eea490a9199d7e0155b1f33cf3500a5e1f2863f
--- /dev/null
+++ b/fda-user-service/rest-service/src/test/resources/application.properties
@@ -0,0 +1,32 @@
+# enable local spring profile
+spring.profiles.active=local
+
+# disable discovery
+spring.cloud.discovery.enabled=false
+
+# disable cloud config and config discovery
+spring.cloud.config.discovery.enabled=false
+spring.cloud.config.enabled=false
+
+# internal datasource
+spring.datasource.url=jdbc:h2:mem:testdb;DATABASE_TO_UPPER=false;DB_CLOSE_ON_EXIT=FALSE;INIT=RUNSCRIPT FROM './src/test/resources/schema.sql'
+spring.datasource.driverClassName=org.h2.Driver
+spring.datasource.username=sa
+spring.datasource.password=password
+spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
+spring.jpa.hibernate.ddl-auto=create-drop
+spring.jpa.show-sql=false
+
+# additional logging
+logging.level.root=error
+logging.level.at.tuwien.=trace
+
+# broker service
+spring.rabbitmq.host=dbrepo-broker-service
+spring.rabbitmq.username=guest
+spring.rabbitmq.password=guest
+
+# search service
+fda.consumers=2
+fda.gateway.endpoint: http://localhost:15672
+fda.elastic.endpoint=dbrepo-search-service:9200
\ No newline at end of file
diff --git a/fda-user-service/rest-service/src/test/resources/schema.sql b/fda-user-service/rest-service/src/test/resources/schema.sql
new file mode 100644
index 0000000000000000000000000000000000000000..906d8df808fa8f79c1f7c1c26088c55da6c9ee9b
--- /dev/null
+++ b/fda-user-service/rest-service/src/test/resources/schema.sql
@@ -0,0 +1,25 @@
+CREATE SCHEMA IF NOT EXISTS `fda`;
+SET SCHEMA `fda`;
+DROP TABLE IF EXISTS fda.mdb_concepts;
+CREATE TABLE IF NOT EXISTS fda.mdb_concepts
+(
+    uri        VARCHAR(500) not null,
+    name       VARCHAR(255),
+    created    timestamp    NOT NULL DEFAULT NOW(),
+    created_by bigint,
+    PRIMARY KEY (uri)
+);
+DROP TABLE IF EXISTS fda.mdb_units;
+CREATE TABLE IF NOT EXISTS fda.mdb_units
+(
+    uri        VARCHAR(500) not null,
+    name       VARCHAR(255),
+    created    timestamp    NOT NULL DEFAULT NOW(),
+    created_by bigint,
+    PRIMARY KEY (uri)
+);
+-- Modified for H2
+-- Assume id=1 is invalid
+-- Assume id=2 is still valid token
+-- CREATE VIEW IF NOT EXISTS fda.mdb_invalid_tokens AS
+-- (SELECT `id`, `token_hash`, `creator`, `created`, `expires`, `last_used` FROM fda.`mdb_tokens` WHERE `id` = 1);
\ No newline at end of file
diff --git a/fda-user-service/rest-service/src/test/resources/sensor/1_querystore.sql b/fda-user-service/rest-service/src/test/resources/sensor/1_querystore.sql
new file mode 100644
index 0000000000000000000000000000000000000000..2762d130a0044c439b41c0215b0d87924bc8f072
--- /dev/null
+++ b/fda-user-service/rest-service/src/test/resources/sensor/1_querystore.sql
@@ -0,0 +1,75 @@
+CREATE SEQUENCE `qs_queries_seq`;
+CREATE TABLE `qs_queries`
+(
+    `id`               bigint       not null primary key default nextval(`qs_queries_seq`),
+    `created`          datetime     not null             default now(),
+    `executed`         datetime     not null             default now(),
+    `created_by`       varchar(255) not null,
+    `query`            text         not null,
+    `query_normalized` text         not null,
+    `is_persisted`     boolean      not null,
+    `query_hash`       varchar(255) not null,
+    `result_hash`      varchar(255),
+    `result_number`    bigint
+);
+DELIMITER $$
+CREATE PROCEDURE hash_table(IN name VARCHAR(255), OUT hash VARCHAR(255))
+BEGIN
+    DECLARE _sql TEXT;
+    SELECT CONCAT('SELECT SHA2(GROUP_CONCAT(CONCAT_WS(\'\',',
+                  GROUP_CONCAT(CONCAT('`', column_name, '`') ORDER BY column_name),
+                  ') SEPARATOR \',\'), 256) AS hash FROM `', name, '` INTO @hash;')
+    FROM `information_schema`.`columns`
+    WHERE `table_schema` = DATABASE()
+      AND `table_name` = name
+    INTO _sql;
+    PREPARE stmt FROM _sql;
+    EXECUTE stmt;
+    DEALLOCATE PREPARE stmt;
+    SET hash = @hash;
+END $$
+DELIMITER $$
+CREATE PROCEDURE store_query(IN query TEXT, IN executed DATETIME, OUT queryId BIGINT)
+BEGIN
+    DECLARE _queryhash varchar(255) DEFAULT SHA2(query, 256);
+    DECLARE _username varchar(255) DEFAULT REGEXP_REPLACE(current_user(), '@.*', '');
+    DECLARE _query TEXT DEFAULT CONCAT('CREATE OR REPLACE TABLE _tmp AS (', query, ')');
+    PREPARE stmt FROM _query; EXECUTE stmt; DEALLOCATE PREPARE stmt; CALL hash_table('_tmp', @hash);
+    SELECT COUNT(*) FROM _tmp INTO @count;
+    IF @hash IS NULL THEN
+        INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`,
+                                  `result_number`, `executed`)
+        SELECT _username, query, query, false, _queryhash, @hash, @count, executed
+        WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL);
+        SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL);
+    ELSE
+        INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`,
+                                  `result_number`, `executed`)
+        SELECT _username, query, query, false, _queryhash, @hash, @count, executed
+        WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash);
+        SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash);
+    END IF;
+END $$
+DELIMITER $$
+CREATE
+    DEFINER = 'root' PROCEDURE _store_query(IN _username VARCHAR(255), IN query TEXT, IN executed DATETIME, OUT queryId BIGINT)
+BEGIN
+    DECLARE _queryhash varchar(255) DEFAULT SHA2(query, 256);
+    DECLARE _query TEXT DEFAULT CONCAT('CREATE OR REPLACE TABLE _tmp AS (', query, ')');
+    PREPARE stmt FROM _query; EXECUTE stmt; DEALLOCATE PREPARE stmt; CALL hash_table('_tmp', @hash);
+    SELECT COUNT(*) FROM _tmp INTO @count;
+    IF @hash IS NULL THEN
+        INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`,
+                                  `result_number`, `executed`)
+        SELECT _username, query, query, false, _queryhash, @hash, @count, executed
+        WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL);
+        SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL);
+    ELSE
+        INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`,
+                                  `result_number`, `executed`)
+        SELECT _username, query, query, false, _queryhash, @hash, @count, executed
+        WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash);
+        SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash);
+    END IF;
+END $$
+DELIMITER ;
\ No newline at end of file
diff --git a/fda-user-service/rest-service/src/test/resources/sensor/2_traffic.sql b/fda-user-service/rest-service/src/test/resources/sensor/2_traffic.sql
new file mode 100644
index 0000000000000000000000000000000000000000..93d293a6fe9ee11cbd662208a85089ac3f9fab99
--- /dev/null
+++ b/fda-user-service/rest-service/src/test/resources/sensor/2_traffic.sql
@@ -0,0 +1,8 @@
+CREATE SEQUENCE seq_sensor
+    START 1;
+
+CREATE TABLE sensor
+(
+    `timestamp` TIMESTAMP NULL,
+    primary key (`timestamp`)
+) with system versioning;
\ No newline at end of file
diff --git a/fda-user-service/rest-service/src/test/resources/traffic/1_querystore.sql b/fda-user-service/rest-service/src/test/resources/traffic/1_querystore.sql
new file mode 100644
index 0000000000000000000000000000000000000000..2762d130a0044c439b41c0215b0d87924bc8f072
--- /dev/null
+++ b/fda-user-service/rest-service/src/test/resources/traffic/1_querystore.sql
@@ -0,0 +1,75 @@
+CREATE SEQUENCE `qs_queries_seq`;
+CREATE TABLE `qs_queries`
+(
+    `id`               bigint       not null primary key default nextval(`qs_queries_seq`),
+    `created`          datetime     not null             default now(),
+    `executed`         datetime     not null             default now(),
+    `created_by`       varchar(255) not null,
+    `query`            text         not null,
+    `query_normalized` text         not null,
+    `is_persisted`     boolean      not null,
+    `query_hash`       varchar(255) not null,
+    `result_hash`      varchar(255),
+    `result_number`    bigint
+);
+DELIMITER $$
+CREATE PROCEDURE hash_table(IN name VARCHAR(255), OUT hash VARCHAR(255))
+BEGIN
+    DECLARE _sql TEXT;
+    SELECT CONCAT('SELECT SHA2(GROUP_CONCAT(CONCAT_WS(\'\',',
+                  GROUP_CONCAT(CONCAT('`', column_name, '`') ORDER BY column_name),
+                  ') SEPARATOR \',\'), 256) AS hash FROM `', name, '` INTO @hash;')
+    FROM `information_schema`.`columns`
+    WHERE `table_schema` = DATABASE()
+      AND `table_name` = name
+    INTO _sql;
+    PREPARE stmt FROM _sql;
+    EXECUTE stmt;
+    DEALLOCATE PREPARE stmt;
+    SET hash = @hash;
+END $$
+DELIMITER $$
+CREATE PROCEDURE store_query(IN query TEXT, IN executed DATETIME, OUT queryId BIGINT)
+BEGIN
+    DECLARE _queryhash varchar(255) DEFAULT SHA2(query, 256);
+    DECLARE _username varchar(255) DEFAULT REGEXP_REPLACE(current_user(), '@.*', '');
+    DECLARE _query TEXT DEFAULT CONCAT('CREATE OR REPLACE TABLE _tmp AS (', query, ')');
+    PREPARE stmt FROM _query; EXECUTE stmt; DEALLOCATE PREPARE stmt; CALL hash_table('_tmp', @hash);
+    SELECT COUNT(*) FROM _tmp INTO @count;
+    IF @hash IS NULL THEN
+        INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`,
+                                  `result_number`, `executed`)
+        SELECT _username, query, query, false, _queryhash, @hash, @count, executed
+        WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL);
+        SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL);
+    ELSE
+        INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`,
+                                  `result_number`, `executed`)
+        SELECT _username, query, query, false, _queryhash, @hash, @count, executed
+        WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash);
+        SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash);
+    END IF;
+END $$
+DELIMITER $$
+CREATE
+    DEFINER = 'root' PROCEDURE _store_query(IN _username VARCHAR(255), IN query TEXT, IN executed DATETIME, OUT queryId BIGINT)
+BEGIN
+    DECLARE _queryhash varchar(255) DEFAULT SHA2(query, 256);
+    DECLARE _query TEXT DEFAULT CONCAT('CREATE OR REPLACE TABLE _tmp AS (', query, ')');
+    PREPARE stmt FROM _query; EXECUTE stmt; DEALLOCATE PREPARE stmt; CALL hash_table('_tmp', @hash);
+    SELECT COUNT(*) FROM _tmp INTO @count;
+    IF @hash IS NULL THEN
+        INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`,
+                                  `result_number`, `executed`)
+        SELECT _username, query, query, false, _queryhash, @hash, @count, executed
+        WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL);
+        SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL);
+    ELSE
+        INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`,
+                                  `result_number`, `executed`)
+        SELECT _username, query, query, false, _queryhash, @hash, @count, executed
+        WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash);
+        SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash);
+    END IF;
+END $$
+DELIMITER ;
\ No newline at end of file
diff --git a/fda-user-service/rest-service/src/test/resources/traffic/2_traffic.sql b/fda-user-service/rest-service/src/test/resources/traffic/2_traffic.sql
new file mode 100644
index 0000000000000000000000000000000000000000..3038fe5a7a99e882504fc8382a5bf2ba7763543d
--- /dev/null
+++ b/fda-user-service/rest-service/src/test/resources/traffic/2_traffic.sql
@@ -0,0 +1,43 @@
+/* https://www.kaggle.com/laa283/zurich-public-transport/version/2 */
+CREATE SEQUENCE seq_traffic
+    START 1;
+
+CREATE TABLE traffic_zurich
+(
+    linie                bigint                                null,
+    richtung             bigint                                null,
+    betriebsdatum        date                                  null,
+    fahrzeug             bigint                                null,
+    kurs                 bigint                                null,
+    seq_von              bigint                                null,
+    halt_diva_von        bigint                                null,
+    halt_punkt_diva_von  bigint                                null,
+    halt_kurz_von1       varchar(255)                          null,
+    datum_von            date                                  null,
+    soll_an_von          bigint                                null,
+    ist_an_von           bigint                                null,
+    soll_ab_von          bigint                                null,
+    ist_ab_von           bigint                                null,
+    seq_nach             bigint                                null,
+    halt_diva_nach       bigint                                null,
+    halt_punkt_diva_nach bigint                                null,
+    halt_kurz_nach1      varchar(255)                          null,
+    datum_nach           DATE                                  null,
+    soll_an_nach         bigint                                null,
+    ist_an_nach1         bigint                                null,
+    soll_ab_nach         bigint                                null,
+    ist_ab_nach          bigint                                null,
+    fahrt_id             bigint                                null,
+    fahrweg_id           bigint                                null,
+    fw_no                bigint                                null,
+    fw_typ               bigint                                null,
+    fw_kurz              bigint                                null,
+    fw_lang              varchar(255)                          null,
+    umlauf_von           varchar(255)                          null,
+    halt_id_von          bigint                                null,
+    halt_id_nach         bigint                                null,
+    halt_punkt_id_von    bigint                                null,
+    halt_punkt_id_nach   bigint                                null,
+    id                   bigint default nextval(`seq_traffic`) not null,
+    primary key (id)
+) with system versioning;
\ No newline at end of file
diff --git a/fda-user-service/rest-service/src/test/resources/weather/1_querystore.sql b/fda-user-service/rest-service/src/test/resources/weather/1_querystore.sql
new file mode 100644
index 0000000000000000000000000000000000000000..2762d130a0044c439b41c0215b0d87924bc8f072
--- /dev/null
+++ b/fda-user-service/rest-service/src/test/resources/weather/1_querystore.sql
@@ -0,0 +1,75 @@
+CREATE SEQUENCE `qs_queries_seq`;
+CREATE TABLE `qs_queries`
+(
+    `id`               bigint       not null primary key default nextval(`qs_queries_seq`),
+    `created`          datetime     not null             default now(),
+    `executed`         datetime     not null             default now(),
+    `created_by`       varchar(255) not null,
+    `query`            text         not null,
+    `query_normalized` text         not null,
+    `is_persisted`     boolean      not null,
+    `query_hash`       varchar(255) not null,
+    `result_hash`      varchar(255),
+    `result_number`    bigint
+);
+DELIMITER $$
+CREATE PROCEDURE hash_table(IN name VARCHAR(255), OUT hash VARCHAR(255))
+BEGIN
+    DECLARE _sql TEXT;
+    SELECT CONCAT('SELECT SHA2(GROUP_CONCAT(CONCAT_WS(\'\',',
+                  GROUP_CONCAT(CONCAT('`', column_name, '`') ORDER BY column_name),
+                  ') SEPARATOR \',\'), 256) AS hash FROM `', name, '` INTO @hash;')
+    FROM `information_schema`.`columns`
+    WHERE `table_schema` = DATABASE()
+      AND `table_name` = name
+    INTO _sql;
+    PREPARE stmt FROM _sql;
+    EXECUTE stmt;
+    DEALLOCATE PREPARE stmt;
+    SET hash = @hash;
+END $$
+DELIMITER $$
+CREATE PROCEDURE store_query(IN query TEXT, IN executed DATETIME, OUT queryId BIGINT)
+BEGIN
+    DECLARE _queryhash varchar(255) DEFAULT SHA2(query, 256);
+    DECLARE _username varchar(255) DEFAULT REGEXP_REPLACE(current_user(), '@.*', '');
+    DECLARE _query TEXT DEFAULT CONCAT('CREATE OR REPLACE TABLE _tmp AS (', query, ')');
+    PREPARE stmt FROM _query; EXECUTE stmt; DEALLOCATE PREPARE stmt; CALL hash_table('_tmp', @hash);
+    SELECT COUNT(*) FROM _tmp INTO @count;
+    IF @hash IS NULL THEN
+        INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`,
+                                  `result_number`, `executed`)
+        SELECT _username, query, query, false, _queryhash, @hash, @count, executed
+        WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL);
+        SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL);
+    ELSE
+        INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`,
+                                  `result_number`, `executed`)
+        SELECT _username, query, query, false, _queryhash, @hash, @count, executed
+        WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash);
+        SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash);
+    END IF;
+END $$
+DELIMITER $$
+CREATE
+    DEFINER = 'root' PROCEDURE _store_query(IN _username VARCHAR(255), IN query TEXT, IN executed DATETIME, OUT queryId BIGINT)
+BEGIN
+    DECLARE _queryhash varchar(255) DEFAULT SHA2(query, 256);
+    DECLARE _query TEXT DEFAULT CONCAT('CREATE OR REPLACE TABLE _tmp AS (', query, ')');
+    PREPARE stmt FROM _query; EXECUTE stmt; DEALLOCATE PREPARE stmt; CALL hash_table('_tmp', @hash);
+    SELECT COUNT(*) FROM _tmp INTO @count;
+    IF @hash IS NULL THEN
+        INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`,
+                                  `result_number`, `executed`)
+        SELECT _username, query, query, false, _queryhash, @hash, @count, executed
+        WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL);
+        SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL);
+    ELSE
+        INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`,
+                                  `result_number`, `executed`)
+        SELECT _username, query, query, false, _queryhash, @hash, @count, executed
+        WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash);
+        SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash);
+    END IF;
+END $$
+DELIMITER ;
\ No newline at end of file
diff --git a/fda-user-service/rest-service/src/test/resources/weather/2_weather.sql b/fda-user-service/rest-service/src/test/resources/weather/2_weather.sql
new file mode 100644
index 0000000000000000000000000000000000000000..29967f2977ef6b6f12da00b0c218382e5c024322
--- /dev/null
+++ b/fda-user-service/rest-service/src/test/resources/weather/2_weather.sql
@@ -0,0 +1,55 @@
+/* https://www.kaggle.com/jsphyg/weather-dataset-rattle-package */
+CREATE TABLE weather_location
+(
+    location VARCHAR(255) PRIMARY KEY,
+    lat      DOUBLE PRECISION NULL,
+    lng      DOUBLE PRECISION NULL
+) WITH SYSTEM VERSIONING;
+
+CREATE TABLE weather_aus
+(
+    id       BIGINT           NOT NULL PRIMARY KEY,
+    `date`   DATE             NOT NULL,
+    location VARCHAR(255)     NULL,
+    mintemp  DOUBLE PRECISION NULL,
+    rainfall DOUBLE PRECISION NULL,
+    FOREIGN KEY (location) REFERENCES weather_location (location),
+    UNIQUE (`date`),
+    CHECK (`mintemp` > 0)
+) WITH SYSTEM VERSIONING;
+
+CREATE TABLE sensor
+(
+    `timestamp` TIMESTAMP NOT NULL,
+    PRIMARY KEY (`timestamp`),
+    UNIQUE (`timestamp`)
+) WITH SYSTEM VERSIONING;
+
+INSERT INTO weather_location (location, lat, lng)
+VALUES ('Albury', -36.0653583, 146.9112214),
+       ('Melbourne', null, null),
+       ('Sydney', -33.847927, 150.6517942);
+
+INSERT INTO weather_aus (id, `date`, location, mintemp, rainfall)
+VALUES (1, '2008-12-01', 'Albury', 13.4, 0.6),
+       (2, '2008-12-02', 'Albury', 7.4, 0),
+       (3, '2008-12-03', 'Albury', 12.9, 0);
+
+########################################################################################################################
+## TEST CASE PRE-REQUISITE                                                                                            ##
+########################################################################################################################
+
+CREATE VIEW junit2 AS
+(
+SELECT `location`, `lat`, `lng`
+FROM `weather_location`
+WHERE `location` = 'Albury');
+
+CREATE VIEW `hs_weather_aus` AS
+SELECT *
+FROM (SELECT `id`, ROW_START AS inserted_at, IF(ROW_END > NOW(), NULL, ROW_END) AS deleted_at, COUNT(*) as total
+      FROM `weather_aus` FOR SYSTEM_TIME ALL
+      GROUP BY inserted_at, deleted_at
+      ORDER BY deleted_at DESC
+      LIMIT 50) AS v
+ORDER BY v.inserted_at, v.deleted_at ASC;
diff --git a/fda-user-service/rest-service/src/test/resources/weather/location.csv b/fda-user-service/rest-service/src/test/resources/weather/location.csv
new file mode 100644
index 0000000000000000000000000000000000000000..b9410c65c9b4169eb1a231dbe5ca04ff20c116cf
--- /dev/null
+++ b/fda-user-service/rest-service/src/test/resources/weather/location.csv
@@ -0,0 +1,2 @@
+Albury,-36.0653583,146.9112214
+Sydney,-33.847927,150.6517942
\ No newline at end of file
diff --git a/fda-user-service/rest-service/src/test/resources/zoo/1_querystore.sql b/fda-user-service/rest-service/src/test/resources/zoo/1_querystore.sql
new file mode 100644
index 0000000000000000000000000000000000000000..2762d130a0044c439b41c0215b0d87924bc8f072
--- /dev/null
+++ b/fda-user-service/rest-service/src/test/resources/zoo/1_querystore.sql
@@ -0,0 +1,75 @@
+CREATE SEQUENCE `qs_queries_seq`;
+CREATE TABLE `qs_queries`
+(
+    `id`               bigint       not null primary key default nextval(`qs_queries_seq`),
+    `created`          datetime     not null             default now(),
+    `executed`         datetime     not null             default now(),
+    `created_by`       varchar(255) not null,
+    `query`            text         not null,
+    `query_normalized` text         not null,
+    `is_persisted`     boolean      not null,
+    `query_hash`       varchar(255) not null,
+    `result_hash`      varchar(255),
+    `result_number`    bigint
+);
+DELIMITER $$
+CREATE PROCEDURE hash_table(IN name VARCHAR(255), OUT hash VARCHAR(255))
+BEGIN
+    DECLARE _sql TEXT;
+    SELECT CONCAT('SELECT SHA2(GROUP_CONCAT(CONCAT_WS(\'\',',
+                  GROUP_CONCAT(CONCAT('`', column_name, '`') ORDER BY column_name),
+                  ') SEPARATOR \',\'), 256) AS hash FROM `', name, '` INTO @hash;')
+    FROM `information_schema`.`columns`
+    WHERE `table_schema` = DATABASE()
+      AND `table_name` = name
+    INTO _sql;
+    PREPARE stmt FROM _sql;
+    EXECUTE stmt;
+    DEALLOCATE PREPARE stmt;
+    SET hash = @hash;
+END $$
+DELIMITER $$
+CREATE PROCEDURE store_query(IN query TEXT, IN executed DATETIME, OUT queryId BIGINT)
+BEGIN
+    DECLARE _queryhash varchar(255) DEFAULT SHA2(query, 256);
+    DECLARE _username varchar(255) DEFAULT REGEXP_REPLACE(current_user(), '@.*', '');
+    DECLARE _query TEXT DEFAULT CONCAT('CREATE OR REPLACE TABLE _tmp AS (', query, ')');
+    PREPARE stmt FROM _query; EXECUTE stmt; DEALLOCATE PREPARE stmt; CALL hash_table('_tmp', @hash);
+    SELECT COUNT(*) FROM _tmp INTO @count;
+    IF @hash IS NULL THEN
+        INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`,
+                                  `result_number`, `executed`)
+        SELECT _username, query, query, false, _queryhash, @hash, @count, executed
+        WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL);
+        SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL);
+    ELSE
+        INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`,
+                                  `result_number`, `executed`)
+        SELECT _username, query, query, false, _queryhash, @hash, @count, executed
+        WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash);
+        SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash);
+    END IF;
+END $$
+DELIMITER $$
+CREATE
+    DEFINER = 'root' PROCEDURE _store_query(IN _username VARCHAR(255), IN query TEXT, IN executed DATETIME, OUT queryId BIGINT)
+BEGIN
+    DECLARE _queryhash varchar(255) DEFAULT SHA2(query, 256);
+    DECLARE _query TEXT DEFAULT CONCAT('CREATE OR REPLACE TABLE _tmp AS (', query, ')');
+    PREPARE stmt FROM _query; EXECUTE stmt; DEALLOCATE PREPARE stmt; CALL hash_table('_tmp', @hash);
+    SELECT COUNT(*) FROM _tmp INTO @count;
+    IF @hash IS NULL THEN
+        INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`,
+                                  `result_number`, `executed`)
+        SELECT _username, query, query, false, _queryhash, @hash, @count, executed
+        WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL);
+        SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` IS NULL);
+    ELSE
+        INSERT INTO `qs_queries` (`created_by`, `query`, `query_normalized`, `is_persisted`, `query_hash`, `result_hash`,
+                                  `result_number`, `executed`)
+        SELECT _username, query, query, false, _queryhash, @hash, @count, executed
+        WHERE NOT EXISTS (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash);
+        SET queryId = (SELECT `id` FROM `qs_queries` WHERE `query_hash` = _queryhash AND `result_hash` = @hash);
+    END IF;
+END $$
+DELIMITER ;
\ No newline at end of file
diff --git a/fda-user-service/rest-service/src/test/resources/zoo/2_zoo.sql b/fda-user-service/rest-service/src/test/resources/zoo/2_zoo.sql
new file mode 100644
index 0000000000000000000000000000000000000000..50f02ee2c53f5afc8a472b1c418225434b45d98e
--- /dev/null
+++ b/fda-user-service/rest-service/src/test/resources/zoo/2_zoo.sql
@@ -0,0 +1,191 @@
+create sequence seq_zoo_id;
+create sequence seq_names_id;
+create table zoo
+(
+    id          bigint       not null default nextval(`seq_zoo_id`),
+    animal_name varchar(255) null,
+    hair        tinyint(1)   null,
+    feathers    tinyint(1)   null,
+    eggs        tinyint(1)   null,
+    milk        tinyint(1)   null,
+    airborne    tinyint(1)   null,
+    aquatic     tinyint(1)   null,
+    predator    tinyint(1)   null,
+    toothed     tinyint(1)   null,
+    backbone    tinyint(1)   null,
+    breathes    tinyint(1)   null,
+    venomous    tinyint(1)   null,
+    fins        tinyint(1)   null,
+    legs        bigint       null,
+    tail        tinyint(1)   null,
+    domestic    tinyint(1)   null,
+    catsize     tinyint(1)   null,
+    class_type  bigint       null,
+    primary key (id)
+) with system versioning;
+
+create table names
+(
+    id        bigint not null default nextval(`seq_names_id`),
+    firstname varchar(255),
+    lastname  varchar(255),
+    primary key (id),
+    unique key (firstname, lastname)
+) with system versioning;
+
+create table likes
+(
+    name_id bigint not null,
+    zoo_id  bigint not null,
+    primary key (name_id, zoo_id),
+    foreign key (name_id) references names (id),
+    foreign key (zoo_id) references zoo (id)
+) with system versioning;
+
+INSERT INTO zoo (id, animal_name, hair, feathers, eggs, milk, airborne, aquatic, predator, toothed, backbone, breathes,
+                 venomous, fins, legs, tail, domestic, catsize, class_type)
+VALUES (1, 'aardvark', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 0, 0, 1, 1),
+       (2, 'antelope', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1),
+       (3, 'bass', 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 4),
+       (4, 'bear', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 0, 0, 1, 1),
+       (5, 'boar', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1),
+       (6, 'buffalo', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1),
+       (7, 'calf', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 1, 1, 1),
+       (8, 'carp', 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 4),
+       (9, 'catfish', 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 4),
+       (10, 'cavy', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 0, 1, 0, 1),
+       (11, 'cheetah', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1),
+       (12, 'chicken', 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 2, 1, 1, 0, 2),
+       (13, 'chub', 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 4),
+       (14, 'clam', 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7),
+       (15, 'crab', 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 7),
+       (16, 'crayfish', 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 6, 0, 0, 0, 7),
+       (17, 'crow', 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2),
+       (18, 'deer', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1),
+       (19, 'dogfish', 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 4),
+       (20, 'dolphin', 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1),
+       (21, 'dove', 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 2, 1, 1, 0, 2),
+       (22, 'duck', 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2),
+       (23, 'elephant', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1),
+       (24, 'flamingo', 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 2, 1, 0, 1, 2),
+       (25, 'flea', 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 6, 0, 0, 0, 6),
+       (26, 'frog', 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 4, 0, 0, 0, 5),
+       (27, 'frog', 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 4, 0, 0, 0, 5),
+       (28, 'fruitbat', 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 2, 1, 0, 0, 1),
+       (29, 'giraffe', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1),
+       (30, 'girl', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 2, 0, 1, 1, 1),
+       (31, 'gnat', 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 6, 0, 0, 0, 6),
+       (32, 'goat', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 1, 1, 1),
+       (33, 'gorilla', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 2, 0, 0, 1, 1),
+       (34, 'gull', 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2),
+       (35, 'haddock', 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 4),
+       (36, 'hamster', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 1, 0, 1),
+       (37, 'hare', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 0, 0, 1),
+       (38, 'hawk', 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2),
+       (39, 'herring', 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 4),
+       (40, 'honeybee', 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 6, 0, 1, 0, 6),
+       (41, 'housefly', 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 6, 0, 0, 0, 6),
+       (42, 'kiwi', 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2),
+       (43, 'ladybird', 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 6, 0, 0, 0, 6),
+       (44, 'lark', 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2),
+       (45, 'leopard', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1),
+       (46, 'lion', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1),
+       (47, 'lobster', 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 6, 0, 0, 0, 7),
+       (48, 'lynx', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1),
+       (49, 'mink', 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1),
+       (50, 'mole', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 0, 1),
+       (51, 'mongoose', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1),
+       (52, 'moth', 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 6, 0, 0, 0, 6),
+       (53, 'newt', 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 4, 1, 0, 0, 5),
+       (54, 'octopus', 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 8, 0, 0, 1, 7),
+       (55, 'opossum', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 0, 1),
+       (56, 'oryx', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1),
+       (57, 'ostrich', 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 2, 1, 0, 1, 2),
+       (58, 'parakeet', 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 2, 1, 1, 0, 2),
+       (59, 'penguin', 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 2, 1, 0, 1, 2),
+       (60, 'pheasant', 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2),
+       (61, 'pike', 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 4),
+       (62, 'piranha', 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 4),
+       (63, 'pitviper', 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 3),
+       (64, 'platypus', 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 4, 1, 0, 1, 1),
+       (65, 'polecat', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1),
+       (66, 'pony', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 1, 1, 1),
+       (67, 'porpoise', 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1),
+       (68, 'puma', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1),
+       (69, 'pussycat', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 1, 1, 1),
+       (70, 'raccoon', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1),
+       (71, 'reindeer', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 1, 1, 1),
+       (72, 'rhea', 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 2, 1, 0, 1, 2),
+       (73, 'scorpion', 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 8, 1, 0, 0, 7),
+       (74, 'seahorse', 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 4),
+       (75, 'seal', 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1),
+       (76, 'sealion', 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 2, 1, 0, 1, 1),
+       (77, 'seasnake', 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 3),
+       (78, 'seawasp', 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 7),
+       (79, 'skimmer', 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2),
+       (80, 'skua', 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2),
+       (81, 'slowworm', 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 3),
+       (82, 'slug', 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 7),
+       (83, 'sole', 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 4),
+       (84, 'sparrow', 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2),
+       (85, 'squirrel', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 2, 1, 0, 0, 1),
+       (86, 'starfish', 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 5, 0, 0, 0, 7),
+       (87, 'stingray', 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 4),
+       (88, 'swan', 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 2, 1, 0, 1, 2),
+       (89, 'termite', 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 6, 0, 0, 0, 6),
+       (90, 'toad', 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 4, 0, 0, 0, 5),
+       (91, 'tortoise', 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 4, 1, 0, 1, 3),
+       (92, 'tuatara', 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 0, 3),
+       (93, 'tuna', 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 4),
+       (94, 'vampire', 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 2, 1, 0, 0, 1),
+       (95, 'vole', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 4, 1, 0, 0, 1),
+       (96, 'vulture', 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 2, 1, 0, 1, 2),
+       (97, 'wallaby', 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 2, 1, 0, 1, 1),
+       (98, 'wasp', 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 6, 0, 0, 0, 6),
+       (99, 'wolf', 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 4, 1, 0, 1, 1),
+       (100, 'worm', 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 7),
+       (101, 'wren', 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2);
+
+INSERT INTO names (firstname, lastname)
+VALUES ('Moritz', 'Staudinger'),
+       ('Martin', 'Weise'),
+       ('Eva', 'Gergely'),
+       ('Cornelia', 'Michlits'),
+       ('Kirill', 'Stytsenko');
+
+INSERT INTO likes (name_id, zoo_id)
+VALUES (1, 5),
+       (1, 10),
+       (2, 3),
+       (2, 80),
+       (3, 4),
+       (4, 4),
+       (5, 100);
+
+########################################################################################################################
+## TEST CASE PRE-REQUISITE                                                                                            ##
+########################################################################################################################
+
+CREATE VIEW mock_view AS
+(
+SELECT `id`,
+       `animal_name`,
+       `hair`,
+       `feathers`,
+       `eggs`,
+       `milk`,
+       `airborne`,
+       `aquatic`,
+       `predator`,
+       `toothed`,
+       `backbone`,
+       `breathes`,
+       `venomous`,
+       `fins`,
+       `legs`,
+       `tail`,
+       `domestic`,
+       `catsize`,
+       `class_type`
+FROM `zoo`
+WHERE `class_type` = 1);
diff --git a/fda-user-service/service_ready b/fda-user-service/service_ready
new file mode 100644
index 0000000000000000000000000000000000000000..b2e4f9df6804f249ba8aadd72f742929072badaa
--- /dev/null
+++ b/fda-user-service/service_ready
@@ -0,0 +1,6 @@
+#!/bin/bash
+if [ -f /ready ]; then
+  echo "service is ready and accepting connections"
+  exit 0
+fi
+exit 1
\ No newline at end of file
diff --git a/fda-user-service/services/pom.xml b/fda-user-service/services/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a42439c741b47488ded1aaa259a3c362cecc87eb
--- /dev/null
+++ b/fda-user-service/services/pom.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <artifactId>fda-user-service</artifactId>
+        <groupId>at.tuwien</groupId>
+        <version>1.1.0-alpha</version>
+    </parent>
+
+    <artifactId>services</artifactId>
+    <version>1.1.0-alpha</version>
+    <name>fda-user-service-services</name>
+
+</project>
diff --git a/fda-user-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java b/fda-user-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..b2b01c42ee9868215962426b18b3a70450bff7ac
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java
@@ -0,0 +1,100 @@
+package at.tuwien.auth;
+
+import at.tuwien.api.auth.RealmAccessDto;
+import at.tuwien.api.user.UserDetailsDto;
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.JWTVerifier;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.stream.Collectors;
+
+@Slf4j
+public class AuthTokenFilter extends OncePerRequestFilter {
+
+    @Value("${fda.jwt.issuer}")
+    private String issuer;
+
+    @Value("${fda.jwt.public_key}")
+    private String publicKey;
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+            throws ServletException, IOException {
+        final String jwt = parseJwt(request);
+        if (jwt != null) {
+            final UserDetails userDetails = verifyJwt(jwt);
+            log.debug("authenticated user {}", userDetails);
+            final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
+                    userDetails, null, userDetails.getAuthorities());
+            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+
+            SecurityContextHolder.getContext().setAuthentication(authentication);
+        }
+        filterChain.doFilter(request, response);
+    }
+
+    public UserDetails verifyJwt(String token) throws ServletException {
+        final KeyFactory kf;
+        try {
+            kf = KeyFactory.getInstance("RSA");
+        } catch (NoSuchAlgorithmException e) {
+            log.error("Failed to find RSA algorithm");
+            throw new ServletException("Failed to find RSA algorithm", e);
+        }
+        final X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey));
+        final RSAPublicKey pubKey;
+        try {
+            pubKey = (RSAPublicKey) kf.generatePublic(keySpecX509);
+        } catch (InvalidKeySpecException e) {
+            log.error("Provided public key is invalid");
+            throw new ServletException("Provided public key is invalid", e);
+        }
+        final Algorithm algorithm = Algorithm.RSA256(pubKey, null);
+        JWTVerifier verifier = JWT.require(algorithm)
+                .withIssuer(issuer)
+                .withAudience("spring")
+                .build();
+        final DecodedJWT jwt = verifier.verify(token);
+        final RealmAccessDto realmAccess = jwt.getClaim("realm_access").as(RealmAccessDto.class);
+        return UserDetailsDto.builder()
+                .username(jwt.getClaim("client_id").asString())
+                .authorities(Arrays.stream(realmAccess.getRoles()).map(SimpleGrantedAuthority::new).collect(Collectors.toList()))
+                .build();
+    }
+
+    /**
+     * Parses the token from the HTTP header of the request
+     *
+     * @param request The request.
+     * @return The token.
+     */
+    public String parseJwt(HttpServletRequest request) {
+        String headerAuth = request.getHeader("Authorization");
+        if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
+            return headerAuth.substring(7, headerAuth.length());
+        }
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/fda-user-service/services/src/main/java/at/tuwien/config/JacksonConfig.java b/fda-user-service/services/src/main/java/at/tuwien/config/JacksonConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..fba7f99cf2bf1cbb12ac51cd6fd5b80751ff43c1
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/config/JacksonConfig.java
@@ -0,0 +1,31 @@
+package at.tuwien.config;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.Date;
+import java.util.TimeZone;
+
+@Slf4j
+@Configuration
+public class JacksonConfig {
+
+    @Bean
+    public ObjectMapper objectMapper() throws JsonProcessingException {
+        final ObjectMapper objectMapper = new ObjectMapper();
+        objectMapper.findAndRegisterModules();
+        objectMapper.registerModule(new Jdk8Module());
+        objectMapper.registerModule(new JavaTimeModule());
+        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
+        objectMapper.setTimeZone(TimeZone.getTimeZone("UTC"));
+        log.debug("current time is {}", objectMapper.writeValueAsString(new Date()));
+        return objectMapper;
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/config/ReadyConfig.java b/fda-user-service/services/src/main/java/at/tuwien/config/ReadyConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..2250fa50884df3f47b0b063975aea74f06203f80
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/config/ReadyConfig.java
@@ -0,0 +1,25 @@
+package at.tuwien.config;
+
+import com.google.common.io.Files;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.event.EventListener;
+
+import java.io.File;
+import java.io.IOException;
+
+@Log4j2
+@Configuration
+public class ReadyConfig {
+
+    @Value("${fda.ready.path}")
+    private String readyPath;
+
+    @EventListener(ApplicationReadyEvent.class)
+    public void init() throws IOException {
+        Files.touch(new File(readyPath));
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/fda-user-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..0a2af8f294a81b4e9e133ec7a4ef91c412ca4d17
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java
@@ -0,0 +1,87 @@
+package at.tuwien.config;
+
+import at.tuwien.auth.AuthTokenFilter;
+import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
+import io.swagger.v3.oas.annotations.security.SecurityScheme;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+
+import javax.servlet.http.HttpServletResponse;
+
+@Configuration
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+@SecurityScheme(
+        name = "bearerAuth",
+        type = SecuritySchemeType.HTTP,
+        bearerFormat = "JWT",
+        scheme = "bearer"
+)
+public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
+
+    @Bean
+    public AuthTokenFilter authTokenFilter() {
+        return new AuthTokenFilter();
+    }
+
+    @Override
+    protected void configure(HttpSecurity http) throws Exception {
+        /* enable CORS and disable CSRF */
+        http = http.cors().and().csrf().disable();
+        /* set session management to stateless */
+        http = http
+                .sessionManagement()
+                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+                .and();
+        /* set unauthorized requests exception handler */
+        http = http
+                .exceptionHandling()
+                .authenticationEntryPoint(
+                        (request, response, ex) -> {
+                            response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
+                                    ex.getMessage()
+                            );
+                        }
+                ).and();
+        /* set permissions on endpoints */
+        http.authorizeRequests()
+                /* our internal endpoints */
+                .antMatchers(HttpMethod.GET, "/actuator/prometheus/**").permitAll()
+                /* our public endpoints */
+                .antMatchers(HttpMethod.GET, "/api/user/**").permitAll()
+                .antMatchers(HttpMethod.POST, "/api/user/**").permitAll()
+                .antMatchers("/v3/api-docs.yaml",
+                        "/v3/api-docs/**",
+                        "/swagger-ui/**",
+                        "/swagger-ui.html").permitAll()
+                /* our private endpoints */
+                .anyRequest().authenticated();
+        /* add JWT token filter */
+        http.addFilterBefore(authTokenFilter(),
+                UsernamePasswordAuthenticationFilter.class
+        );
+    }
+
+    @Bean
+    public CorsFilter corsFilter() {
+        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        final CorsConfiguration config = new CorsConfiguration();
+        config.setAllowCredentials(true);
+        config.addAllowedOrigin("*");
+        config.addAllowedHeader("*");
+        config.addAllowedMethod("*");
+        source.registerCorsConfiguration("/**", config);
+        return new CorsFilter(source);
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/AmqpException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/AmqpException.java
new file mode 100644
index 0000000000000000000000000000000000000000..6af0750d6f5089a8442a7159eac2076462df2825
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/AmqpException.java
@@ -0,0 +1,21 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.NOT_ACCEPTABLE)
+public class AmqpException extends Exception {
+
+    public AmqpException(String msg) {
+        super(msg);
+    }
+
+    public AmqpException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public AmqpException(Throwable thr) {
+        super(thr);
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/ColumnParseException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/ColumnParseException.java
new file mode 100644
index 0000000000000000000000000000000000000000..c0c1e109de740d33646d7b91bb1a68ebabdc7616
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/ColumnParseException.java
@@ -0,0 +1,21 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.EXPECTATION_FAILED)
+public class ColumnParseException extends Exception {
+
+    public ColumnParseException(String msg) {
+        super(msg);
+    }
+
+    public ColumnParseException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public ColumnParseException(Throwable thr) {
+        super(thr);
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/ContainerNotFoundException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/ContainerNotFoundException.java
new file mode 100644
index 0000000000000000000000000000000000000000..85d49d4cb34b20b15fdc1441c69cbab0fe0a34f3
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/ContainerNotFoundException.java
@@ -0,0 +1,21 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.NOT_FOUND)
+public class ContainerNotFoundException extends Exception {
+
+    public ContainerNotFoundException(String msg) {
+        super(msg);
+    }
+
+    public ContainerNotFoundException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public ContainerNotFoundException(Throwable thr) {
+        super(thr);
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/DatabaseConnectionException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/DatabaseConnectionException.java
new file mode 100644
index 0000000000000000000000000000000000000000..3c1f797647d67b4e1cb8148a4aba11b7d06aefc0
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/DatabaseConnectionException.java
@@ -0,0 +1,23 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+import java.io.IOException;
+
+@ResponseStatus(code = HttpStatus.METHOD_NOT_ALLOWED)
+public class DatabaseConnectionException extends Exception {
+
+    public DatabaseConnectionException(String msg) {
+        super(msg);
+    }
+
+    public DatabaseConnectionException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public DatabaseConnectionException(Throwable thr) {
+        super(thr);
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/DatabaseNotFoundException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/DatabaseNotFoundException.java
new file mode 100644
index 0000000000000000000000000000000000000000..b9ca79c783048e8d0298db273abdb9462efeeec8
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/DatabaseNotFoundException.java
@@ -0,0 +1,23 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+import java.io.IOException;
+
+@ResponseStatus(code = HttpStatus.NOT_FOUND)
+public class DatabaseNotFoundException extends Exception {
+
+    public DatabaseNotFoundException(String msg) {
+        super(msg);
+    }
+
+    public DatabaseNotFoundException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public DatabaseNotFoundException(Throwable thr) {
+        super(thr);
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/FileStorageException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/FileStorageException.java
new file mode 100644
index 0000000000000000000000000000000000000000..ab068e4245526e77b611d1c8571df867d5fc2cb6
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/FileStorageException.java
@@ -0,0 +1,20 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.BAD_REQUEST)
+public class FileStorageException extends Exception {
+
+    public FileStorageException(String msg) {
+        super(msg);
+    }
+
+    public FileStorageException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public FileStorageException(Throwable thr) {
+        super(thr);
+    }
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/IdentifierNotFoundException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/IdentifierNotFoundException.java
new file mode 100644
index 0000000000000000000000000000000000000000..f0bb71f36492511efbe2c8c959dcdb97c679702f
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/IdentifierNotFoundException.java
@@ -0,0 +1,21 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.NOT_FOUND)
+public class IdentifierNotFoundException extends Exception {
+
+    public IdentifierNotFoundException(String msg) {
+        super(msg);
+    }
+
+    public IdentifierNotFoundException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public IdentifierNotFoundException(Throwable thr) {
+        super(thr);
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/ImageNotSupportedException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/ImageNotSupportedException.java
new file mode 100644
index 0000000000000000000000000000000000000000..70963128f8410f856029916c606efe152957f12a
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/ImageNotSupportedException.java
@@ -0,0 +1,23 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+import java.io.IOException;
+
+@ResponseStatus(code = HttpStatus.CONFLICT)
+public class ImageNotSupportedException extends Exception {
+
+    public ImageNotSupportedException(String msg) {
+        super(msg);
+    }
+
+    public ImageNotSupportedException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public ImageNotSupportedException(Throwable thr) {
+        super(thr);
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/NotAllowedException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/NotAllowedException.java
new file mode 100644
index 0000000000000000000000000000000000000000..44c3d430f91d0ff44ce3fb4d1773b53231902d2b
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/NotAllowedException.java
@@ -0,0 +1,21 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.METHOD_NOT_ALLOWED)
+public class NotAllowedException extends Exception {
+
+    public NotAllowedException(String msg) {
+        super(msg);
+    }
+
+    public NotAllowedException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public NotAllowedException(Throwable thr) {
+        super(thr);
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/PaginationException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/PaginationException.java
new file mode 100644
index 0000000000000000000000000000000000000000..9d56aec9c2752c2c37e7b31f227950ad94c95ef3
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/PaginationException.java
@@ -0,0 +1,21 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.BAD_REQUEST)
+public class PaginationException extends Exception {
+
+    public PaginationException(String msg) {
+        super(msg);
+    }
+
+    public PaginationException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public PaginationException(Throwable thr) {
+        super(thr);
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/QueryAlreadyPersistedException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/QueryAlreadyPersistedException.java
new file mode 100644
index 0000000000000000000000000000000000000000..48d3bb0ad9ec2fb1f7c6b9727f2a3e700291cbc6
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/QueryAlreadyPersistedException.java
@@ -0,0 +1,19 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.CONFLICT)
+public class QueryAlreadyPersistedException extends Exception {
+
+    public QueryAlreadyPersistedException(String msg) {
+        super(msg);
+    }
+
+    public QueryAlreadyPersistedException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public QueryAlreadyPersistedException(Throwable thr) { super(thr);
+    }
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/QueryMalformedException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/QueryMalformedException.java
new file mode 100644
index 0000000000000000000000000000000000000000..3d81b6ba4e659b49b59591fa8e9baec31767615a
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/QueryMalformedException.java
@@ -0,0 +1,23 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+import java.io.IOException;
+
+@ResponseStatus(code = HttpStatus.BAD_REQUEST)
+public class QueryMalformedException extends Exception {
+
+    public QueryMalformedException(String msg) {
+        super(msg);
+    }
+
+    public QueryMalformedException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public QueryMalformedException(Throwable thr) {
+        super(thr);
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/QueryNotFoundException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/QueryNotFoundException.java
new file mode 100644
index 0000000000000000000000000000000000000000..a5e90754898f19f6cce8938d2385f3f9fecd43e4
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/QueryNotFoundException.java
@@ -0,0 +1,21 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.NOT_FOUND)
+public class QueryNotFoundException extends Exception {
+
+    public QueryNotFoundException(String msg) {
+        super(msg);
+    }
+
+    public QueryNotFoundException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public QueryNotFoundException(Throwable thr) {
+        super(thr);
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/QueryStoreException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/QueryStoreException.java
new file mode 100644
index 0000000000000000000000000000000000000000..b1f472f2a1179200475b7952dd459af0ee7f7b6a
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/QueryStoreException.java
@@ -0,0 +1,19 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.GATEWAY_TIMEOUT)
+public class QueryStoreException  extends Exception {
+
+    public QueryStoreException(String msg) {
+        super(msg);
+    }
+
+    public QueryStoreException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public QueryStoreException(Throwable thr) { super(thr);
+    }
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/SortException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/SortException.java
new file mode 100644
index 0000000000000000000000000000000000000000..7415590ad637461baac4c9bf1d68b7d411054b19
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/SortException.java
@@ -0,0 +1,21 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.BAD_REQUEST)
+public class SortException extends Exception {
+
+    public SortException(String msg) {
+        super(msg);
+    }
+
+    public SortException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public SortException(Throwable thr) {
+        super(thr);
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/TableMalformedException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/TableMalformedException.java
new file mode 100644
index 0000000000000000000000000000000000000000..542c789ad548084b1a98720630246514b431efcb
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/TableMalformedException.java
@@ -0,0 +1,21 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.LOCKED)
+public class TableMalformedException extends Exception {
+
+    public TableMalformedException(String msg) {
+        super(msg);
+    }
+
+    public TableMalformedException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public TableMalformedException(Throwable thr) {
+        super(thr);
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/TableNotFoundException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/TableNotFoundException.java
new file mode 100644
index 0000000000000000000000000000000000000000..89fa3ed467e76998431c2b366bceb83804824f38
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/TableNotFoundException.java
@@ -0,0 +1,21 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.NOT_FOUND)
+public class TableNotFoundException extends Exception {
+
+    public TableNotFoundException(String msg) {
+        super(msg);
+    }
+
+    public TableNotFoundException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public TableNotFoundException(Throwable thr) {
+        super(thr);
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/TupleDeleteException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/TupleDeleteException.java
new file mode 100644
index 0000000000000000000000000000000000000000..87e5e4a483f9dfe38f9563be9a9d275415b5a0d9
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/TupleDeleteException.java
@@ -0,0 +1,19 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.CONFLICT)
+public class TupleDeleteException extends Exception {
+
+    public TupleDeleteException(String msg) {
+        super(msg);
+    }
+
+    public TupleDeleteException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public TupleDeleteException(Throwable thr) { super(thr);
+    }
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/UserNotFoundException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/UserNotFoundException.java
new file mode 100644
index 0000000000000000000000000000000000000000..0abb87f609f0a6706c8e499eabc23e3d46d3304d
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/UserNotFoundException.java
@@ -0,0 +1,21 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "User not found")
+public class UserNotFoundException extends Exception {
+
+    public UserNotFoundException(String message) {
+        super(message);
+    }
+
+    public UserNotFoundException(String message, Throwable thr) {
+        super(message, thr);
+    }
+
+    public UserNotFoundException(Throwable thr) {
+        super(thr);
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/ViewMalformedException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/ViewMalformedException.java
new file mode 100644
index 0000000000000000000000000000000000000000..5dfdaf170eaccc76da2290673448be19511e4fab
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/ViewMalformedException.java
@@ -0,0 +1,21 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.LOCKED)
+public class ViewMalformedException extends Exception {
+
+    public ViewMalformedException(String msg) {
+        super(msg);
+    }
+
+    public ViewMalformedException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public ViewMalformedException(Throwable thr) {
+        super(thr);
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/ViewNotFoundException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/ViewNotFoundException.java
new file mode 100644
index 0000000000000000000000000000000000000000..2f260975ff4746858184a4c8e5d4ee8268425625
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/ViewNotFoundException.java
@@ -0,0 +1,21 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "View not found")
+public class ViewNotFoundException extends Exception {
+
+    public ViewNotFoundException(String message) {
+        super(message);
+    }
+
+    public ViewNotFoundException(String message, Throwable thr) {
+        super(message, thr);
+    }
+
+    public ViewNotFoundException(Throwable thr) {
+        super(thr);
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/mapper/UserMapper.java b/fda-user-service/services/src/main/java/at/tuwien/mapper/UserMapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..cba134ae89d4f6aeae126b925437bb92bff55d21
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/mapper/UserMapper.java
@@ -0,0 +1,31 @@
+package at.tuwien.mapper;
+
+import at.tuwien.api.auth.SignupRequestDto;
+import at.tuwien.api.user.GrantedAuthorityDto;
+import at.tuwien.api.user.UserBriefDto;
+import at.tuwien.api.user.UserDetailsDto;
+import at.tuwien.api.user.UserDto;
+import at.tuwien.entities.user.User;
+import org.mapstruct.Mapper;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+
+@Mapper(componentModel = "spring")
+public interface UserMapper {
+
+    org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(UserMapper.class);
+
+    UserDetailsDto userDtoToUserDetailsDto(UserDto data);
+
+    UserDto userToUserDto(User data);
+
+    UserBriefDto userToUserBriefDto(User data);
+
+    User signupRequestDtoToUser(SignupRequestDto data);
+
+    default GrantedAuthority grantedAuthorityDtoToGrantedAuthority(GrantedAuthorityDto data) {
+        final GrantedAuthority authority = new SimpleGrantedAuthority(data.getAuthority());
+        log.trace("mapped granted authority {} to granted authority {}", data, authority);
+        return authority;
+    }
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/UserRepository.java b/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/UserRepository.java
new file mode 100644
index 0000000000000000000000000000000000000000..e77de8f6a8160d1f2e04a0ce7575b1ee3d833a0a
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/UserRepository.java
@@ -0,0 +1,14 @@
+package at.tuwien.repository.jpa;
+
+import at.tuwien.entities.user.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface UserRepository extends JpaRepository<User, String> {
+
+    Optional<User> findByUsername(String username);
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/service/UserService.java b/fda-user-service/services/src/main/java/at/tuwien/service/UserService.java
new file mode 100644
index 0000000000000000000000000000000000000000..d9371a4695a522f4a079dc8cb53ee95309eb7a3b
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/service/UserService.java
@@ -0,0 +1,39 @@
+package at.tuwien.service;
+
+import at.tuwien.api.auth.SignupRequestDto;
+import at.tuwien.entities.container.Container;
+import at.tuwien.entities.user.User;
+import at.tuwien.exception.UserNotFoundException;
+
+import java.security.Principal;
+import java.util.List;
+
+public interface UserService {
+
+    /**
+     * Finds all users
+     *
+     * @return The list of users.
+     */
+    List<User> findAll();
+
+    /**
+     * Finds a user by username.
+     *
+     * @param username The username.
+     * @return The user.
+     * @throws UserNotFoundException The user was not found in the metadata database.
+     */
+    User findByUsername(String username) throws UserNotFoundException;
+
+    User create(SignupRequestDto data);
+
+    /**
+     * Finds a user by id.
+     *
+     * @param id The id.
+     * @return The user.
+     * @throws UserNotFoundException The user was not found in the metadata database.
+     */
+    User find(String id) throws UserNotFoundException;
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java b/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..5578d9186b04dce27aa8f54d826a4bc3334d2a9b
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java
@@ -0,0 +1,66 @@
+package at.tuwien.service.impl;
+
+import at.tuwien.api.auth.SignupRequestDto;
+import at.tuwien.entities.user.User;
+import at.tuwien.exception.UserNotFoundException;
+import at.tuwien.mapper.UserMapper;
+import at.tuwien.repository.jpa.UserRepository;
+import at.tuwien.service.UserService;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+@Log4j2
+@Service
+public class UserServiceImpl implements UserService {
+
+    private final UserMapper userMapper;
+    private final UserRepository userRepository;
+
+    @Autowired
+    public UserServiceImpl(UserMapper userMapper, UserRepository userRepository) {
+        this.userMapper = userMapper;
+        this.userRepository = userRepository;
+    }
+
+    @Override
+    public List<User> findAll() {
+        return userRepository.findAll();
+    }
+
+    @Override
+    public User findByUsername(String username) throws UserNotFoundException {
+        final Optional<User> optional = userRepository.findByUsername(username);
+        if (optional.isEmpty()) {
+            log.error("Failed to retrieve user with username {}", username);
+            throw new UserNotFoundException("Failed to retrieve user");
+        }
+        return optional.get();
+    }
+
+    @Override
+    public User create(SignupRequestDto data) {
+        final User user = userMapper.signupRequestDtoToUser(data);
+        user.setRealmId("82c39861-d877-4667-a0f3-4daa2ee230e0");
+        user.setEmailVerified(false);
+        user.setId(UUID.randomUUID().toString());
+        final User entity = userRepository.save(user);
+        log.info("Created user with id {}", entity.getId());
+        return entity;
+    }
+
+    @Override
+    public User find(String id) throws UserNotFoundException {
+        final Optional<User> optional = userRepository.findById(id);
+        if (optional.isEmpty()) {
+            log.error("Failed to retrieve user with id {}", id);
+            throw new UserNotFoundException("Failed to retrieve user");
+        }
+        return optional.get();
+    }
+
+}
diff --git a/fda-user-service/services/src/test/resources/application.properties b/fda-user-service/services/src/test/resources/application.properties
new file mode 100644
index 0000000000000000000000000000000000000000..69df4a312304990e2f284318cd5428d960f14f0b
--- /dev/null
+++ b/fda-user-service/services/src/test/resources/application.properties
@@ -0,0 +1,14 @@
+# disable discovery
+spring.cloud.discovery.enabled = false
+
+# disable cloud config and config discovery
+spring.cloud.config.discovery.enabled = false
+spring.cloud.config.enabled = false
+
+# disable datasource
+spring.datasource.url=jdbc:h2:mem:testdb
+spring.datasource.driverClassName=org.h2.Driver
+spring.datasource.username=sa
+spring.datasource.password=password
+spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
+spring.jpa.hibernate.ddl-auto=update
\ No newline at end of file