diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 4222609531b74ee07ca97ff4f53cc1711c95839c..e88765a522ee299d86ed6d2965ead5ef22035c74 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,7 +9,8 @@ New features
 
 * Two-factor authentication with TOTP (Google Authenticator), Yubikey, SMS
   and phone call.
-
+* Devs: CRUDMixin provides a crud_event relation that returns all CRUD
+  events for an object
 
 `1.0a2`_
 --------
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 1837bdd5b6d0d5fc97e320aeee7e40d55f569e11..25557e7e06ce82b323a83889f07a7a9ab444e350 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -54,6 +54,15 @@ Commit messages
 Commit messages should be written as described in `How to Write a Git Commit
 Message`_.
 
+Commit messages should mention or even close any related issues. For merely
+mentioning progress on an issue, use the keyword `advances`; for closing an
+issue, use `closes`; for referring to a related issue for informational
+purposes, use `cf.`. This should be done in the body of the commit message.
+
+The subject of a commit message can (and should) be prepended with a tag in
+square brackets if it relates to a certain part of the repository, e.g. [CI]
+when changing CI/CD configuration or support code, [Dev] when changing
+something in the development utilities, etc.
 
 Manifestos governing development
 --------------------------------
diff --git a/biscuit/core/apps.py b/biscuit/core/apps.py
index 2d6a4873e4da00c4d6dd7cd83dd0e100230e3e45..b40f987a4274c49df092a8535ce425bd848ca55f 100644
--- a/biscuit/core/apps.py
+++ b/biscuit/core/apps.py
@@ -1,10 +1,8 @@
 from glob import glob
 import os
-from warnings import warn
 
-from django.apps import AppConfig, apps
+from django.apps import AppConfig
 from django.conf import settings
-from django.db.utils import ProgrammingError
 
 
 class CoreConfig(AppConfig):
@@ -19,14 +17,5 @@ class CoreConfig(AppConfig):
                 # Ignore because old is better than nothing
                 pass  # noqa
 
-    def setup_data(self) -> None:
-        try:
-            apps.get_model('otp_yubikey', 'ValidationService').objects.update_or_create(
-                name='default', defaults={'use_ssl': True, 'param_sl': '', 'param_timeout': ''}
-            )
-        except ProgrammingError:
-            warn('Yubikey validation service could not be created yet. If you are currently in a migration, this is expected.')
-
     def ready(self) -> None:
         self.clean_scss()
-#        self.setup_data()
diff --git a/biscuit/core/migrations/0001_initial.py b/biscuit/core/migrations/0001_initial.py
index d0c78a5ae09b6a42c6721bc7d64b741e94ad6877..1a88ae010c8497c6e432931ec15438feda3e0aa1 100644
--- a/biscuit/core/migrations/0001_initial.py
+++ b/biscuit/core/migrations/0001_initial.py
@@ -1,6 +1,5 @@
 # Generated by Django 2.2.5 on 2019-09-03 18:30
 
-import biscuit.core.util.core_helpers
 from django.conf import settings
 from django.db import migrations, models
 from django.utils.translation import ugettext_lazy as _
@@ -70,7 +69,7 @@ class Migration(migrations.Migration):
                 ('import_ref', models.CharField(blank=True, editable=False, max_length=64, null=True, verbose_name='Reference ID of import source')),
                 ('guardians', models.ManyToManyField(related_name='children', to='core.Person', verbose_name='Guardians / Parents')),
                 ('primary_group', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.Group')),
-                ('school', models.ForeignKey(default=biscuit.core.util.core_helpers.get_current_school, on_delete=django.db.models.deletion.CASCADE, to='core.School')),
+                ('school', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='core.School')),
                 ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='person', to=settings.AUTH_USER_MODEL)),
             ],
             options={
@@ -96,7 +95,7 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='group',
             name='school',
-            field=models.ForeignKey(default=biscuit.core.util.core_helpers.get_current_school, on_delete=django.db.models.deletion.CASCADE, to='core.School'),
+            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='core.School'),
         ),
         migrations.AlterUniqueTogether(
             name='group',
diff --git a/biscuit/core/migrations/0002_school_term.py b/biscuit/core/migrations/0002_school_term.py
index e3d25010df8d9f81116e2c5d1ccb7b7516a3f9bd..026d7e3a518fe4a4d299ca01500605afbdced4ca 100644
--- a/biscuit/core/migrations/0002_school_term.py
+++ b/biscuit/core/migrations/0002_school_term.py
@@ -1,6 +1,5 @@
 # Generated by Django 2.2.5 on 2019-09-14 12:55
 
-import biscuit.core.util.core_helpers
 from django.db import migrations, models
 import django.db.models.deletion
 from django.utils.translation import ugettext_lazy as _
@@ -32,7 +31,7 @@ class Migration(migrations.Migration):
                 ('caption', models.CharField(max_length=30, verbose_name='Visible caption of the term')),
                 ('date_start', models.DateField(null=True, verbose_name='Effective start date of term')),
                 ('date_end', models.DateField(null=True, verbose_name='Effective end date of term')),
-                ('school', models.ForeignKey(default=biscuit.core.util.core_helpers.get_current_school, on_delete=django.db.models.deletion.CASCADE, to='core.School')),
+                ('school', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='core.School')),
             ],
             options={
                 'abstract': False,
diff --git a/biscuit/core/migrations/0004_yubi_otp.py b/biscuit/core/migrations/0004_yubi_otp.py
new file mode 100644
index 0000000000000000000000000000000000000000..fa7c40346d627fbbe00f27122a979941334a5e86
--- /dev/null
+++ b/biscuit/core/migrations/0004_yubi_otp.py
@@ -0,0 +1,19 @@
+from django.db import migrations
+
+
+def create_validation_service(apps, schema_editor):
+    apps.get_model('otp_yubikey', 'ValidationService').objects.update_or_create(
+        name='default', defaults={'use_ssl': True, 'param_sl': '', 'param_timeout': ''}
+    )
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0003_school_logo'),
+        ('otp_yubikey', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.RunPython(create_validation_service),
+    ]
diff --git a/biscuit/core/mixins.py b/biscuit/core/mixins.py
index 024dd4870d076da301dd3c51be1a0e603e399144..9ec842a5ea99b793c688a63a9a2e8c1d5d9203b4 100644
--- a/biscuit/core/mixins.py
+++ b/biscuit/core/mixins.py
@@ -6,8 +6,6 @@ from django.db.models import QuerySet
 
 from easyaudit.models import CRUDEvent
 
-from .util.core_helpers import get_current_school
-
 
 class ExtensibleModel(object):
     """ Allow injection of code from BiscuIT apps to extend model functionality.
@@ -69,16 +67,10 @@ class ExtensibleModel(object):
 
         cls._safe_add(func, func.__name__)
 
-
-class SchoolRelated(models.Model):
+class CRUDMixin(models.Model):
     class Meta:
         abstract = True
 
-#    objects = SchoolRelatedManager()
-
-    school = models.ForeignKey(
-        'core.School', on_delete=models.CASCADE, default=get_current_school)
-
     @property
     def crud_events(self) -> QuerySet:
         """Get all CRUD events connected to this object from easyaudit."""
diff --git a/biscuit/core/models.py b/biscuit/core/models.py
index 6e1f6fb032dabea0c775cea3a0e45c315f5e4cb8..02071d09d4ae6cf04228be86ea3c5dd417234ace 100644
--- a/biscuit/core/models.py
+++ b/biscuit/core/models.py
@@ -8,7 +8,7 @@ import dbsettings
 from image_cropping import ImageCropField, ImageRatioField
 from phonenumber_field.modelfields import PhoneNumberField
 
-from .mixins import ExtensibleModel, SchoolRelated
+from .mixins import ExtensibleModel
 
 
 class ThemeSettings(dbsettings.Group):
@@ -44,7 +44,7 @@ class School(models.Model):
         ordering = ['name', 'name_official']
 
 
-class SchoolTerm(SchoolRelated):
+class SchoolTerm(models.Model):
     """ Information about a term (limited time frame) that data can
     be linked to.
     """
@@ -58,13 +58,12 @@ class SchoolTerm(SchoolRelated):
         'Effective end date of term'), null=True)
 
 
-class Person(SchoolRelated, ExtensibleModel):
+class Person(models.Model, ExtensibleModel):
     """ A model describing any person related to a school, including, but not
     limited to, students, teachers and guardians (parents).
     """
 
     class Meta:
-        unique_together = [['school', 'short_name'], ['school', 'import_ref']]
         ordering = ['last_name', 'first_name']
 
     SEX_CHOICES = [
@@ -84,7 +83,7 @@ class Person(SchoolRelated, ExtensibleModel):
         'Additional name(s)'), max_length=30, blank=True)
 
     short_name = models.CharField(verbose_name=_(
-        'Short name'), max_length=5, blank=True, null=True)
+        'Short name'), max_length=5, blank=True, null=True, unique=True)
 
     street = models.CharField(verbose_name=_(
         'Street'), max_length=30, blank=True)
@@ -110,7 +109,8 @@ class Person(SchoolRelated, ExtensibleModel):
     photo_cropping = ImageRatioField('photo', '600x800', size_warning=True)
 
     import_ref = models.CharField(verbose_name=_(
-        'Reference ID of import source'), max_length=64, blank=True, null=True, editable=False)
+        'Reference ID of import source'), max_length=64,
+        blank=True, null=True, editable=False, unique=True)
 
     guardians = models.ManyToManyField('self', verbose_name=_('Guardians / Parents'),
                                        symmetrical=False, related_name='children')
@@ -146,19 +146,18 @@ class Person(SchoolRelated, ExtensibleModel):
         return self.full_name
 
 
-class Group(SchoolRelated, ExtensibleModel):
+class Group(models.Model, ExtensibleModel):
     """Any kind of group of persons in a school, including, but not limited
     classes, clubs, and the like.
     """
 
     class Meta:
-        unique_together = [['school', 'name'], ['school', 'short_name']]
         ordering = ['short_name', 'name']
 
     name = models.CharField(verbose_name=_(
-        'Long name of group'), max_length=60)
+        'Long name of group'), max_length=60, unique=True)
     short_name = models.CharField(verbose_name=_(
-        'Short name of group'), max_length=16)
+        'Short name of group'), max_length=16, unique=True)
 
     members = models.ManyToManyField('Person', related_name='member_of')
     owners = models.ManyToManyField('Person', related_name='owner_of')
diff --git a/biscuit/core/util/core_helpers.py b/biscuit/core/util/core_helpers.py
index bf19da058ae245bf4e9f47247c40786ab1d2d761..e8c0b1fe28c167b25f39eae946077c2d28832723 100644
--- a/biscuit/core/util/core_helpers.py
+++ b/biscuit/core/util/core_helpers.py
@@ -29,10 +29,6 @@ def get_app_packages() -> Sequence[str]:
     return pkgs
 
 
-def get_current_school() -> int:
-    return 1
-
-
 def is_impersonate(request: HttpRequest) -> bool:
     if hasattr(request, 'user'):
         return getattr(request.user, 'is_impersonate', False)
diff --git a/biscuit/core/views.py b/biscuit/core/views.py
index a32b9abe19b3131304c0a568d643a8336acd70d1..585d049ae72bd65c0a8ffd16bba88056850f2f04 100644
--- a/biscuit/core/views.py
+++ b/biscuit/core/views.py
@@ -10,7 +10,7 @@ from django_cron.models import CronJobLog
 
 from .decorators import admin_required
 from .forms import PersonsAccountsFormSet, EditPersonForm, EditGroupForm, EditSchoolForm, EditTermForm
-from .models import Person, Group
+from .models import Person, Group, School
 from .tables import PersonsTable, GroupsTable
 from .util import messages
 
@@ -198,7 +198,7 @@ def school_management(request: HttpRequest) -> HttpResponse:
 def edit_school(request: HttpRequest) -> HttpResponse:
     context = {}
 
-    school = request.user.person.school
+    school = School.objects.first()
     edit_school_form = EditSchoolForm(request.POST or None, request.FILES or None, instance=school)
 
     context['school'] = school
@@ -219,7 +219,7 @@ def edit_school(request: HttpRequest) -> HttpResponse:
 def edit_schoolterm(request: HttpRequest) -> HttpResponse:
     context = {}
 
-    term = request.user.person.school.current_term
+    term = School.objects.first().current_term
     edit_term_form = EditTermForm(request.POST or None, instance=term)
 
     if request.method == 'POST':
diff --git a/dev.sh b/dev.sh
index aa951f4d73254b51fbab0eda25a4521678a28b35..6a183f4000d6ce6e9ccf71c186e35193ec1a34c7 100755
--- a/dev.sh
+++ b/dev.sh
@@ -17,6 +17,7 @@ case "$1" in
 	remove_pip_metadata
 	poetry run ./manage.py migrate
 	poetry run ./manage.py compilemessages
+	poetry run ./manage.py yarn install
 	poetry run ./manage.py collectstatic --no-input
 	set +e
 	;;
diff --git a/docs/dev/01_setup.rst b/docs/dev/01_setup.rst
index c036f5c831a0b18f0c43bbb71a75b6b5edb475b0..8d4cd0d8b9cfae6c631d7803eec7565274f5149d 100644
--- a/docs/dev/01_setup.rst
+++ b/docs/dev/01_setup.rst
@@ -9,6 +9,7 @@ Poetry makes a lot of stuff very easy, especially managing a virtual
 environment that contains BiscuIT and everything you need to run the
 framework and selected apps.
 
+Also, `Yarn`_ is needed to resolve JavaScript dependencies.
 
 Get the source tree
 -------------------
@@ -16,7 +17,7 @@ Get the source tree
 To download BiscuIT and all officially bundled apps in their
 development version, use Git like so::
 
-  git clone --recurse-submodules https://edugit.org/Teckids/BiscuIT/BiscuIT-ng
+  git clone --recurse-submodules https://edugit.org/BiscuIT/BiscuIT-ng
 
 If you do not want to download the bundled apps, leave out the
 ``--recurse-submodules`` option.
@@ -45,13 +46,36 @@ installing BiscuIT is a matter of::
   poetry install
 
 
-Running commands in the virtual environment
--------------------------------------------
+Regular tasks
+-------------
 
-To run commands in the virtual environment, use Poetry's ``run``
-command::
+After making changes to the environment, e.g. installing apps or updates,
+some maintenance tasks need to be done:
 
-  poetry run ./manage.py runserver
+1. Download and install JavaScript dependencies
+2. Collect static files
+3. Run database migrations
+
+All three steps can be done with the ``poetry run`` command and
+``manage.py``::
+
+  poetry run ./manage.py yarn install
+  poetry run ./manage.py collectstatic
+  poetry run ./manage.py migrate
+
+(You might need database settings for the `migrate` command; see below.)
+
+Running the development server
+------------------------------
+
+The development server can be started using Django's ``runserver`` command.
+You can either configure BiscuIT like in a production environment, or pass
+basic settings in as environment variable. Here is an example that runs the
+development server against a local PostgreSQL database with password
+`biscuit` (all else remains default) and with the `debug` setting enabled::
+
+  BISCUIT_debug=true BISCUIT_database__password=biscuit poetry run ./manage.py runserver
 
 .. _Poetry: https://poetry.eustace.io/
 .. _Poetry installation methods: https://poetry.eustace.io/docs/#installation
+.. _Yarn: https://yarnpkg.com
diff --git a/docs/dev/02_install_apps.rst b/docs/dev/02_install_apps.rst
index 9ecfcc595f24cf6a6577264aea617ce2088c1c37..b083baf6cb5710db1a84a7cb8255ee678e9db07e 100644
--- a/docs/dev/02_install_apps.rst
+++ b/docs/dev/02_install_apps.rst
@@ -17,11 +17,5 @@ This will install the Exlibris app (library management) app by using a
 shell for first ``cd``'ing into the app directory and then using
 poetry to install the app.
 
-
-Migrate the database
---------------------
-
-After installing or updating any apps, the database must be updated as
-well by running Django's ``migrate`` command::
-
-  poetry run ./manage.py migrate
+DO not forget to run the maintenance tasks described earlier after
+installign any app.
diff --git a/docs/dev/03_run.rst b/docs/dev/03_run.rst
deleted file mode 100644
index ab28dc471cf7d90a74a1602c5eb5a571d131797a..0000000000000000000000000000000000000000
--- a/docs/dev/03_run.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-Running BiscuIT in development mode
-===================================
-
-Using Django's development server
----------------------------------
-
-After you installed the framework and all desired apps and migrated
-the database, you are ready to run BiscuIT::
-
-  poetry run ./manage.py runserver