diff --git a/api/funkwhale_api/federation/filters.py b/api/funkwhale_api/federation/filters.py
index 2803186baf036b6418d806a493adcb142af49d95..c911f1a891966adf0ba942670ad1a74fbaca4da7 100644
--- a/api/funkwhale_api/federation/filters.py
+++ b/api/funkwhale_api/federation/filters.py
@@ -43,6 +43,7 @@ class LibraryTrackFilter(django_filters.FilterSet):
 
 
 class FollowFilter(django_filters.FilterSet):
+    pending = django_filters.CharFilter(method='filter_pending')
     ordering = django_filters.OrderingFilter(
         # tuple-mapping retains order
         fields=(
@@ -50,9 +51,16 @@ class FollowFilter(django_filters.FilterSet):
             ('modification_date', 'modification_date'),
         ),
     )
+    q = fields.SearchFilter(search_fields=[
+        'actor__domain',
+        'actor__preferred_username',
+    ])
 
     class Meta:
         model = models.Follow
-        fields = {
-            'approved': ['exact'],
-        }
+        fields = ['approved', 'pending', 'q']
+
+    def filter_pending(self, queryset, field_name, value):
+        if value.lower() in ['true', '1', 'yes']:
+            queryset = queryset.filter(approved__isnull=True)
+        return queryset
diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py
index e6ad0c0be02d330e880ff8b2c7e9d7b4b8c143db..4964106d8f3d5be3f1029b0811aec2beffcabc7f 100644
--- a/api/funkwhale_api/federation/serializers.py
+++ b/api/funkwhale_api/federation/serializers.py
@@ -190,6 +190,35 @@ class APILibraryScanSerializer(serializers.Serializer):
     until = serializers.DateTimeField(required=False)
 
 
+class APILibraryFollowUpdateSerializer(serializers.Serializer):
+    follow = serializers.IntegerField()
+    approved = serializers.BooleanField()
+
+    def validate_follow(self, value):
+        from . import actors
+        library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+        qs = models.Follow.objects.filter(
+            pk=value,
+            target=library_actor,
+        )
+        try:
+            return qs.get()
+        except models.Follow.DoesNotExist:
+            raise serializers.ValidationError('Invalid follow')
+
+    def save(self):
+        new_status = self.validated_data['approved']
+        follow = self.validated_data['follow']
+        if new_status == follow.approved:
+            return follow
+
+        follow.approved = new_status
+        follow.save(update_fields=['approved', 'modification_date'])
+        if new_status:
+            activity.accept_follow(follow)
+        return follow
+
+
 class APILibraryCreateSerializer(serializers.ModelSerializer):
     actor = serializers.URLField()
     federation_enabled = serializers.BooleanField()
@@ -233,8 +262,13 @@ class APILibraryCreateSerializer(serializers.ModelSerializer):
         library_data = library.get_library_data(
             acs.validated_data['library_url'])
         if 'errors' in library_data:
-            raise serializers.ValidationError(str(library_data['errors']))
+            # we pass silently because it may means we require permission
+            # before scanning
+            pass
         validated_data['library'] = library_data
+        validated_data['library'].setdefault(
+            'id', acs.validated_data['library_url']
+        )
         validated_data['actor'] = actor
         return validated_data
 
@@ -244,7 +278,7 @@ class APILibraryCreateSerializer(serializers.ModelSerializer):
             defaults={
                 'actor': validated_data['actor'],
                 'follow': validated_data['follow'],
-                'tracks_count': validated_data['library']['totalItems'],
+                'tracks_count': validated_data['library'].get('totalItems'),
                 'federation_enabled': validated_data['federation_enabled'],
                 'autoimport': validated_data['autoimport'],
                 'download_files': validated_data['download_files'],
diff --git a/api/funkwhale_api/federation/views.py b/api/funkwhale_api/federation/views.py
index a3f02a372879d44a6a906d207784df498dea0c9b..381f87eff2e90f4a64feade856756044393d4f4d 100644
--- a/api/funkwhale_api/federation/views.py
+++ b/api/funkwhale_api/federation/views.py
@@ -221,31 +221,42 @@ class LibraryViewSet(
         queryset = models.Follow.objects.filter(
             actor=library_actor
         ).select_related(
-            'target',
+            'actor',
             'target',
         ).order_by('-creation_date')
         filterset = filters.FollowFilter(request.GET, queryset=queryset)
-        serializer = serializers.APIFollowSerializer(filterset.qs, many=True)
+        final_qs = filterset.qs
+        serializer = serializers.APIFollowSerializer(final_qs, many=True)
         data = {
             'results': serializer.data,
-            'count': len(filterset.qs),
+            'count': len(final_qs),
         }
         return response.Response(data)
 
-    @list_route(methods=['get'])
+    @list_route(methods=['get', 'patch'])
     def followers(self, request, *args, **kwargs):
+        if request.method.lower() == 'patch':
+            serializer = serializers.APILibraryFollowUpdateSerializer(
+                data=request.data)
+            serializer.is_valid(raise_exception=True)
+            follow = serializer.save()
+            return response.Response(
+                serializers.APIFollowSerializer(follow).data
+            )
+
         library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
         queryset = models.Follow.objects.filter(
             target=library_actor
         ).select_related(
-            'target',
+            'actor',
             'target',
         ).order_by('-creation_date')
         filterset = filters.FollowFilter(request.GET, queryset=queryset)
-        serializer = serializers.APIFollowSerializer(filterset.qs, many=True)
+        final_qs = filterset.qs
+        serializer = serializers.APIFollowSerializer(final_qs, many=True)
         data = {
             'results': serializer.data,
-            'count': len(filterset.qs),
+            'count': len(final_qs),
         }
         return response.Response(data)
 
diff --git a/api/tests/federation/test_views.py b/api/tests/federation/test_views.py
index 3e5bdf1a5ec1b46816ba5ab6e66f83dc22dea1ee..8c5235b8bbeb0f8a42b5a79296d4902067312a2a 100644
--- a/api/tests/federation/test_views.py
+++ b/api/tests/federation/test_views.py
@@ -346,3 +346,37 @@ def test_list_library_tracks(factories, superuser_api_client):
         'previous': None,
         'next': None,
     }
+
+
+def test_can_update_follow_status(factories, superuser_api_client, mocker):
+    patched_accept = mocker.patch(
+        'funkwhale_api.federation.activity.accept_follow'
+    )
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    follow = factories['federation.Follow'](target=library_actor)
+
+    payload = {
+        'follow': follow.pk,
+        'approved': True
+    }
+    url = reverse('api:v1:federation:libraries-followers')
+    response = superuser_api_client.patch(url, payload)
+    follow.refresh_from_db()
+
+    assert response.status_code == 200
+    assert follow.approved is True
+    patched_accept.assert_called_once_with(follow)
+
+
+def test_can_filter_pending_follows(factories, superuser_api_client):
+    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    follow = factories['federation.Follow'](
+        target=library_actor,
+        approved=True)
+
+    params = {'pending': True}
+    url = reverse('api:v1:federation:libraries-followers')
+    response = superuser_api_client.get(url, params)
+
+    assert response.status_code == 200
+    assert len(response.data['results']) == 0
diff --git a/front/src/components/common/DangerousButton.vue b/front/src/components/common/DangerousButton.vue
index 910209b9dc1c6f506ab759404e3393e484b5b456..690291d5b1d08abaa60f8576fce71b25a73f1047 100644
--- a/front/src/components/common/DangerousButton.vue
+++ b/front/src/components/common/DangerousButton.vue
@@ -26,7 +26,7 @@ import Modal from '@/components/semantic/Modal'
 
 export default {
   props: {
-    action: {type: Function, required: true},
+    action: {type: Function, required: false},
     disabled: {type: Boolean, default: false},
     color: {type: String, default: 'red'}
   },
@@ -41,7 +41,10 @@ export default {
   methods: {
     confirm () {
       this.showModal = false
-      this.action()
+      this.$emit('confirm')
+      if (this.action) {
+        this.action()
+      }
     }
   }
 }
diff --git a/front/src/components/federation/LibraryCard.vue b/front/src/components/federation/LibraryCard.vue
index 267d41bd0796bbe5e20c1f0d2bcc74ff745aa244..a5579c125acb8cf5691cfc572b03a6ce03fd5204 100644
--- a/front/src/components/federation/LibraryCard.vue
+++ b/front/src/components/federation/LibraryCard.vue
@@ -15,7 +15,7 @@
       <span class="right floated" v-else>
         <i class="open lock icon"></i> Open
       </span>
-      <span>
+      <span v-if="totalItems">
         <i class="music icon"></i>
         {{ totalItems }} tracks
       </span>
@@ -25,10 +25,6 @@
         <i class="clock icon"></i>
         Follow request pending approval
       </template>
-      <template v-else-if="following">
-        <i class="check icon"></i>
-        Already following this library
-      </template>
       <div
         v-if="!library"
         @click="follow"
diff --git a/front/src/components/federation/LibraryFollowTable.vue b/front/src/components/federation/LibraryFollowTable.vue
new file mode 100644
index 0000000000000000000000000000000000000000..9a35e0db636b92be2a953253d09c2418de731702
--- /dev/null
+++ b/front/src/components/federation/LibraryFollowTable.vue
@@ -0,0 +1,161 @@
+<template>
+  <div>
+    <div class="ui form">
+      <div class="fields">
+        <div class="ui six wide field">
+          <input type="text" v-model="search" placeholder="Search by username, domain..." />
+        </div>
+        <div class="ui four wide inline field">
+          <div class="ui checkbox">
+            <input v-model="pending" type="checkbox">
+            <label>Pending approval</label>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="ui hidden divider"></div>
+    <table v-if="result" class="ui very basic single line unstackable table">
+      <thead>
+        <tr>
+          <th>Actor</th>
+          <th>Creation date</th>
+          <th>Status</th>
+          <th>Actions</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr v-for="follow in result.results">
+          <td>
+            {{ follow.actor.preferred_username }}@{{ follow.actor.domain }}
+          </td>
+          <td>
+            <human-date :date="follow.creation_date"></human-date>
+          </td>
+          <td>
+            <template v-if="follow.approved === true">
+              <i class="check icon"></i> Approved
+            </template>
+            <template v-else-if="follow.approved === false">
+              <i class="x icon"></i> Refused
+            </template>
+            <template v-else>
+              <i class="clock icon"></i> Pending
+            </template>
+          </td>
+          <td>
+            <dangerous-button v-if="follow.approved !== false" class="tiny basic labeled icon" color='red' @confirm="updateFollow(follow, false)">
+              <i class="x icon"></i> Deny
+              <p slot="modal-header">Deny access?</p>
+              <p slot="modal-content">By confirming, {{ follow.actor.preferred_username }}@{{ follow.actor.domain }} will be denied access to your library.</p>
+              <p slot="modal-confirm">Deny</p>
+            </dangerous-button>
+            <dangerous-button v-if="follow.approved !== true" class="tiny basic labeled icon" color='green' @confirm="updateFollow(follow, true)">
+              <i class="x icon"></i> Approve
+              <p slot="modal-header">Approve access?</p>
+              <p slot="modal-content">By confirming, {{ follow.actor.preferred_username }}@{{ follow.actor.domain }} will be granted access to your library.</p>
+              <p slot="modal-confirm">Approve</p>
+            </dangerous-button>
+          </td>
+        </tr>
+      </tbody>
+      <tfoot class="full-width">
+        <tr>
+          <th>
+            <pagination
+            v-if="result && result.results.length > 0"
+            @page-changed="selectPage"
+            :compact="true"
+            :current="page"
+            :paginate-by="paginateBy"
+            :total="result.count"
+            ></pagination>
+          </th>
+          <th v-if="result && result.results.length > 0">
+            Showing results {{ ((page-1) * paginateBy) + 1 }}-{{ ((page-1) * paginateBy) + result.results.length }} on {{ result.count }}</th>
+          <th></th>
+          <th></th>
+        </tr>
+      </tfoot>
+    </table>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+import _ from 'lodash'
+
+import Pagination from '@/components/Pagination'
+
+export default {
+  props: {
+    filters: {type: Object, required: false, default: () => {}}
+  },
+  components: {
+    Pagination
+  },
+  data () {
+    return {
+      isLoading: false,
+      result: null,
+      page: 1,
+      paginateBy: 25,
+      search: '',
+      pending: false
+    }
+  },
+  created () {
+    this.fetchData()
+  },
+  methods: {
+    fetchData () {
+      let params = _.merge({
+        'page': this.page,
+        'page_size': this.paginateBy,
+        'q': this.search
+      }, this.filters)
+      if (this.pending) {
+        params.pending = true
+      }
+      let self = this
+      self.isLoading = true
+      axios.get('/federation/libraries/followers/', {params: params}).then((response) => {
+        self.result = response.data
+        self.isLoading = false
+      }, error => {
+        self.isLoading = false
+        self.errors = error.backendErrors
+      })
+    },
+    selectPage: function (page) {
+      this.page = page
+    },
+    updateFollow (follow, approved) {
+      let payload = {
+        follow: follow.id,
+        approved: approved
+      }
+      let self = this
+      axios.patch('/federation/libraries/followers/', payload).then((response) => {
+        follow.approved = response.data.approved
+        self.isLoading = false
+      }, error => {
+        self.isLoading = false
+        self.errors = error.backendErrors
+      })
+    }
+  },
+  watch: {
+    search (newValue) {
+      if (newValue.length > 0) {
+        this.fetchData()
+      }
+    },
+    page () {
+      this.fetchData()
+    },
+    pending () {
+      this.fetchData()
+    }
+  }
+}
+</script>
diff --git a/front/src/components/federation/LibraryTrackTable.vue b/front/src/components/federation/LibraryTrackTable.vue
index e5255252e463747149b210c1fbe2493f181dd4b6..6404f39905814e88812c3012e6be0d8e58fd4a52 100644
--- a/front/src/components/federation/LibraryTrackTable.vue
+++ b/front/src/components/federation/LibraryTrackTable.vue
@@ -64,13 +64,19 @@
             ></pagination>
 
           </th>
-          <th>Showing results {{ ((page-1) * paginateBy) + 1 }}-{{ ((page-1) * paginateBy) + result.results.length }} on {{ result.count }}</th>
+          <th v-if="result && result.results.length > 0">
+            Showing results {{ ((page-1) * paginateBy) + 1 }}-{{ ((page-1) * paginateBy) + result.results.length }} on {{ result.count }}</th>
           <th>
             <button
               @click="launchImport"
               :disabled="checked.length === 0 || isImporting"
               :class="['ui', 'green', {loading: isImporting}, 'button']">Import {{ checked.length }} tracks
             </button>
+            <router-link
+              v-if="importBatch"
+              :to="{name: 'library.import.batches.detail', params: {id: importBatch.id }}">
+              Import #{{ importBatch.id }} launched
+            </router-link>
           </th>
           <th></th>
           <th></th>
@@ -104,7 +110,8 @@ export default {
       paginateBy: 25,
       search: '',
       checked: {},
-      isImporting: false
+      isImporting: false,
+      importBatch: null
     }
   },
   created () {
@@ -135,6 +142,7 @@ export default {
         library_tracks: this.checked
       }
       axios.post('/submit/federation/', payload).then((response) => {
+        self.importBatch = response.data
         self.isImporting = false
         self.fetchData()
       }, error => {
diff --git a/front/src/router/index.js b/front/src/router/index.js
index 0ef3dcf24cefbe78045b1bc96bf55417b99cbd3f..a2bf781956a3350d514d108f5a8812be2e5f6156 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -30,6 +30,7 @@ import FederationScan from '@/views/federation/Scan'
 import FederationLibraryDetail from '@/views/federation/LibraryDetail'
 import FederationLibraryList from '@/views/federation/LibraryList'
 import FederationTrackList from '@/views/federation/LibraryTrackList'
+import FederationFollowersList from '@/views/federation/LibraryFollowersList'
 
 Vue.use(Router)
 
@@ -118,6 +119,17 @@ export default new Router({
             defaultPage: route.query.page
           })
         },
+        {
+          path: 'followers',
+          name: 'federation.followers.list',
+          component: FederationFollowersList,
+          props: (route) => ({
+            defaultOrdering: route.query.ordering,
+            defaultQuery: route.query.query,
+            defaultPaginateBy: route.query.paginateBy,
+            defaultPage: route.query.page
+          })
+        },
         { path: 'libraries/:id', name: 'federation.libraries.detail', component: FederationLibraryDetail, props: true }
       ]
     },
diff --git a/front/src/views/federation/Base.vue b/front/src/views/federation/Base.vue
index b90ba723a29563bcb3c947071524ce22b1821be5..7958bb36b65332b840111cba5ab37c5e2a7962b0 100644
--- a/front/src/views/federation/Base.vue
+++ b/front/src/views/federation/Base.vue
@@ -7,10 +7,39 @@
       <router-link
         class="ui item"
         :to="{name: 'federation.tracks.list'}">Tracks</router-link>
+        <div class="ui secondary right menu">
+          <router-link
+            class="ui item"
+            :to="{name: 'federation.followers.list'}">
+            Followers
+            <div class="ui teal label" title="Pending requests">{{ requestsCount }}</div>
+          </router-link>
+        </div>
     </div>
     <router-view :key="$route.fullPath"></router-view>
   </div>
 </template>
+<script>
+import axios from 'axios'
+export default {
+  data () {
+    return {
+      requestsCount: 0
+    }
+  },
+  created () {
+    this.fetchRequestsCount()
+  },
+  methods: {
+    fetchRequestsCount () {
+      let self = this
+      axios.get('federation/libraries/followers/', {params: {pending: true}}).then(response => {
+        self.requestsCount = response.data.count
+      })
+    }
+  }
+}
+</script>
 <style lang="scss">
 @import '../../style/vendor/media';
 
diff --git a/front/src/views/federation/LibraryFollowersList.vue b/front/src/views/federation/LibraryFollowersList.vue
new file mode 100644
index 0000000000000000000000000000000000000000..8ca120e8b54e3178d16dfcd7cd6f4af9468ddf1d
--- /dev/null
+++ b/front/src/views/federation/LibraryFollowersList.vue
@@ -0,0 +1,27 @@
+<template>
+  <div v-title="'Followers'">
+    <div class="ui vertical stripe segment">
+      <h2 class="ui header">Browsing followers</h2>
+      <p>
+        Be careful when accepting follow requests, as it means the follower
+        will have access to your entire library.
+      </p>
+      <div class="ui hidden divider"></div>
+      <library-follow-table></library-follow-table>
+    </div>
+  </div>
+</template>
+
+<script>
+import LibraryFollowTable from '@/components/federation/LibraryFollowTable'
+
+export default {
+  components: {
+    LibraryFollowTable
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>