diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index a836dfdfddb096384bfff92855a34cf478f0aaa6..b74c2bdfe499af75053748ddb8d90b96b8b42760 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -461,3 +461,7 @@ MUSIC_DIRECTORY_PATH = env("MUSIC_DIRECTORY_PATH", default=None)
 MUSIC_DIRECTORY_SERVE_PATH = env(
     "MUSIC_DIRECTORY_SERVE_PATH", default=MUSIC_DIRECTORY_PATH
 )
+
+USERS_INVITATION_EXPIRATION_DAYS = env.int(
+    "USERS_INVITATION_EXPIRATION_DAYS", default=14
+)
diff --git a/api/funkwhale_api/users/factories.py b/api/funkwhale_api/users/factories.py
index eed8c7175a2dacdb65404aaf827c2bee04582dbb..5fceb57bbc17bdd8ac70e95bc81e672869b4abe8 100644
--- a/api/funkwhale_api/users/factories.py
+++ b/api/funkwhale_api/users/factories.py
@@ -1,5 +1,6 @@
 import factory
 from django.contrib.auth.models import Permission
+from django.utils import timezone
 
 from funkwhale_api.factories import ManyToManyFromList, registry
 
@@ -28,6 +29,17 @@ class GroupFactory(factory.django.DjangoModelFactory):
             self.permissions.add(*perms)
 
 
+@registry.register
+class InvitationFactory(factory.django.DjangoModelFactory):
+    owner = factory.LazyFunction(lambda: UserFactory())
+
+    class Meta:
+        model = "users.Invitation"
+
+    class Params:
+        expired = factory.Trait(expiration_date=factory.LazyFunction(timezone.now))
+
+
 @registry.register
 class UserFactory(factory.django.DjangoModelFactory):
     username = factory.Sequence(lambda n: "user-{0}".format(n))
@@ -40,6 +52,9 @@ class UserFactory(factory.django.DjangoModelFactory):
         model = "users.User"
         django_get_or_create = ("username",)
 
+    class Params:
+        invited = factory.Trait(invitation=factory.SubFactory(InvitationFactory))
+
     @factory.post_generation
     def perms(self, create, extracted, **kwargs):
         if not create:
diff --git a/api/funkwhale_api/users/migrations/0009_auto_20180619_2024.py b/api/funkwhale_api/users/migrations/0009_auto_20180619_2024.py
new file mode 100644
index 0000000000000000000000000000000000000000..e8204c4e4748371b7906d9115b0d529f30a35db7
--- /dev/null
+++ b/api/funkwhale_api/users/migrations/0009_auto_20180619_2024.py
@@ -0,0 +1,31 @@
+# Generated by Django 2.0.6 on 2018-06-19 20:24
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('users', '0008_auto_20180617_1531'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Invitation',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
+                ('expiration_date', models.DateTimeField()),
+                ('code', models.CharField(max_length=50, unique=True)),
+                ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
+        migrations.AddField(
+            model_name='user',
+            name='invitation',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='users', to='users.Invitation'),
+        ),
+    ]
diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py
index 15d16db2369967cddf38310d871c6bae3733dab0..61f57a3c55dcced4af65586b2c20012b6af6f391 100644
--- a/api/funkwhale_api/users/models.py
+++ b/api/funkwhale_api/users/models.py
@@ -4,6 +4,8 @@ from __future__ import absolute_import, unicode_literals
 import binascii
 import datetime
 import os
+import random
+import string
 import uuid
 
 from django.conf import settings
@@ -79,6 +81,14 @@ class User(AbstractUser):
 
     last_activity = models.DateTimeField(default=None, null=True, blank=True)
 
+    invitation = models.ForeignKey(
+        "Invitation",
+        related_name="users",
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+    )
+
     def __str__(self):
         return self.username
 
@@ -138,3 +148,39 @@ class User(AbstractUser):
         if current is None or current < now - datetime.timedelta(seconds=delay):
             self.last_activity = now
             self.save(update_fields=["last_activity"])
+
+
+def generate_code(length=10):
+    return "".join(
+        random.SystemRandom().choice(string.ascii_lowercase) for _ in range(length)
+    )
+
+
+class InvitationQuerySet(models.QuerySet):
+    def open(self):
+        now = timezone.now()
+        qs = self.annotate(_users=models.Count("users"))
+        qs = qs.filter(_users=0)
+        qs = qs.exclude(expiration_date__lte=now)
+        return qs
+
+
+class Invitation(models.Model):
+    creation_date = models.DateTimeField(default=timezone.now)
+    expiration_date = models.DateTimeField()
+    owner = models.ForeignKey(
+        User, related_name="invitations", on_delete=models.CASCADE
+    )
+    code = models.CharField(max_length=50, unique=True)
+
+    objects = InvitationQuerySet.as_manager()
+
+    def save(self, **kwargs):
+        if not self.code:
+            self.code = generate_code()
+        if not self.expiration_date:
+            self.expiration_date = self.creation_date + datetime.timedelta(
+                days=settings.USERS_INVITATION_EXPIRATION_DAYS
+            )
+
+        return super().save(**kwargs)
diff --git a/api/tests/users/test_models.py b/api/tests/users/test_models.py
index 74bb091e54270c4034050b7810e2b6ba3cb4bde3..475691293dd347cd81946743994cb20fef62be09 100644
--- a/api/tests/users/test_models.py
+++ b/api/tests/users/test_models.py
@@ -1,3 +1,4 @@
+import datetime
 import pytest
 
 from funkwhale_api.users import models
@@ -95,3 +96,25 @@ def test_record_activity_does_nothing_if_already(factories, now, mocker):
     user.record_activity()
 
     save.assert_not_called()
+
+
+def test_invitation_generates_random_code_on_save(factories):
+    invitation = factories["users.Invitation"]()
+    assert len(invitation.code) >= 6
+
+
+def test_invitation_expires_after_delay(factories, settings):
+    delay = settings.USERS_INVITATION_EXPIRATION_DAYS
+    invitation = factories["users.Invitation"]()
+    assert invitation.expiration_date == (
+        invitation.creation_date + datetime.timedelta(days=delay)
+    )
+
+
+def test_can_filter_open_invitations(factories):
+    okay = factories["users.Invitation"]()
+    factories["users.Invitation"](expired=True)
+    factories["users.User"](invited=True)
+
+    assert models.Invitation.objects.count() == 3
+    assert list(models.Invitation.objects.open()) == [okay]