diff --git a/api/funkwhale_api/common/validators.py b/api/funkwhale_api/common/validators.py
index b5f26cac5421450fccaac039720e1238cf118ca8..78a4b4c7c4c3c9bf40d33616f4e0538ba7ca6ebc 100644
--- a/api/funkwhale_api/common/validators.py
+++ b/api/funkwhale_api/common/validators.py
@@ -1,6 +1,7 @@
 import mimetypes
 from os.path import splitext
 
+from django.core import validators
 from django.core.exceptions import ValidationError
 from django.core.files.images import get_image_dimensions
 from django.template.defaultfilters import filesizeformat
@@ -150,3 +151,17 @@ class FileValidator(object):
             }
 
             raise ValidationError(message)
+
+
+class DomainValidator(validators.URLValidator):
+    message = "Enter a valid domain name."
+
+    def __call__(self, value):
+        """
+        This is a bit hackish but since we don't have any built-in domain validator,
+        we use the url one, and prepend http:// in front of it.
+
+        If it fails, we know the domain is not valid.
+        """
+        super().__call__("http://{}".format(value))
+        return value
diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py
index 2fdeaaa7675abf7ccef9e3a2ce05319682ea0f85..59360aea10374ff2d4e804008fb9f62aeecb156f 100644
--- a/api/funkwhale_api/federation/models.py
+++ b/api/funkwhale_api/federation/models.py
@@ -13,6 +13,7 @@ from django.urls import reverse
 
 from funkwhale_api.common import session
 from funkwhale_api.common import utils as common_utils
+from funkwhale_api.common import validators as common_validators
 from funkwhale_api.music import utils as music_utils
 
 from . import utils as federation_utils
@@ -83,7 +84,11 @@ class DomainQuerySet(models.QuerySet):
 
 
 class Domain(models.Model):
-    name = models.CharField(primary_key=True, max_length=255)
+    name = models.CharField(
+        primary_key=True,
+        max_length=255,
+        validators=[common_validators.DomainValidator()],
+    )
     creation_date = models.DateTimeField(default=timezone.now)
     nodeinfo_fetch_date = models.DateTimeField(default=None, null=True, blank=True)
     nodeinfo = JSONField(default=empty_dict, max_length=50000, blank=True)
diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py
index 0697c6c14e3609fc32c508807312d47913e11407..763b37497f44b405d85f1644fd68ffaddb4fa3d1 100644
--- a/api/funkwhale_api/manage/views.py
+++ b/api/funkwhale_api/manage/views.py
@@ -98,7 +98,10 @@ class ManageInvitationViewSet(
 
 
 class ManageDomainViewSet(
-    mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
+    mixins.CreateModelMixin,
+    mixins.ListModelMixin,
+    mixins.RetrieveModelMixin,
+    viewsets.GenericViewSet,
 ):
     lookup_value_regex = r"[a-zA-Z0-9\-\.]+"
     queryset = (
diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py
index 803820b489350fcbf795abb424bace0f3e3b614c..74ba96ba8a595a94fb7ce94968791fec0fda4567 100644
--- a/api/tests/manage/test_serializers.py
+++ b/api/tests/manage/test_serializers.py
@@ -1,3 +1,5 @@
+import pytest
+
 from funkwhale_api.manage import serializers
 
 
@@ -53,6 +55,13 @@ def test_manage_domain_serializer(factories, now):
     assert s.data == expected
 
 
+def test_manage_domain_serializer_validates_hostname(db):
+    s = serializers.ManageDomainSerializer(data={"name": "hello world"})
+
+    with pytest.raises(serializers.serializers.ValidationError):
+        s.is_valid(raise_exception=True)
+
+
 def test_manage_actor_serializer(factories, now):
     actor = factories["federation.Actor"]()
     setattr(actor, "uploads_count", 66)
diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py
index 72e945bcad5186c2f6919b3db8bffa15aad54ef5..4591f7b1bca88f5ec2b6c7b28a78a7c3329464fa 100644
--- a/api/tests/manage/test_views.py
+++ b/api/tests/manage/test_views.py
@@ -1,6 +1,7 @@
 import pytest
 from django.urls import reverse
 
+from funkwhale_api.federation import models as federation_models
 from funkwhale_api.federation import tasks as federation_tasks
 from funkwhale_api.manage import serializers, views
 
@@ -90,6 +91,14 @@ def test_domain_detail(factories, superuser_api_client):
     assert response.data["name"] == d.pk
 
 
+def test_domain_create(superuser_api_client):
+    url = reverse("api:v1:manage:federation:domains-list")
+    response = superuser_api_client.post(url, {"name": "test.federation"})
+
+    assert response.status_code == 201
+    assert federation_models.Domain.objects.filter(pk="test.federation").exists()
+
+
 def test_domain_nodeinfo(factories, superuser_api_client, mocker):
     domain = factories["federation.Domain"]()
     url = reverse(
diff --git a/front/src/views/admin/moderation/DomainsList.vue b/front/src/views/admin/moderation/DomainsList.vue
index 84fb1df4300dffecc15db22048bafc50fb0e27dc..259d05f0cecbdb1019d59201d38d374cbe155447 100644
--- a/front/src/views/admin/moderation/DomainsList.vue
+++ b/front/src/views/admin/moderation/DomainsList.vue
@@ -1,26 +1,70 @@
 <template>
   <main v-title="labels.domains">
     <section class="ui vertical stripe segment">
-      <h2 class="ui header"><translate>Domains</translate></h2>
-      <div class="ui hidden divider"></div>
+      <h2 class="ui left floated header"><translate>Domains</translate></h2>
+      <form class="ui right floated form" @submit.prevent="createDomain">
+        <div v-if="errors && errors.length > 0" class="ui negative message">
+          <div class="header"><translate>Error while creating domain</translate></div>
+          <ul class="list">
+            <li v-for="error in errors">{{ error }}</li>
+          </ul>
+        </div>
+        <div class="inline fields">
+          <div class="field">
+            <label for="domain"><translate>Add a domain</translate></label>
+            <input type="text" id="domain" v-model="domainName">
+          </div>
+          <div class="field">
+            <button :class="['ui', {'loading': isCreating}, 'green', 'button']" type="submit" :disabled="isCreating">
+              <label for="domain"><translate>Add</translate></label>
+            </button>
+          </div>
+        </div>
+      </form>
+      <div class="ui clearing hidden divider"></div>
       <domains-table></domains-table>
     </section>
   </main>
 </template>
 
 <script>
-import DomainsTable from "@/components/manage/moderation/DomainsTable"
+import axios from 'axios'
 
+import DomainsTable from "@/components/manage/moderation/DomainsTable"
 export default {
   components: {
     DomainsTable
   },
+  data () {
+    return {
+      domainName: '',
+      isCreating: false,
+      errors: []
+    }
+  },
   computed: {
     labels() {
       return {
         domains: this.$gettext("Domains")
       }
     }
+  },
+  methods: {
+    createDomain () {
+      let self = this
+      this.isCreating = true
+      this.errors = []
+      axios.post('manage/federation/domains/', {name: this.domainName}).then((response) => {
+        this.isCreating = false
+        this.$router.push({
+          name: "manage.moderation.domains.detail",
+          params: {'id': response.data.name}
+        })
+      }, (error) => {
+        self.isCreating = false
+        self.errors = error.backendErrors
+      })
+    }
   }
 }
 </script>