diff --git a/api/funkwhale_api/common/factories.py b/api/funkwhale_api/common/factories.py
index 6919f9c3771ec81c9e4019cfa9e42d4d30e99494..9f46669ba8680f1976ef4713e06822cbad9c49cc 100644
--- a/api/funkwhale_api/common/factories.py
+++ b/api/funkwhale_api/common/factories.py
@@ -3,6 +3,7 @@ import factory
 from funkwhale_api.factories import registry, NoUpdateOnCreate
 
 from funkwhale_api.federation import factories as federation_factories
+from funkwhale_api.users import factories as users_factories
 
 
 @registry.register
@@ -23,3 +24,26 @@ class MutationFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
             return
         self.target = extracted
         self.save()
+
+
+@registry.register
+class ServiceMessageFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
+    code = factory.Faker("word")
+    title = factory.Faker("paragraph")
+    content = factory.Faker("paragraph")
+    expiration_date = None
+
+    class Meta:
+        model = "common.ServiceMessage"
+
+
+@registry.register
+class ServiceMessageNotificationFactory(
+    NoUpdateOnCreate, factory.django.DjangoModelFactory
+):
+    user = factory.SubFactory(users_factories.UserFactory)
+    service_message = factory.SubFactory(ServiceMessageFactory)
+    is_read = False
+
+    class Meta:
+        model = "common.ServiceMessageNotification"
diff --git a/api/funkwhale_api/common/migrations/0004_auto_20190725_1510.py b/api/funkwhale_api/common/migrations/0004_auto_20190725_1510.py
new file mode 100644
index 0000000000000000000000000000000000000000..773fe3eb76573e7211aea7a60b4a4714f78ed789
--- /dev/null
+++ b/api/funkwhale_api/common/migrations/0004_auto_20190725_1510.py
@@ -0,0 +1,78 @@
+# Generated by Django 2.2.3 on 2019-07-25 15:10
+
+from django.conf import settings
+import django.contrib.postgres.fields.jsonb
+import django.core.serializers.json
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+import uuid
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ("common", "0003_cit_extension"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="ServiceMessage",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "code",
+                    models.CharField(
+                        db_index=True, default=uuid.uuid4, max_length=255, unique=True
+                    ),
+                ),
+                ("title", models.CharField(max_length=255, null=True)),
+                ("content", models.TextField(max_length=255, null=True)),
+                (
+                    "creation_date",
+                    models.DateTimeField(default=django.utils.timezone.now),
+                ),
+                ("expiration_date", models.DateTimeField(null=True)),
+            ],
+        ),
+        migrations.CreateModel(
+            name="ServiceMessageNotification",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("is_read", models.BooleanField(default=False)),
+                (
+                    "service_message",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="notifications",
+                        to="common.ServiceMessage",
+                    ),
+                ),
+                (
+                    "user",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="service_message_notifications",
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+            ],
+        ),
+    ]
diff --git a/api/funkwhale_api/common/models.py b/api/funkwhale_api/common/models.py
index 52a02cad9433f4e79972b6cfe858534c830cb513..9e0cd638b4aff5fe88febec9838a75753276c104 100644
--- a/api/funkwhale_api/common/models.py
+++ b/api/funkwhale_api/common/models.py
@@ -150,3 +150,26 @@ class Mutation(models.Model):
         self.applied_date = timezone.now()
         self.save(update_fields=["is_applied", "applied_date", "previous_state"])
         return previous_state
+
+
+class ServiceMessage(models.Model):
+    code = models.CharField(
+        unique=True, db_index=True, default=uuid.uuid4, max_length=255
+    )
+    title = models.CharField(null=True, max_length=255)
+    content = models.TextField(null=True, max_length=255)
+
+    creation_date = models.DateTimeField(default=timezone.now)
+    expiration_date = models.DateTimeField(null=True)
+
+
+class ServiceMessageNotification(models.Model):
+    service_message = models.ForeignKey(
+        ServiceMessage, related_name="notifications", on_delete=models.CASCADE
+    )
+    user = models.ForeignKey(
+        "users.User",
+        related_name="service_message_notifications",
+        on_delete=models.CASCADE,
+    )
+    is_read = models.BooleanField(default=False)
diff --git a/api/funkwhale_api/common/serializers.py b/api/funkwhale_api/common/serializers.py
index 59b513f37aa057d843df6a4a5405381be71c2c8f..a70dbed7fe0eb956a819ef93d273e0d3dc5b9465 100644
--- a/api/funkwhale_api/common/serializers.py
+++ b/api/funkwhale_api/common/serializers.py
@@ -272,3 +272,20 @@ class APIMutationSerializer(serializers.ModelSerializer):
         if value not in self.context["registry"]:
             raise serializers.ValidationError("Invalid mutation type {}".format(value))
         return value
+
+
+class ServiceMessageSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = models.ServiceMessage
+        fields = ["code", "title", "content", "creation_date", "expiration_date"]
+
+
+class ServiceMessageNotificationSerializer(serializers.Serializer):
+    class Meta:
+        model = models.ServiceMessageNotification
+        fields = ["is_read"]
+
+    def to_representation(self, instance):
+        data = ServiceMessageSerializer(instance.service_message).data
+        data["is_read"] = instance.is_read
+        return data
diff --git a/api/funkwhale_api/common/tasks.py b/api/funkwhale_api/common/tasks.py
index 994b0bdfff13a27a5eec0e99a87c72c11b39287d..6f26cd7df864b5f417bdd92baa5dcc0c1151631b 100644
--- a/api/funkwhale_api/common/tasks.py
+++ b/api/funkwhale_api/common/tasks.py
@@ -4,6 +4,7 @@ from django.dispatch import receiver
 
 from funkwhale_api.common import channels
 from funkwhale_api.taskapp import celery
+from funkwhale_api.users.models import User
 
 from . import models
 from . import serializers
@@ -57,3 +58,24 @@ def broadcast_mutation_update(mutation, old_is_approved, new_is_approved, **kwar
             },
         },
     )
+
+
+@celery.app.task(name="common.broadcast_service_message")
+@transaction.atomic
+@celery.require_instance(models.ServiceMessage.objects.all(), "message")
+def broadcast_service_message(message, user_ids):
+    existing_ids = list(
+        User.objects.filter(pk__in=user_ids).values_list("pk", flat=True)
+    )
+
+    notifications = []
+    for user_id in existing_ids:
+        notifications.append(
+            models.ServiceMessageNotification(
+                service_message_id=message.pk, user_id=user_id
+            )
+        )
+
+    models.ServiceMessageNotification.objects.bulk_create(
+        notifications, batch_size=2000, ignore_conflicts=True
+    )
diff --git a/api/tests/common/test_serializers.py b/api/tests/common/test_serializers.py
index 6d20443af6b97d603f029482816893c5afeab683..8e0e57f5d8186f2e86140309d8640eb98618272a 100644
--- a/api/tests/common/test_serializers.py
+++ b/api/tests/common/test_serializers.py
@@ -182,3 +182,21 @@ def test_strip_exif_field():
 
     cleaned = PIL.Image.open(field.to_internal_value(uploaded))
     assert cleaned._getexif() is None
+
+
+def test_message_notification_serializer(factories, now):
+    notification = factories["common.ServiceMessageNotification"](
+        service_message__expiration_date=now
+    )
+    message = notification.service_message
+    expected = {
+        "code": message.code,
+        "title": message.title,
+        "content": message.content,
+        "creation_date": message.creation_date.isoformat().split("+")[0] + "Z",
+        "expiration_date": message.expiration_date.isoformat().split("+")[0] + "Z",
+        "is_read": False,
+    }
+    serializer = serializers.ServiceMessageNotificationSerializer(notification)
+
+    assert serializer.data == expected
diff --git a/api/tests/common/test_tasks.py b/api/tests/common/test_tasks.py
index f097c44231af27de220df3889cf7f1db2fe48ea0..54bc5751deaf6fcce984893ba8e0fd3c7a824e65 100644
--- a/api/tests/common/test_tasks.py
+++ b/api/tests/common/test_tasks.py
@@ -63,3 +63,28 @@ def test_cannot_apply_already_applied_migration(factories):
     mutation = factories["common.Mutation"](payload={}, is_applied=True)
     with pytest.raises(mutation.__class__.DoesNotExist):
         tasks.apply_mutation(mutation_id=mutation.pk)
+
+
+def test_can_broadcast_service_message(factories, now):
+    message = factories["common.ServiceMessage"](
+        code="test", content="Hello world", expiration_date=now
+    )
+
+    user1 = factories["users.User"]()
+    user2 = factories["users.User"]()
+    factories["users.User"]()
+
+    tasks.broadcast_service_message(
+        message_id=message.pk, user_ids=[user1.pk, user2.pk]
+    )
+
+    assert message.notifications.count() == 2
+
+    assert (
+        user1.service_message_notifications.get(service_message=message).is_read
+        is False
+    )
+    assert (
+        user2.service_message_notifications.get(service_message=message).is_read
+        is False
+    )