diff --git a/oser_backend/settings/common.py b/oser_backend/settings/common.py
index 85aadb61e3d8b6251379cbdf43d5223f4cb7b788..1a1b18a429a33d7d47392c1bc3da94de942961c0 100644
--- a/oser_backend/settings/common.py
+++ b/oser_backend/settings/common.py
@@ -35,6 +35,7 @@ DJANGO_APPS = [
     'whitenoise.runserver_nostatic',
     'django.contrib.staticfiles',
     'django.forms',
+    'django.contrib.sites',
 ]
 
 THIRD_PARTY_APPS = [
@@ -60,6 +61,7 @@ THIRD_PARTY_APPS = [
     # Easy filtering on the API
     'django_filters',
 ]
+
 PROJECT_APPS = [
     'core.apps.CoreConfig',
     'users.apps.UsersConfig',
@@ -72,8 +74,14 @@ PROJECT_APPS = [
     'dynamicforms.apps.DynamicformsConfig',
     'projects.apps.ProjectsConfig',
 ]
+
 INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + PROJECT_APPS
 
+# Activate the sites framework
+# It is used to define the domain of the frontend website in
+# the admin (via the 'Sites' section)
+SITE_ID = 1
+
 MIDDLEWARE = [
     'django.middleware.security.SecurityMiddleware',
     'corsheaders.middleware.CorsMiddleware',
diff --git a/profiles/factory.py b/profiles/factory.py
index ea03ddd9b733673ce25ad06b25aea18143a7d93c..639c70fdc222f00e863aa590741453282a2283fe 100644
--- a/profiles/factory.py
+++ b/profiles/factory.py
@@ -9,6 +9,7 @@ from django.contrib.auth.models import Group
 
 from tutoring.factory import TutoringGroupFactory
 from tutoring.models import TutoringGroup
+from core.factory import AddressFactory
 from users.factory import UserFactory
 
 from . import models
@@ -49,6 +50,7 @@ class TutorFactory(factory.DjangoModelFactory):
 
     user = factory.SubFactory(UserFactory)
     promotion = factory.Iterator([_this_year, _this_year + 1, _this_year + 2])
+    address = factory.SubFactory(AddressFactory)
 
 
 class TutorInGroupFactory(TutorFactory):
diff --git a/projects/models.py b/projects/models.py
index 876c3519148674c2bd2053275afe0a89190e16dd..dc7d675e8db5f98cbb651cc861b5d1ccd0a89cd3 100644
--- a/projects/models.py
+++ b/projects/models.py
@@ -1,6 +1,7 @@
 """Projects models."""
 
 from django.db import models
+from django.contrib.sites.models import Site
 from django.core.validators import ValidationError
 
 from markdownx.models import MarkdownxField
@@ -82,6 +83,14 @@ class Edition(models.Model):
         verbose_name = 'édition'
         get_latest_by = 'year'
 
+    def get_projects_site_url(self) -> str:
+        site = Site.objects.get_current()
+        return f'http://{site.domain}/projets/'
+
+    def get_registration_url(self) -> str:
+        site = Site.objects.get_current()
+        return f'http://{site.domain}/projets/mes-inscriptions'
+
     def __str__(self) -> str:
         """Represent using the project name, the year and the edition name."""
         s = f'{self.project} édition {self.year}'
@@ -219,6 +228,16 @@ class Participation(models.Model):
     class Meta:  # noqa
         ordering = ('-submitted',)
 
+    def __init__(self, *args, **kwargs):
+        """Store the initial value of `state` to detect changes."""
+        super().__init__(*args, **kwargs)
+        self.initial_state = self.state
+
+    @property
+    def state_changed(self) -> bool:
+        """Return whether the `state` field has changed."""
+        return self.initial_state != self.state
+
     def __str__(self):
         """Represent by its user."""
         return str(self.user)
diff --git a/projects/notifications.py b/projects/notifications.py
new file mode 100644
index 0000000000000000000000000000000000000000..2ef5f91831925d4d35efdfeafcb9611481087aa2
--- /dev/null
+++ b/projects/notifications.py
@@ -0,0 +1,111 @@
+"""Projects notifications."""
+
+
+from mails import Notification
+from django.utils.timezone import now
+from users.models import User
+from .models import Edition, Project
+
+
+class _BaseParticipationNotification(Notification):
+    """Base notification for project participations."""
+
+    args = ('user', 'edition',)
+
+    @classmethod
+    def example(cls):
+        """Example notification."""
+        user = User(email='john.doe@example.com', first_name='John')
+        project = Project(title='Focus Europe')
+        edition = Edition(project=project, year=now().year)
+        return cls(user=user, edition=edition)
+
+
+class _NotifyOrgnizers(_BaseParticipationNotification):
+    """Notify the edition organizers a participation has changed."""
+
+    title: str
+
+    def get_subject(self):
+        return f'{self.title}: {self.edition}'
+
+        def get_recipients(self):
+            """Return the email of each organizer."""
+            edition = self.kwargs['edition']
+            # TODO add the project team's email
+            return list(edition.values_list('organizers__email', flat=True))
+
+
+class _NotifyUser(_BaseParticipationNotification):
+    """Notify a user their participation state has changed."""
+
+    verb: str
+
+    def get_subject(self):
+        return f'Dossier {self.verb}: {self.edition}'
+
+        def get_recipients(self):
+            return [self.kwargs['user'].email]
+
+
+class OrganizersReceived(_NotifyOrgnizers):
+    """Notify the edition organizers that a new participation was created."""
+
+    title = 'Nouvelle participation'
+    template_name = 'projects/organizers_participation_received.md'
+
+
+class UserReceived(_NotifyUser):
+    """Notify a user their participation was well received."""
+
+    verb = 'en attente'
+    template_name = 'projects/participation_received.md'
+
+
+class UserValid(_NotifyUser):
+    """Notify a user their participation was marked as valid."""
+
+    verb = 'validé'
+    template_name = 'projects/participation_valid.md'
+
+
+class UserAccepted(_NotifyUser):
+    """Notify a user their participation was marked as accepted."""
+
+    verb = 'accepté'
+    template_name = 'projects/participation_accepted.md'
+
+
+class UserRejected(_NotifyUser):
+    """Notify a user their participation was marked as rejected."""
+
+    verb = 'rejeté'
+    template_name = 'projects/participation_rejected.md'
+
+
+class UserCancelled(_NotifyUser):
+    """Notify a user their participation was correctly cancelled."""
+
+    verb = 'annulé'
+    template_name = 'projects/participation_cancelled.md'
+
+
+class UserDeleted(_NotifyUser):
+    """Notify a user their participation was correctly deleted."""
+
+    verb = 'supprimé'
+    template_name = 'projects/participation_deleted.md'
+
+
+class OrganizersCancelled(_NotifyOrgnizers):
+    """Notify organizers that a user has cancelled their participation."""
+
+    title = 'Participation annulée'
+    template_name = 'projects/organizers_participation_cancelled.md'
+
+
+class OrganizersDeleted(_NotifyOrgnizers):
+    """Notify organizers a user has deleted their participation."""
+
+    title = 'Participation supprimée'
+    template_name = 'projects/organizers_participation_deleted.md'
diff --git a/projects/signals.py b/projects/signals.py
index 4b75cea9a475f587c0874e5a2b8582cb55b24124..bea1c4e96bfd551b39aa46dd89e427955f2cb583 100644
--- a/projects/signals.py
+++ b/projects/signals.py
@@ -1,20 +1,93 @@
 """Projects app signals."""
 
 import logging
-from django.db.models.signals import pre_delete
-from django.dispatch import receiver
 
-from .models import Participation
+from django.db.models.signals import post_save, pre_delete
+from django.dispatch import receiver, Signal
 
+from . import notifications
+from .models import Participation
 
 logger = logging.getLogger('web.projects.signals')
 
 
+pending = Signal(providing_args=('instance',))
+valid = Signal(providing_args=('instance',))
+accepted = Signal(providing_args=('instance',))
+rejected = Signal(providing_args=('instance',))
+cancelled = Signal(providing_args=('instance',))
+deleted = Signal(providing_args=('instance',))
+deleted_organizers = Signal(providing_args=('instance',))
+
+
+def _send(cls, instance: Participation):
+    cls(user=instance.user, edition=instance.edition).send()
+
+
 @receiver(pre_delete, sender=Participation)
-def delete_associated_form_entry(sender, instance: Participation,
-                                 *args, **kwargs):
+def delete_associated_form_entry(sender, instance: Participation, **kwargs):
     """Delete the form entry associated to a participation being deleted."""
     entry = instance.entry
     if entry:
         entry.delete()
         logger.info('entry %s deleted', entry.id)
+
+
+@receiver(post_save, sender=Participation)
+def send_state_notifications(sender, instance: Participation,
+                             created, **kwargs):
+    """Send notifications when the state of a participation has changed."""
+    if not created and not instance.state_changed:
+        return
+    signals = {
+        Participation.STATE_PENDING: pending,
+        Participation.STATE_VALIDATED: valid,
+        Participation.STATE_ACCEPTED: accepted,
+        Participation.STATE_REJECTED: rejected,
+        Participation.STATE_CANCELLED: cancelled,
+    }
+    if instance.state in signals.keys():
+        signals[instance.state].send(Participation, instance=instance)
+
+
+@receiver(pre_delete, sender=Participation)
+def send_participation_deleted_notifications(sender, instance: Participation,
+                                             **kwargs):
+    """Send notifications when a participation is deleted."""
+    deleted.send(Participation, instance=instance)
+
+
+# Notiication senders
+
+@receiver(pending)
+def notify_pending(sender, instance, **kwargs):
+    print('hello')
+    _send(notifications.UserReceived, instance)
+    _send(notifications.OrganizersReceived, instance)
+
+
+@receiver(valid)
+def notify_valid(sender, instance, **kwargs):
+    _send(notifications.UserValid, instance)
+
+
+@receiver(accepted)
+def notify_accepted(sender, instance, **kwargs):
+    _send(notifications.UserAccepted, instance)
+
+
+@receiver(rejected)
+def notify_rejected(sender, instance, **kwargs):
+    _send(notifications.UserRejected, instance)
+
+
+@receiver(cancelled)
+def notify_cancelled(sender, instance, **kwargs):
+    _send(notifications.UserCancelled, instance)
+    _send(notifications.OrganizersCancelled, instance)
+
+
+@receiver(deleted)
+def notify_deleted(sender, instance, **kwargs):
+    _send(notifications.UserDeleted, instance)
+    _send(notifications.OrganizersDeleted, instance)
diff --git a/projects/templates/projects/organizers_participation_cancelled.md b/projects/templates/projects/organizers_participation_cancelled.md
new file mode 100644
index 0000000000000000000000000000000000000000..b3c82fdecad51c73ec2c3f4e608823c7e8a233cd
--- /dev/null
+++ b/projects/templates/projects/organizers_participation_cancelled.md
@@ -0,0 +1,9 @@
+{% extends 'projects/to_user.md' %}
+
+{% block body %}
+L'utilisateur {{ user }} a supprimé sa participation à {{ edition }}.
+
+Si besoin, vous pouvez contacter {{ user }} via son adresse email : {{ user.email }}.
+
+Vous pouvez télécharger la feuille des inscrits mise à jour sur [le site d'administration]({% url 'admin:projects_editionform_changelist' %}).
+{% endblock %}
diff --git a/projects/templates/projects/organizers_participation_deleted.md b/projects/templates/projects/organizers_participation_deleted.md
new file mode 100644
index 0000000000000000000000000000000000000000..b6648b782fd8f479aa3289e88ba1b086d9505ddf
--- /dev/null
+++ b/projects/templates/projects/organizers_participation_deleted.md
@@ -0,0 +1,9 @@
+{% extends 'projects/to_user.md' %}
+
+{% block body %}
+L'utilisateur {{ user }} a annulé sa participation à {{ edition }}.
+
+Si besoin, vous pouvez contacter {{ user }} via son adresse email : {{ user.email }}.
+
+Vous pouvez télécharger la feuille des inscrits mise à jour sur [le site d'administration]({% url 'admin:projects_editionform_changelist' %}).
+{% endblock %}
diff --git a/projects/templates/projects/organizers_participation_received.md b/projects/templates/projects/organizers_participation_received.md
new file mode 100644
index 0000000000000000000000000000000000000000..e3c715a793a76b6d1acdcab7ca469abc402dbfac
--- /dev/null
+++ b/projects/templates/projects/organizers_participation_received.md
@@ -0,0 +1,7 @@
+{% extends 'projects/to_user.md' %}
+
+{% block body %}
+{{ user }} s'est inscrit à {{ edition }}.
+
+Vous pouvez télécharger la feuille des inscrits mise à jour sur [le site d'administration]({% url 'admin:projects_editionform_changelist' %}).
+{% endblock %}
diff --git a/projects/templates/projects/participation_accepted.md b/projects/templates/projects/participation_accepted.md
new file mode 100644
index 0000000000000000000000000000000000000000..2774311572aaee752fa24ee787c4da28197ae3b2
--- /dev/null
+++ b/projects/templates/projects/participation_accepted.md
@@ -0,0 +1,8 @@
+{% extends 'projects/to_user.md' %}
+
+{% block body %}
+
+Nous avons le plaisir de te confirmer ta participation à {{ edition }} ! 🎉
+
+Nous te recontacterons très prochainement pour te communiquer les derniers détails pratiques.
+{% endblock %}
diff --git a/projects/templates/projects/participation_cancelled.md b/projects/templates/projects/participation_cancelled.md
new file mode 100644
index 0000000000000000000000000000000000000000..58eef49ca2a93650c11608d5fb16a4019f019d42
--- /dev/null
+++ b/projects/templates/projects/participation_cancelled.md
@@ -0,0 +1,9 @@
+{% extends 'projects/to_user.md' %}
+
+{% block body %}
+Ta participation à {{ edition }} a bien été annulée.
+
+Si tu souhaites réactiver ta demande de participation, tu peux le faire
+avant le {{ edition.edition_form.deadline | date }} dans la section
+[Mes inscriptions]({{ edition.get_registration_url }}).
+{% endblock %}
diff --git a/projects/templates/projects/participation_deleted.md b/projects/templates/projects/participation_deleted.md
new file mode 100644
index 0000000000000000000000000000000000000000..def88a1caee4dcc3c06f462eaf2cf1743fd7229f
--- /dev/null
+++ b/projects/templates/projects/participation_deleted.md
@@ -0,0 +1,7 @@
+{% extends 'projects/to_user.md' %}
+
+{% block body %}
+Ta participation à {{ edition }} a bien été supprimée.
+
+Si tu souhaites finalement participer à ce projet, tu devras te réinscrire en te rendant sur [l'espace projets]({{ edition.get_projects_site_url }}).
+{% endblock %}
diff --git a/projects/templates/projects/participation_received.md b/projects/templates/projects/participation_received.md
new file mode 100644
index 0000000000000000000000000000000000000000..9cb4bfa2c9114d1ccaa532b049082d9f57c9ff82
--- /dev/null
+++ b/projects/templates/projects/participation_received.md
@@ -0,0 +1,19 @@
+{% extends 'projects/to_user.md' %}
+
+{% block body %}
+Tu as demandé t'inscrire à {{ edition }} via l'espace projets.
+
+Nous avons bien reçu ta demande et allons vérifier ton dossier dès que possible.
+
+{% if edition.edition_form.form.files.count %}
+**Rappel** : l'inscription à ce projet nécessite de fournir des documents complémentaires. 📖 Tu devras nous les faire parvenir **impérativement avant le {{ edition.edition_form.deadline | date }}** à l'adresse suivante :
+
+**{{ edition.edition_form.recipient.user.get_full_name }}**
+
+{{ edition.edition_form.recipient.address }}
+
+🔗 Tu peux télécharger ces documents à tout moment en te rendant dans la section [Mes inscriptions]({{ edition.get_registration_url }}).
+
+⚠️ Nous ne pourrons valider ton dossier qu'une fois ces documents reçus.
+{% endif %}
+{% endblock %}
diff --git a/projects/templates/projects/participation_rejected.md b/projects/templates/projects/participation_rejected.md
new file mode 100644
index 0000000000000000000000000000000000000000..e55a80cc09322d4754e880273f0cc7ec77bc8d66
--- /dev/null
+++ b/projects/templates/projects/participation_rejected.md
@@ -0,0 +1,9 @@
+{% extends 'projects/to_user.md' %}
+
+{% block body %}
+Nous avons le regret de t'annoncer qu'en raison du nombre de places limité,
+nous n'avons pas pu retenir ta demande de participation à {{ edition }}. 😔
+
+Ne t'en fais pas ! Tu as été placé sur la liste d'attente et
+nous te recontacterons si des places se libèrent suite à des désistements.
+{% endblock %}
diff --git a/projects/templates/projects/participation_valid.md b/projects/templates/projects/participation_valid.md
new file mode 100644
index 0000000000000000000000000000000000000000..f31c3b649cac2fa9ce734ec1ce2e47c832c9d3f8
--- /dev/null
+++ b/projects/templates/projects/participation_valid.md
@@ -0,0 +1,10 @@
+{% extends 'projects/to_user.md' %}
+
+{% block body %}
+Nous venons de valider ton dossier pour {{ edition }} : celui-ci est bien complet ! ✅
+
+Une fois la période des inscriptions terminée, nous étudierons chaque demande
+avec soin pour établir la liste des inscrits. Cela se fera à partir du {{ edition.edition_form.deadline | date }}.
+
+Nous te recontacterons alors pour te confirmer ta participation.
+{% endblock %}
diff --git a/projects/templates/projects/to_user.md b/projects/templates/projects/to_user.md
new file mode 100644
index 0000000000000000000000000000000000000000..00fc3dffc3de9c18ca092ddeaa54f288df36f031
--- /dev/null
+++ b/projects/templates/projects/to_user.md
@@ -0,0 +1,11 @@
+{% extends 'mails/notification.md' %}
+
+{% block greeting %}
+Bonjour{% if participation.user.first_name %} {{ participation.user.first_name }}{% endif %},
+{% endblock %}
+
+{% block signature %}
+À très bientôt,
+
+Les organisateurs
+{% endblock %}
diff --git a/tests/test_projects/test_signals.py b/tests/test_projects/test_signals.py
new file mode 100644
index 0000000000000000000000000000000000000000..62e7f10a4d8eaf047e00c60ec936537ffa47dbda
--- /dev/null
+++ b/tests/test_projects/test_signals.py
@@ -0,0 +1,68 @@
+"""Test the projects app signals."""
+
+from django.test import TestCase
+from django.utils.timezone import now
+from tests.utils.mixins import SignalTestMixin
+
+from mails.signals import notification_sent
+
+from dynamicforms.models import Form
+from profiles.factory import TutorFactory
+from projects.factory import (EditionFactory, ParticipationFactory,
+                              ProjectFactory)
+from projects.models import EditionForm, Participation
+from projects.signals import (accepted, cancelled, deleted, pending, rejected,
+                              valid)
+
+
+class NotifyParticipationTest(SignalTestMixin, TestCase):
+    """Test project participation signal handlers."""
+
+    def setUp(self):
+        # Create all objects that need to exist for rendering emails
+        project = ProjectFactory.create(name='Focus Europe')
+        self.edition = EditionFactory.create(project=project, year=2018)
+        recipient = TutorFactory.create()
+        form = Form.objects.create(title=f'Inscriptions à {self.edition}')
+        EditionForm.objects.create(
+            edition=self.edition,
+            form=form,
+            recipient=recipient,
+            deadline=now(),
+        )
+        self.obj = self.create_obj()
+
+    def create_obj(self):
+        return ParticipationFactory.create(edition=self.edition)
+
+    def change(self, state):
+        self.obj.state = state
+        self.obj.save()
+
+    def test_create_participation_pending_called(self):
+        with self.assertCalled(pending):
+            self.create_obj()
+
+    def test_save_without_changing_state_not_called(self):
+        with self.assertNotCalled(pending):
+            self.obj.save()
+
+    def test_valid_called(self):
+        with self.assertCalled(valid):
+            self.change(Participation.STATE_VALIDATED)
+
+    def test_accepted_called(self):
+        with self.assertCalled(accepted):
+            self.change(Participation.STATE_ACCEPTED)
+
+    def test_rejected_called(self):
+        with self.assertCalled(rejected):
+            self.change(Participation.STATE_REJECTED)
+
+    def test_cancelled_called(self):
+        with self.assertCalled(cancelled):
+            self.change(Participation.STATE_CANCELLED)
+
+    def test_deleted_called(self):
+        with self.assertCalled(deleted):
+            self.obj.delete()
diff --git a/tests/test_visits/test_signals.py b/tests/test_visits/test_signals.py
index 01027bc744db579ed305fc79f5bb90a585fb8133..188bd95bf69c6669e32b1999182c07aeae732141 100644
--- a/tests/test_visits/test_signals.py
+++ b/tests/test_visits/test_signals.py
@@ -4,7 +4,6 @@ from django.test import TestCase
 from tests.utils.mixins import SignalTestMixin
 
 from mails.signals import notification_sent
-from users.factory import UserFactory
 from visits.factory import ParticipationFactory, VisitFactory
 from visits.notifications import ConfirmParticipation
 from visits.signals import accepted_changed
diff --git a/tests/utils/mixins.py b/tests/utils/mixins.py
index b7cd15e056ef14ec1f40db339caf9ba3b551c101..37de2965a851af517cbfce6ff3420726b565c1e5 100644
--- a/tests/utils/mixins.py
+++ b/tests/utils/mixins.py
@@ -52,7 +52,7 @@ class SignalTestMixin:
 
         Pass `sender` to check the signal's sender too.
         """
-        called = {'value': None}
+        called = {'value': False}
 
         def listen(sender, **kwargs):
             called['value'] = True
@@ -65,3 +65,17 @@ class SignalTestMixin:
         self.assertTrue(called['value'])
         if 'sender' in kwargs:
             self.assertEqual(called['sender'], kwargs['sender'])
+
+    @contextmanager
+    def assertNotCalled(self, signal):
+        """Verify that a signal is NOT called."""
+        called = {'value': False}
+
+        def listen(sender, **kwargs):
+            called['value'] = True
+
+        signal.connect(listen)
+        yield
+        signal.disconnect(listen)
+
+        self.assertFalse(called['value'])
diff --git a/visits/models.py b/visits/models.py
index 62aef12da83f59786fb49b24ebd251169244e56b..e8f73336a2ace397ab3e13e8d3e56244c5b2c70e 100644
--- a/visits/models.py
+++ b/visits/models.py
@@ -1,5 +1,6 @@
 """Visits models."""
 from django.db import models
+from django.contrib.sites.models import Site
 from django.shortcuts import reverse
 from django.utils.timezone import now
 from dry_rest_permissions.generics import authenticated_users
@@ -214,7 +215,8 @@ class Visit(models.Model):
         return reverse('api:visit-detail', args=[str(self.pk)])
 
     def get_site_url(self):
-        return f'http://oser-cs.fr/visits/{self.pk}'
+        site = Site.objects.get_current()
+        return f'http://{site.domain}/visits/{self.pk}'
 
     def __str__(self):
         return str(self.title)