diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py
index 16abf80dc921347a0041148c3f7b73c46e443d4b..277b9ce0c56c96a2a6e21d9cc0c6aeb2dedfb1b6 100644
--- a/api/funkwhale_api/federation/factories.py
+++ b/api/funkwhale_api/federation/factories.py
@@ -97,6 +97,15 @@ class FollowFactory(factory.DjangoModelFactory):
         )
 
 
+@registry.register
+class FollowRequestFactory(factory.DjangoModelFactory):
+    target = factory.SubFactory(ActorFactory)
+    actor = factory.SubFactory(ActorFactory)
+
+    class Meta:
+        model = models.FollowRequest
+
+
 @registry.register(name='federation.Note')
 class NoteFactory(factory.Factory):
     type = 'Note'
diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py
index 50ff9d319ca167039d3b0d30eabe21c2135fdada..833b5d8f38ae19c397ef7aeb10123ad031261d78 100644
--- a/api/funkwhale_api/federation/models.py
+++ b/api/funkwhale_api/federation/models.py
@@ -35,6 +35,13 @@ class Actor(models.Model):
     last_fetch_date = models.DateTimeField(
         default=timezone.now)
     manually_approves_followers = models.NullBooleanField(default=None)
+    followers = models.ManyToManyField(
+        to='self',
+        symmetrical=False,
+        through='Follow',
+        through_fields=('target', 'actor'),
+        related_name='following',
+    )
 
     class Meta:
         unique_together = ['domain', 'preferred_username']
@@ -65,6 +72,10 @@ class Actor(models.Model):
 
         super().save(**kwargs)
 
+    @property
+    def is_local(self):
+        return self.domain == settings.FEDERATION_HOSTNAME
+
     @property
     def is_system(self):
         from . import actors
@@ -121,3 +132,28 @@ class FollowRequest(models.Model):
     last_modification_date = models.DateTimeField(
         default=timezone.now)
     approved = models.NullBooleanField(default=None)
+
+    def approve(self):
+        from . import activity
+        from . import serializers
+        self.approved = True
+        self.save(update_fields=['approved'])
+        Follow.objects.get_or_create(
+            target=self.target,
+            actor=self.actor
+        )
+        if self.target.is_local:
+            follow = {
+                '@context': serializers.AP_CONTEXT,
+                'actor': self.actor.url,
+                'id': self.actor.url + '#follows/{}'.format(uuid.uuid4()),
+                'object': self.target.url,
+                'type': 'Follow'
+            }
+            activity.accept_follow(
+                self.target, follow, self.actor
+            )
+
+    def refuse(self):
+        self.approved = False
+        self.save(update_fields=['approved'])
diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py
index 5ade9cdc8c94c5fde73f977b3ecbf47322bb199d..be24a5360440107c5106defc319001c189937d2f 100644
--- a/api/tests/federation/test_actors.py
+++ b/api/tests/federation/test_actors.py
@@ -351,7 +351,7 @@ def test_library_actor_handles_follow_manual_approval(
 
 def test_library_actor_handles_follow_auto_approval(
         settings, mocker, factories):
-    settings.FEDERATION_MUSIC_NEEDS_APPROVAL = True
+    settings.FEDERATION_MUSIC_NEEDS_APPROVAL = False
     actor = factories['federation.Actor']()
     accept_follow = mocker.patch(
         'funkwhale_api.federation.activity.accept_follow')
@@ -363,3 +363,8 @@ def test_library_actor_handles_follow_auto_approval(
         'object': library_actor.url,
     }
     library_actor.system_conf.post_inbox(data, actor=actor)
+
+    assert library_actor.received_follow_requests.count() == 0
+    accept_follow.assert_called_once_with(
+        library_actor, data, actor
+    )
diff --git a/api/tests/federation/test_models.py b/api/tests/federation/test_models.py
index 18daf87887f94abb12a43a35a06ef3aabd7485d6..86e4f4a847bac8350a1fcc8d49c47a2e503eabc3 100644
--- a/api/tests/federation/test_models.py
+++ b/api/tests/federation/test_models.py
@@ -1,8 +1,10 @@
 import pytest
+import uuid
 
 from django import db
 
 from funkwhale_api.federation import models
+from funkwhale_api.federation import serializers
 
 
 def test_cannot_duplicate_actor(factories):
@@ -30,3 +32,47 @@ def test_follow_federation_url(factories):
         follow.actor.url, follow.uuid)
 
     assert follow.get_federation_url() == expected
+
+
+def test_follow_request_approve(mocker, factories):
+    uid = uuid.uuid4()
+    mocker.patch('uuid.uuid4', return_value=uid)
+    accept_follow = mocker.patch(
+        'funkwhale_api.federation.activity.accept_follow')
+    fr = factories['federation.FollowRequest'](target__local=True)
+    fr.approve()
+
+    follow = {
+        '@context': serializers.AP_CONTEXT,
+        'actor': fr.actor.url,
+        'id': fr.actor.url + '#follows/{}'.format(uid),
+        'object': fr.target.url,
+        'type': 'Follow'
+    }
+
+    assert fr.approved is True
+    assert list(fr.target.followers.all()) == [fr.actor]
+    accept_follow.assert_called_once_with(
+        fr.target, follow, fr.actor
+    )
+
+
+def test_follow_request_approve_non_local(mocker, factories):
+    uid = uuid.uuid4()
+    mocker.patch('uuid.uuid4', return_value=uid)
+    accept_follow = mocker.patch(
+        'funkwhale_api.federation.activity.accept_follow')
+    fr = factories['federation.FollowRequest']()
+    fr.approve()
+
+    assert fr.approved is True
+    assert list(fr.target.followers.all()) == [fr.actor]
+    accept_follow.assert_not_called()
+
+
+def test_follow_request_refused(mocker, factories):
+    fr = factories['federation.FollowRequest']()
+    fr.refuse()
+
+    assert fr.approved is False
+    assert fr.target.followers.count() == 0