diff --git a/api/funkwhale_api/audio/factories.py b/api/funkwhale_api/audio/factories.py
index 0c57eeb2e24018b329f76f0da28662b449f1c23f..dabaa5114679fae671460f02eed1eb373b9577ab 100644
--- a/api/funkwhale_api/audio/factories.py
+++ b/api/funkwhale_api/audio/factories.py
@@ -18,6 +18,7 @@ class ChannelFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
     library = factory.SubFactory(
         federation_factories.MusicLibraryFactory,
         actor=factory.SelfAttribute("..attributed_to"),
+        privacy_level="everyone",
     )
     actor = factory.LazyAttribute(set_actor)
     artist = factory.SubFactory(music_factories.ArtistFactory)
@@ -27,6 +28,8 @@ class ChannelFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
 
     class Params:
         local = factory.Trait(
-            attributed_to__fid=factory.Faker("federation_url", local=True),
+            attributed_to=factory.SubFactory(
+                federation_factories.ActorFactory, local=True
+            ),
             artist__local=True,
         )
diff --git a/api/funkwhale_api/audio/serializers.py b/api/funkwhale_api/audio/serializers.py
index e2e469b7e63bacef9aad186da4fbde1a702ab6c8..dc3c114848e7f24b3e3201a27eb8e5e40cc1429e 100644
--- a/api/funkwhale_api/audio/serializers.py
+++ b/api/funkwhale_api/audio/serializers.py
@@ -37,7 +37,7 @@ class ChannelCreateSerializer(serializers.Serializer):
 
         channel.library = music_models.Library.objects.create(
             name=channel.actor.preferred_username,
-            privacy_level="public",
+            privacy_level="everyone",
             actor=validated_data["attributed_to"],
         )
         channel.save()
diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py
index 096fd767f86583fa875570691b3b71d1bc10873e..2bac41887c696edec23f795e274e3e78ee7e8e58 100644
--- a/api/funkwhale_api/federation/activity.py
+++ b/api/funkwhale_api/federation/activity.py
@@ -118,7 +118,7 @@ def should_reject(fid, actor_id=None, payload={}):
 
 
 @transaction.atomic
-def receive(activity, on_behalf_of):
+def receive(activity, on_behalf_of, inbox_actor=None):
     from . import models
     from . import serializers
     from . import tasks
@@ -131,7 +131,12 @@ def receive(activity, on_behalf_of):
     # we ensure the activity has the bare minimum structure before storing
     # it in our database
     serializer = serializers.BaseActivitySerializer(
-        data=activity, context={"actor": on_behalf_of, "local_recipients": True}
+        data=activity,
+        context={
+            "actor": on_behalf_of,
+            "local_recipients": True,
+            "recipients": [inbox_actor] if inbox_actor else [],
+        },
     )
     serializer.is_valid(raise_exception=True)
 
@@ -161,14 +166,19 @@ def receive(activity, on_behalf_of):
 
     local_to_recipients = get_actors_from_audience(activity.get("to", []))
     local_to_recipients = local_to_recipients.exclude(user=None)
+    local_to_recipients = local_to_recipients.values_list("pk", flat=True)
+    local_to_recipients = list(local_to_recipients)
+    if inbox_actor:
+        local_to_recipients.append(inbox_actor.pk)
 
     local_cc_recipients = get_actors_from_audience(activity.get("cc", []))
     local_cc_recipients = local_cc_recipients.exclude(user=None)
+    local_cc_recipients = local_cc_recipients.values_list("pk", flat=True)
 
     inbox_items = []
     for recipients, type in [(local_to_recipients, "to"), (local_cc_recipients, "cc")]:
 
-        for r in recipients.values_list("pk", flat=True):
+        for r in recipients:
             inbox_items.append(models.InboxItem(actor_id=r, type=type, activity=copy))
 
     models.InboxItem.objects.bulk_create(inbox_items)
diff --git a/api/funkwhale_api/federation/api_serializers.py b/api/funkwhale_api/federation/api_serializers.py
index dbc655a47d11c677f7b98ec1a34cbbeacc6882c0..93dae4e55ab9d66ed597233e676d66328ce5e08c 100644
--- a/api/funkwhale_api/federation/api_serializers.py
+++ b/api/funkwhale_api/federation/api_serializers.py
@@ -86,7 +86,12 @@ class LibraryFollowSerializer(serializers.ModelSerializer):
 
 
 def serialize_generic_relation(activity, obj):
-    data = {"uuid": obj.uuid, "type": obj._meta.label}
+    data = {"type": obj._meta.label}
+    if data["type"] == "federation.Actor":
+        data["full_username"] = obj.full_username
+    else:
+        data["uuid"] = obj.uuid
+
     if data["type"] == "music.Library":
         data["name"] = obj.name
     if data["type"] == "federation.LibraryFollow":
diff --git a/api/funkwhale_api/federation/authentication.py b/api/funkwhale_api/federation/authentication.py
index 123d7bd89679ae0ece57b49b3f6f21fcfcc17193..b059b2f814fe9b36ecebc184f97da21683f01cd5 100644
--- a/api/funkwhale_api/federation/authentication.py
+++ b/api/funkwhale_api/federation/authentication.py
@@ -52,9 +52,13 @@ class SignatureAuthentication(authentication.BaseAuthentication):
             actor = actors.get_actor(actor_url)
         except Exception as e:
             logger.info(
-                "Discarding HTTP request from blocked actor/domain %s", actor_url
+                "Discarding HTTP request from blocked actor/domain %s, %s",
+                actor_url,
+                str(e),
+            )
+            raise rest_exceptions.AuthenticationFailed(
+                "Cannot fetch remote actor to authenticate signature"
             )
-            raise rest_exceptions.AuthenticationFailed(str(e))
 
         if not actor.public_key:
             raise rest_exceptions.AuthenticationFailed("No public key found")
diff --git a/api/funkwhale_api/federation/jsonld.py b/api/funkwhale_api/federation/jsonld.py
index ad67323f239bb7be2fb42be90a75091915971eba..c0170b2350e7815c4b0d45dfb0d4bf4534e54ef6 100644
--- a/api/funkwhale_api/federation/jsonld.py
+++ b/api/funkwhale_api/federation/jsonld.py
@@ -214,14 +214,18 @@ def get_ids(v):
 
 
 def get_default_context():
-    return ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {}]
+    return [
+        "https://www.w3.org/ns/activitystreams",
+        "https://w3id.org/security/v1",
+        {"manuallyApprovesFollowers": "as:manuallyApprovesFollowers"},
+    ]
 
 
 def get_default_context_fw():
     return [
         "https://www.w3.org/ns/activitystreams",
         "https://w3id.org/security/v1",
-        {},
+        {"manuallyApprovesFollowers": "as:manuallyApprovesFollowers"},
         "https://funkwhale.audio/ns",
     ]
 
diff --git a/api/funkwhale_api/federation/migrations/0022_auto_20191204_1539.py b/api/funkwhale_api/federation/migrations/0022_auto_20191204_1539.py
new file mode 100644
index 0000000000000000000000000000000000000000..61ba5621a9b75aba44030eea06aae9cd8aab15b5
--- /dev/null
+++ b/api/funkwhale_api/federation/migrations/0022_auto_20191204_1539.py
@@ -0,0 +1,23 @@
+# Generated by Django 2.2.7 on 2019-12-04 15:39
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('federation', '0021_auto_20191029_1257'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='actor',
+            name='inbox_url',
+            field=models.URLField(blank=True, max_length=500, null=True),
+        ),
+        migrations.AlterField(
+            model_name='actor',
+            name='outbox_url',
+            field=models.URLField(blank=True, max_length=500, null=True),
+        ),
+    ]
diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py
index 2fc7a79344c0a8594824e2c517c171a7ca7be4fa..60cf26054ee5b3bb1273f6a2e1f3914f981a532e 100644
--- a/api/funkwhale_api/federation/models.py
+++ b/api/funkwhale_api/federation/models.py
@@ -180,8 +180,8 @@ class Actor(models.Model):
 
     fid = models.URLField(unique=True, max_length=500, db_index=True)
     url = models.URLField(max_length=500, null=True, blank=True)
-    outbox_url = models.URLField(max_length=500)
-    inbox_url = models.URLField(max_length=500)
+    outbox_url = models.URLField(max_length=500, null=True, blank=True)
+    inbox_url = models.URLField(max_length=500, null=True, blank=True)
     following_url = models.URLField(max_length=500, null=True, blank=True)
     followers_url = models.URLField(max_length=500, null=True, blank=True)
     shared_inbox_url = models.URLField(max_length=500, null=True, blank=True)
diff --git a/api/funkwhale_api/federation/renderers.py b/api/funkwhale_api/federation/renderers.py
index a92658e595b749783a4cf3a00320dc7b1f0f8c9a..5b58cf031384fa771ca0d946422dc940ba6f8268 100644
--- a/api/funkwhale_api/federation/renderers.py
+++ b/api/funkwhale_api/federation/renderers.py
@@ -6,6 +6,7 @@ def get_ap_renderers():
         ("APActivity", "application/activity+json"),
         ("APLD", "application/ld+json"),
         ("APJSON", "application/json"),
+        ("HTML", "text/html"),
     ]
 
     return [
diff --git a/api/funkwhale_api/federation/routes.py b/api/funkwhale_api/federation/routes.py
index 41497e9f232f8bc8e29e583ff3859ca7d1633738..32a8357dba613f36aafc0ef7ee5c17505fbb9310 100644
--- a/api/funkwhale_api/federation/routes.py
+++ b/api/funkwhale_api/federation/routes.py
@@ -131,21 +131,28 @@ def outbox_follow(context):
 @outbox.register({"type": "Create", "object.type": "Audio"})
 def outbox_create_audio(context):
     upload = context["upload"]
+    channel = upload.library.get_channel()
+    upload_serializer = (
+        serializers.ChannelUploadSerializer if channel else serializers.UploadSerializer
+    )
+    followers_target = channel.actor if channel else upload.library
+    actor = channel.actor if channel else upload.library.actor
+
     serializer = serializers.ActivitySerializer(
         {
             "type": "Create",
-            "actor": upload.library.actor.fid,
-            "object": serializers.UploadSerializer(upload).data,
+            "actor": actor.fid,
+            "object": upload_serializer(upload).data,
         }
     )
     yield {
         "type": "Create",
-        "actor": upload.library.actor,
+        "actor": actor,
         "payload": with_recipients(
-            serializer.data, to=[{"type": "followers", "target": upload.library}]
+            serializer.data, to=[{"type": "followers", "target": followers_target}]
         ),
         "object": upload,
-        "target": upload.library,
+        "target": None if channel else upload.library,
     }
 
 
@@ -258,6 +265,9 @@ def inbox_delete_audio(payload, context):
 def outbox_delete_audio(context):
     uploads = context["uploads"]
     library = uploads[0].library
+    channel = library.get_channel()
+    followers_target = channel.actor if channel else library
+    actor = channel.actor if channel else library.actor
     serializer = serializers.ActivitySerializer(
         {
             "type": "Delete",
@@ -266,9 +276,9 @@ def outbox_delete_audio(context):
     )
     yield {
         "type": "Delete",
-        "actor": library.actor,
+        "actor": actor,
         "payload": with_recipients(
-            serializer.data, to=[{"type": "followers", "target": library}]
+            serializer.data, to=[{"type": "followers", "target": followers_target}]
         ),
     }
 
diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py
index 76c2514698b4319a6b01642a695745c2a587a1fe..d81436d11217c875c8726cf4dc280ef9be70e9f3 100644
--- a/api/funkwhale_api/federation/serializers.py
+++ b/api/funkwhale_api/federation/serializers.py
@@ -68,8 +68,8 @@ class PublicKeySerializer(jsonld.JsonLdSerializer):
 
 class ActorSerializer(jsonld.JsonLdSerializer):
     id = serializers.URLField(max_length=500)
-    outbox = serializers.URLField(max_length=500)
-    inbox = serializers.URLField(max_length=500)
+    outbox = serializers.URLField(max_length=500, required=False)
+    inbox = serializers.URLField(max_length=500, required=False)
     type = serializers.ChoiceField(
         choices=[getattr(contexts.AS, c[0]) for c in models.TYPE_CHOICES]
     )
@@ -77,7 +77,7 @@ class ActorSerializer(jsonld.JsonLdSerializer):
     manuallyApprovesFollowers = serializers.NullBooleanField(required=False)
     name = serializers.CharField(required=False, max_length=200)
     summary = serializers.CharField(max_length=None, required=False)
-    followers = serializers.URLField(max_length=500)
+    followers = serializers.URLField(max_length=500, required=False)
     following = serializers.URLField(max_length=500, required=False, allow_null=True)
     publicKey = PublicKeySerializer(required=False)
     endpoints = EndpointsSerializer(required=False)
@@ -142,8 +142,8 @@ class ActorSerializer(jsonld.JsonLdSerializer):
     def prepare_missing_fields(self):
         kwargs = {
             "fid": self.validated_data["id"],
-            "outbox_url": self.validated_data["outbox"],
-            "inbox_url": self.validated_data["inbox"],
+            "outbox_url": self.validated_data.get("outbox"),
+            "inbox_url": self.validated_data.get("inbox"),
             "following_url": self.validated_data.get("following"),
             "followers_url": self.validated_data.get("followers"),
             "summary": self.validated_data.get("summary"),
@@ -244,7 +244,7 @@ class BaseActivitySerializer(serializers.Serializer):
         to = payload.get("to", [])
         cc = payload.get("cc", [])
 
-        if not to and not cc:
+        if not to and not cc and not self.context.get("recipients"):
             raise serializers.ValidationError(
                 "We cannot handle an activity with no recipient"
             )
@@ -801,6 +801,10 @@ class TagSerializer(jsonld.JsonLdSerializer):
         return value
 
 
+def repr_tag(tag_name):
+    return {"type": "Hashtag", "name": "#{}".format(tag_name)}
+
+
 class MusicEntitySerializer(jsonld.JsonLdSerializer):
     id = serializers.URLField(max_length=500)
     published = serializers.DateTimeField()
@@ -831,7 +835,7 @@ class MusicEntitySerializer(jsonld.JsonLdSerializer):
 
     def get_tags_repr(self, instance):
         return [
-            {"type": "Hashtag", "name": "#{}".format(item.tag.name)}
+            repr_tag(item.tag.name)
             for item in sorted(instance.tagged_items.all(), key=lambda i: i.tag.name)
         ]
 
@@ -1182,3 +1186,71 @@ class NodeInfoLinkSerializer(serializers.Serializer):
 
 class NodeInfoSerializer(serializers.Serializer):
     links = serializers.ListField(child=NodeInfoLinkSerializer(), min_length=1)
+
+
+class ChannelOutboxSerializer(PaginatedCollectionSerializer):
+    type = serializers.ChoiceField(choices=[contexts.AS.OrderedCollection])
+
+    class Meta:
+        jsonld_mapping = PAGINATED_COLLECTION_JSONLD_MAPPING
+
+    def to_representation(self, channel):
+        conf = {
+            "id": channel.actor.outbox_url,
+            "page_size": 100,
+            "attributedTo": channel.actor,
+            "actor": channel.actor,
+            "items": channel.library.uploads.for_federation()
+            .order_by("-creation_date")
+            .filter(track__artist=channel.artist),
+            "type": "OrderedCollection",
+        }
+        r = super().to_representation(conf)
+        return r
+
+
+class ChannelUploadSerializer(serializers.Serializer):
+    def to_representation(self, upload):
+        data = {
+            "id": upload.fid,
+            "type": "Audio",
+            "name": upload.track.full_name,
+            "attributedTo": upload.library.channel.actor.fid,
+            "published": upload.creation_date.isoformat(),
+            "to": contexts.AS.Public
+            if upload.library.privacy_level == "everyone"
+            else "",
+            "url": [
+                {
+                    "type": "Link",
+                    "mimeType": upload.mimetype,
+                    "href": utils.full_url(upload.listen_url),
+                },
+                {
+                    "type": "Link",
+                    "mimeType": "text/html",
+                    "href": utils.full_url(upload.track.get_absolute_url()),
+                },
+            ],
+        }
+        tags = [item.tag.name for item in upload.get_all_tagged_items()]
+        if tags:
+            data["tag"] = [repr_tag(name) for name in tags]
+            data["summary"] = " ".join(["#{}".format(name) for name in tags])
+
+        if self.context.get("include_ap_context", True):
+            data["@context"] = jsonld.get_default_context()
+
+        return data
+
+
+class ChannelCreateUploadSerializer(serializers.Serializer):
+    def to_representation(self, upload):
+        return {
+            "@context": jsonld.get_default_context(),
+            "type": "Create",
+            "actor": upload.library.channel.actor.fid,
+            "object": ChannelUploadSerializer(
+                upload, context={"include_ap_context": False}
+            ).data,
+        }
diff --git a/api/funkwhale_api/federation/views.py b/api/funkwhale_api/federation/views.py
index 671d2f83ad0e060309b5d3749842ac9984ea2b2e..35043e8ed5cc35a530b75de0c00dd30001cc3632 100644
--- a/api/funkwhale_api/federation/views.py
+++ b/api/funkwhale_api/federation/views.py
@@ -55,18 +55,57 @@ class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericV
 
     @action(methods=["get", "post"], detail=True)
     def inbox(self, request, *args, **kwargs):
+        inbox_actor = self.get_object()
         if request.method.lower() == "post" and request.actor is None:
             raise exceptions.AuthenticationFailed(
                 "You need a valid signature to send an activity"
             )
         if request.method.lower() == "post":
-            activity.receive(activity=request.data, on_behalf_of=request.actor)
+            activity.receive(
+                activity=request.data,
+                on_behalf_of=request.actor,
+                inbox_actor=inbox_actor,
+            )
         return response.Response({}, status=200)
 
     @action(methods=["get", "post"], detail=True)
     def outbox(self, request, *args, **kwargs):
+        actor = self.get_object()
+        channel = actor.channel
+        if channel:
+            return self.get_channel_outbox_response(request, channel)
         return response.Response({}, status=200)
 
+    def get_channel_outbox_response(self, request, channel):
+        conf = {
+            "id": channel.actor.outbox_url,
+            "actor": channel.actor,
+            "items": channel.library.uploads.for_federation()
+            .order_by("-creation_date")
+            .prefetch_related("library__channel__actor", "track__artist"),
+            "item_serializer": serializers.ChannelCreateUploadSerializer,
+        }
+        page = request.GET.get("page")
+        if page is None:
+            serializer = serializers.ChannelOutboxSerializer(channel)
+            data = serializer.data
+        else:
+            try:
+                page_number = int(page)
+            except Exception:
+                return response.Response({"page": ["Invalid page number"]}, status=400)
+            conf["page_size"] = preferences.get("federation__collection_page_size")
+            p = paginator.Paginator(conf["items"], conf["page_size"])
+            try:
+                page = p.page(page_number)
+                conf["page"] = page
+                serializer = serializers.CollectionPageSerializer(conf)
+                data = serializer.data
+            except paginator.EmptyPage:
+                return response.Response(status=404)
+
+        return response.Response(data)
+
     @action(methods=["get"], detail=True)
     def followers(self, request, *args, **kwargs):
         self.get_object()
@@ -251,6 +290,11 @@ class MusicUploadViewSet(
         actor = music_utils.get_actor_from_request(self.request)
         return queryset.playable_by(actor)
 
+    def get_serializer(self, obj):
+        if obj.library.get_channel():
+            return serializers.ChannelUploadSerializer(obj)
+        return super().get_serializer(obj)
+
 
 class MusicArtistViewSet(
     FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py
index f15e02d63bc5d8e460400ea937cbf8f611c5813a..9f627d47c1e1babfe47f1870107d6ec775bdf814 100644
--- a/api/funkwhale_api/music/models.py
+++ b/api/funkwhale_api/music/models.py
@@ -634,7 +634,10 @@ class UploadQuerySet(common_models.NullsLastQuerySet):
         return self.exclude(library__in=libraries, import_status="finished")
 
     def local(self, include=True):
-        return self.exclude(library__actor__user__isnull=include)
+        query = models.Q(library__actor__domain_id=settings.FEDERATION_HOSTNAME)
+        if not include:
+            query = ~query
+        return self.filter(query)
 
     def for_federation(self):
         return self.filter(import_status="finished", mimetype__startswith="audio/")
@@ -904,6 +907,14 @@ class Upload(models.Model):
             # external storage
             return self.audio_file.name
 
+    def get_all_tagged_items(self):
+        track_tags = self.track.tagged_items.all()
+        album_tags = self.track.album.tagged_items.all()
+        artist_tags = self.track.artist.tagged_items.all()
+
+        items = (track_tags | album_tags | artist_tags).order_by("tag__name")
+        return items
+
 
 MIMETYPE_CHOICES = [(mt, ext) for ext, mt in utils.AUDIO_EXTENSIONS_AND_MIMETYPE]
 
diff --git a/api/funkwhale_api/music/spa_views.py b/api/funkwhale_api/music/spa_views.py
index cbf143162944ba431abebb9b2ed402422a00e47e..e96ce5fee345522adaa9f7c52aa4540c9234d48c 100644
--- a/api/funkwhale_api/music/spa_views.py
+++ b/api/funkwhale_api/music/spa_views.py
@@ -4,6 +4,7 @@ from django.conf import settings
 from django.urls import reverse
 from django.db.models import Q
 
+from funkwhale_api.common import preferences
 from funkwhale_api.common import utils
 from funkwhale_api.playlists import models as playlists_models
 
@@ -65,8 +66,9 @@ def library_track(request, pk):
                 "content": obj.album.attachment_cover.download_url_medium_square_crop,
             }
         )
-
-    if obj.uploads.playable_by(None).exists():
+    playable_uploads = obj.uploads.playable_by(None).order_by("id")
+    upload = playable_uploads.first()
+    if upload:
         metas.append(
             {
                 "tag": "meta",
@@ -74,7 +76,15 @@ def library_track(request, pk):
                 "content": utils.join_url(settings.FUNKWHALE_URL, obj.listen_url),
             }
         )
-
+        if preferences.get("federation__enabled"):
+            metas.append(
+                {
+                    "tag": "link",
+                    "rel": "alternate",
+                    "type": "application/activity+json",
+                    "href": upload.fid,
+                }
+            )
         metas.append(
             {
                 "tag": "link",
@@ -133,6 +143,15 @@ def library_album(request, pk):
             }
         )
 
+    if preferences.get("federation__enabled"):
+        metas.append(
+            {
+                "tag": "link",
+                "rel": "alternate",
+                "type": "application/activity+json",
+                "href": obj.fid,
+            }
+        )
     if models.Upload.objects.filter(track__album=obj).playable_by(None).exists():
         metas.append(
             {
@@ -179,6 +198,16 @@ def library_artist(request, pk):
             }
         )
 
+    if preferences.get("federation__enabled"):
+        metas.append(
+            {
+                "tag": "link",
+                "rel": "alternate",
+                "type": "application/activity+json",
+                "href": obj.fid,
+            }
+        )
+
     if (
         models.Upload.objects.filter(Q(track__artist=obj) | Q(track__album__artist=obj))
         .playable_by(None)
diff --git a/api/requirements/local.txt b/api/requirements/local.txt
index dc39e4218bfcb2d31cef38499106834034c4f265..d8cc4d4eddfda535db70f5cefb4b5ffa8387476d 100644
--- a/api/requirements/local.txt
+++ b/api/requirements/local.txt
@@ -9,6 +9,7 @@ django-debug-toolbar>=1.11,<1.12
 
 # improved REPL
 ipdb==0.11
+prompt_toolkit<3
 black
 profiling
 
diff --git a/api/tests/audio/test_serializers.py b/api/tests/audio/test_serializers.py
index 02737a852951759062321704248c8bcb644877b3..a9f3948fb96b3f7a6b598a17423c5447249a7702 100644
--- a/api/tests/audio/test_serializers.py
+++ b/api/tests/audio/test_serializers.py
@@ -29,7 +29,7 @@ def test_channel_serializer_create(factories):
     assert channel.actor.summary == data["summary"]
     assert channel.actor.preferred_username == data["username"]
     assert channel.actor.name == data["name"]
-    assert channel.library.privacy_level == "public"
+    assert channel.library.privacy_level == "everyone"
     assert channel.library.actor == attributed_to
 
 
diff --git a/api/tests/audio/test_views.py b/api/tests/audio/test_views.py
index d12caa3dd9af2df8c78c1592a3912d45faa74388..074046d0acb9761ac89e17c3a1bd4103724e9082 100644
--- a/api/tests/audio/test_views.py
+++ b/api/tests/audio/test_views.py
@@ -34,7 +34,7 @@ def test_channel_create(logged_in_api_client):
     assert channel.attributed_to == actor
     assert channel.actor.summary == data["summary"]
     assert channel.actor.preferred_username == data["username"]
-    assert channel.library.privacy_level == "public"
+    assert channel.library.privacy_level == "everyone"
     assert channel.library.actor == actor
 
 
diff --git a/api/tests/federation/test_activity.py b/api/tests/federation/test_activity.py
index ccc27c7dfd693efcaaa5781bee9e03f5b0dc8346..bac27efc542bf085a699461b14e8892acf29a009 100644
--- a/api/tests/federation/test_activity.py
+++ b/api/tests/federation/test_activity.py
@@ -16,11 +16,14 @@ from funkwhale_api.federation import (
 def test_receive_validates_basic_attributes_and_stores_activity(
     mrf_inbox_registry, factories, now, mocker
 ):
+
     mocker.patch.object(
         activity.InboxRouter, "get_matching_handlers", return_value=True
     )
     mrf_inbox_registry_apply = mocker.spy(mrf_inbox_registry, "apply")
+    serializer_init = mocker.spy(serializers.BaseActivitySerializer, "__init__")
     mocked_dispatch = mocker.patch("funkwhale_api.common.utils.on_commit")
+    inbox_actor = factories["federation.Actor"]()
     local_to_actor = factories["users.User"]().create_actor()
     local_cc_actor = factories["users.User"]().create_actor()
     remote_actor = factories["federation.Actor"]()
@@ -33,7 +36,9 @@ def test_receive_validates_basic_attributes_and_stores_activity(
         "cc": [local_cc_actor.fid, activity.PUBLIC_ADDRESS],
     }
 
-    copy = activity.receive(activity=a, on_behalf_of=remote_actor)
+    copy = activity.receive(
+        activity=a, on_behalf_of=remote_actor, inbox_actor=inbox_actor
+    )
     mrf_inbox_registry_apply.assert_called_once_with(a, sender_id=a["actor"])
 
     assert copy.payload == a
@@ -45,13 +50,24 @@ def test_receive_validates_basic_attributes_and_stores_activity(
         tasks.dispatch_inbox.delay, activity_id=copy.pk
     )
 
-    assert models.InboxItem.objects.count() == 2
-    for actor, t in [(local_to_actor, "to"), (local_cc_actor, "cc")]:
+    assert models.InboxItem.objects.count() == 3
+    for actor, t in [
+        (local_to_actor, "to"),
+        (inbox_actor, "to"),
+        (local_cc_actor, "cc"),
+    ]:
         ii = models.InboxItem.objects.get(actor=actor)
         assert ii.type == t
         assert ii.activity == copy
         assert ii.is_read is False
 
+    assert serializer_init.call_args[1]["context"] == {
+        "actor": remote_actor,
+        "local_recipients": True,
+        "recipients": [inbox_actor],
+    }
+    assert serializer_init.call_args[1]["data"] == a
+
 
 def test_receive_uses_mrf_returned_payload(mrf_inbox_registry, factories, now, mocker):
     mocker.patch.object(
diff --git a/api/tests/federation/test_api_serializers.py b/api/tests/federation/test_api_serializers.py
index d7c5836cab2ced75c340d98bcb7dc4e1cc41eb5c..ab621abde4f764739c1c6b7c69654a1acbb7fb7e 100644
--- a/api/tests/federation/test_api_serializers.py
+++ b/api/tests/federation/test_api_serializers.py
@@ -85,3 +85,29 @@ def test_manage_upload_action_read(factories):
     s.handle_read(ii.__class__.objects.all())
 
     assert ii.__class__.objects.filter(is_read=False).count() == 0
+
+
+@pytest.mark.parametrize(
+    "factory_name, factory_kwargs, expected",
+    [
+        (
+            "federation.Actor",
+            {"preferred_username": "hello", "domain__name": "world"},
+            {"full_username": "hello@world"},
+        ),
+        (
+            "music.Library",
+            {"name": "hello", "uuid": "ad1ee1f7-589c-4abe-b303-e4fe7a889260"},
+            {"uuid": "ad1ee1f7-589c-4abe-b303-e4fe7a889260", "name": "hello"},
+        ),
+        (
+            "federation.LibraryFollow",
+            {"approved": False, "uuid": "ad1ee1f7-589c-4abe-b303-e4fe7a889260"},
+            {"uuid": "ad1ee1f7-589c-4abe-b303-e4fe7a889260", "approved": False},
+        ),
+    ],
+)
+def test_serialize_generic_relation(factory_name, factory_kwargs, expected, factories):
+    obj = factories[factory_name](**factory_kwargs)
+    expected["type"] = factory_name
+    assert api_serializers.serialize_generic_relation({}, obj) == expected
diff --git a/api/tests/federation/test_routes.py b/api/tests/federation/test_routes.py
index a632c5153dfc9f2dea926960c1888c8fb1d9e89b..995cb1e446e1b6274f87b435a391e0406b46b5ef 100644
--- a/api/tests/federation/test_routes.py
+++ b/api/tests/federation/test_routes.py
@@ -261,6 +261,26 @@ def test_outbox_create_audio(factories, mocker):
     assert activity["object"] == upload
 
 
+def test_outbox_create_audio_channel(factories, mocker):
+    channel = factories["audio.Channel"]()
+    upload = factories["music.Upload"](library=channel.library)
+    activity = list(routes.outbox_create_audio({"upload": upload}))[0]
+    serializer = serializers.ActivitySerializer(
+        {
+            "type": "Create",
+            "object": serializers.ChannelUploadSerializer(upload).data,
+            "actor": channel.actor.fid,
+        }
+    )
+    expected = serializer.data
+    expected["to"] = [{"type": "followers", "target": upload.library.channel.actor}]
+
+    assert dict(activity["payload"]) == dict(expected)
+    assert activity["actor"] == channel.actor
+    assert activity["target"] is None
+    assert activity["object"] == upload
+
+
 def test_inbox_create_audio(factories, mocker):
     activity = factories["federation.Activity"]()
     upload = factories["music.Upload"](bitrate=42, duration=55)
@@ -442,6 +462,20 @@ def test_outbox_delete_audio(factories):
     assert activity["actor"] == upload.library.actor
 
 
+def test_outbox_delete_audio_channel(factories):
+    channel = factories["audio.Channel"]()
+    upload = factories["music.Upload"](library=channel.library)
+    activity = list(routes.outbox_delete_audio({"uploads": [upload]}))[0]
+    expected = serializers.ActivitySerializer(
+        {"type": "Delete", "object": {"type": "Audio", "id": [upload.fid]}}
+    ).data
+
+    expected["to"] = [{"type": "followers", "target": channel.actor}]
+
+    assert dict(activity["payload"]) == dict(expected)
+    assert activity["actor"] == channel.actor
+
+
 def test_inbox_delete_follow_library(factories):
     local_actor = factories["users.User"]().create_actor()
     remote_actor = factories["federation.Actor"]()
diff --git a/api/tests/federation/test_serializers.py b/api/tests/federation/test_serializers.py
index b7c4c4a9e3a98e82eca23602c778aad8a8c182b9..e1ac72fdc912d508b783d1e8f9ecb7a3b02b3b20 100644
--- a/api/tests/federation/test_serializers.py
+++ b/api/tests/federation/test_serializers.py
@@ -1022,6 +1022,12 @@ def test_activity_serializer_validate_recipients_empty(db):
         s.validate_recipients({"cc": []})
 
 
+def test_activity_serializer_validate_recipients_context(db):
+    s = serializers.BaseActivitySerializer(context={"recipients": ["dummy"]})
+
+    assert s.validate_recipients({}) is None
+
+
 def test_track_serializer_update_license(factories):
     licenses.load(licenses.LICENSES)
 
@@ -1033,3 +1039,93 @@ def test_track_serializer_update_license(factories):
     obj.refresh_from_db()
 
     assert obj.license_id == "cc-by-2.0"
+
+
+def test_channel_actor_outbox_serializer(factories):
+    channel = factories["audio.Channel"]()
+    uploads = factories["music.Upload"].create_batch(
+        5,
+        track__artist=channel.artist,
+        library=channel.library,
+        import_status="finished",
+    )
+
+    expected = {
+        "@context": jsonld.get_default_context(),
+        "type": "OrderedCollection",
+        "id": channel.actor.outbox_url,
+        "actor": channel.actor.fid,
+        "attributedTo": channel.actor.fid,
+        "totalItems": len(uploads),
+        "first": channel.actor.outbox_url + "?page=1",
+        "last": channel.actor.outbox_url + "?page=1",
+        "current": channel.actor.outbox_url + "?page=1",
+    }
+
+    serializer = serializers.ChannelOutboxSerializer(channel)
+
+    assert serializer.data == expected
+
+
+def test_channel_upload_serializer(factories):
+    channel = factories["audio.Channel"]()
+    upload = factories["music.Upload"](
+        playable=True,
+        library=channel.library,
+        import_status="finished",
+        track__set_tags=["Punk"],
+        track__album__set_tags=["Rock"],
+        track__artist__set_tags=["Indie"],
+    )
+
+    expected = {
+        "@context": jsonld.get_default_context(),
+        "type": "Audio",
+        "id": upload.fid,
+        "name": upload.track.full_name,
+        "summary": "#Indie #Punk #Rock",
+        "attributedTo": channel.actor.fid,
+        "published": upload.creation_date.isoformat(),
+        "to": "https://www.w3.org/ns/activitystreams#Public",
+        "url": [
+            {
+                "type": "Link",
+                "mimeType": upload.mimetype,
+                "href": utils.full_url(upload.listen_url),
+            },
+            {
+                "type": "Link",
+                "mimeType": "text/html",
+                "href": utils.full_url(upload.track.get_absolute_url()),
+            },
+        ],
+        "tag": [
+            {"type": "Hashtag", "name": "#Indie"},
+            {"type": "Hashtag", "name": "#Punk"},
+            {"type": "Hashtag", "name": "#Rock"},
+        ],
+    }
+
+    serializer = serializers.ChannelUploadSerializer(upload)
+
+    assert serializer.data == expected
+
+
+def test_channel_create_upload_serializer(factories):
+    channel = factories["audio.Channel"]()
+    upload = factories["music.Upload"](
+        playable=True, library=channel.library, import_status="finished"
+    )
+
+    expected = {
+        "@context": jsonld.get_default_context(),
+        "type": "Create",
+        "actor": upload.library.channel.actor.fid,
+        "object": serializers.ChannelUploadSerializer(
+            upload, context={"include_ap_context": False}
+        ).data,
+    }
+
+    serializer = serializers.ChannelCreateUploadSerializer(upload)
+
+    assert serializer.data == expected
diff --git a/api/tests/federation/test_views.py b/api/tests/federation/test_views.py
index 51d8e79a93f76d3696b27a6d77bfd9d93476f851..a47fdd198d08a0e051e3cd0f81f5f53ea7cf2eff 100644
--- a/api/tests/federation/test_views.py
+++ b/api/tests/federation/test_views.py
@@ -103,7 +103,9 @@ def test_local_actor_inbox_post(factories, api_client, mocker, authenticated_act
 
     assert response.status_code == 200
     patched_receive.assert_called_once_with(
-        activity={"hello": "world"}, on_behalf_of=authenticated_actor
+        activity={"hello": "world"},
+        on_behalf_of=authenticated_actor,
+        inbox_actor=user.actor,
     )
 
 
@@ -196,6 +198,56 @@ def test_music_library_retrieve_page_public(factories, api_client):
     assert response.data == expected
 
 
+def test_channel_outbox_retrieve(factories, api_client):
+    channel = factories["audio.Channel"](actor__local=True)
+    expected = serializers.ChannelOutboxSerializer(channel).data
+
+    url = reverse(
+        "federation:actors-outbox",
+        kwargs={"preferred_username": channel.actor.preferred_username},
+    )
+    response = api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data == expected
+
+
+def test_channel_outbox_retrieve_page(factories, api_client):
+    channel = factories["audio.Channel"](actor__local=True)
+    upload = factories["music.Upload"](library=channel.library, playable=True)
+    url = reverse(
+        "federation:actors-outbox",
+        kwargs={"preferred_username": channel.actor.preferred_username},
+    )
+
+    expected = serializers.CollectionPageSerializer(
+        {
+            "id": channel.actor.outbox_url,
+            "item_serializer": serializers.ChannelCreateUploadSerializer,
+            "actor": channel.actor,
+            "page": Paginator([upload], 1).page(1),
+        }
+    ).data
+
+    response = api_client.get(url, {"page": 1})
+
+    assert response.status_code == 200
+    assert response.data == expected
+
+
+def test_channel_upload_retrieve(factories, api_client):
+    channel = factories["audio.Channel"](local=True)
+    upload = factories["music.Upload"](library=channel.library, playable=True)
+    url = reverse("federation:music:uploads-detail", kwargs={"uuid": upload.uuid},)
+
+    expected = serializers.ChannelUploadSerializer(upload).data
+
+    response = api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data == expected
+
+
 @pytest.mark.parametrize("privacy_level", ["me", "instance"])
 def test_music_library_retrieve_page_private(factories, api_client, privacy_level):
     library = factories["music.Library"](privacy_level=privacy_level)
diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py
index e1c698894049e84fbcf822f566b540dedae96141..aed9c0c119affbb21cb8894592116b4ee39ce813 100644
--- a/api/tests/manage/test_views.py
+++ b/api/tests/manage/test_views.py
@@ -326,7 +326,7 @@ def test_library_detail(factories, superuser_api_client):
 
 
 def test_library_update(factories, superuser_api_client):
-    library = factories["music.Library"](privacy_level="public")
+    library = factories["music.Library"](privacy_level="everyone")
     url = reverse(
         "api:v1:manage:library:libraries-detail", kwargs={"uuid": library.uuid}
     )
diff --git a/api/tests/music/test_spa_views.py b/api/tests/music/test_spa_views.py
index 42f7234602689cf9c7673f33dd531bc397f77bbb..f140143ec7c4acb74fa1b57123a4ae1a68be4840 100644
--- a/api/tests/music/test_spa_views.py
+++ b/api/tests/music/test_spa_views.py
@@ -7,7 +7,8 @@ from funkwhale_api.music import serializers
 
 
 def test_library_track(spa_html, no_api_auth, client, factories, settings):
-    track = factories["music.Upload"](playable=True, track__disc_number=1).track
+    upload = factories["music.Upload"](playable=True, track__disc_number=1)
+    track = upload.track
     url = "/library/tracks/{}".format(track.pk)
 
     response = client.get(url)
@@ -56,6 +57,12 @@ def test_library_track(spa_html, no_api_auth, client, factories, settings):
             "property": "og:audio",
             "content": utils.join_url(settings.FUNKWHALE_URL, track.listen_url),
         },
+        {
+            "tag": "link",
+            "rel": "alternate",
+            "type": "application/activity+json",
+            "href": upload.fid,
+        },
         {
             "tag": "link",
             "rel": "alternate",
@@ -116,6 +123,12 @@ def test_library_album(spa_html, no_api_auth, client, factories, settings):
             "property": "og:image",
             "content": album.attachment_cover.download_url_medium_square_crop,
         },
+        {
+            "tag": "link",
+            "rel": "alternate",
+            "type": "application/activity+json",
+            "href": album.fid,
+        },
         {
             "tag": "link",
             "rel": "alternate",
@@ -164,6 +177,12 @@ def test_library_artist(spa_html, no_api_auth, client, factories, settings):
             "property": "og:image",
             "content": album.attachment_cover.download_url_medium_square_crop,
         },
+        {
+            "tag": "link",
+            "rel": "alternate",
+            "type": "application/activity+json",
+            "href": artist.fid,
+        },
         {
             "tag": "link",
             "rel": "alternate",
diff --git a/changes/changelog.d/961.bugfix b/changes/changelog.d/961.bugfix
new file mode 100644
index 0000000000000000000000000000000000000000..b2813ff31659069b01d78142ca8f4811181d0b91
--- /dev/null
+++ b/changes/changelog.d/961.bugfix
@@ -0,0 +1 @@
+    Added missing manuallyApprovesFollowers entry in JSON-LD contexts (#961)
diff --git a/dev.yml b/dev.yml
index 4d781ed9f84182b366797f5c7317a9e5a8bb099b..c332f97c46f9213f23c8f0d6f760f93fb4ddb460 100644
--- a/dev.yml
+++ b/dev.yml
@@ -135,7 +135,7 @@ services:
 
     labels:
       traefik.backend: "${COMPOSE_PROJECT_NAME-node1}"
-      traefik.frontend.rule: "Host:${COMPOSE_PROJECT_NAME-node1}.funkwhale.test,${NODE_IP-127.0.0.1}"
+      traefik.frontend.rule: "Host:${COMPOSE_PROJECT_NAME-node1}.funkwhale.test,${NODE_IP-127.0.0.1},${DJANGO_ALLOWED_HOSTS}"
       traefik.enable: "true"
       traefik.federation.protocol: "http"
       traefik.federation.port: "80"