diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 206bb50cc4a08fd53c59dcfa64c7f44427f2c823..4005c8911d6107b04c4686fc931459d382d49ddd 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -7,11 +7,53 @@ variables:
 
 
 stages:
+  - review
   - lint
   - test
   - build
   - deploy
 
+review:
+  stage: review
+  image: node:9
+  when: manual
+  allow_failure: true
+  before_script:
+    - cd front
+  script:
+    - yarn install
+    # this is to ensure we don't have any errors in the output,
+    # cf https://code.eliotberriot.com/funkwhale/funkwhale/issues/169
+    - INSTANCE_URL=$REVIEW_INSTANCE_URL yarn run build | tee /dev/stderr | (! grep -i 'ERROR in')
+    - mkdir -p /static/$CI_BUILD_REF_SLUG
+    - cp -r dist/* /static/$CI_BUILD_REF_SLUG
+  cache:
+    key: "$CI_PROJECT_ID__front_dependencies"
+    paths:
+      - front/node_modules
+      - front/yarn.lock
+  environment:
+    name: review/$CI_BUILD_REF_NAME
+    url: http://$CI_BUILD_REF_SLUG.$REVIEW_DOMAIN
+    on_stop: stop_review
+  only:
+    - branches@funkwhale/funkwhale
+  tags:
+    - funkwhale-review
+
+stop_review:
+  stage: review
+  script:
+    - rm -rf /static/$CI_BUILD_REF_SLUG/
+  variables:
+    GIT_STRATEGY: none
+  when: manual
+  environment:
+    name: review/$CI_BUILD_REF_NAME
+    action: stop
+  tags:
+    - funkwhale-review
+
 black:
   image: python:3.6
   stage: lint
diff --git a/CONTRIBUTING b/CONTRIBUTING
index 33f2c07478b2535a86bafed379860ca6ea17ad73..6fb76a56c08d0699dc4f69318552d7ef0f5b3de8 100644
--- a/CONTRIBUTING
+++ b/CONTRIBUTING
@@ -12,6 +12,42 @@ This document will guide you through common operations such as:
 - Writing unit tests to validate your work
 - Submit your work
 
+A quick path to contribute on the front-end
+-------------------------------------------
+
+The next sections of this document include a full installation guide to help
+you setup a local, development version of Funkwhale. If you only want to fix small things
+on the front-end, and don't want to manage a full development environment, there is anoter way.
+
+As the front-end can work with any Funkwhale server, you can work with the front-end only,
+and make it talk with an existing instance (like the demo one, or you own instance, if you have one).
+
+If even that is too much for you, you can also make your changes without any development environment,
+and open a merge request. We will be able to to review your work easily by spawning automatically a
+live version of your changes, thanks to Gitlab Review apps.
+
+Setup front-end only development environment
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+1. Clone the repository::
+
+    git clone ssh://git@code.eliotberriot.com:2222/funkwhale/funkwhale.git
+    cd funkwhale
+    cd front
+
+2. Install [nodejs](https://nodejs.org/en/download/package-manager/) and [yarn](https://yarnpkg.com/lang/en/docs/install/#debian-stable)
+3. Install the dependencies::
+
+    yarn install
+
+4. Launch the development server::
+
+    # this will serve the front-end on http://localhost:8000
+    WEBPACK_DEVSERVER_PORT=8000 yarn dev
+
+5. Make the front-end talk with an existing server (like https://demo.funkwhale.audio),
+   by clicking on the corresponding link in the footer
+6. Start hacking!
 
 Setup your development environment
 ----------------------------------
diff --git a/changes/changelog.d/327.feature b/changes/changelog.d/327.feature
new file mode 100644
index 0000000000000000000000000000000000000000..8e22e6542c64f31bb69a20e0bb094704c9433805
--- /dev/null
+++ b/changes/changelog.d/327.feature
@@ -0,0 +1,22 @@
+Funkwhale's front-end can now point to any instance (#327)
+
+Removed front-end and back-end coupling
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Eventhough Funkwhale's front-end has always been a Single Page Application,
+talking to an API, it was only able to talk to an API on the same domain.
+
+There was no real technical justification behind this (only lazyness), and it was
+also blocking interesting use cases:
+
+- Use multiple customized versions of the front-end with the same instance
+- Use a customized version of the front-end with multiple instances
+- Use a locally hosted front-end with a remote API, which is especially useful in development
+
+From now on, Funkwhale's front-end can connect to any Funkwhale server. You can
+change the server you are connecting to in the footer.
+
+Fixing this also unlocked a really interesting feature in our development/review workflow:
+by leveraging Gitlab CI and review apps, we are now able to deploy automatically live versions of
+a merge request, making it possible for anyone to review front-end changes easily, without
+the need to install a local environment.
diff --git a/front/config/prod.env.js b/front/config/prod.env.js
index decfe36154adc59fbf4a432cecac77119bbcdbf7..40cf48973416fbe1cfaa181e54821b51730f5398 100644
--- a/front/config/prod.env.js
+++ b/front/config/prod.env.js
@@ -1,4 +1,5 @@
+let url = process.env.INSTANCE_URL || '/'
 module.exports = {
   NODE_ENV: '"production"',
-  BACKEND_URL: '"/"'
+  INSTANCE_URL: `"${url}"`
 }
diff --git a/front/src/App.vue b/front/src/App.vue
index 2eb673ab4bf1800920111616091aa80cffba1c08..56dbe0aad41f7af3d382202832ab6828bb431837 100644
--- a/front/src/App.vue
+++ b/front/src/App.vue
@@ -1,44 +1,71 @@
 <template>
   <div id="app">
-    <sidebar></sidebar>
-    <service-messages v-if="messages.length > 0" />
-    <router-view :key="$route.fullPath"></router-view>
-    <div class="ui fitted divider"></div>
-    <div id="footer" class="ui vertical footer segment">
-      <div class="ui container">
-        <div class="ui stackable equal height stackable grid">
-          <div class="three wide column">
-            <i18next tag="h4" class="ui header" path="Links"></i18next>
-            <div class="ui link list">
-              <router-link class="item" to="/about">
-                <i18next path="About this instance" />
-              </router-link>
-              <a href="https://funkwhale.audio" class="item" target="_blank">{{ $t('Official website') }}</a>
-              <a href="https://docs.funkwhale.audio" class="item" target="_blank">{{ $t('Documentation') }}</a>
-              <a href="https://code.eliotberriot.com/funkwhale/funkwhale" class="item" target="_blank">
-                <template v-if="version">{{ $t('Source code ({% version %})', {version: version}) }}</template>
-                <template v-else>{{ $t('Source code') }}</template>
-              </a>
-              <a href="https://code.eliotberriot.com/funkwhale/funkwhale/issues" class="item" target="_blank">{{ $t('Issue tracker') }}</a>
+    <div class="ui main text container instance-chooser" v-if="!$store.state.instance.instanceUrl">
+      <div class="ui padded segment">
+        <h1 class="ui header">{{ $t('Choose your instance') }}</h1>
+        <form class="ui form" @submit.prevent="$store.dispatch('instance/setUrl', instanceUrl)">
+          <p>{{ $t('You need to select an instance in order to continue') }}</p>
+          <div class="ui action input">
+            <input type="text" v-model="instanceUrl">
+            <button type="submit" class="ui button">{{ $t('Submit') }}</button>
+          </div>
+          <p>{{ $t('Suggested choices') }}</p>
+          <div class="ui bulleted list">
+            <div class="ui item" v-for="url in suggestedInstances">
+              <a @click="instanceUrl = url">{{ url }}</a>
             </div>
           </div>
-          <div class="ten wide column">
-            <i18next tag="h4" class="ui header" path="About funkwhale" />
-            <p>
-              <i18next path="Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!"/>
-            </p>
-            <p>
-              <i18next path="The funkwhale logo was kindly designed and provided by Francis Gading."/>
-            </p>
+        </form>
+      </div>
+    </div>
+    <template v-else>
+      <sidebar></sidebar>
+      <service-messages v-if="messages.length > 0" />
+      <router-view :key="$route.fullPath"></router-view>
+      <div class="ui fitted divider"></div>
+      <div id="footer" class="ui vertical footer segment">
+        <div class="ui container">
+          <div class="ui stackable equal height stackable grid">
+            <div class="three wide column">
+              <i18next tag="h4" class="ui header" path="Links"></i18next>
+              <div class="ui link list">
+                <router-link class="item" to="/about">
+                  <i18next path="About this instance" />
+                </router-link>
+                <a href="https://funkwhale.audio" class="item" target="_blank">{{ $t('Official website') }}</a>
+                <a href="https://docs.funkwhale.audio" class="item" target="_blank">{{ $t('Documentation') }}</a>
+                <a href="https://code.eliotberriot.com/funkwhale/funkwhale" class="item" target="_blank">
+                  <template v-if="version">{{ $t('Source code ({% version %})', {version: version}) }}</template>
+                  <template v-else>{{ $t('Source code') }}</template>
+                </a>
+                <a href="https://code.eliotberriot.com/funkwhale/funkwhale/issues" class="item" target="_blank">{{ $t('Issue tracker') }}</a>
+                <a @click="switchInstance" class="item" >
+                  {{ $t('Use another instance') }}
+                  <template v-if="$store.state.instance.instanceUrl !== '/'">
+                    <br>
+                    ({{ $store.state.instance.instanceUrl }})
+                  </template>
+                </a>
+              </div>
+            </div>
+            <div class="ten wide column">
+              <i18next tag="h4" class="ui header" path="About funkwhale" />
+              <p>
+                <i18next path="Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!"/>
+              </p>
+              <p>
+                <i18next path="The funkwhale logo was kindly designed and provided by Francis Gading."/>
+              </p>
+            </div>
           </div>
         </div>
       </div>
-    </div>
-    <raven
-      v-if="$store.state.instance.settings.raven.front_enabled.value"
-      :dsn="$store.state.instance.settings.raven.front_dsn.value">
-    </raven>
-    <playlist-modal v-if="$store.state.auth.authenticated"></playlist-modal>
+      <raven
+        v-if="$store.state.instance.settings.raven.front_enabled.value"
+        :dsn="$store.state.instance.settings.raven.front_dsn.value">
+      </raven>
+      <playlist-modal v-if="$store.state.auth.authenticated"></playlist-modal>
+    </template>
   </div>
 </template>
 
@@ -63,17 +90,22 @@ export default {
   },
   data () {
     return {
-      nodeinfo: null
+      nodeinfo: null,
+      instanceUrl: null
     }
   },
   created () {
-    this.$store.dispatch('instance/fetchSettings')
     let self = this
     setInterval(() => {
       // used to redraw ago dates every minute
       self.$store.commit('ui/computeLastDate')
     }, 1000 * 60)
-    this.fetchNodeInfo()
+    if (this.$store.state.instance.instanceUrl) {
+      this.$store.commit('instance/instanceUrl', this.$store.state.instance.instanceUrl)
+      this.$store.dispatch('auth/check')
+      this.$store.dispatch('instance/fetchSettings')
+      this.fetchNodeInfo()
+    }
   },
   methods: {
     fetchNodeInfo () {
@@ -81,18 +113,38 @@ export default {
       axios.get('instance/nodeinfo/2.0/').then(response => {
         self.nodeinfo = response.data
       })
+    },
+    switchInstance () {
+      let confirm = window.confirm(this.$t('This will erase your local data and disconnect you, do you want to continue?'))
+      if (confirm) {
+        this.$store.commit('instance/instanceUrl', null)
+      }
     }
   },
   computed: {
     ...mapState({
       messages: state => state.ui.messages
     }),
+    suggestedInstances () {
+      let rootUrl = (
+        window.location.protocol + '//' + window.location.hostname +
+        (window.location.port ? ':' + window.location.port : '')
+      )
+      let instances = [rootUrl, 'https://demo.funkwhale.audio']
+      return instances
+    },
     version () {
       if (!this.nodeinfo) {
         return null
       }
       return _.get(this.nodeinfo, 'software.version')
     }
+  },
+  watch: {
+    '$store.state.instance.instanceUrl' () {
+      this.$store.dispatch('instance/fetchSettings')
+      this.fetchNodeInfo()
+    }
   }
 }
 </script>
@@ -116,6 +168,11 @@ html, body {
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
 }
+
+.instance-chooser {
+  margin-top: 2em;
+}
+
 .main.pusher, .footer {
   @include media(">desktop") {
     margin-left: 350px !important;
@@ -173,7 +230,7 @@ html, body {
 .ui.icon.header .circular.icon {
   display: flex;
   justify-content: center;
-  
+
 }
 
 .segment-content .button{
diff --git a/front/src/audio/backend.js b/front/src/audio/backend.js
index 619f3cefdbd7b08f9879be343ce246e41e86de0d..5a82719a3a0d403ce6ee264eed47e2f5926068f0 100644
--- a/front/src/audio/backend.js
+++ b/front/src/audio/backend.js
@@ -1,5 +1,3 @@
-import config from '@/config'
-
 var Album = {
   clean (album) {
     // we manually rebind the album and artist to each child track
@@ -21,21 +19,6 @@ var Artist = {
   }
 }
 export default {
-  absoluteUrl (url) {
-    if (url.startsWith('http')) {
-      return url
-    }
-    if (url.startsWith('/')) {
-      let rootUrl = (
-        window.location.protocol + '//' + window.location.hostname +
-        (window.location.port ? ':' + window.location.port : '')
-      )
-      return rootUrl + url
-    } else {
-      return config.BACKEND_URL + url
-    }
-  },
   Artist: Artist,
   Album: Album
-
 }
diff --git a/front/src/audio/track.js b/front/src/audio/track.js
deleted file mode 100644
index 9873b74ec5405bd941999df63e6071a3ac8056d3..0000000000000000000000000000000000000000
--- a/front/src/audio/track.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import backend from './backend'
-
-export default {
-  getCover (track) {
-    return backend.absoluteUrl(track.album.cover)
-  }
-}
diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue
index 065a0a03a76b039100a1f930e1a346e07a5bfc36..5415e1b0e036e66b83cb72f4e8611b376550e15d 100644
--- a/front/src/components/Sidebar.vue
+++ b/front/src/components/Sidebar.vue
@@ -120,7 +120,7 @@
           <tr @click="$store.dispatch('queue/currentIndex', index)" v-for="(track, index) in queue.tracks" :key="index" :class="[{'active': index === queue.currentIndex}]">
               <td class="right aligned">{{ index + 1}}</td>
               <td class="center aligned">
-                  <img class="ui mini image" v-if="track.album.cover" :src="backend.absoluteUrl(track.album.cover)">
+                  <img class="ui mini image" v-if="track.album.cover" :src="$store.getters['instance/absoluteUrl'](track.album.cover)">
                   <img class="ui mini image" v-else src="../assets/audio/default-cover.png">
               </td>
               <td colspan="4">
diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue
index 3c922e14ad323d75413d969ba7c033add19e92e0..1cc27970b47aabc5571047713d1ee45e3f7b853a 100644
--- a/front/src/components/audio/Player.vue
+++ b/front/src/components/audio/Player.vue
@@ -14,7 +14,7 @@
       <div v-if="currentTrack" class="track-area ui unstackable items">
         <div class="ui inverted item">
           <div class="ui tiny image">
-            <img ref="cover" @load="updateBackground" v-if="currentTrack.album.cover" :src="Track.getCover(currentTrack)">
+            <img ref="cover" @load="updateBackground" v-if="currentTrack.album.cover" :src="$store.getters['instance/absoluteUrl'](currentTrack.album.cover)">
             <img v-else src="../../assets/audio/default-cover.png">
           </div>
           <div class="middle aligned content">
@@ -143,7 +143,6 @@ import {mapState, mapGetters, mapActions} from 'vuex'
 import GlobalEvents from '@/components/utils/global-events'
 import ColorThief from '@/vendor/color-thief'
 
-import Track from '@/audio/track'
 import AudioTrack from '@/components/audio/Track'
 import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
 import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon'
@@ -162,7 +161,6 @@ export default {
       isShuffling: false,
       renderAudio: true,
       sliderVolume: this.volume,
-      Track: Track,
       defaultAmbiantColors: defaultAmbiantColors,
       ambiantColors: defaultAmbiantColors
     }
diff --git a/front/src/components/audio/SearchBar.vue b/front/src/components/audio/SearchBar.vue
index 99896d04beda7bc51a559fb10b8205a45e254610..9b6dc50e2343cf514fced16d4b46d9431494b6db 100644
--- a/front/src/components/audio/SearchBar.vue
+++ b/front/src/components/audio/SearchBar.vue
@@ -11,11 +11,8 @@
 
 <script>
 import jQuery from 'jquery'
-import config from '@/config'
 import router from '@/router'
 
-const SEARCH_URL = config.API_URL + 'search?query={query}'
-
 export default {
   mounted () {
     let self = this
@@ -94,7 +91,7 @@ export default {
           })
           return {results: results}
         },
-        url: SEARCH_URL
+        url: this.$store.getters['instance/absoluteUrl']('search?query={query}')
       }
     })
   }
diff --git a/front/src/components/audio/Track.vue b/front/src/components/audio/Track.vue
index 366f104f1fc021fbe5478346439e94034b345e2a..2ded7cb0076f29b2f78956aae2ff8703fb2da9c8 100644
--- a/front/src/components/audio/Track.vue
+++ b/front/src/components/audio/Track.vue
@@ -49,7 +49,7 @@ export default {
         return []
       }
       let sources = [
-        {type: file.mimetype, url: file.path}
+        {type: file.mimetype, url: this.$store.getters['instance/absoluteUrl'](file.path)}
       ]
       if (this.$store.state.auth.authenticated) {
         // we need to send the token directly in url
diff --git a/front/src/components/audio/album/Card.vue b/front/src/components/audio/album/Card.vue
index 6742dca4f9b94e88983ad6ec6d4396b11dc14648..3782771803edce6432a125957e9d10346b073fbd 100644
--- a/front/src/components/audio/album/Card.vue
+++ b/front/src/components/audio/album/Card.vue
@@ -2,7 +2,7 @@
     <div class="ui card">
       <div class="content">
         <div class="right floated tiny ui image">
-          <img v-if="album.cover" v-lazy="backend.absoluteUrl(album.cover)">
+          <img v-if="album.cover" v-lazy="$store.getters['instance/absoluteUrl'](album.cover)">
           <img v-else src="../../../assets/audio/default-cover.png">
         </div>
         <div class="header">
diff --git a/front/src/components/audio/artist/Card.vue b/front/src/components/audio/artist/Card.vue
index a46506791e083eb5cc750c8f84918a6bc1fb2318..b19c5e12d727b6a32c6a57c04e9dab4b0b9c3134 100644
--- a/front/src/components/audio/artist/Card.vue
+++ b/front/src/components/audio/artist/Card.vue
@@ -11,7 +11,7 @@
             <tbody>
               <tr v-for="album in albums">
                 <td>
-                  <img class="ui mini image" v-if="album.cover" :src="backend.absoluteUrl(album.cover)">
+                  <img class="ui mini image" v-if="album.cover" :src="$store.getters['instance/absoluteUrl'](album.cover)">
                   <img class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
                 </td>
                 <td colspan="4">
diff --git a/front/src/components/audio/track/Row.vue b/front/src/components/audio/track/Row.vue
index 8310e89c4a4ad6ab749f2386003f1c59ae7dccea..bd3ceb2aaa869350fa013ca36c709f16cc97cdbc 100644
--- a/front/src/components/audio/track/Row.vue
+++ b/front/src/components/audio/track/Row.vue
@@ -4,7 +4,7 @@
       <play-button class="basic icon" :discrete="true" :track="track"></play-button>
     </td>
     <td>
-      <img class="ui mini image" v-if="track.album.cover" v-lazy="backend.absoluteUrl(track.album.cover)">
+      <img class="ui mini image" v-if="track.album.cover" v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover)">
       <img class="ui mini image" v-else src="../../..//assets/audio/default-cover.png">
     </td>
     <td colspan="6">
diff --git a/front/src/components/audio/track/Table.vue b/front/src/components/audio/track/Table.vue
index 4559b3c41b59ccf798eec071b97c1ab55a81dceb..81869ff564af2f8b5ee5260535b7fda5f95f924e 100644
--- a/front/src/components/audio/track/Table.vue
+++ b/front/src/components/audio/track/Table.vue
@@ -35,7 +35,7 @@
                 <pre>
 export PRIVATE_TOKEN="{{ $store.state.auth.token }}"
 <template v-for="track in tracks"><template v-if="track.files.length > 0">
-curl -G -o "{{ track.files[0].filename }}" <template v-if="$store.state.auth.authenticated">--header "Authorization: JWT $PRIVATE_TOKEN"</template> "{{ backend.absoluteUrl(track.files[0].path) }}"</template></template>
+curl -G -o "{{ track.files[0].filename }}" <template v-if="$store.state.auth.authenticated">--header "Authorization: JWT $PRIVATE_TOKEN"</template> "{{ $store.getters['instance/absoluteUrl'](track.files[0].path) }}"</template></template>
 </pre>
               </div>
             </div>
diff --git a/front/src/components/library/Album.vue b/front/src/components/library/Album.vue
index 1681d46e3b44466f022f18d22ffa3ba78e1643b7..9a4288b8ac46a8e584fa8dbf23bae3a96fa4e5cc 100644
--- a/front/src/components/library/Album.vue
+++ b/front/src/components/library/Album.vue
@@ -87,7 +87,7 @@ export default {
       if (!this.album.cover) {
         return ''
       }
-      return 'background-image: url(' + backend.absoluteUrl(this.album.cover) + ')'
+      return 'background-image: url(' + this.$store.getters['instance/absoluteUrl'](this.album.cover) + ')'
     }
   },
   watch: {
diff --git a/front/src/components/library/Artist.vue b/front/src/components/library/Artist.vue
index 7d0a41d8988055316ecb4773103e129e970f92c7..171b80e8b7b1f9a2ad8c7a65e9dc3c227e6e1b5a 100644
--- a/front/src/components/library/Artist.vue
+++ b/front/src/components/library/Artist.vue
@@ -127,7 +127,7 @@ export default {
       if (!this.cover) {
         return ''
       }
-      return 'background-image: url(' + backend.absoluteUrl(this.cover) + ')'
+      return 'background-image: url(' + this.$store.getters['instance/absoluteUrl'](this.cover) + ')'
     }
   },
   watch: {
diff --git a/front/src/components/library/Track.vue b/front/src/components/library/Track.vue
index 24acca75b809445ce711f9fb90e38a65f7c974dd..af364e94d6af39e4d459e1cbdc9ab105560d2f5f 100644
--- a/front/src/components/library/Track.vue
+++ b/front/src/components/library/Track.vue
@@ -108,7 +108,6 @@ import time from '@/utils/time'
 import axios from 'axios'
 import url from '@/utils/url'
 import logger from '@/logging'
-import backend from '@/audio/backend'
 import PlayButton from '@/components/audio/PlayButton'
 import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
 import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon'
@@ -169,7 +168,7 @@ export default {
     },
     downloadUrl () {
       if (this.track.files.length > 0) {
-        let u = backend.absoluteUrl(this.track.files[0].path)
+        let u = this.$store.getters['instance/absoluteUrl'](this.track.files[0].path)
         if (this.$store.state.auth.authenticated) {
           u = url.updateQueryString(u, 'jwt', this.$store.state.auth.token)
         }
@@ -191,7 +190,7 @@ export default {
       if (!this.cover) {
         return ''
       }
-      return 'background-image: url(' + backend.absoluteUrl(this.cover) + ')'
+      return 'background-image: url(' + this.$store.getters['instance/absoluteUrl'](this.cover) + ')'
     }
   },
   watch: {
diff --git a/front/src/components/library/radios/Filter.vue b/front/src/components/library/radios/Filter.vue
index b27c36077c113b801bf842f84ad868cfecc87542..0d268dc60faad8355650aed431ad4174d22ce464 100644
--- a/front/src/components/library/radios/Filter.vue
+++ b/front/src/components/library/radios/Filter.vue
@@ -63,7 +63,6 @@
 </template>
 <script>
 import axios from 'axios'
-import config from '@/config'
 import $ from 'jquery'
 import _ from 'lodash'
 
@@ -86,7 +85,7 @@ export default {
     return {
       checkResult: null,
       showCandidadesModal: false,
-      exclude: config.not
+      exclude: this.config.not
     }
   },
   mounted: function () {
diff --git a/front/src/components/manage/users/InvitationForm.vue b/front/src/components/manage/users/InvitationForm.vue
index 9429c1ae16294c49b91aa5a6ee43780a7b596c90..d9f0969e67853c841cbb0a10ebd9735792723d97 100644
--- a/front/src/components/manage/users/InvitationForm.vue
+++ b/front/src/components/manage/users/InvitationForm.vue
@@ -43,8 +43,6 @@
 <script>
 import axios from 'axios'
 
-import backend from '@/audio/backend'
-
 export default {
   data () {
     return {
@@ -72,7 +70,7 @@ export default {
       })
     },
     getUrl (code) {
-      return backend.absoluteUrl(this.$router.resolve({name: 'signup', query: {invitation: code.toUpperCase()}}).href)
+      return this.$store.getters['instance/absoluteUrl'](this.$router.resolve({name: 'signup', query: {invitation: code.toUpperCase()}}).href)
     }
   }
 }
diff --git a/front/src/components/metadata/Search.vue b/front/src/components/metadata/Search.vue
index 305aa7a3d6b3aa7e695c9613f566946c3e7be805..0379cb188ae7e8739ca8c2e5247856f9110dbfe7 100644
--- a/front/src/components/metadata/Search.vue
+++ b/front/src/components/metadata/Search.vue
@@ -22,7 +22,6 @@
 
 <script>
 import jQuery from 'jquery'
-import config from '@/config'
 
 export default {
   props: {
@@ -117,7 +116,7 @@ export default {
       })[0]
     },
     searchUrl: function () {
-      return config.API_URL + 'providers/musicbrainz/search/' + this.currentTypeObject.value + 's/?query={query}'
+      return this.$store.getters['instance/absoluteUrl']('providers/musicbrainz/search/' + this.currentTypeObject.value + 's/?query={query}')
     },
     types: function () {
       return [
diff --git a/front/src/config.js b/front/src/config.js
deleted file mode 100644
index 47d9d7b8b2cdc83ded7330e2400c48d4b72b8e2e..0000000000000000000000000000000000000000
--- a/front/src/config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-class Config {
-  constructor () {
-    this.BACKEND_URL = process.env.BACKEND_URL
-    this.API_URL = this.BACKEND_URL + 'api/v1/'
-  }
-}
-
-export default new Config()
diff --git a/front/src/main.js b/front/src/main.js
index 7973e4bb7e87222097c649f44fbba13c318a7256..181fd66b3b63197660794af819c24fa1751866fb 100644
--- a/front/src/main.js
+++ b/front/src/main.js
@@ -15,7 +15,6 @@ import i18next from 'i18next'
 import i18nextFetch from 'i18next-fetch-backend'
 import VueI18Next from '@panter/vue-i18next'
 import store from './store'
-import config from './config'
 import { sync } from 'vuex-router-sync'
 import filters from '@/filters' // eslint-disable-line
 import globals from '@/components/globals' // eslint-disable-line
@@ -56,8 +55,6 @@ Vue.directive('title', {
     document.title = parts.join(' - ')
   }
 })
-
-axios.defaults.baseURL = config.API_URL
 axios.interceptors.request.use(function (config) {
   // Do something before request is sent
   if (store.state.auth.token) {
@@ -104,7 +101,6 @@ axios.interceptors.response.use(function (response) {
   // Do something with response error
   return Promise.reject(error)
 })
-store.dispatch('auth/check')
 
 // i18n
 i18next
diff --git a/front/src/store/index.js b/front/src/store/index.js
index 298fa04ec13166fead7a12955636d3bc4340948a..0c2908d83d5cceee72997111f099aeda555d2337 100644
--- a/front/src/store/index.js
+++ b/front/src/store/index.js
@@ -34,7 +34,7 @@ export default new Vuex.Store({
     }),
     createPersistedState({
       key: 'instance',
-      paths: ['instance.events']
+      paths: ['instance.events', 'instance.instanceUrl']
     }),
     createPersistedState({
       key: 'radios',
diff --git a/front/src/store/instance.js b/front/src/store/instance.js
index e78e804898c8c02a1f237297d3ab3dc653e62c0e..555bd82391fe56a4ec5d18baa71e0e64dceabf35 100644
--- a/front/src/store/instance.js
+++ b/front/src/store/instance.js
@@ -6,6 +6,7 @@ export default {
   namespaced: true,
   state: {
     maxEvents: 200,
+    instanceUrl: process.env.INSTANCE_URL,
     events: [],
     settings: {
       instance: {
@@ -51,9 +52,46 @@ export default {
     },
     events: (state, value) => {
       state.events = value
+    },
+    instanceUrl: (state, value) => {
+      state.instanceUrl = value
+      if (!value) {
+        axios.defaults.baseURL = null
+        return
+      }
+      let apiUrl
+      let suffix = 'api/v1/'
+      if (state.instanceUrl.endsWith('/')) {
+        apiUrl = state.instanceUrl + suffix
+      } else {
+        apiUrl = state.instanceUrl + '/' + suffix
+      }
+      axios.defaults.baseURL = apiUrl
+    }
+  },
+  getters: {
+    absoluteUrl: (state) => (relativeUrl) => {
+      if (relativeUrl.startsWith('http')) {
+        return relativeUrl
+      }
+      return state.instanceUrl + relativeUrl
     }
   },
   actions: {
+    setUrl ({commit, dispatch}, url) {
+      commit('instanceUrl', url)
+      let modules = [
+        'auth',
+        'favorites',
+        'player',
+        'playlists',
+        'queue',
+        'radios'
+      ]
+      modules.forEach(m => {
+        commit(`${m}/reset`, null, {root: true})
+      })
+    },
     // Send a request to the login URL and save the returned JWT
     fetchSettings ({commit}, payload) {
       return axios.get('instance/settings/').then(response => {