diff --git a/front/src/App.vue b/front/src/App.vue
index 673f8386460ecba32737c129e3421adc06881f04..91cf29843429eb06e55d1aefd861b7eb3f81f1b4 100644
--- a/front/src/App.vue
+++ b/front/src/App.vue
@@ -1,6 +1,7 @@
 <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">
@@ -44,9 +45,11 @@
 <script>
 import axios from 'axios'
 import _ from 'lodash'
+import {mapState} from 'vuex'
 
 import Sidebar from '@/components/Sidebar'
 import Raven from '@/components/Raven'
+import ServiceMessages from '@/components/ServiceMessages'
 
 import PlaylistModal from '@/components/playlists/PlaylistModal'
 
@@ -55,7 +58,8 @@ export default {
   components: {
     Sidebar,
     Raven,
-    PlaylistModal
+    PlaylistModal,
+    ServiceMessages
   },
   data () {
     return {
@@ -80,6 +84,9 @@ export default {
     }
   },
   computed: {
+    ...mapState({
+      messages: state => state.ui.messages
+    }),
     version () {
       if (!this.nodeinfo) {
         return null
@@ -115,6 +122,14 @@ html, body {
   }
   transform: none !important;
 }
+.service-messages {
+  position: fixed;
+  bottom: 1em;
+  left: 1em;
+  @include media(">desktop") {
+    left: 350px;
+  }
+}
 .main-pusher {
   padding: 1.5rem 0;
 }
diff --git a/front/src/components/ServiceMessages.vue b/front/src/components/ServiceMessages.vue
new file mode 100644
index 0000000000000000000000000000000000000000..90f6f06791785d5fb675f84c7a21647b2077178f
--- /dev/null
+++ b/front/src/components/ServiceMessages.vue
@@ -0,0 +1,83 @@
+<template>
+  <div class="service-messages">
+    <message v-for="message in displayedMessages" :key="String(message.date)" :class="['large', getLevel(message)]">
+      <p>{{ message.content }}</p>
+    </message>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash'
+import {mapState} from 'vuex'
+
+export default {
+  data () {
+    return {
+      date: new Date(),
+      interval: null
+    }
+  },
+  created () {
+    this.setupInterval()
+  },
+  destroyed () {
+    if (this.interval) {
+      clearInterval(this.interval)
+    }
+  },
+  computed: {
+    ...mapState({
+      messages: state => state.ui.messages,
+      displayDuration: state => state.ui.messageDisplayDuration
+    }),
+    displayedMessages () {
+      let now = this.date
+      let interval = this.displayDuration
+      let toDisplay = this.messages.filter(m => {
+        return now - m.date <= interval
+      })
+      return _.reverse(toDisplay).slice(0, 5)
+    }
+  },
+  methods: {
+    setupInterval () {
+      if (this.interval) {
+        return
+      }
+      let self = this
+      this.interval = setInterval(() => {
+        if (self.displayedMessages.length === 0) {
+          clearInterval(self.interval)
+          this.interval = null
+        }
+        self.date = new Date()
+      }, 1000)
+    },
+    getLevel (message) {
+      return message.level || 'blue'
+    }
+  },
+  watch: {
+    messages: {
+      handler (v) {
+        if (v.length > 0 && !this.interval) {
+          this.setupInterval()
+        }
+      },
+      deep: true
+    }
+  }
+}
+</script>
+
+<style>
+.service-messages {
+  z-index: 9999;
+  margin-left: 1em;
+  min-width: 20em;
+  max-width: 40em;
+}
+.service-messages .message:last-child {
+  margin-bottom: 0;
+}
+</style>
diff --git a/front/src/components/common/Message.vue b/front/src/components/common/Message.vue
new file mode 100644
index 0000000000000000000000000000000000000000..772071db78c98881dab84405b2bc0903beff8106
--- /dev/null
+++ b/front/src/components/common/Message.vue
@@ -0,0 +1,36 @@
+<template>
+  <div class="ui message">
+    <div class="content">
+      <slot></slot>
+    </div>
+    <i class="close icon"></i>
+  </div>
+</template>
+<script>
+import $ from 'jquery'
+
+export default {
+  mounted () {
+    let self = this
+    $(this.$el).find('.close.icon').on('click', function () {
+      $(self.$el).transition('fade', 125)
+    })
+    $(this.$el).on('click', function () {
+      $(self.$el).transition('fade', 125)
+    })
+  }
+}
+</script>
+<style scoped>
+.ui.message .content {
+  padding-right: 0.5em;
+  cursor: pointer;
+}
+.ui.message .content :first-child {
+  margin-top: 0;
+}
+
+.ui.message .content :last-child {
+  margin-bottom: 0;
+}
+</style>
diff --git a/front/src/components/globals.js b/front/src/components/globals.js
index 79bbcf1b93a4a74ecf3c3b3d3d4e724870d7120c..4ad09f70425a987fd99e86f6e4277cd07d797605 100644
--- a/front/src/components/globals.js
+++ b/front/src/components/globals.js
@@ -12,4 +12,8 @@ import DangerousButton from '@/components/common/DangerousButton'
 
 Vue.component('dangerous-button', DangerousButton)
 
+import Message from '@/components/common/Message'
+
+Vue.component('message', Message)
+
 export default {}
diff --git a/front/src/store/ui.js b/front/src/store/ui.js
index f0935e491bb1a48abc5f90d3482e3d5eab7ee9a5..be744afe51ad954a4bae722f9442a9d71ad85730 100644
--- a/front/src/store/ui.js
+++ b/front/src/store/ui.js
@@ -2,11 +2,20 @@
 export default {
   namespaced: true,
   state: {
-    lastDate: new Date()
+    lastDate: new Date(),
+    maxMessages: 100,
+    messageDisplayDuration: 10000,
+    messages: []
   },
   mutations: {
     computeLastDate: (state) => {
       state.lastDate = new Date()
+    },
+    addMessage (state, message) {
+      state.messages.push(message)
+      if (state.messages.length > state.maxMessages) {
+        state.messages.shift()
+      }
     }
   }
 }
diff --git a/front/test/unit/specs/store/ui.spec.js b/front/test/unit/specs/store/ui.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..adcfa87d8f34bd600f113fc4100c0c6318bc730e
--- /dev/null
+++ b/front/test/unit/specs/store/ui.spec.js
@@ -0,0 +1,18 @@
+import store from '@/store/ui'
+
+import { testAction } from '../../utils'
+
+describe('store/ui', () => {
+  describe('mutations', () => {
+    it('addMessage', () => {
+      const state = {maxMessages: 100, messages: []}
+      store.mutations.addMessage(state, 'hello')
+      expect(state.messages).to.deep.equal(['hello'])
+    })
+    it('addMessage', () => {
+      const state = {maxMessages: 1, messages: ['hello']}
+      store.mutations.addMessage(state, 'world')
+      expect(state.messages).to.deep.equal(['world'])
+    })
+  })
+})