diff --git a/apps/api/__init__.py b/apps/api/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..1b17aec62c98e038bdaf4d806f46da8fbaf1eca1
--- /dev/null
+++ b/apps/api/__init__.py
@@ -0,0 +1,4 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+default_app_config = 'api.apps.APIConfig'
diff --git a/apps/api/apps.py b/apps/api/apps.py
new file mode 100644
index 0000000000000000000000000000000000000000..11d78652eaddc30348c6ad273bed1c13ff123908
--- /dev/null
+++ b/apps/api/apps.py
@@ -0,0 +1,10 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from django.apps import AppConfig
+from django.utils.translation import gettext_lazy as _
+
+
+class APIConfig(AppConfig):
+    name = 'api'
+    verbose_name = _('API')
diff --git a/apps/logs/__init__.py b/apps/logs/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..58ee5b085c8efabe9785c69ef048e63030fc7a80
--- /dev/null
+++ b/apps/logs/__init__.py
@@ -0,0 +1,4 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+default_app_config = 'logs.apps.LogsConfig'
diff --git a/apps/logs/apps.py b/apps/logs/apps.py
new file mode 100644
index 0000000000000000000000000000000000000000..f48820c7b91f2e9818aa179ce94ff2960d07884e
--- /dev/null
+++ b/apps/logs/apps.py
@@ -0,0 +1,14 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from django.apps import AppConfig
+from django.utils.translation import gettext_lazy as _
+
+
+class LogsConfig(AppConfig):
+    name = 'logs'
+    verbose_name = _('Logs')
+
+    def ready(self):
+        # noinspection PyUnresolvedReferences
+        import logs.signals
diff --git a/apps/logs/migrations/__init__.py b/apps/logs/migrations/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/apps/logs/models.py b/apps/logs/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..337315bb1b215e840b581bcc157bb9f9cab0ac3b
--- /dev/null
+++ b/apps/logs/models.py
@@ -0,0 +1,71 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from django.contrib.contenttypes.models import ContentType
+from django.utils.translation import gettext_lazy as _
+from django.conf import settings
+from django.core.exceptions import ValidationError
+from django.db import models
+
+
+class Changelog(models.Model):
+    """
+    Store each modification on the database (except sessions and logging),
+    including creating, editing and deleting models.
+    """
+
+    user = models.ForeignKey(
+        settings.AUTH_USER_MODEL,
+        on_delete=models.PROTECT,
+        null=True,
+        verbose_name=_('user'),
+    )
+
+    ip = models.GenericIPAddressField(
+        null=True,
+        blank=True,
+        verbose_name=_("IP Address")
+    )
+
+    model = models.ForeignKey(
+        ContentType,
+        on_delete=models.PROTECT,
+        null=False,
+        blank=False,
+        verbose_name=_('model'),
+    )
+
+    instance_pk = models.CharField(
+        max_length=255,
+        null=False,
+        blank=False,
+        verbose_name=_('identifier'),
+    )
+
+    previous = models.TextField(
+        null=True,
+        verbose_name=_('previous data'),
+    )
+
+    data = models.TextField(
+        null=True,
+        verbose_name=_('new data'),
+    )
+
+    action = models.CharField(  # create, edit or delete
+        max_length=16,
+        null=False,
+        blank=False,
+        verbose_name=_('action'),
+    )
+
+    timestamp = models.DateTimeField(
+        null=False,
+        blank=False,
+        auto_now_add=True,
+        name='timestamp',
+        verbose_name=_('timestamp'),
+    )
+
+    def delete(self, using=None, keep_parents=False):
+        raise ValidationError(_("Logs cannot be destroyed."))
diff --git a/apps/logs/signals.py b/apps/logs/signals.py
new file mode 100644
index 0000000000000000000000000000000000000000..55e0f041ba11b8c61d7b3f69a593a921d3db725e
--- /dev/null
+++ b/apps/logs/signals.py
@@ -0,0 +1,119 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+import inspect
+
+from django.contrib.contenttypes.models import ContentType
+from django.core import serializers
+from django.db.models.signals import pre_save, post_save, post_delete
+from django.dispatch import receiver
+from .models import Changelog
+
+
+def get_request_in_signal(sender):
+    req = None
+    for entry in reversed(inspect.stack()):
+        try:
+            req = entry[0].f_locals['request']
+            # Check if there is a user
+            # noinspection PyStatementEffect
+            req.user
+            break
+        except:
+            pass
+
+    if not req:
+        print("WARNING: Attempt to save " + str(sender) + " with no user")
+
+    return req
+
+
+def get_user_and_ip(sender):
+    req = get_request_in_signal(sender)
+    try:
+        user = req.user
+        if 'HTTP_X_FORWARDED_FOR' in req.META:
+            ip = req.META.get('HTTP_X_FORWARDED_FOR')
+        else:
+            ip = req.META.get('REMOTE_ADDR')
+    except:
+        user = None
+        ip = None
+    return user, ip
+
+
+EXCLUDED = [
+        'admin.logentry',
+        'authtoken.token',
+        'cas_server.user',
+        'cas_server.userattributes',
+        'contenttypes.contenttype',
+        'logs.changelog',
+        'migrations.migration',
+        'note.noteuser',
+        'note.noteclub',
+        'note.notespecial',
+        'sessions.session',
+        'reversion.revision',
+        'reversion.version',
+    ]
+
+
+@receiver(pre_save)
+def pre_save_object(sender, instance, **kwargs):
+    qs = sender.objects.filter(pk=instance.pk).all()
+    if qs.exists():
+        instance._previous = qs.get()
+    else:
+        instance._previous = None
+
+
+@receiver(post_save)
+def save_object(sender, instance, **kwargs):
+    # noinspection PyProtectedMember
+    if instance._meta.label_lower in EXCLUDED:
+        return
+
+    previous = instance._previous
+
+    user, ip = get_user_and_ip(sender)
+
+    if user is not None and instance._meta.label_lower == "auth.user" and previous:
+        # Don't save last login modifications
+        if instance.last_login != previous.last_login:
+            return
+
+    previous_json = serializers.serialize('json', [previous, ])[1:-1] if previous else None
+    instance_json = serializers.serialize('json', [instance, ])[1:-1]
+
+    if previous_json == instance_json:
+        # No modification
+        return
+
+    Changelog.objects.create(user=user,
+                             ip=ip,
+                             model=ContentType.objects.get_for_model(instance),
+                             instance_pk=instance.pk,
+                             previous=previous_json,
+                             data=instance_json,
+                             action=("edit" if previous else "create")
+                             ).save()
+
+
+@receiver(post_delete)
+def delete_object(sender, instance, **kwargs):
+    # noinspection PyProtectedMember
+    if instance._meta.label_lower in EXCLUDED:
+        return
+
+    user, ip = get_user_and_ip(sender)
+
+    instance_json = serializers.serialize('json', [instance, ])[1:-1]
+    Changelog.objects.create(user=user,
+                             ip=ip,
+                             model=ContentType.objects.get_for_model(instance),
+                             instance_pk=instance.pk,
+                             previous=instance_json,
+                             data=None,
+                             action="delete"
+                             ).save()
diff --git a/apps/logs/urls.py b/apps/logs/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..6d76674c0b0835042b4c6ae15d843d29dd070612
--- /dev/null
+++ b/apps/logs/urls.py
@@ -0,0 +1,8 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+app_name = 'logs'
+
+# TODO User interface
+urlpatterns = [
+]
diff --git a/apps/member/apps.py b/apps/member/apps.py
index 2d7f4ab7daf5701bd0c54e4714b87d245a79c5f7..83dfbc405c09191d1e5333f55f89c6c7aa36c694 100644
--- a/apps/member/apps.py
+++ b/apps/member/apps.py
@@ -2,9 +2,22 @@
 # SPDX-License-Identifier: GPL-3.0-or-later
 
 from django.apps import AppConfig
+from django.conf import settings
+from django.db.models.signals import post_save
 from django.utils.translation import gettext_lazy as _
 
+from .signals import save_user_profile
+
 
 class MemberConfig(AppConfig):
     name = 'member'
     verbose_name = _('member')
+
+    def ready(self):
+        """
+        Define app internal signals to interact with other apps
+        """
+        post_save.connect(
+            save_user_profile,
+            sender=settings.AUTH_USER_MODEL,
+        )
diff --git a/apps/member/signals.py b/apps/member/signals.py
index 4e945ad504ec4889419b9064e021183f6bc5aaaa..b17b3ae84dfdee1105204a863f1df53ed64c8cc1 100644
--- a/apps/member/signals.py
+++ b/apps/member/signals.py
@@ -1,2 +1,15 @@
 # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
+
+def save_user_profile(instance, created, raw, **_kwargs):
+    """
+    Hook to create and save a profile when an user is updated if it is not registered with the signup form
+    """
+    if raw:
+        # When provisionning data, do not try to autocreate
+        return
+
+    if created:
+        from .models import Profile
+        Profile.objects.get_or_create(user=instance)
+    instance.profile.save()
diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po
index 9e7b56d5af99cf32fe4dc29415c6c7c4f1a5353c..386db34c4f0fda73222f1ac61f9743c07c9265e8 100644
--- a/locale/de/LC_MESSAGES/django.po
+++ b/locale/de/LC_MESSAGES/django.po
@@ -82,6 +82,46 @@ msgstr ""
 msgid "guests"
 msgstr ""
 
+#: apps/api/apps.py:10
+msgid "API"
+msgstr ""
+
+#: apps/logs/apps.py:10
+msgid "Logs"
+msgstr ""
+
+#: apps/logs/models.py:20 apps/note/models/notes.py:105
+msgid "user"
+msgstr ""
+
+#: apps/logs/models.py:27
+msgid "model"
+msgstr ""
+
+#: apps/logs/models.py:34
+msgid "identifier"
+msgstr ""
+
+#: apps/logs/models.py:39
+msgid "previous data"
+msgstr ""
+
+#: apps/logs/models.py:44
+msgid "new data"
+msgstr ""
+
+#: apps/logs/models.py:51
+msgid "action"
+msgstr ""
+
+#: apps/logs/models.py:59
+msgid "timestamp"
+msgstr ""
+
+#: apps/logs/models.py:63
+msgid "Logs cannot be destroyed."
+msgstr ""
+
 #: apps/member/apps.py:10
 msgid "member"
 msgstr ""
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index 4bd4f5f2989c5e15fe79ab019ae00b8c2228ccd6..e73417406cf66d2ac35eaa003746c10b05a618ee 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -77,6 +77,50 @@ msgstr "invité"
 msgid "guests"
 msgstr "invités"
 
+#: apps/api/apps.py:10
+msgid "API"
+msgstr ""
+
+#: apps/logs/apps.py:10
+msgid "Logs"
+msgstr ""
+
+#: apps/logs/models.py:20 apps/note/models/notes.py:105
+msgid "user"
+msgstr "utilisateur"
+
+#: apps/logs/models.py:27
+msgid "model"
+msgstr "Modèle"
+
+#: apps/logs/models.py:34
+msgid "identifier"
+msgstr "Identifiant"
+
+#: apps/logs/models.py:39
+msgid "previous data"
+msgstr "Données précédentes"
+
+#: apps/logs/models.py:44
+#, fuzzy
+#| msgid "end date"
+msgid "new data"
+msgstr "Nouvelles données"
+
+#: apps/logs/models.py:51
+#, fuzzy
+#| msgid "section"
+msgid "action"
+msgstr "Action"
+
+#: apps/logs/models.py:59
+msgid "timestamp"
+msgstr "Date"
+
+#: apps/logs/models.py:63
+msgid "Logs cannot be destroyed."
+msgstr "Les logs ne peuvent pas être détruits."
+
 #: apps/member/apps.py:10
 msgid "member"
 msgstr "adhérent"
diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py
index 5a3c3f6b348d9b2703b1f32bf0afeba8fb26ce24..1810989d2bccb5c69c33196f68c290909d15a652 100644
--- a/note_kfet/settings/base.py
+++ b/note_kfet/settings/base.py
@@ -64,6 +64,7 @@ INSTALLED_APPS = [
     'member',
     'note',
     'api',
+    'logs',
 ]
 LOGIN_REDIRECT_URL = '/note/transfer/'
 
diff --git a/note_kfet/urls.py b/note_kfet/urls.py
index a5502412785677d2f0a4609cc53de34887b14c02..a261a9eb9891d9430989d46dfe2f2613e3c77c84 100644
--- a/note_kfet/urls.py
+++ b/note_kfet/urls.py
@@ -32,6 +32,8 @@ urlpatterns = [
 
     # Include Django REST API
     path('api/', include('api.urls')),
+
+    path('logs/', include('logs.urls')),
 ]
 
 urlpatterns += static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)