diff --git a/api/funkwhale_api/users/admin.py b/api/funkwhale_api/users/admin.py
index 5c694ab0ee962c2977dd227c709861dfef93098f..2affd083669ec2aad814dfdeb09c0898f803612a 100644
--- a/api/funkwhale_api/users/admin.py
+++ b/api/funkwhale_api/users/admin.py
@@ -7,12 +7,12 @@ from django.contrib.auth.admin import UserAdmin as AuthUserAdmin
 from django.contrib.auth.forms import UserChangeForm, UserCreationForm
 from django.utils.translation import ugettext_lazy as _
 
-from .models import User
+from . import models
 
 
 class MyUserChangeForm(UserChangeForm):
     class Meta(UserChangeForm.Meta):
-        model = User
+        model = models.User
 
 
 class MyUserCreationForm(UserCreationForm):
@@ -22,7 +22,7 @@ class MyUserCreationForm(UserCreationForm):
     )
 
     class Meta(UserCreationForm.Meta):
-        model = User
+        model = models.User
 
     def clean_username(self):
         username = self.cleaned_data["username"]
@@ -33,7 +33,7 @@ class MyUserCreationForm(UserCreationForm):
         raise forms.ValidationError(self.error_messages["duplicate_username"])
 
 
-@admin.register(User)
+@admin.register(models.User)
 class UserAdmin(AuthUserAdmin):
     form = MyUserChangeForm
     add_form = MyUserCreationForm
@@ -74,3 +74,11 @@ class UserAdmin(AuthUserAdmin):
         (_("Important dates"), {"fields": ("last_login", "date_joined")}),
         (_("Useless fields"), {"fields": ("user_permissions", "groups")}),
     )
+
+
+@admin.register(models.Invitation)
+class InvitationAdmin(admin.ModelAdmin):
+    list_select_related = True
+    list_display = ["owner", "code", "creation_date", "expiration_date"]
+    search_fields = ["owner__username", "code"]
+    readonly_fields = ["expiration_date", "code"]
diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py
index b3bd431c722fc5f8e4751270a9b1690973119de2..f857e8da68ba3df9bb559472b24f424bf72559d1 100644
--- a/api/funkwhale_api/users/serializers.py
+++ b/api/funkwhale_api/users/serializers.py
@@ -1,5 +1,6 @@
 from django.conf import settings
 from rest_auth.serializers import PasswordResetSerializer as PRS
+from rest_auth.registration.serializers import RegisterSerializer as RS
 from rest_framework import serializers
 
 from funkwhale_api.activity import serializers as activity_serializers
@@ -7,6 +8,28 @@ from funkwhale_api.activity import serializers as activity_serializers
 from . import models
 
 
+class RegisterSerializer(RS):
+    invitation = serializers.CharField(
+        required=False, allow_null=True, allow_blank=True
+    )
+
+    def validate_invitation(self, value):
+        if not value:
+            return
+
+        try:
+            return models.Invitation.objects.open().get(code=value.lower())
+        except models.Invitation.DoesNotExist:
+            raise serializers.ValidationError("Invalid invitation code")
+
+    def save(self, request):
+        user = super().save(request)
+        if self.validated_data.get("invitation"):
+            user.invitation = self.validated_data.get("invitation")
+            user.save(update_fields=["invitation"])
+        return user
+
+
 class UserActivitySerializer(activity_serializers.ModelSerializer):
     type = serializers.SerializerMethodField()
     name = serializers.CharField(source="username")
diff --git a/api/funkwhale_api/users/views.py b/api/funkwhale_api/users/views.py
index 69e69d26e6b987426ef59a450d07e3d6458f6ab2..20d63d788f349643b9065038102cabfd60a49ce0 100644
--- a/api/funkwhale_api/users/views.py
+++ b/api/funkwhale_api/users/views.py
@@ -10,8 +10,11 @@ from . import models, serializers
 
 
 class RegisterView(BaseRegisterView):
+    serializer_class = serializers.RegisterSerializer
+
     def create(self, request, *args, **kwargs):
-        if not self.is_open_for_signup(request):
+        invitation_code = request.data.get("invitation")
+        if not invitation_code and not self.is_open_for_signup(request):
             r = {"detail": "Registration has been disabled"}
             return Response(r, status=403)
         return super().create(request, *args, **kwargs)
diff --git a/api/tests/users/test_views.py b/api/tests/users/test_views.py
index 00272c2aea76026c3df2b125c39fdee852acf1ec..0ad67fb86910e675f842fea1292ae3383f37ad0f 100644
--- a/api/tests/users/test_views.py
+++ b/api/tests/users/test_views.py
@@ -50,6 +50,39 @@ def test_can_disable_registration_view(preferences, api_client, db):
     assert response.status_code == 403
 
 
+def test_can_signup_with_invitation(preferences, factories, api_client):
+    url = reverse("rest_register")
+    invitation = factories["users.Invitation"](code="hello")
+    data = {
+        "username": "test1",
+        "email": "test1@test.com",
+        "password1": "testtest",
+        "password2": "testtest",
+        "invitation": "hello",
+    }
+    preferences["users__registration_enabled"] = False
+    response = api_client.post(url, data)
+    assert response.status_code == 201
+    u = User.objects.get(email="test1@test.com")
+    assert u.username == "test1"
+    assert u.invitation == invitation
+
+
+def test_can_signup_with_invitation_invalid(preferences, factories, api_client):
+    url = reverse("rest_register")
+    invitation = factories["users.Invitation"](code="hello")
+    data = {
+        "username": "test1",
+        "email": "test1@test.com",
+        "password1": "testtest",
+        "password2": "testtest",
+        "invitation": "nope",
+    }
+    response = api_client.post(url, data)
+    assert response.status_code == 400
+    assert "invitation" in response.data
+
+
 def test_can_fetch_data_from_api(api_client, factories):
     url = reverse("api:v1:users:users-me")
     response = api_client.get(url)
diff --git a/front/src/components/auth/Signup.vue b/front/src/components/auth/Signup.vue
index 89f4cb1f1266c956283142cfc4f470e3b0c5d031..e4e5cebbce950b7470204f710e06ffe01e7632e7 100644
--- a/front/src/components/auth/Signup.vue
+++ b/front/src/components/auth/Signup.vue
@@ -2,19 +2,22 @@
   <div class="main pusher" v-title="'Sign Up'">
     <div class="ui vertical stripe segment">
       <div class="ui small text container">
-        <h2><i18next path="Create a funkwhale account"/></h2>
+        <h2>{{ $t("Create a funkwhale account") }}</h2>
         <form
-          v-if="$store.state.instance.settings.users.registration_enabled.value"
           :class="['ui', {'loading': isLoadingInstanceSetting}, 'form']"
           @submit.prevent="submit()">
+          <p class="ui message" v-if="!$store.state.instance.settings.users.registration_enabled.value">
+            {{ $t('Registration are closed on this instance, you will need an invitation code to signup.') }}
+          </p>
+
           <div v-if="errors.length > 0" class="ui negative message">
-            <div class="header"><i18next path="We cannot create your account"/></div>
+            <div class="header">{{ $t("We cannot create your account") }}</div>
             <ul class="list">
               <li v-for="error in errors">{{ error }}</li>
             </ul>
           </div>
           <div class="field">
-            <i18next tag="label" path="Username"/>
+            <label>{{ $t("Username") }}</label>
             <input
             ref="username"
             required
@@ -24,7 +27,7 @@
             v-model="username">
           </div>
           <div class="field">
-            <i18next tag="label" path="Email"/>
+            <label>{{ $t("Email") }}</label>
             <input
             ref="email"
             required
@@ -33,12 +36,22 @@
             v-model="email">
           </div>
           <div class="field">
-            <i18next tag="label" path="Password"/>
+            <label>{{ $t("Password") }}</label>
             <password-input v-model="password" />
           </div>
-          <button :class="['ui', 'green', {'loading': isLoading}, 'button']" type="submit"><i18next path="Create my account"/></button>
+          <div class="field">
+            <label v-if="!$store.state.instance.settings.users.registration_enabled.value">{{ $t("Invitation code") }}</label>
+            <label v-else>{{ $t("Invitation code (optional)") }}</label>
+            <input
+            :required="!$store.state.instance.settings.users.registration_enabled.value"
+            type="text"
+            :placeholder="$t('Enter your invitation code (case insensitive)')"
+            v-model="invitation">
+          </div>
+          <button :class="['ui', 'green', {'loading': isLoading}, 'button']" type="submit">
+            {{ $t("Create my account") }}
+          </button>
         </form>
-        <i18next v-else tag="p" path="Registration is currently disabled on this instance, please try again later."/>
       </div>
     </div>
   </div>
@@ -51,13 +64,13 @@ import logger from '@/logging'
 import PasswordInput from '@/components/forms/PasswordInput'
 
 export default {
-  name: 'login',
-  components: {
-    PasswordInput
-  },
   props: {
+    invitation: {type: String, required: false, default: null},
     next: {type: String, default: '/'}
   },
+  components: {
+    PasswordInput
+  },
   data () {
     return {
       username: '',
@@ -85,7 +98,8 @@ export default {
         username: this.username,
         password1: this.password,
         password2: this.password,
-        email: this.email
+        email: this.email,
+        invitation: this.invitation
       }
       return axios.post('auth/registration/', payload).then(response => {
         logger.default.info('Successfully created account')
diff --git a/front/src/router/index.js b/front/src/router/index.js
index 0d2ad34f98aceff40b212625bac52b9ab9be9bf7..5528addd43f5e761bed56a0d7d4d245961980d63 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -96,7 +96,10 @@ export default new Router({
     {
       path: '/signup',
       name: 'signup',
-      component: Signup
+      component: Signup,
+      props: (route) => ({
+        invitation: route.query.invitation
+      })
     },
     {
       path: '/logout',