diff --git a/dbrepo-ui/api/container.service.js b/dbrepo-ui/api/container.service.js
index 77f56972b5e85df9c3a5a844551c349645f7c1e6..c24ffbd9d57aebc5d31f872d982e81b60e169727 100644
--- a/dbrepo-ui/api/container.service.js
+++ b/dbrepo-ui/api/container.service.js
@@ -19,6 +19,23 @@ class ContainerService {
     })
   }
 
+  findAllImages () {
+    return new Promise((resolve, reject) => {
+      api.get('/api/image', { headers: { Accept: 'application/json' } })
+        .then((response) => {
+          const images = response.data
+          console.debug('response images', images)
+          resolve(images)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to load images', error)
+          Vue.$toast.error(`[${code}] Failed to load images: ${message}`)
+          reject(error)
+        })
+    })
+  }
+
   findOne (id) {
     return new Promise((resolve, reject) => {
       api.get(`/api/container/${id}`, { headers: { Accept: 'application/json' } })
diff --git a/dbrepo-ui/api/database.service.js b/dbrepo-ui/api/database.service.js
index 17cbf65479badd1aeb6c21d61e9828d22115ab8f..85756eaca0db53e7b963558abcb68af4a8a51de4 100644
--- a/dbrepo-ui/api/database.service.js
+++ b/dbrepo-ui/api/database.service.js
@@ -176,6 +176,23 @@ class DatabaseService {
     })
   }
 
+  findView (id, databaseId, viewId) {
+    return new Promise((resolve, reject) => {
+      api.get(`/api/container/${id}/database/${databaseId}/view/${viewId}`, { headers: { Accept: 'application/json' } })
+        .then((response) => {
+          const view = response.data
+          console.debug('response view', view)
+          resolve(view)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to find view', error)
+          Vue.$toast.error(`[${code}] Failed to find view: ${message}`)
+          reject(error)
+        })
+    })
+  }
+
   createView (id, databaseId, data) {
     return new Promise((resolve, reject) => {
       api.post(`/api/container/${id}/database/${databaseId}/view`, data, { headers: { Accept: 'application/json' } })
diff --git a/dbrepo-ui/api/identifier.service.js b/dbrepo-ui/api/identifier.service.js
index 51181e9c590de56c1d69856e6d61585848579281..9410ccf9043299e6bd98bc2398782c593e247456 100644
--- a/dbrepo-ui/api/identifier.service.js
+++ b/dbrepo-ui/api/identifier.service.js
@@ -20,9 +20,13 @@ class IdentifierService {
     })
   }
 
-  findOne (id) {
+  find (id) {
+    return this.findAccept(id, 'application/json')
+  }
+
+  findAccept (id, accept) {
     return new Promise((resolve, reject) => {
-      api.get(`/api/pid/${id}`, { headers: { Accept: 'application/json' } })
+      api.get(`/api/pid/${id}`, { headers: { Accept: accept } })
         .then((response) => {
           const identifier = response.data
           console.debug('response identifier', identifier)
diff --git a/dbrepo-ui/api/query.service.js b/dbrepo-ui/api/query.service.js
index 09db2312e76e49ebf8cdf11f40a2fa2664c4fdb4..a7900546e0cd3cf7a7c99db12c7a677296673bc3 100644
--- a/dbrepo-ui/api/query.service.js
+++ b/dbrepo-ui/api/query.service.js
@@ -119,7 +119,9 @@ class QueryService {
     return new Promise((resolve, reject) => {
       api.get(`/api/container/${id}/database/${databaseId}/query/${queryId}/export`, { headers: { Accept: 'text/csv' } })
         .then((response) => {
-          resolve(response.data)
+          const subset = response.data
+          console.debug('response subset', subset)
+          resolve(subset)
         })
         .catch((error) => {
           const { code, message } = error
@@ -134,12 +136,99 @@ class QueryService {
     return new Promise((resolve, reject) => {
       api.get(`/api/pid/${id}`, { headers: { Accept: mime } })
         .then((response) => {
-          resolve(response.data)
+          const metadata = response.data
+          console.debug('response metadata', metadata)
+          resolve(metadata)
         })
         .catch((error) => {
           const { code, message } = error
-          console.error('Failed to export query', error)
-          Vue.$toast.error(`[${code}] Failed to export query: ${message}`)
+          console.error('Failed to export metadata', error)
+          Vue.$toast.error(`[${code}] Failed to export metadata: ${message}`)
+          reject(error)
+        })
+    })
+  }
+
+  execute (id, databaseId, data, page, size) {
+    return new Promise((resolve, reject) => {
+      api.post(`/api/container/${id}/database/${databaseId}?page=${page}&size=${size}`, data, { headers: { Accept: 'application/json' } })
+        .then((response) => {
+          const result = response.data
+          console.debug('response result', result)
+          resolve(result)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to execute statement', error)
+          Vue.$toast.error(`[${code}] Failed to execute statement: ${message}`)
+          reject(error)
+        })
+    })
+  }
+
+  reExecuteQuery (id, databaseId, queryId, page, size) {
+    return new Promise((resolve, reject) => {
+      api.get(`/api/container/${id}/database/${databaseId}/query/${queryId}/data?page=${page}&size=${size}`, { headers: { Accept: 'application/json' } })
+        .then((response) => {
+          const result = response.data
+          console.debug('response result', result)
+          resolve(result)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to re-execute query', error)
+          Vue.$toast.error(`[${code}] Failed to re-execute query: ${message}`)
+          reject(error)
+        })
+    })
+  }
+
+  reExecuteQueryCount (id, databaseId, queryId) {
+    return new Promise((resolve, reject) => {
+      api.get(`/api/container/${id}/database/${databaseId}/query/${queryId}/data/count`, { headers: { Accept: 'application/json' } })
+        .then((response) => {
+          const count = response.data
+          console.debug('response count', count)
+          resolve(count)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to re-execute query count', error)
+          Vue.$toast.error(`[${code}] Failed to re-execute query count: ${message}`)
+          reject(error)
+        })
+    })
+  }
+
+  reExecuteView (id, databaseId, viewId, page, size) {
+    return new Promise((resolve, reject) => {
+      api.get(`/api/container/${id}/database/${databaseId}/view/${viewId}/data?page=${page}&size=${size}`, { headers: { Accept: 'application/json' } })
+        .then((response) => {
+          const result = response.data
+          console.debug('response result', result)
+          resolve(result)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to re-execute view', error)
+          Vue.$toast.error(`[${code}] Failed to re-execute view: ${message}`)
+          reject(error)
+        })
+    })
+  }
+
+  reExecuteViewCount (id, databaseId, viewId) {
+    return new Promise((resolve, reject) => {
+      api.get(`/api/container/${id}/database/${databaseId}/view/${viewId}/data/count`, { headers: { Accept: 'application/json' } })
+        .then((response) => {
+          const count = response.data
+          console.debug('response count', count)
+          resolve(count)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to re-execute view count', error)
+          Vue.$toast.error(`[${code}] Failed to re-execute view count: ${message}`)
           reject(error)
         })
     })
diff --git a/dbrepo-ui/api/search.service.js b/dbrepo-ui/api/search.service.js
new file mode 100644
index 0000000000000000000000000000000000000000..4cdd577bb8094a210d3171e842c190281f7102b8
--- /dev/null
+++ b/dbrepo-ui/api/search.service.js
@@ -0,0 +1,24 @@
+import Vue from 'vue'
+import axios from 'axios'
+import { elasticPassword } from '../config'
+
+class SearchService {
+  search (query) {
+    return new Promise((resolve, reject) => {
+      axios.get(`/retrieve/_all/_search?q=${query}*&terminate_after=50`, { headers: { Accept: 'application/json' }, auth: { username: 'elastic', password: elasticPassword } })
+        .then((response) => {
+          const hits = response.data.hits.hits
+          console.debug('response hits', hits)
+          resolve(hits)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to load search results', error)
+          Vue.$toast.error(`[${code}] Failed to load search results: ${message}`)
+          reject(error)
+        })
+    })
+  }
+}
+
+export default new SearchService()
diff --git a/dbrepo-ui/api/table.service.js b/dbrepo-ui/api/table.service.js
index d5fc87b38bdb0927fb435e5306871a23df7e64ad..930aae5a8ee79ef8f008860fbae3cd1f7de94cc0 100644
--- a/dbrepo-ui/api/table.service.js
+++ b/dbrepo-ui/api/table.service.js
@@ -41,6 +41,95 @@ class TableService {
     })
   }
 
+  updateColumn (id, databaseId, tableId, columnId, data) {
+    return new Promise((resolve, reject) => {
+      api.put(`/api/container/${id}/database/${databaseId}/table/${tableId}/column/${columnId}`, data, { headers: { Accept: 'application/json' } })
+        .then((response) => {
+          const column = response.data
+          console.debug('response column', column)
+          resolve(column)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to update column', error)
+          Vue.$toast.error(`[${code}] Failed to update column: ${message}`)
+          reject(error)
+        })
+    })
+  }
+
+  data (id, databaseId, tableId, page, size, timestamp) {
+    return new Promise((resolve, reject) => {
+      api.get(`/api/container/${id}/database/${databaseId}/table/${tableId}/data?page=${page}&size=${size}&timestamp=${timestamp}`, { headers: { Accept: 'application/json' } })
+        .then((response) => {
+          const data = response.data
+          console.debug('response data', data)
+          resolve(data)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to load table data', error)
+          Vue.$toast.error(`[${code}] Failed to load table data: ${message}`)
+          reject(error)
+        })
+    })
+  }
+
+  dataCount (id, databaseId, tableId, timestamp) {
+    return new Promise((resolve, reject) => {
+      api.get(`/api/container/${id}/database/${databaseId}/table/${tableId}/data/count?timestamp=${timestamp}`, { headers: { Accept: 'application/json' } })
+        .then((response) => {
+          const count = response.data
+          console.debug('response count', count)
+          resolve(count)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to load table count', error)
+          Vue.$toast.error(`[${code}] Failed to load table count: ${message}`)
+          reject(error)
+        })
+    })
+  }
+
+  findHistory (id, databaseId, tableId) {
+    return new Promise((resolve, reject) => {
+      api.get(`/api/container/${id}/database/${databaseId}/table/${tableId}/history`, { headers: { Accept: 'application/json' } })
+        .then((response) => {
+          const history = response.data
+          console.debug('response history', history)
+          resolve(history)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to load table history', error)
+          Vue.$toast.error(`[${code}] Failed to load table history: ${message}`)
+          reject(error)
+        })
+    })
+  }
+
+  exportData (id, databaseId, tableId) {
+    return this.exportDataTimestamp(id, databaseId, tableId, null)
+  }
+
+  exportDataTimestamp (id, databaseId, tableId, timestamp) {
+    return new Promise((resolve, reject) => {
+      api.get(`/api/container/${id}/database/${databaseId}/table/${tableId}/export?timestamp=${timestamp}`, { responseType: 'text' })
+        .then((response) => {
+          const data = response.data
+          console.debug('response data', data)
+          resolve(data)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to export table data', error)
+          Vue.$toast.error(`[${code}] Failed to export table data: ${message}`)
+          reject(error)
+        })
+    })
+  }
+
   create (id, databaseId, data) {
     return new Promise((resolve, reject) => {
       api.post(`/api/container/${id}/database/${databaseId}/table`, data, { headers: { Accept: 'application/json' } })
diff --git a/dbrepo-ui/components/TableList.vue b/dbrepo-ui/components/TableList.vue
index 86aed98663b9b03ce9f2f121342b87fa302a2f8c..f627f9387e4e329e73c7ff922e55fe2387c26c0a 100644
--- a/dbrepo-ui/components/TableList.vue
+++ b/dbrepo-ui/components/TableList.vue
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <v-progress-linear v-if="loading" :color="loadingColor" indeterminate />
+    <v-progress-linear v-if="loading" indeterminate />
     <v-card v-if="!loading && tables && tables.length === 0" flat>
       <v-card-text>
         (no tables)
@@ -24,7 +24,6 @@
 
 <script>
 import { formatTimestampUTCLabel } from '@/utils'
-import TableService from '@/api/table.service'
 
 export default {
   data () {
@@ -79,17 +78,6 @@ export default {
     token () {
       return this.$store.state.token
     },
-    loadingColor () {
-      return this.error ? 'red lighten-2' : 'primary'
-    },
-    config () {
-      if (this.token === null) {
-        return {}
-      }
-      return {
-        headers: { Authorization: `Bearer ${this.token}` }
-      }
-    },
     user () {
       return this.$store.state.user
     },
@@ -144,53 +132,12 @@ export default {
       }
       return column.column_type
     },
-    details (table) {
-      /* use cache */
-      this.tableDetails = table
-      /* load remaining info */
-      if (this.canRead) {
-        this.loadingDetails = true
-        TableService.findOne(this.$route.params.container_id, this.$route.params.database_id, table.id)
-          .then((table) => {
-            this.tableDetails = table
-            if (table.id) {
-              this.openPanelByTableId(table.id)
-            }
-          })
-          .finally(() => {
-            this.loadingDetails = false
-          })
-      }
-    },
-    is_owner (table) {
-      if (!this.user) {
-        return false
-      }
-      return table.creator.username === this.user.username
-    },
     closed (data) {
       console.debug('closed dialog', data)
       this.dialogSemantic = false
     },
     created (created) {
       return formatTimestampUTCLabel(created)
-    },
-    async deleteTable () {
-      try {
-        this.loading = true
-        await this.$axios.delete(`/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/table/${this.deleteTableId}`, this.config)
-        this.loading = false
-        this.refresh()
-      } catch (err) {
-        this.$toast.error('Could not delete table')
-      }
-      this.dialogDelete = false
-    },
-    /**
-     * open up the accordion with the table that has been updated (by the ColumnUnit dialog)
-     */
-    openPanelByTableId (id) {
-      this.panel = this.tables.findIndex(t => t.id === id)
     }
   }
 }
diff --git a/dbrepo-ui/components/dialogs/CreateDB.vue b/dbrepo-ui/components/dialogs/CreateDB.vue
index e73cd6f336e9b332c0c2c127d5c3394bfb6cdd19..0259ba127e6470ca28562c0c97536d3ba7cfc263 100644
--- a/dbrepo-ui/components/dialogs/CreateDB.vue
+++ b/dbrepo-ui/components/dialogs/CreateDB.vue
@@ -65,7 +65,6 @@ export default {
     return {
       valid: false,
       loading: false,
-      error: false,
       engine: {
         repository: null,
         tag: null
@@ -90,23 +89,9 @@ export default {
     }
   },
   computed: {
-    loadingColor () {
-      return this.error ? 'red lighten-2' : 'primary'
-    },
     token () {
       return this.$store.state.token
     },
-    config () {
-      if (this.token === null) {
-        return {
-          headers: {}
-        }
-      }
-      return {
-        headers: { Authorization: `Bearer ${this.token}` },
-        progress: false
-      }
-    },
     user () {
       return this.$store.state.user
     }
@@ -121,20 +106,18 @@ export default {
     cancel () {
       this.$emit('close', { success: false })
     },
-    async getImages () {
-      try {
-        this.loading = true
-        const res = await this.$axios.get('/api/image')
-        this.engines = res.data
-        console.debug('engines', this.engines)
-        if (this.engines.length > 0) {
-          this.engine = this.engines[0]
-        }
-      } catch (err) {
-        this.error = true
-        this.$toast.error('Failed to fetch supported engines. Try reload the page')
-      }
-      this.loading = false
+    getImages () {
+      this.loading = true
+      ContainerService.findAllImages()
+        .then((images) => {
+          this.engines = images
+          if (this.engines.length > 0) {
+            this.engine = this.engines[0]
+          }
+        })
+        .finally(() => {
+          this.loading = false
+        })
     },
     async create () {
       await this.createContainer()
diff --git a/dbrepo-ui/components/dialogs/EditRoles.vue b/dbrepo-ui/components/dialogs/EditRoles.vue
deleted file mode 100644
index 81860aa010978cfab86fdb329c6cb85e8089657f..0000000000000000000000000000000000000000
--- a/dbrepo-ui/components/dialogs/EditRoles.vue
+++ /dev/null
@@ -1,174 +0,0 @@
-<template>
-  <div>
-    <v-form ref="form" v-model="valid" autocomplete="off" @submit.prevent="submit">
-      <v-card>
-        <v-progress-linear v-if="loading" :color="loadingColor" :indeterminate="!error" />
-        <v-card-title>
-          User Role
-        </v-card-title>
-        <v-card-subtitle>
-          Modify user role
-        </v-card-subtitle>
-        <v-card-text>
-          <v-alert
-            v-if="becomeDeveloper"
-            border="left"
-            color="warning">
-            <strong>Dangerous operation:</strong> you are giving this user developer access. This cannot be undone.
-          </v-alert>
-          <v-row>
-            <v-col>
-              <v-autocomplete
-                v-model="selectedUser"
-                :items="users"
-                :loading="loadingUsers"
-                :rules="[v => !!v || $t('Required')]"
-                required
-                hide-no-data
-                hide-selected
-                hide-details
-                return-object
-                item-text="username"
-                item-value="username"
-                single-line
-                label="Username" />
-            </v-col>
-          </v-row>
-          <v-row>
-            <v-col>
-              <v-select
-                v-model="modify.roles"
-                :items="roles"
-                multiple
-                item-value="code"
-                item-text="text"
-                :rules="[v => !!v || $t('Required')]"
-                required
-                label="Role type" />
-            </v-col>
-          </v-row>
-        </v-card-text>
-        <v-card-actions>
-          <v-spacer />
-          <v-btn
-            class="mb-2"
-            @click="cancel">
-            Cancel
-          </v-btn>
-          <v-btn
-            id="database"
-            class="mb-2 ml-3 mr-2"
-            :disabled="!valid || loading"
-            color="primary"
-            type="submit"
-            :loading="loading"
-            @click="updateRoles">
-            Save
-          </v-btn>
-        </v-card-actions>
-      </v-card>
-    </v-form>
-  </div>
-</template>
-
-<script>
-export default {
-  props: {
-    user: {
-      type: Object,
-      default () {
-        return {}
-      }
-    }
-  },
-  data () {
-    return {
-      valid: false,
-      loading: false,
-      loadingUsers: false,
-      selectedUser: null,
-      users: [],
-      error: false,
-      roles: [
-        { text: 'Researcher', value: 'researcher', code: 'ROLE_RESEARCHER' },
-        { text: 'Data Steward', value: 'data_steward', code: 'ROLE_DATA_STEWARD' },
-        { text: 'Developer', value: 'developer', code: 'ROLE_DEVELOPER' }
-      ],
-      modify: {
-        roles: []
-      }
-    }
-  },
-  computed: {
-    loadingColor () {
-      return this.error ? 'red lighten-2' : 'primary'
-    },
-    token () {
-      return this.$store.state.token
-    },
-    config () {
-      if (this.token === null) {
-        return {}
-      }
-      return {
-        headers: { Authorization: `Bearer ${this.token}` }
-      }
-    },
-    becomeDeveloper () {
-      return this.modify.roles.filter(r => r === 'ROLE_DEVELOPER').length > 0
-    }
-  },
-  watch: {
-    user (newVal, oldVal) {
-      this.modify.roles = newVal.roles
-      this.selectedUser = newVal
-    }
-  },
-  mounted () {
-    this.loadUsers()
-    this.modify.roles = this.user.roles
-    this.selectedUser = this.user
-  },
-  methods: {
-    submit () {
-      this.$refs.form.validate()
-    },
-    cancel () {
-      this.$emit('close-dialog', { success: false })
-    },
-    async updateRoles () {
-      this.loading = true
-      const roles = {
-        roles: this.modify.roles.map(role => this.roles.filter(r => r.code === role)[0].value)
-      }
-      try {
-        const res = await this.$axios.put(`/api/user/${this.selectedUser.id}/roles`, roles, this.config)
-        console.debug('roles', res.data)
-        this.$toast.success('Updated user roles')
-        this.$emit('close-dialog', { success: true })
-      } catch (error) {
-        const { message } = error.response
-        this.$toast.error('Failed to update user roles: ' + message)
-        console.error('Failed to update user roles', error)
-      }
-      this.loading = false
-    },
-    async loadUsers () {
-      this.loading = true
-      try {
-        const res = await this.$axios.get('/api/user', this.config)
-        this.users = res.data.map((user) => {
-          user.roles_pretty = user.roles.join(',')
-          return user
-        })
-        console.debug('users', this.users)
-      } catch (error) {
-        const { message } = error.response
-        this.$toast.error('Failed to load users: ' + message)
-        console.error('Failed to load users', error)
-      }
-      this.loading = false
-    }
-  }
-}
-</script>
diff --git a/dbrepo-ui/components/dialogs/Persist.vue b/dbrepo-ui/components/dialogs/Persist.vue
index 1f87e7ef2d30383c52d8cdf75b6eed62af5728bb..d44f784ca5eb2e2ff982b29befca12fe13ccebe9 100644
--- a/dbrepo-ui/components/dialogs/Persist.vue
+++ b/dbrepo-ui/components/dialogs/Persist.vue
@@ -1,7 +1,6 @@
 <template>
   <div>
     <v-card>
-      <v-progress-linear v-if="loading" :color="loadingColor" :indeterminate="!error" />
       <v-card-title v-text="`Persist ${title}`" />
       <v-card-text>
         <v-alert
@@ -169,6 +168,7 @@
         </v-btn>
         <v-btn
           class="mb-2"
+          :loading="loading"
           :disabled="!formValid || loading"
           color="primary"
           @click="persist">
diff --git a/dbrepo-ui/components/dialogs/TimeTravel.vue b/dbrepo-ui/components/dialogs/TimeTravel.vue
index 4866c7974cf588dab26c27af9b8611cb37888fd5..1007db8b50784920d627063bc05db023c86b9d4f 100644
--- a/dbrepo-ui/components/dialogs/TimeTravel.vue
+++ b/dbrepo-ui/components/dialogs/TimeTravel.vue
@@ -1,7 +1,7 @@
 <template>
   <div>
     <v-card>
-      <v-progress-linear v-if="loading" :color="loadingColor" :indeterminate="!error" />
+      <v-progress-linear v-if="loading" color="primary" />
       <v-card-title>
         Versioning
       </v-card-title>
@@ -55,6 +55,7 @@
 </template>
 
 <script>
+import TableService from '@/api/table.service'
 import { Bar } from 'vue-chartjs/legacy'
 import { Chart as ChartJS, Title, Tooltip, BarElement, CategoryScale, LinearScale, LogarithmicScale } from 'chart.js'
 import { formatTimestampUTC, formatTimestampUTCLabel } from '@/utils'
@@ -69,7 +70,6 @@ export default {
     return {
       formValid: false,
       loading: false,
-      error: false,
       datetime: null,
       chartData: {
         labels: [],
@@ -89,22 +89,6 @@ export default {
       totalChanges: 0
     }
   },
-  computed: {
-    loadingColor () {
-      return this.error ? 'error' : 'primary'
-    },
-    token () {
-      return this.$store.state.token
-    },
-    config () {
-      if (this.token === null) {
-        return {}
-      }
-      return {
-        headers: { Authorization: `Bearer ${this.token}` }
-      }
-    }
-  },
   mounted () {
     this.loadHistory()
   },
@@ -129,29 +113,25 @@ export default {
         time: this.datetime
       })
     },
-    async loadHistory () {
-      try {
-        this.loading = true
-        const res = await this.$axios.get(`/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/table/${this.$route.params.table_id}/history`, this.config)
-        this.error = false
-        this.chartData.labels = res.data.map(function (d, idx) {
-          if (idx === 0) {
-            return 'Origin'
-          }
-          return formatTimestampUTCLabel(d.timestamp)
+    loadHistory () {
+      this.loading = true
+      TableService.findHistory(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.table_id)
+        .then((history) => {
+          this.chartData.labels = history.map(function (d, idx) {
+            if (idx === 0) {
+              return 'Origin'
+            }
+            return formatTimestampUTCLabel(d.timestamp)
+          })
+          this.chartData.dates = history.map(d => formatTimestampUTC(d.timestamp))
+          this.chartData.datasets = [{
+            backgroundColor: this.$vuetify.theme.themes.light.primary,
+            data: history.map(d => d.total)
+          }]
+        })
+        .finally(() => {
+          this.loading = false
         })
-        this.chartData.dates = res.data.map(d => formatTimestampUTC(d.timestamp))
-        this.chartData.datasets = [{
-          backgroundColor: this.$vuetify.theme.themes.light.primary,
-          data: res.data.map(d => d.total)
-        }]
-        // this.totalChanges = this.res.data.length
-        console.debug('history', this.chartData)
-      } catch (err) {
-        this.error = true
-        console.error('failed to load table history', err)
-      }
-      this.loading = false
     }
   }
 }
diff --git a/dbrepo-ui/components/identifier/Banner.vue b/dbrepo-ui/components/identifier/Banner.vue
index ece371663f1d888d8c4ee90c316cc45a163c1eb0..990bf61f5c6dda27ee8998dddc5aee261be368a6 100644
--- a/dbrepo-ui/components/identifier/Banner.vue
+++ b/dbrepo-ui/components/identifier/Banner.vue
@@ -15,7 +15,7 @@ export default {
   },
   computed: {
     baseUrl () {
-      return `${location.protocol}//${location.host}`
+      return `${this.$config.baseUrl}`
     },
     baseDoi () {
       return this.$config.doiUrl
diff --git a/dbrepo-ui/components/identifier/Citation.vue b/dbrepo-ui/components/identifier/Citation.vue
index 9d3037905c1e43eb593f003c21fab7ec4f9bc88f..479243cc458e930387e90cbacb37287607136248 100644
--- a/dbrepo-ui/components/identifier/Citation.vue
+++ b/dbrepo-ui/components/identifier/Citation.vue
@@ -58,17 +58,9 @@ export default {
       citation: null
     }
   },
-  computed: {
-    config () {
-      return {
-        headers: { Accept: 'text/bibliography;style=apa' },
-        progress: false
-      }
-    }
-  },
   watch: {
-    style (newVal, _) {
-      this.loadCitation(newVal)
+    style () {
+      this.loadCitation(this.style)
     },
     pid () {
       this.loadCitation(this.style)
@@ -79,25 +71,18 @@ export default {
     this.loadCitation(null)
   },
   methods: {
-    async loadCitation (accept) {
+    loadCitation (accept) {
       if (!this.pid) {
         return
       }
       this.loading = true
-      try {
-        const config = this.config
-        if (accept != null) {
-          config.headers.Accept = accept
-        }
-        const res = await this.$axios.get(`/api/pid/${this.pid}`, config)
-        this.citation = res.data
-        console.debug('citation', this.citation)
-      } catch (err) {
-        console.error('Could not cite identifier', err)
-        this.$toast.error('Could not cite identifier')
-        this.error = true
-      }
-      this.loading = false
+      IdentifierService.findAccept(this.pid, accept)
+        .then((citation) => {
+          this.citation = citation
+        })
+        .finally(() => {
+          this.loading = false
+        })
     }
   }
 }
diff --git a/dbrepo-ui/components/query/Results.vue b/dbrepo-ui/components/query/Results.vue
index d04b59378614effed3b3a6595bbb912f13c164bc..1f30ee22566bf88f9184576f86ffac03c42c8967 100644
--- a/dbrepo-ui/components/query/Results.vue
+++ b/dbrepo-ui/components/query/Results.vue
@@ -9,6 +9,7 @@
 </template>
 
 <script>
+import QueryService from '@/api/query.service'
 export default {
   props: {
     type: {
@@ -32,24 +33,6 @@ export default {
       total: -1
     }
   },
-  computed: {
-    token () {
-      return this.$store.state.token
-    },
-    config () {
-      if (this.token === null) {
-        return {}
-      }
-      return {
-        headers: { Authorization: `Bearer ${this.token}` }
-      }
-    },
-    executeUrl () {
-      const page = 0
-      const urlParams = `page=${page}&size=${this.options.itemsPerPage}`
-      return `/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/query?${urlParams}`
-    }
-  },
   watch: {
     options: { /* keep */
       handler () {
@@ -59,28 +42,20 @@ export default {
     }
   },
   methods: {
-    async executeFirstTime (parent, sql, timestamp) {
+    executeFirstTime (parent, sql, timestamp) {
       this.loading++
-      try {
-        const res = await this.$axios.post(this.executeUrl, { statement: sql, timestamp }, this.config)
-        console.debug('query result', res.data)
-        this.$toast.success('Successfully executed query')
-        this.mapResults(res.data)
-        parent.resultId = res.data.id
-      } catch (error) {
-        console.error('Failed to execute query', error)
-        const { status, data } = error.response
-        const { message, code } = data
-        if (status === 504) {
-          console.error('Failed to execute query: container not online', code)
-          this.$toast.error('Failed to execute query: container not online')
-        } else {
-          console.error('Failed to execute query', code)
-          this.$toast.error('Failed to execute query: ' + message)
-        }
-        this.error = true
+      const payload = {
+        statement: sql,
+        timestamp
       }
-      this.loading--
+      QueryService.execute(this.$route.params.container_id, this.$route.params.database_id, payload, 0, this.options.itemsPerPage)
+        .then((result) => {
+          this.mapResults(result)
+          parent.resultId = result.id
+        })
+        .finally(() => {
+          this.loading--
+        })
     },
     buildHeaders (firstLine) {
       return Object.keys(firstLine).map(k => ({
@@ -89,42 +64,53 @@ export default {
         sortable: false
       }))
     },
-    reExecuteUrl (resultId) {
-      const page = this.options.page - 1
-      const urlParams = `page=${page}&size=${this.options.itemsPerPage}`
-      return `/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/${this.type}/${resultId}/data?${urlParams}`
-    },
-    reExecuteCountUrl (resultId) {
-      return `/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/${this.type}/${resultId}/data/count`
-    },
-    async reExecute (id) {
+    reExecute (id) {
       if (id === null) {
         return
       }
       this.loading++
-      try {
-        const res = await this.$axios.get(this.reExecuteUrl(id), this.config)
-        this.mapResults(res.data)
-        this.id = id
-      } catch (error) {
-        console.error('failed to execute query', error)
-        this.error = true
+      if (this.type === 'query') {
+        QueryService.reExecuteQuery(this.$route.params.container_id, this.$route.params.database_id, this.resultId, 0, this.options.itemsPerPage)
+          .then((result) => {
+            this.mapResults(result)
+            this.id = id
+          })
+          .finally(() => {
+            this.loading--
+          })
+      } else {
+        QueryService.reExecuteView(this.$route.params.container_id, this.$route.params.database_id, this.resultId, 0, this.options.itemsPerPage)
+          .then((result) => {
+            this.mapResults(result)
+            this.id = id
+          })
+          .finally(() => {
+            this.loading--
+          })
       }
-      this.loading--
     },
-    async reExecuteCount (id) {
+    reExecuteCount (id) {
       if (id === null) {
         return
       }
       this.loading++
-      try {
-        const res = await this.$axios.get(this.reExecuteCountUrl(id), this.config)
-        this.total = res.data
-      } catch (error) {
-        console.error('failed to execute query count', error)
-        this.error = true
+      if (this.type === 'query') {
+        QueryService.reExecuteQueryCount(this.$route.params.container_id, this.$route.params.database_id, this.resultId)
+          .then((count) => {
+            this.total = count
+          })
+          .finally(() => {
+            this.loading--
+          })
+      } else {
+        QueryService.reExecuteViewCount(this.$route.params.container_id, this.$route.params.database_id, this.resultId)
+          .then((count) => {
+            this.total = count
+          })
+          .finally(() => {
+            this.loading--
+          })
       }
-      this.loading--
     },
     mapResults (data) {
       if (data.result.length) {
diff --git a/dbrepo-ui/layouts/default.vue b/dbrepo-ui/layouts/default.vue
index 6c32c271c6cb43916edf25eb1338d0321d547d30..82ed12293a3f0a75108dff7f901ac7747ce69e65 100644
--- a/dbrepo-ui/layouts/default.vue
+++ b/dbrepo-ui/layouts/default.vue
@@ -301,7 +301,7 @@ export default {
         return
       }
       this.loading = true
-      IdentifierService.findOne(this.database.identifier.id)
+      IdentifierService.find(this.database.identifier.id)
         .then((identifier) => {
           this.database.identifier = identifier
           this.$store.commit('SET_DATABASE', this.database)
diff --git a/dbrepo-ui/nuxt.config.js b/dbrepo-ui/nuxt.config.js
index 5cf3275c2760b0410c480a6c20105749327b25a5..9c06aff0c1b6dadcfebcf8354041a1a89ccb1562 100644
--- a/dbrepo-ui/nuxt.config.js
+++ b/dbrepo-ui/nuxt.config.js
@@ -1,6 +1,6 @@
 import path from 'path'
 import colors from 'vuetify/es5/util/colors'
-import { api, icon, search, clientSecret, title, sandbox, logo, version, defaultPublisher, doiUrl } from './config'
+import { api, icon, search, clientSecret, title, sandbox, logo, version, defaultPublisher, doiUrl, baseUrl } from './config'
 
 const proxy = {}
 
@@ -93,7 +93,8 @@ export default {
     logo,
     clientSecret,
     defaultPublisher,
-    doiUrl
+    doiUrl,
+    baseUrl
   },
 
   serverMiddleware: [
@@ -109,7 +110,7 @@ export default {
           primary: colors.blue.darken2,
           accent: colors.amber.darken3,
           secondary: colors.blueGrey.base,
-          info: colors.amber.lighten4,
+          info: colors.amber.lighten1,
           code: colors.grey.lighten4,
           warning: colors.orange.lighten2,
           error: colors.red.base /* is used by forms */,
diff --git a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue
index 59e8aa9acf679c91e21f5f02161e790a4ace0cb5..dfd27fc763f40c273408f55263ea0ecd76647700 100644
--- a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue
+++ b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue
@@ -41,6 +41,7 @@
 <script>
 import TimeTravel from '@/components/dialogs/TimeTravel'
 import TableToolbar from '@/components/TableToolbar'
+import TableService from '@/api/table.service'
 import { formatTimestampUTC, formatDateUTC, formatTimestamp } from '@/utils'
 
 export default {
@@ -98,17 +99,6 @@ export default {
     table () {
       return this.$store.state.table
     },
-    config () {
-      if (this.token === null) {
-        return {
-          headers: {},
-          progress: false
-        }
-      }
-      return {
-        headers: { Authorization: `Bearer ${this.token}` }
-      }
-    },
     user () {
       return this.$store.state.user
     },
@@ -118,17 +108,6 @@ export default {
     access () {
       return this.$store.state.access
     },
-    downloadConfig () {
-      if (this.token === null) {
-        return {
-          responseType: 'text'
-        }
-      }
-      return {
-        headers: { Authorization: `Bearer ${this.token}` },
-        responseType: 'text'
-      }
-    },
     versionColor () {
       if (this.version === null) {
         return 'secondary white--text'
@@ -193,28 +172,35 @@ export default {
     this.loadProperties()
   },
   methods: {
-    async download () {
+    download () {
       this.downloadLoading = true
-      try {
-        let exportUrl = `/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/table/${this.$route.params.table_id}/export`
-        if (this.version) {
-          exportUrl += `?timestamp=${this.versionISO}`
-        }
-        const res = await this.$axios.get(exportUrl, this.downloadConfig)
-        console.debug('export table', res)
-        const url = window.URL.createObjectURL(new Blob([res.data]))
-        const link = document.createElement('a')
-        link.href = url
-        link.setAttribute('download', 'table.csv')
-        document.body.appendChild(link)
-        link.click()
-      } catch (error) {
-        console.error('Failed to export table', error)
-        const { message } = error.response
-        this.$toast.error('Failed to export table: ' + message)
-        this.error = true
+      if (!this.version) {
+        TableService.exportData(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.table_id)
+          .then((data) => {
+            const url = window.URL.createObjectURL(new Blob([data]))
+            const link = document.createElement('a')
+            link.href = url
+            link.setAttribute('download', 'table.csv')
+            document.body.appendChild(link)
+            link.click()
+          })
+          .finally(() => {
+            this.downloadLoading = false
+          })
+      } else {
+        TableService.exportData(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.table_id, this.versionISO)
+          .then((data) => {
+            const url = window.URL.createObjectURL(new Blob([data]))
+            const link = document.createElement('a')
+            link.href = url
+            link.setAttribute('download', `table_${this.versionISO}.csv`)
+            document.body.appendChild(link)
+            link.click()
+          })
+          .finally(() => {
+            this.downloadLoading = false
+          })
       }
-      this.downloadLoading = false
     },
     pick () {
       if (this.$refs.timeTravel !== undefined) {
@@ -273,71 +259,37 @@ export default {
       this.loadData()
       this.loadCount()
     },
-    async loadData () {
-      try {
-        this.loadingData++
-        const url = `/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/table/${this.$route.params.table_id}/data?page=${this.options.page - 1}&size=${this.options.itemsPerPage}&timestamp=${this.versionISO || this.lastReload.toISOString()}`
-        if (this.version !== null) {
-          console.info('versioning active', this.version)
-        }
-        const res = await this.$axios.get(url, this.config)
-        this.rows = res.data.result.map((row) => {
-          for (const col in row) {
-            const columnDefinition = this.dateColumns.filter(c => c.internal_name === col)
-            if (columnDefinition.length > 0) {
-              if (columnDefinition[0].column_type === 'date') {
-                row[col] = formatDateUTC(row[col])
-              } else if (columnDefinition[0].column_type === 'timestamp') {
-                row[col] = formatTimestampUTC(row[col])
+    loadData () {
+      this.loadingData++
+      TableService.data(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.table_id, (this.options.page - 1), this.options.itemsPerPage, (this.versionISO || this.lastReload.toISOString()))
+        .then((data) => {
+          this.rows = data.result.map((row) => {
+            for (const col in row) {
+              const columnDefinition = this.dateColumns.filter(c => c.internal_name === col)
+              if (columnDefinition.length > 0) {
+                if (columnDefinition[0].column_type === 'date') {
+                  row[col] = formatDateUTC(row[col])
+                } else if (columnDefinition[0].column_type === 'timestamp') {
+                  row[col] = formatTimestampUTC(row[col])
+                }
               }
             }
-          }
-          return row
+            return row
+          })
+        })
+        .finally(() => {
+          this.loadingData--
         })
-        console.debug('rows', this.rows)
-      } catch (error) {
-        console.error('Failed to load data', error)
-        this.error = true
-        this.loadProgress = 100
-        const { status, data } = error.response
-        const { message, code } = data
-        if (status === 423) {
-          console.error('Database is offline', code)
-          this.$toast.error('Database is offline: ' + message)
-        } else {
-          console.error('Failed to load data', code)
-          this.$toast.error('Failed to load data: ' + message)
-        }
-      } finally {
-        this.loadingData--
-      }
     },
-    async loadCount () {
-      try {
-        this.loadingData++
-        const url = `/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/table/${this.$route.params.table_id}/data/count?timestamp=${this.versionISO || this.lastReload.toISOString()}`
-        if (this.version !== null) {
-          console.info('versioning active', this.version)
-        }
-        const res = await this.$axios.get(url, this.config)
-        this.total = res.data
-        console.info('total', this.total)
-      } catch (error) {
-        console.error('Failed to load count', error)
-        this.error = true
-        this.loadProgress = 100
-        const { status, data } = error.response
-        const { message, code } = data
-        if (status === 423) {
-          console.error('Database is offline', code)
-          this.$toast.error('Database is offline: ' + message)
-        } else {
-          console.error('Failed to load data', code)
-          this.$toast.error('Failed to load data: ' + message)
-        }
-      } finally {
-        this.loadingData--
-      }
+    loadCount () {
+      this.loadingData++
+      TableService.dataCount(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.table_id, (this.versionISO || this.lastReload.toISOString()))
+        .then((count) => {
+          this.total = count
+        })
+        .finally(() => {
+          this.loadingData--
+        })
     },
     simulateProgress () {
       if (this.loadProgress !== 0) {
diff --git a/dbrepo-ui/pages/container/_container_id/database/_database_id/view/_view_id/index.vue b/dbrepo-ui/pages/container/_container_id/database/_database_id/view/_view_id/index.vue
index 55930ae93e6856d48598213f02e4ded25c69ba24..9d95d70e3df1c8fdf6bbdd1d11e97379f4cd8ae8 100644
--- a/dbrepo-ui/pages/container/_container_id/database/_database_id/view/_view_id/index.vue
+++ b/dbrepo-ui/pages/container/_container_id/database/_database_id/view/_view_id/index.vue
@@ -103,7 +103,9 @@
   </div>
 </template>
 <script>
-import { formatTimestampUTCLabel, formatUser } from '@/utils'
+import { formatTimestampUTCLabel } from '@/utils'
+import DatabaseService from '@/api/database.service'
+import UserMapper from '@/api/user.mapper'
 
 export default {
   data () {
@@ -163,7 +165,7 @@ export default {
       if (!this.view) {
         return null
       }
-      return formatUser(this.view.creator)
+      return UserMapper.userToFullName(this.view.creator)
     }
   },
   mounted () {
@@ -171,20 +173,15 @@ export default {
     this.loadResult(this.$route.params.view_id)
   },
   methods: {
-    async loadView () {
+    loadView () {
       this.loadingView = true
-      try {
-        const res = await this.$axios.get(`/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/view/${this.$route.params.view_id}`, this.config)
-        console.debug('view', res.data)
-        this.view = res.data
-      } catch (err) {
-        if (err.response.status !== 401 && err.response.status !== 405) {
-          console.error('Could not load view', err)
-          this.$toast.error('Could not load view')
-        }
-        this.error = true
-      }
-      this.loadingView = false
+      DatabaseService.findView(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.view_id)
+        .then((view) => {
+          this.view = view
+        })
+        .then(() => {
+          this.loadingView = false
+        })
     },
     loadResult (viewId) {
       if (!viewId) {
diff --git a/dbrepo-ui/pages/login.vue b/dbrepo-ui/pages/login.vue
index d7d26e64a919a0c98d63db1a0973502f903eca92..23944a38039347ecd81d7cb6834750c4785c6a7c 100644
--- a/dbrepo-ui/pages/login.vue
+++ b/dbrepo-ui/pages/login.vue
@@ -1,12 +1,12 @@
 <template>
   <div>
-    <v-toolbar flat>
+    <v-toolbar v-if="!token" flat>
       <v-toolbar-title>
         Login
       </v-toolbar-title>
     </v-toolbar>
-    <v-form ref="form" v-model="valid" @submit.prevent="submit">
-      <v-card v-if="!token" flat tile>
+    <v-form v-if="!token" ref="form" v-model="valid" @submit.prevent="submit">
+      <v-card flat tile>
         <v-card-text>
           <v-alert
             border="left"
@@ -53,7 +53,6 @@
         </v-card-actions>
       </v-card>
     </v-form>
-    <p v-if="token">Already logged-in</p>
   </div>
 </template>
 
@@ -72,9 +71,6 @@ export default {
     }
   },
   computed: {
-    loadingColor () {
-      return this.error ? 'red lighten-2' : 'primary'
-    },
     token () {
       return this.$store.state.token
     },
@@ -83,17 +79,11 @@ export default {
     },
     user () {
       return this.$store.state.user
-    },
-    clientSecret () {
-      return this.$config.clientSecret
-    },
-    config () {
-      if (this.token === null) {
-        return {}
-      }
-      return {
-        headers: { Authorization: `Bearer ${this.token}` }
-      }
+    }
+  },
+  mounted () {
+    if (this.token) {
+      this.$router.push('/container')
     }
   },
   methods: {
@@ -106,10 +96,10 @@ export default {
         .then(() => {
           const userId = UserMapper.tokenToUserId(this.token)
           UserService.findOne(userId)
-            .then((user) => {
+            .then(async (user) => {
               this.$store.commit('SET_USER', user)
               this.$vuetify.theme.dark = user.attributes.theme_dark
-              this.$router.push('/container')
+              await this.$router.push('/container')
             })
         })
         .catch(() => {
@@ -118,9 +108,6 @@ export default {
     },
     signup () {
       this.$router.push('/signup')
-    },
-    forgot () {
-      this.$router.push('/forgot')
     }
   }
 }
diff --git a/dbrepo-ui/pages/search/index.vue b/dbrepo-ui/pages/search/index.vue
index d13f084ad5eaee140ee3a3503b7eb9c66aa46757..b812900945a5d81897cddc251086bebdc311db9c 100644
--- a/dbrepo-ui/pages/search/index.vue
+++ b/dbrepo-ui/pages/search/index.vue
@@ -33,6 +33,7 @@
 </template>
 
 <script>
+import SearchService from '@/api/search.service'
 export default {
   data () {
     return {
@@ -58,14 +59,6 @@ export default {
         return `${this.results.length} results`
       }
       return `${this.results.length} result`
-    },
-    elasticConfig () {
-      return {
-        auth: {
-          username: 'elastic',
-          password: this.$config.elasticPassword
-        }
-      }
     }
   },
   watch: {
@@ -90,20 +83,18 @@ export default {
     }
   },
   methods: {
-    async retrieve () {
+    retrieve () {
       if (this.loading) {
         return
       }
       this.loading = true
-      try {
-        const res = await this.$axios.get(`/retrieve/_all/_search?q=${this.query}*&terminate_after=50`, this.elasticConfig)
-        console.info('search results', res.data.hits.total.value)
-        console.debug('search results for', this.$route.query.q, 'are', res.data.hits.hits)
-        this.results = res.data.hits.hits.map(h => h._source)
-      } catch (err) {
-        console.error('Failed to load search results', err)
-      }
-      this.loading = false
+      SearchService.search(this.query)
+        .then((hits) => {
+          this.results = hits.map(h => h._source)
+        })
+        .finally(() => {
+          this.loading = false
+        })
     },
     isDatabase (item) {
       if (!item) {