diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/SecurityConfig.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/SecurityConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..fa123439669c224cfb20866febc9f11a355ffc6e
--- /dev/null
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/SecurityConfig.java
@@ -0,0 +1,41 @@
+package it.inaf.ia2.vospace.ui;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class SecurityConfig {
+
+    private static final Logger LOG = LoggerFactory.getLogger(SecurityConfig.class);
+
+    @Value("${cors.allowed.origin}")
+    private String corsAllowedOrigin;
+
+    /**
+     * CORS are necessary only for development (API access from npm server).
+     */
+    @Bean
+    @Profile("dev")
+    public WebMvcConfigurer corsConfigurer() {
+
+        return new WebMvcConfigurer() {
+
+            @Override
+            public void addCorsMappings(CorsRegistry registry) {
+
+                LOG.warn("Development profile active: CORS filter enabled");
+
+                registry.addMapping("/**")
+                        .allowedOrigins(corsAllowedOrigin)
+                        .allowedMethods("*")
+                        .allowCredentials(true);
+            }
+        };
+    }
+}
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceException.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceException.java
new file mode 100644
index 0000000000000000000000000000000000000000..2b227f5cd5b27b4bd956a6bb2690a0b5e7aebb80
--- /dev/null
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceException.java
@@ -0,0 +1,12 @@
+package it.inaf.ia2.vospace.ui;
+
+public class VOSpaceException extends RuntimeException {
+
+    public VOSpaceException(String message) {
+        super(message);
+    }
+
+    public VOSpaceException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VospaceUiApplication.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java
similarity index 69%
rename from vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VospaceUiApplication.java
rename to vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java
index ad9aac9ef8cee7664dca87a71b666c9a4fef9788..010de2fa4abd09bf7dc2a68c10b09a4c19e36ce0 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VospaceUiApplication.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java
@@ -4,9 +4,9 @@ import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 
 @SpringBootApplication
-public class VospaceUiApplication {
+public class VOSpaceUiApplication {
 
     public static void main(String[] args) {
-        SpringApplication.run(VospaceUiApplication.class, args);
+        SpringApplication.run(VOSpaceUiApplication.class, args);
     }
 }
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java
new file mode 100644
index 0000000000000000000000000000000000000000..9a0aebbd4c08b9e0e84b5038353ced4ed10c5341
--- /dev/null
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java
@@ -0,0 +1,121 @@
+package it.inaf.ia2.vospace.ui.client;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import it.inaf.ia2.vospace.ui.VOSpaceException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.ConnectException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.util.Scanner;
+import java.util.concurrent.CompletionException;
+import java.util.function.Function;
+import net.ivoa.xml.vospace.v2.Node;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Component
+public class VOSpaceClient {
+
+    private static final Logger LOG = LoggerFactory.getLogger(VOSpaceClient.class);
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private final HttpClient httpClient;
+    private final String baseUrl;
+
+    public VOSpaceClient(@Value("${vospace-backend-url}") String backendUrl) {
+        if (backendUrl.endsWith("/")) {
+            // Remove final slash from configured URL
+            backendUrl = backendUrl.substring(0, backendUrl.length() - 1);
+        }
+        baseUrl = backendUrl;
+
+        httpClient = HttpClient.newBuilder()
+                .followRedirects(HttpClient.Redirect.ALWAYS)
+                .version(HttpClient.Version.HTTP_1_1)
+                .build();
+    }
+
+    public Node getNode(String path) {
+
+        HttpRequest request = getRequest("/nodes/" + path)
+                .header("Accept", "application/json")
+                .build();
+
+        return call(request, BodyHandlers.ofInputStream(), 200, res -> parseJson(res, Node.class));
+    }
+
+    private <T, U> U call(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler, int expectedStatusCode, Function<T, U> responseHandler) {
+        try {
+            return httpClient.sendAsync(request, responseBodyHandler)
+                    .thenApply(response -> {
+                        if (response.statusCode() == expectedStatusCode) {
+                            return response.body();
+                        }
+                        logServerError(request, response);
+                        throw new VOSpaceException("Error calling " + request.uri().toString() + ". Server response code is " + response.statusCode());
+                    })
+                    .thenApply(response -> responseHandler.apply(response))
+                    .join();
+        } catch (CompletionException ex) {
+            if (ex.getCause() != null) {
+                if (ex.getCause() instanceof ConnectException) {
+                    throw new VOSpaceException("Cannot connect to " + request.uri().getHost() + " on port " + request.uri().getPort());
+                }
+                if (ex.getCause() instanceof VOSpaceException) {
+                    throw (VOSpaceException) ex.getCause();
+                }
+                LOG.error("Error calling " + request.uri().toString(), ex.getCause());
+                throw new VOSpaceException("Error calling " + request.uri().toString(), ex.getCause());
+            }
+            LOG.error("Error calling " + request.uri().toString(), ex);
+            throw new VOSpaceException("Error calling " + request.uri().toString(), ex);
+        }
+    }
+
+    private HttpRequest.Builder getRequest(String path) {
+        return HttpRequest.newBuilder(URI.create(baseUrl + path));
+    }
+
+    private static <T> T parseJson(InputStream in, Class<T> type) {
+        try {
+            return MAPPER.readValue(in, type);
+        } catch (IOException ex) {
+            LOG.error("Invalid JSON for class {}", type.getCanonicalName());
+            throw new UncheckedIOException(ex);
+        }
+    }
+
+    private static <T> void logServerError(HttpRequest request, HttpResponse<T> response) {
+        if (response.body() instanceof String) {
+            logServerErrorString(request, (HttpResponse<String>) response);
+        } else if (response.body() instanceof InputStream) {
+            logServerErrorInputStream(request, (HttpResponse<InputStream>) response);
+        } else {
+            throw new UnsupportedOperationException("Unable to log error for response body type " + response.body().getClass().getCanonicalName());
+        }
+    }
+
+    private static void logServerErrorString(HttpRequest request, HttpResponse<String> response) {
+
+        LOG.error("Error while reading " + request.uri()
+                + "\nServer response status code is " + response.statusCode()
+                + "\nServer response text is " + response.body());
+    }
+
+    private static void logServerErrorInputStream(HttpRequest request, HttpResponse<InputStream> response) {
+        Scanner s = new Scanner(response.body()).useDelimiter("\\A");
+        String responseBody = s.hasNext() ? s.next() : "";
+        String error = "Error while reading " + request.uri()
+                + "\nServer response status code is " + response.statusCode()
+                + "\nServer response text is " + responseBody;
+        LOG.error(error);
+    }
+}
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/IndexController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/IndexController.java
new file mode 100644
index 0000000000000000000000000000000000000000..a0c826534f17bc15c319f5a186941fb852cc3754
--- /dev/null
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/IndexController.java
@@ -0,0 +1,16 @@
+package it.inaf.ia2.vospace.ui.controller;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+
+@Controller
+public class IndexController {
+
+    @GetMapping(value = "/", produces = MediaType.TEXT_HTML_VALUE)
+    public String index(HttpServletRequest request, HttpServletResponse response) {
+        return "index.html";
+    }
+}
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java
new file mode 100644
index 0000000000000000000000000000000000000000..9f406d101c53266166ea6321a8040989ac99d672
--- /dev/null
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java
@@ -0,0 +1,32 @@
+package it.inaf.ia2.vospace.ui.controller;
+
+import it.inaf.ia2.vospace.ui.service.NodesService;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class NodesController {
+
+    @Autowired
+    private NodesService nodesService;
+
+    /**
+     * This is the only API endpoint that returns HTML code instead of JSON. The
+     * reason is that JavaScript frameworks are not very efficient in handling
+     * very long lists and tables, so this part of the code is generated
+     * server-side.
+     */
+    @GetMapping(value = {"/nodes", "/nodes/{path}"}, produces = MediaType.TEXT_HTML_VALUE)
+    public void listNodes(@PathVariable(value = "path", required = false) String path, HttpServletResponse response) throws Exception {
+
+        if (path == null || path.isBlank()) {
+            path = "/";
+        }
+
+        nodesService.generateNodesHtml(path, response.getOutputStream());
+    }
+}
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java
new file mode 100644
index 0000000000000000000000000000000000000000..96c21b822d6f80ea45380cdbc5c960242891d178
--- /dev/null
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java
@@ -0,0 +1,60 @@
+package it.inaf.ia2.vospace.ui.service;
+
+import it.inaf.ia2.vospace.ui.VOSpaceException;
+import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import net.ivoa.xml.vospace.v2.ContainerNode;
+import net.ivoa.xml.vospace.v2.Node;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Service
+public class NodesService {
+
+    @Autowired
+    private VOSpaceClient client;
+
+    @Value("${vospace-authority}")
+    private String authority;
+
+    public void generateNodesHtml(String path, OutputStream out) {
+
+        Node node = client.getNode(path);
+
+        try ( PrintWriter pw = new PrintWriter(out)) {
+
+            if (node instanceof ContainerNode) {
+                ContainerNode folder = (ContainerNode) node;
+                pw.println("<tbody id=\"nodes\">");
+                for (Node child : folder.getNodes().getNode()) {
+                    pw.println(getNodeHtml(child));
+                }
+                pw.println("</tbody>");
+            }
+        }
+    }
+
+    private String getNodeHtml(Node node) {
+        String html = "<tr>";
+        html += "<td><a href=\"#\">";
+        html += getName(node);
+        html += "</a></td>";
+        html += "</tr>";
+        return html;
+    }
+
+    private String getName(Node node) {
+
+        String uri = node.getUri();
+
+        String prefix = "vos://" + authority;
+
+        if (!uri.startsWith(prefix)) {
+            throw new VOSpaceException("Node authority is different from configured one! Configured is " + authority + ", but node URI is " + uri);
+        }
+
+        return uri.substring(prefix.length() + 1);
+    }
+}
diff --git a/vospace-ui-backend/src/main/resources/application.properties b/vospace-ui-backend/src/main/resources/application.properties
index 8b137891791fe96927ad78e64b0aad7bded08bdc..934e28401708dbfcd3b590f1385ba466ca827601 100644
--- a/vospace-ui-backend/src/main/resources/application.properties
+++ b/vospace-ui-backend/src/main/resources/application.properties
@@ -1 +1,8 @@
+server.port=8085
 
+vospace-backend-url=http://localhost:8083/vospace
+vospace-authority=example.com!vospace
+
+# For development only:
+spring.profiles.active=dev
+cors.allowed.origin=http://localhost:8080
diff --git a/vospace-ui-frontend/.env.production b/vospace-ui-frontend/.env.production
new file mode 100644
index 0000000000000000000000000000000000000000..6010f55022316c82ff11bfeeb216720afd9614ea
--- /dev/null
+++ b/vospace-ui-frontend/.env.production
@@ -0,0 +1,2 @@
+VUE_APP_API_CLIENT = 'server'
+VUE_APP_API_BASE_URL = ''
diff --git a/vospace-ui-frontend/package-lock.json b/vospace-ui-frontend/package-lock.json
index 178aac529ca10dd4405c86f182502433c624fef5..80ecd14f2baab7fa25a6b2b9535978bf8550446d 100644
--- a/vospace-ui-frontend/package-lock.json
+++ b/vospace-ui-frontend/package-lock.json
@@ -2538,6 +2538,14 @@
       "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
       "dev": true
     },
+    "axios": {
+      "version": "0.21.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz",
+      "integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==",
+      "requires": {
+        "follow-redirects": "^1.10.0"
+      }
+    },
     "babel-eslint": {
       "version": "10.1.0",
       "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz",
@@ -5523,8 +5531,7 @@
     "follow-redirects": {
       "version": "1.13.0",
       "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz",
-      "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==",
-      "dev": true
+      "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA=="
     },
     "for-in": {
       "version": "1.0.2",
diff --git a/vospace-ui-frontend/package.json b/vospace-ui-frontend/package.json
index 1864acb38c91a2f3bf0a99de79b07bffb8a113da..c452bb8b40c5640b7f051e973b68f50f1bb4a4cf 100644
--- a/vospace-ui-frontend/package.json
+++ b/vospace-ui-frontend/package.json
@@ -8,6 +8,7 @@
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
+    "axios": "^0.21.0",
     "bootstrap-vue": "^2.19.0",
     "core-js": "^3.6.5",
     "vue": "^2.6.11",
diff --git a/vospace-ui-frontend/src/App.vue b/vospace-ui-frontend/src/App.vue
index 2363d6b7c2837555c5f10609f66f69f5c18a4e0b..938c672de1302520b952ab38f2680ae70823b3fb 100644
--- a/vospace-ui-frontend/src/App.vue
+++ b/vospace-ui-frontend/src/App.vue
@@ -1,9 +1,48 @@
 <template>
   <div id="app">
+    <TopMenu />
+    <div class="container">
+      <router-view></router-view>
+    </div>
+    <div id="footer-fix"></div>
+    <footer class="text-center" id="site-footer">
+        —&nbsp;Powered by <img alt="IA2 logo" src="./assets/ia2-logo-footer.png">
+        <strong class="text-primary"><a href="http://www.ia2.inaf.it/" target="blank_">IA2</a></strong>&nbsp;—
+    </footer>
+    <div id="loading" v-if="loading">
+      <div class="spinner-wrapper">
+        <b-spinner variant="primary" style="width: 3rem; height: 3rem;" label="Loading"></b-spinner>
+      </div>
+    </div>
   </div>
 </template>
 
 <script>
+import { mapState } from 'vuex'
+import TopMenu from './components/TopMenu.vue'
+
+export default {
+  name: 'App',
+  computed: mapState({
+    loading: state => state.loading
+  }),
+  components: {
+    TopMenu
+  },
+  mounted: function() {
+    var self = this;
+    document.addEventListener('apiError', function(event) {
+      self.$bvToast.toast(event.message.body, {
+        title: event.message.title,
+        variant: 'danger',
+        solid: true
+      });
+    });
+    document.addEventListener('loading', function(event) {
+      self.$store.commit('setLoading', event.value);
+    });
+  }
+}
 </script>
 
 <style>
@@ -13,6 +52,40 @@
   -moz-osx-font-smoothing: grayscale;
   text-align: center;
   color: #2c3e50;
-  margin-top: 60px;
+}
+
+#footer-fix {
+    display: block;
+    width: 100%;
+    height: 100px;
+}
+
+#site-footer {
+    color: #666;
+    border-top: 1px #e7e7e7 solid;
+    background-color: #f8f8f8;
+    padding: 5px 0;
+    position: fixed;
+    bottom: 0;
+    right: 0;
+    left: 0;
+}
+
+#loading {
+  position: fixed;
+  top: 0;
+  bottom: 0;
+  right: 0;
+  left: 0;
+  background-color: rgba(255, 255, 255, 0.7);
+  z-index: 1000;
+}
+
+.spinner-wrapper {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100%;
+  width: 100%;
 }
 </style>
diff --git a/vospace-ui-frontend/src/api/server/index.js b/vospace-ui-frontend/src/api/server/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..31c8eb7d8192fed385abf5bf863531b24dfbfa0a
--- /dev/null
+++ b/vospace-ui-frontend/src/api/server/index.js
@@ -0,0 +1,66 @@
+const BASE_API_URL = process.env.VUE_APP_API_BASE_URL;
+
+import axios from 'axios';
+
+function apiRequest(options, showLoading = true, handleValidationErrors = false) {
+  if (showLoading) {
+    loading(true);
+  }
+  return new Promise((resolve, reject) => {
+    axios(options)
+      .then(response => {
+        if (response.status === 204) {
+          resolve({});
+        } else {
+          resolve(response.data);
+        }
+        loading(false);
+      })
+      .catch(error => {
+        loading(false);
+        if (handleValidationErrors && error.response && error.response.status === 400) {
+          reject(error.response.data);
+        } else {
+          dispatchApiErrorEvent(error);
+        }
+      });
+  });
+}
+
+function dispatchApiErrorEvent(error) {
+  let event = new CustomEvent('apiError');
+  let errorMessage;
+  if (error.response && error.response.data && error.response.data.message) {
+    errorMessage = error.response.data.message;
+  } else if (error.message) {
+    errorMessage = error.message;
+  } else {
+    errorMessage = 'Unknown error';
+  }
+  event.message = {
+    title: error.error || 'Error',
+    body: errorMessage
+  };
+  document.dispatchEvent(event);
+}
+
+/* For loading animation */
+function loading(value) {
+  let event = new CustomEvent('loading');
+  event.value = value;
+  document.dispatchEvent(event);
+}
+
+export default {
+  getNode(path) {
+    let url = BASE_API_URL + 'nodes' + path;
+    return apiRequest({
+      method: 'GET',
+      url: url,
+      withCredentials: true,
+      headers: {
+        'Cache-Control': 'no-cache'
+      }
+    }, true, true);
+  }
+}
diff --git a/vospace-ui-frontend/src/assets/ia2-logo-footer.png b/vospace-ui-frontend/src/assets/ia2-logo-footer.png
new file mode 100644
index 0000000000000000000000000000000000000000..cb913c9d1bfb1815de802febddfdec6dd2b56552
Binary files /dev/null and b/vospace-ui-frontend/src/assets/ia2-logo-footer.png differ
diff --git a/vospace-ui-frontend/src/components/Main.vue b/vospace-ui-frontend/src/components/Main.vue
index 7b8b46cb048401ef9ed9fcfed81ea73eb7b0bd35..482dffd8d8f3c1736916ac0a52d594e139646aa3 100644
--- a/vospace-ui-frontend/src/components/Main.vue
+++ b/vospace-ui-frontend/src/components/Main.vue
@@ -1,3 +1,27 @@
 <template>
-  <div></div>
+<div class="container">
+  <b-card>
+    <table class="table b-table table-striped table-hover">
+      <thead>
+        <tr>
+          <th>File</th>
+        </tr>
+      </thead>
+      <tbody id="nodes"></tbody>
+    </table>
+  </b-card>
+</div>
 </template>
+
+<script>
+import client from 'api-client';
+
+export default {
+  mounted() {
+    client.getNode(this.$store.state.path)
+      .then(res => {
+        document.getElementById('nodes').outerHTML = res;
+      });
+  }
+}
+</script>
diff --git a/vospace-ui-frontend/src/components/TopMenu.vue b/vospace-ui-frontend/src/components/TopMenu.vue
new file mode 100644
index 0000000000000000000000000000000000000000..1115343a9e7ffbd5b1defe3993f5c3dcbd617efc
--- /dev/null
+++ b/vospace-ui-frontend/src/components/TopMenu.vue
@@ -0,0 +1,14 @@
+<template>
+<div>
+  <b-navbar toggleable="lg" type="dark" id="top-menu">
+    <b-navbar-brand href="#" class="d-none d-md-block">VOSpace</b-navbar-brand>
+  </b-navbar>
+</div>
+</template>
+
+<style>
+#top-menu {
+  background-color: #093784;
+  margin-bottom: 20px;
+}
+</style>
diff --git a/vospace-ui-frontend/src/main.js b/vospace-ui-frontend/src/main.js
index 8761db38cfb514741f54f9da615cb33b615beff3..9324cf322260b752aa4ad03f43c26bb27a4823f8 100644
--- a/vospace-ui-frontend/src/main.js
+++ b/vospace-ui-frontend/src/main.js
@@ -1,6 +1,7 @@
 import Vue from 'vue'
 import App from './App.vue'
 import store from './store.js'
+import './plugins/bootstrap-vue'
 
 import { BootstrapVue } from 'bootstrap-vue'
 
diff --git a/vospace-ui-frontend/src/plugins/bootstrap-vue.js b/vospace-ui-frontend/src/plugins/bootstrap-vue.js
new file mode 100644
index 0000000000000000000000000000000000000000..b9d414fabeae0480e997423c3a02ef5414b5a0b3
--- /dev/null
+++ b/vospace-ui-frontend/src/plugins/bootstrap-vue.js
@@ -0,0 +1,7 @@
+import Vue from 'vue';
+
+import { BootstrapVue } from 'bootstrap-vue';
+import 'bootstrap/dist/css/bootstrap.min.css';
+import 'bootstrap-vue/dist/bootstrap-vue.css';
+
+Vue.use(BootstrapVue);
diff --git a/vospace-ui-frontend/src/store.js b/vospace-ui-frontend/src/store.js
index 4eadaf171bc563471dcabc07bb995fe65f162c76..464672b52388d1583acf79eb5fc412e832355318 100644
--- a/vospace-ui-frontend/src/store.js
+++ b/vospace-ui-frontend/src/store.js
@@ -7,9 +7,13 @@ Vue.use(Vuex);
 
 export default new Vuex.Store({
   state: {
+    path: '/',
+    loading: true
   },
   mutations: {
+    setLoading(state, loading) {
+      state.loading = loading;
+    }
   },
-  actions: {
-  }
+  actions: {}
 });
diff --git a/vospace-ui-frontend/vue.config.js b/vospace-ui-frontend/vue.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..93ef4cbf7ba4ae11f7d8cde4b5cfd575e48eeae1
--- /dev/null
+++ b/vospace-ui-frontend/vue.config.js
@@ -0,0 +1,12 @@
+const path = require('path');
+
+module.exports = {
+  publicPath: '',
+  chainWebpack: config => {
+    const apiClient = process.env.VUE_APP_API_CLIENT // mock or server
+    config.resolve.alias.set(
+      'api-client',
+      path.resolve(__dirname, `src/api/${apiClient}`)
+    )
+  }
+}