diff --git a/SciPost_v1/settings/base.py b/SciPost_v1/settings/base.py
index f3a4ed8e5a68b10a591d4081d9ea2e8ec02948fa..03d9cf3ce8d1e8936c1642b72c4e9ea0e7df3b2c 100644
--- a/SciPost_v1/settings/base.py
+++ b/SciPost_v1/settings/base.py
@@ -90,6 +90,7 @@ INSTALLED_APPS = (
     'mails',
     'mailing_lists',
     'news',
+    'notifications',
     'scipost',
     'submissions',
     'theses',
@@ -107,11 +108,6 @@ HAYSTACK_CONNECTIONS = {
         'PATH': 'local_files/haystack/',
         'EXCLUDED_INDEXES': ['sphinxdoc.search_indexes.DocumentIndex'],
     },
-    # 'scipost': {
-    #     'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
-    #     'PATH': 'local_files/haystack_scipost/',
-    #     'EXCLUDED_INDEXES': ['sphinxdoc.search_indexes.DocumentIndex'],
-    # },
 }
 
 # Brute force automatically re-index Haystack using post_save signals on all models.
diff --git a/SciPost_v1/urls.py b/SciPost_v1/urls.py
index fc2b471e09c717922963e7287c2600c115d356a1..ba767eb8166b0f60df1e78d25b141bf652b53732 100644
--- a/SciPost_v1/urls.py
+++ b/SciPost_v1/urls.py
@@ -41,6 +41,7 @@ urlpatterns = [
     url(r'^thesis/', include('theses.urls', namespace="_theses")),
     url(r'^meetings/', include('virtualmeetings.urls', namespace="virtualmeetings")),
     url(r'^news/', include('news.urls', namespace="news")),
+    url(r'^notifications/', include('notifications.urls', namespace="notifications")),
     url(r'^production/', include('production.urls', namespace="production")),
     url(r'^partners/', include('partners.urls', namespace="partners")),
 
diff --git a/notifications/__init__.py b/notifications/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/notifications/admin.py b/notifications/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..2cf8c7c7b329a7a98b23bb5332d71059d5023d56
--- /dev/null
+++ b/notifications/admin.py
@@ -0,0 +1,12 @@
+from django.contrib import admin
+from .models import Notification
+
+
+class NotificationAdmin(admin.ModelAdmin):
+    raw_id_fields = ('recipient', )
+    list_display = ('recipient', 'actor',
+                    'level', 'target', 'unread', 'public')
+    list_filter = ('level', 'unread', 'public', 'created', )
+
+
+admin.site.register(Notification, NotificationAdmin)
diff --git a/notifications/apps.py b/notifications/apps.py
new file mode 100644
index 0000000000000000000000000000000000000000..9c260e0b160c191b17c8e13b85986e5368eda49e
--- /dev/null
+++ b/notifications/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class NotificationsConfig(AppConfig):
+    name = 'notifications'
diff --git a/notifications/managers.py b/notifications/managers.py
new file mode 100644
index 0000000000000000000000000000000000000000..31346a0c7d4bde14029fe817ebe24138d81a5cb9
--- /dev/null
+++ b/notifications/managers.py
@@ -0,0 +1,76 @@
+from django.db import models
+
+
+class NotificationQuerySet(models.query.QuerySet):
+    def unsent(self):
+        return self.filter(emailed=False)
+
+    def sent(self):
+        return self.filter(emailed=True)
+
+    def unread(self):
+        """Return only unread items in the current queryset"""
+        return self.filter(unread=True)
+
+    def read(self):
+        """Return only read items in the current queryset"""
+        return self.filter(unread=False)
+
+    def mark_all_as_read(self, recipient=None):
+        """Mark as read any unread messages in the current queryset."""
+        # We want to filter out read ones, as later we will store
+        # the time they were marked as read.
+        qs = self.unread()
+        if recipient:
+            qs = qs.filter(recipient=recipient)
+
+        return qs.update(unread=False)
+
+    def mark_all_as_unread(self, recipient=None):
+        """Mark as unread any read messages in the current queryset."""
+        qs = self.read()
+
+        if recipient:
+            qs = qs.filter(recipient=recipient)
+
+        return qs.update(unread=True)
+
+    def deleted(self):
+        """Return only deleted items in the current queryset"""
+        raise DeprecationWarning
+        return self.filter(deleted=True)
+
+    def active(self):
+        """Return only active(un-deleted) items in the current queryset"""
+        raise DeprecationWarning
+        return self.filter(deleted=False)
+
+    def mark_all_as_deleted(self, recipient=None):
+        """Mark current queryset as deleted."""
+        raise DeprecationWarning
+        qs = self.active()
+        if recipient:
+            qs = qs.filter(recipient=recipient)
+
+        return qs.update(deleted=True)
+
+    def mark_all_as_active(self, recipient=None):
+        """Mark current queryset as active(un-deleted)."""
+        raise DeprecationWarning
+        qs = self.deleted()
+        if recipient:
+            qs = qs.filter(recipient=recipient)
+
+        return qs.update(deleted=False)
+
+    def mark_as_unsent(self, recipient=None):
+        qs = self.sent()
+        if recipient:
+            qs = self.filter(recipient=recipient)
+        return qs.update(emailed=False)
+
+    def mark_as_sent(self, recipient=None):
+        qs = self.unsent()
+        if recipient:
+            qs = self.filter(recipient=recipient)
+        return qs.update(emailed=True)
diff --git a/notifications/migrations/0001_initial.py b/notifications/migrations/0001_initial.py
new file mode 100644
index 0000000000000000000000000000000000000000..8e4dfe442887f4f26d76b60d175abfafb98ce9bd
--- /dev/null
+++ b/notifications/migrations/0001_initial.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2017-09-11 18:33
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('contenttypes', '0002_remove_content_type_name'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Notification',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('level', models.CharField(choices=[('success', 'Success'), ('info', 'Info'), ('warning', 'Warning'), ('error', 'Error')], default='info', max_length=20)),
+                ('unread', models.BooleanField(default=True)),
+                ('actor_object_id', models.CharField(max_length=255)),
+                ('verb', models.CharField(max_length=255)),
+                ('description', models.TextField(blank=True, null=True)),
+                ('target_object_id', models.CharField(blank=True, max_length=255, null=True)),
+                ('action_object_object_id', models.CharField(blank=True, max_length=255, null=True)),
+                ('created', models.DateTimeField(default=django.utils.timezone.now)),
+                ('public', models.BooleanField(default=True)),
+                ('emailed', models.BooleanField(default=False)),
+                ('action_object_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notify_action_object', to='contenttypes.ContentType')),
+                ('actor_content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notify_actor', to='contenttypes.ContentType')),
+                ('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)),
+                ('target_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notify_target', to='contenttypes.ContentType')),
+            ],
+            options={
+                'ordering': ('-created',),
+            },
+        ),
+    ]
diff --git a/notifications/migrations/__init__.py b/notifications/migrations/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/notifications/models.py b/notifications/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..04e2aff1e78dcfd1b8ddb67ae9a6977aa6487abc
--- /dev/null
+++ b/notifications/models.py
@@ -0,0 +1,99 @@
+from django.db import models
+from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.utils import timezone
+
+from .managers import NotificationQuerySet
+
+
+class Notification(models.Model):
+    """
+    Action model describing the actor acting out a verb (on an optional
+    target).
+    Nomenclature based on http://activitystrea.ms/specs/atom/1.0/
+    Generalized Format::
+        <actor> <verb> <time>
+        <actor> <verb> <target> <time>
+        <actor> <verb> <action_object> <target> <time>
+    Examples::
+        <justquick> <reached level 60> <1 minute ago>
+        <brosner> <commented on> <pinax/pinax> <2 hours ago>
+        <washingtontimes> <started follow> <justquick> <8 minutes ago>
+        <mitsuhiko> <closed> <issue 70> on <mitsuhiko/flask> <about 2 hours ago>
+    Unicode Representation::
+        justquick reached level 60 1 minute ago
+        mitsuhiko closed issue 70 on mitsuhiko/flask 3 hours ago
+    """
+    LEVELS = (('success', 'Success'), ('info', 'Info'), ('warning', 'Warning'), ('error', 'Error'))
+    level = models.CharField(choices=LEVELS, default='info', max_length=20)
+
+    recipient = models.ForeignKey(settings.AUTH_USER_MODEL, blank=False,
+                                  related_name='notifications')
+    unread = models.BooleanField(default=True)
+
+    actor_content_type = models.ForeignKey(ContentType, related_name='notify_actor')
+    actor_object_id = models.CharField(max_length=255)
+    actor = GenericForeignKey('actor_content_type', 'actor_object_id')
+
+    verb = models.CharField(max_length=255)
+    description = models.TextField(blank=True, null=True)
+
+    target_content_type = models.ForeignKey(ContentType, related_name='notify_target',
+                                            blank=True, null=True)
+    target_object_id = models.CharField(max_length=255, blank=True, null=True)
+    target = GenericForeignKey('target_content_type', 'target_object_id')
+
+    action_object_content_type = models.ForeignKey(ContentType, blank=True, null=True,
+                                                   related_name='notify_action_object')
+    action_object_object_id = models.CharField(max_length=255, blank=True, null=True)
+    action_object = GenericForeignKey('action_object_content_type', 'action_object_object_id')
+
+    created = models.DateTimeField(default=timezone.now)
+
+    public = models.BooleanField(default=True)
+    emailed = models.BooleanField(default=False)
+
+    objects = NotificationQuerySet.as_manager()
+
+    class Meta:
+        ordering = ('-created', )
+
+    def __unicode__(self):
+        ctx = {
+            'actor': self.actor,
+            'verb': self.verb,
+            'action_object': self.action_object,
+            'target': self.target,
+            'timesince': self.timesince()
+        }
+        if self.target:
+            if self.action_object:
+                return u'%(actor)s %(verb)s %(action_object)s on %(target)s %(timesince)s ago' % ctx
+            return u'%(actor)s %(verb)s %(target)s %(timesince)s ago' % ctx
+        if self.action_object:
+            return u'%(actor)s %(verb)s %(action_object)s %(timesince)s ago' % ctx
+        return u'%(actor)s %(verb)s %(timesince)s ago' % ctx
+
+    def timesince(self, now=None):
+        """
+        Shortcut for the ``django.utils.timesince.timesince`` function of the
+        current timestamp.
+        """
+        from django.utils.timesince import timesince as timesince_
+        return timesince_(self.timestamp, now)
+
+    @property
+    def slug(self):
+        from .utils import id2slug
+        return id2slug(self.id)
+
+    def mark_as_read(self):
+        if self.unread:
+            self.unread = False
+            self.save()
+
+    def mark_as_unread(self):
+        if not self.unread:
+            self.unread = True
+            self.save()
diff --git a/notifications/templates/notifications/notification_list.html b/notifications/templates/notifications/notification_list.html
new file mode 100644
index 0000000000000000000000000000000000000000..6b3192b643d838bbb37bc8d549bd3a2bf9638233
--- /dev/null
+++ b/notifications/templates/notifications/notification_list.html
@@ -0,0 +1,16 @@
+{% extends 'scipost/_personal_page_base.html' %}
+
+{% block breadcrumb_items %}
+    {{block.super}}
+    <span class="breadcrumb-item">My notifications ({{notifications|length}})</span>
+{% endblock %}
+
+{% block pagetitle %}: My notifications ({{notifications|length}}){% endblock pagetitle %}
+
+{% block content %}
+    <ul class="list-group notifications">
+        {% for notice in notifications %}
+            {% include 'notifications/partials/notice.html' %}
+        {% endfor %}
+    </ul>
+{% endblock %}
diff --git a/notifications/templates/notifications/partials/notice.html b/notifications/templates/notifications/partials/notice.html
new file mode 100644
index 0000000000000000000000000000000000000000..2855a53ed7a4bfb4a2436008a54a2ee549d186e6
--- /dev/null
+++ b/notifications/templates/notifications/partials/notice.html
@@ -0,0 +1,18 @@
+<div class="list-group-item list-group-item-action flex-column align-items-start{% if notice.unread %} active{% endif %} px-3 py-2">
+    <div class="">
+        <a href="{% url 'notifications:mark_as_read' notice.slug %}" class="close" aria-hidden="true">&times;</a>
+        <h4 class="mb-1x">
+            <strong>{{ notice.actor }}</strong>
+            {{ notice.verb }}
+            {% if notice.target %}
+                {{ notice.target }}
+            {% endif %}
+        </h4>
+        <div class="text-muted">{{ notice.created|timesince }} ago</div>
+
+    </div>
+    <p class="mb-1">{{ notice.description|linebreaksbr }}</p>
+    {% if notice.target %}
+        <a href="{{notice.target.get_absolute_url}}">Go to {{notice.target_content_type}}</a>
+    {% endif %}
+</div>
diff --git a/notifications/tests.py b/notifications/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6
--- /dev/null
+++ b/notifications/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/notifications/urls.py b/notifications/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..20f0e4db98910b81f95e8eaaf34da119970863eb
--- /dev/null
+++ b/notifications/urls.py
@@ -0,0 +1,17 @@
+from django.conf.urls import url
+
+from . import views
+
+
+urlpatterns = [
+    url(r'^$', views.AllNotificationsList.as_view(), name='all'),
+    url(r'^unread/$', views.UnreadNotificationsList.as_view(), name='unread'),
+    url(r'^mark-all-as-read/$', views.mark_all_as_read, name='mark_all_as_read'),
+    url(r'^mark-as-read/(?P<slug>\d+)/$', views.mark_as_read, name='mark_as_read'),
+    url(r'^mark-as-unread/(?P<slug>\d+)/$', views.mark_as_unread, name='mark_as_unread'),
+    url(r'^delete/(?P<slug>\d+)/$', views.delete, name='delete'),
+    url(r'^api/unread_count/$', views.live_unread_notification_count,
+        name='live_unread_notification_count'),
+    url(r'^api/unread_list/$', views.live_unread_notification_list,
+        name='live_unread_notification_list'),
+]
diff --git a/notifications/utils.py b/notifications/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..9f80b64365f8d617a8b0d6a3787f0b6b360df25e
--- /dev/null
+++ b/notifications/utils.py
@@ -0,0 +1,6 @@
+def slug2id(slug):
+    return int(slug) - 9631
+
+
+def id2slug(id):
+    return id + 9631
diff --git a/notifications/views.py b/notifications/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..d583504aa2776011b448bac76bcc59ae70b70779
--- /dev/null
+++ b/notifications/views.py
@@ -0,0 +1,129 @@
+from django.contrib.auth.decorators import login_required
+from django.forms import model_to_dict
+from django.http import JsonResponse
+from django.shortcuts import get_object_or_404, redirect
+from django.utils.decorators import method_decorator
+from django.views.generic import ListView
+
+from .models import Notification
+from .utils import id2slug, slug2id
+
+
+@method_decorator(login_required, name='dispatch')
+class NotificationViewList(ListView):
+    context_object_name = 'notifications'
+
+
+class AllNotificationsList(NotificationViewList):
+    """
+    Index page for authenticated user
+    """
+    def get_queryset(self):
+        return self.request.user.notifications.all()
+
+
+class UnreadNotificationsList(NotificationViewList):
+    def get_queryset(self):
+        return self.request.user.notifications.unread()
+
+
+@login_required
+def mark_all_as_read(request):
+    request.user.notifications.mark_all_as_read()
+
+    _next = request.GET.get('next')
+
+    if _next:
+        return redirect(_next)
+    return redirect('notifications:unread')
+
+
+@login_required
+def mark_as_read(request, slug=None):
+    id = slug2id(slug)
+
+    notification = get_object_or_404(Notification, recipient=request.user, id=id)
+    notification.mark_as_read()
+
+    _next = request.GET.get('next')
+
+    if _next:
+        return redirect(_next)
+
+    return redirect('notifications:unread')
+
+
+@login_required
+def mark_as_unread(request, slug=None):
+    id = slug2id(slug)
+
+    notification = get_object_or_404(Notification, recipient=request.user, id=id)
+    notification.mark_as_unread()
+
+    _next = request.GET.get('next')
+
+    if _next:
+        return redirect(_next)
+
+    return redirect('notifications:unread')
+
+
+@login_required
+def delete(request, slug=None):
+    id = slug2id(slug)
+
+    notification = get_object_or_404(Notification, recipient=request.user, id=id)
+    notification.delete()
+
+    _next = request.GET.get('next')
+
+    if _next:
+        return redirect(_next)
+
+    return redirect('notifications:all')
+
+
+def live_unread_notification_count(request):
+    if not request.user.is_authenticated():
+        data = {'unread_count': 0}
+    else:
+        data = {'unread_count': request.user.notifications.unread().count()}
+    return JsonResponse(data)
+
+
+def live_unread_notification_list(request):
+    if not request.user.is_authenticated():
+        data = {
+           'unread_count': 0,
+           'unread_list': []
+        }
+        return JsonResponse(data)
+
+    try:
+        # Default to 5 as a max number of notifications
+        num_to_fetch = max(int(request.GET.get('max', 5)))
+        num_to_fetch = min(num_to_fetch, 100)
+    except ValueError:
+        num_to_fetch = 5
+
+    unread_list = []
+
+    for n in request.user.notifications.unread()[:num_to_fetch]:
+        struct = model_to_dict(n)
+        struct['slug'] = id2slug(n.id)
+        if n.actor:
+            struct['actor'] = str(n.actor)
+        if n.target:
+            struct['target'] = str(n.target)
+        if n.action_object:
+            struct['action_object'] = str(n.action_object)
+        if n.data:
+            struct['data'] = n.data
+        unread_list.append(struct)
+        if request.GET.get('mark_as_read'):
+            n.mark_as_read()
+    data = {
+        'unread_count': request.user.notifications.unread().count(),
+        'unread_list': unread_list
+    }
+    return JsonResponse(data)
diff --git a/scipost/static/scipost/assets/config/preconfig.scss b/scipost/static/scipost/assets/config/preconfig.scss
index 37775af6b78bb58872d74ecd493da60bfb6d8464..de82cbe66450c12f521a9d19fe1e880c38d43283 100644
--- a/scipost/static/scipost/assets/config/preconfig.scss
+++ b/scipost/static/scipost/assets/config/preconfig.scss
@@ -93,6 +93,9 @@ $list-group-bg: transparent;
 $list-group-item-padding-x: 0;
 $list-group-item-padding-y: 0;
 $list-group-border-color: #ddd;
+$list-group-active-color: $scipost-darkblue;
+$list-group-active-bg: $scipost-lightestblue;
+$list-group-active-border-color: $scipost-lightestblue;
 
 // Fonts
 //
diff --git a/scipost/static/scipost/assets/css/_alert.scss b/scipost/static/scipost/assets/css/_alert.scss
index b60845a33bf55c71b223feb8416b4d3f27a19902..67ca539f4142e341a56a4f97b659e6e17216339a 100644
--- a/scipost/static/scipost/assets/css/_alert.scss
+++ b/scipost/static/scipost/assets/css/_alert.scss
@@ -1,5 +1,5 @@
 .alert {
-    padding: 0.75rem 3rem 0.75rem 1.25rem;
+    padding: 0.75rem 1.25rem;
     margin-bottom: 0.5rem;
     position: relative;
     clear: both;