diff --git a/.gitignore b/.gitignore
index 59ada396088e1a719b021befa1e5ae43cfaa4745..b7a84431273962c3f95196c56f094ac25c177182 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,8 @@ logs/
 !logs/dummy
 *.env
 
+*.pjentsch-testing
+
 # Byte-compiled / optimized / DLL files
 __pycache__/
 *.py[cod]
diff --git a/app/corpora/routes.py b/app/corpora/routes.py
index 55de99f33566b66ad3d11c0fb12269d9d676b31e..be3e7814cfb12bd2c7ccbceb867fc4da167d8244 100644
--- a/app/corpora/routes.py
+++ b/app/corpora/routes.py
@@ -3,6 +3,7 @@ from flask import (
     abort,
     current_app,
     flash,
+    make_response,
     Markup,
     redirect,
     render_template,
@@ -19,7 +20,7 @@ from app.models import (
     Corpus,
     CorpusFile,
     CorpusFollowerAssociation,
-    CorpusFollowPermission,
+    CorpusFollowerPermission,
     CorpusStatus,
     User
 )
@@ -31,7 +32,7 @@ from .forms import (
 )
 
 
-@bp.route('/<hashid:corpus_id>/enable_is_public', methods=['POST'])
+@bp.route('/<hashid:corpus_id>/is_public/enable', methods=['POST'])
 @login_required
 def enable_corpus_is_public(corpus_id):
     corpus = Corpus.query.get_or_404(corpus_id)
@@ -42,7 +43,7 @@ def enable_corpus_is_public(corpus_id):
     return '', 204
 
 
-@bp.route('/<hashid:corpus_id>/disable_is_public', methods=['POST'])
+@bp.route('/<hashid:corpus_id>/is_public/disable', methods=['POST'])
 @login_required
 def disable_corpus_is_public(corpus_id):
     corpus = Corpus.query.get_or_404(corpus_id)
@@ -69,49 +70,57 @@ def follow_corpus(corpus_id, token):
     return redirect(url_for('.corpus', corpus_id=corpus_id))
 
 
-@bp.route('/<hashid:corpus_id>/unfollow', methods=['GET', 'POST'])
+@bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>/unfollow', methods=['POST'])
 @login_required
-def unfollow_corpus(corpus_id):
+def unfollow_corpus(corpus_id, follower_id):
     corpus = Corpus.query.get_or_404(corpus_id)
-    user_hashid = request.args.get('user_id')
-    if user_hashid is None:
-        user = current_user
-    elif current_user.is_administrator():
-        user_id = hashids.decode(user_hashid)
-        user = User.query.get_or_404(user_id)
-    else:
+    follower = User.query.get_or_404(follower_id)
+    if not (corpus.user == current_user or follower == current_user or current_user.is_administrator()):
         abort(403)
-    if user.is_following_corpus(corpus):
-        user.unfollow_corpus(corpus)
+    if not follower.is_following_corpus(corpus):
+        abort(409)  # 'User is not following the corpus'
+    follower.unfollow_corpus(corpus)
+    db.session.commit()
+    flash(f'{follower.username} is not following {corpus.title} anymore', category='corpus')
+    return '', 204
+
+
+@bp.route('/<hashid:corpus_id>/unfollow', methods=['POST'])
+@login_required
+def current_user_unfollow_corpus(corpus_id):
+    corpus = Corpus.query.get_or_404(corpus_id)
+    if not current_user.is_following_corpus(corpus):
+        abort(409)  # 'You are not following the corpus'
+    current_user.unfollow_corpus(corpus)
     db.session.commit()
     flash(f'You are not following {corpus.title} anymore', category='corpus')
     return '', 204
 
 
-@bp.route('/<hashid:corpus_id>/followers/<hashid:user_id>/permissions/add', methods=['POST'])
-def add_permission(corpus_id, user_id, permission):
-    corpus_follow_association = CorpusFollowerAssociation.query.filter_by(followed_corpus_id=corpus_id, following_user_id=user_id).first_or_404()
-    permission = request.args.get('permission', type=int)
-    corpus = corpus_follow_association.followed_corpus
-    if not (corpus.user == current_user or current_user.is_administrator()):
+@bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>/permissions/<permission_name>/add', methods=['POST'])
+def add_permission(corpus_id, follower_id, permission_name):
+    try:
+        permission = CorpusFollowerPermission[permission_name]
+    except KeyError:
+        abort(409)  # f'Permission "{permission_name}" does not exist'
+    corpus_follower_association = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=follower_id).first_or_404()
+    if not (corpus_follower_association.corpus.user == current_user or current_user.is_administrator()):
         abort(403)
-    if permission is None or permission not in iter(CorpusFollowPermission):
-        abort(400)
-    corpus_follow_association.add_permission(permission)
+    corpus_follower_association.add_permission(permission)
     db.session.commit()
     return '', 204
 
 
-@bp.route('/<hashid:corpus_id>/followers/<hashid:user_id>/permissions/remove', methods=['POST'])
-def remove_permission(corpus_id, user_id, permission):
-    corpus_follow_association = CorpusFollowerAssociation.query.filter_by(followed_corpus_id=corpus_id, following_user_id=user_id).first_or_404()
-    permission = request.args.get('permission')
-    corpus = corpus_follow_association.followed_corpus
-    if not (corpus.user == current_user or current_user.is_administrator()):
+@bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>/permissions/<permission_name>/remove', methods=['POST'])
+def remove_permission(corpus_id, follower_id, permission_name):
+    try:
+        permission = CorpusFollowerPermission[permission_name]
+    except KeyError:
+        return make_response(f'Permission "{permission_name}" does not exist', 409)
+    corpus_follower_association = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=follower_id).first_or_404()
+    if not (corpus_follower_association.corpus.user == current_user or current_user.is_administrator()):
         abort(403)
-    if permission is None or permission not in iter(CorpusFollowPermission):
-        abort(400)
-    corpus_follow_association.remove_permission(permission)
+    corpus_follower_association.remove_permission(permission)
     db.session.commit()
     return '', 204
 
diff --git a/app/models.py b/app/models.py
index 4c7a1362fa8d39ee445ed7a4583f90998bbe0bcf..4e40793b951341023ffd0ce354533f474ad251a1 100644
--- a/app/models.py
+++ b/app/models.py
@@ -69,7 +69,8 @@ class ProfilePrivacySettings(IntEnum):
     SHOW_LAST_SEEN = 2
     SHOW_MEMBER_SINCE = 4
 
-class CorpusFollowPermission(IntEnum):
+
+class CorpusFollowerPermission(IntEnum):
     VIEW = 1
     CONTRIBUTE = 2
     ADMINISTRATE = 4
@@ -199,7 +200,10 @@ class Role(HashidMixin, db.Model):
             'id': self.hashid,
             'default': self.default,
             'name': self.name,
-            'permissions': self.permissions
+            'permissions': [
+                x.name for x in Permission
+                if self.has_permission(x.value)
+            ]
         }
         if relationships:
             json_serializeable['users'] = {
@@ -287,32 +291,45 @@ class Avatar(HashidMixin, FileMixin, db.Model):
         return json_serializeable
 
 
-class CorpusFollowerAssociation(db.Model):
+class CorpusFollowerAssociation(HashidMixin, db.Model):
     __tablename__ = 'corpus_follower_associations'
     # Primary key
     id = db.Column(db.Integer, primary_key=True)
     # Foreign keys
-    following_user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
-    followed_corpus_id = db.Column(db.Integer, db.ForeignKey('corpora.id'))
+    corpus_id = db.Column(db.Integer, db.ForeignKey('corpora.id'))
+    follower_id = db.Column(db.Integer, db.ForeignKey('users.id'))
     # Fields
     permissions = db.Column(db.Integer, default=0, nullable=False)
     # Relationships
-    followed_corpus = db.relationship('Corpus', back_populates='following_user_associations')
-    following_user = db.relationship('User', back_populates='followed_corpus_associations')
+    corpus = db.relationship('Corpus', back_populates='corpus_follower_associations')
+    follower = db.relationship('User', back_populates='corpus_follower_associations')
 
     def __repr__(self):
-        return f'<CorpusFollowerAssociation {self.following_user.__repr__()} ~ {self.followed_corpus.__repr__()}>'
+        return f'<CorpusFollowerAssociation {self.follower.__repr__()} ~ {self.corpus.__repr__()}>'
 
-    def has_permission(self, permission):
-        return self.permissions & permission == permission
+    def has_permission(self, permission: CorpusFollowerPermission):
+        return self.permissions & permission.value == permission.value
     
-    def add_permission(self, permission):
+    def add_permission(self, permission: CorpusFollowerPermission):
         if not self.has_permission(permission):
-            self.permissions += permission
+            self.permissions += permission.value
     
-    def remove_permission(self, permission):
+    def remove_permission(self, permission: CorpusFollowerPermission):
         if self.has_permission(permission):
-            self.permissions -= permission
+            self.permissions -= permission.value
+
+    def to_json_serializeable(self, backrefs=False, relationships=False):
+        json_serializeable = {
+            'id': self.hashid,
+            'permissions': [
+                x.name for x in CorpusFollowerPermission
+                if self.has_permission(x)
+            ],
+            'corpus': self.corpus.to_json_serializeable(),
+            'follower': self.follower.to_json_serializeable()
+        }
+        return json_serializeable
+
 
 class User(HashidMixin, UserMixin, db.Model):
     __tablename__ = 'users'
@@ -351,14 +368,15 @@ class User(HashidMixin, UserMixin, db.Model):
         cascade='all, delete-orphan',
         lazy='dynamic'
     )
-    followed_corpus_associations = db.relationship(
+    corpus_follower_associations = db.relationship(
         'CorpusFollowerAssociation',
-        back_populates='following_user'
+        back_populates='follower',
+        cascade='all, delete-orphan'
     )
     followed_corpora = association_proxy(
-        'followed_corpus_associations',
-        'followed_corpus',
-        creator=lambda c: CorpusFollowerAssociation(followed_corpus=c)
+        'corpus_follower_associations',
+        'corpus',
+        creator=lambda c: CorpusFollowerAssociation(corpus=c)
     )
     jobs = db.relationship(
         'Job',
@@ -634,6 +652,10 @@ class User(HashidMixin, UserMixin, db.Model):
             json_serializeable['role'] = \
                 self.role.to_json_serializeable(backrefs=True)
         if relationships:
+            json_serializeable['corpus_follower_associations'] = {
+                x.hashid: x.to_json_serializeable(relationships=True)
+                for x in self.corpus_follower_associations
+            }
             json_serializeable['corpora'] = {
                 x.hashid: x.to_json_serializeable(relationships=True)
                 for x in self.corpora
@@ -650,10 +672,6 @@ class User(HashidMixin, UserMixin, db.Model):
                 x.hashid: x.to_json_serializeable(relationships=True)
                 for x in self.spacy_nlp_pipeline_models
             }
-            json_serializeable['followed_corpora'] = {
-                x.hashid: x.to_json_serializeable(relationships=True)
-                for x in self.followed_corpora
-            }
 
         if filter_by_privacy_settings:
             if not self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_EMAIL):
@@ -1297,14 +1315,15 @@ class Corpus(HashidMixin, db.Model):
         lazy='dynamic',
         cascade='all, delete-orphan'
     )
-    following_user_associations = db.relationship(
+    corpus_follower_associations = db.relationship(
         'CorpusFollowerAssociation',
-        back_populates='followed_corpus'
+        back_populates='corpus',
+        cascade='all, delete-orphan'
     )
-    following_users = association_proxy(
-        'following_user_associations',
-        'following_user',
-        creator=lambda u: CorpusFollowerAssociation(following_user=u)
+    followers = association_proxy(
+        'corpus_follower_associations',
+        'follower',
+        creator=lambda u: CorpusFollowerAssociation(followers=u)
     )
     user = db.relationship('User', back_populates='corpora')
     # "static" attributes
@@ -1403,8 +1422,13 @@ class Corpus(HashidMixin, db.Model):
             'is_public': self.is_public
         }
         if backrefs:
-            json_serializeable['user'] = self.user.to_json_serializeable(backrefs=True)
+            json_serializeable['user'] = \
+                self.user.to_json_serializeable(backrefs=True)
         if relationships:
+            json_serializeable['corpus_follower_associations'] = {
+                x.hashid: x.to_json_serializeable(relationships=True)
+                for x in self.corpus_follower_associations
+            }
             json_serializeable['files'] = {
                 x.hashid: x.to_json_serializeable(relationships=True)
                 for x in self.files
@@ -1426,12 +1450,27 @@ class Corpus(HashidMixin, db.Model):
 @db.event.listens_for(TesseractOCRPipelineModel, 'after_delete')
 def ressource_after_delete(mapper, connection, ressource):
     jsonpatch = [{'op': 'remove', 'path': ressource.jsonpatch_path}]
-    room = f'users.{ressource.user_hashid}'
-    socketio.emit('users.patch', jsonpatch, room=room)
     room = f'/users/{ressource.user_hashid}'
     socketio.emit('PATCH', jsonpatch, room=room)
 
 
+@db.event.listens_for(CorpusFollowerAssociation, 'after_delete')
+def corpus_follower_association_after_delete_handler(mapper, connection, ressource):
+    corpus_owner_hashid = ressource.corpus.user.hashid
+    corpus_hashid = hashids.encode(ressource.corpus_id)
+    follower_hashid = hashids.encode(ressource.follower_id)
+    # Send a PATCH to the corpus owner
+    jsonpatch_path = f'/users/{corpus_owner_hashid}/corpora/{corpus_hashid}/corpus_follower_associations/{ressource.hashid}'
+    jsonpatch = [{'op': 'remove', 'path': jsonpatch_path}]
+    room = f'/users/{corpus_owner_hashid}'
+    socketio.emit('PATCH', jsonpatch, room=room)
+    # Send a PATCH to the follower
+    jsonpatch_path = f'/users/{follower_hashid}/corpus_follower_associations/{ressource.hashid}'
+    jsonpatch = [{'op': 'remove', 'path': jsonpatch_path}]
+    room = f'/users/{follower_hashid}'
+    socketio.emit('PATCH', jsonpatch, room=room)
+
+
 @db.event.listens_for(Corpus, 'after_insert')
 @db.event.listens_for(CorpusFile, 'after_insert')
 @db.event.listens_for(Job, 'after_insert')
@@ -1450,6 +1489,24 @@ def ressource_after_insert_handler(mapper, connection, ressource):
     socketio.emit('PATCH', jsonpatch, room=room)
 
 
+# @db.event.listens_for(CorpusFollowerAssociation, 'after_insert')
+# def corpus_follower_association_after_insert_handler(mapper, connection, ressource):
+#     corpus_owner_hashid = ressource.corpus.user.hashid
+#     corpus_hashid = hashids.encode(ressource.corpus_id)
+#     follower_hashid = hashids.encode(ressource.follower_id)
+#     value = ressource.to_json_serializeable()
+#     # Send a PATCH to the corpus owner
+#     jsonpatch_path = f'/users/{corpus_owner_hashid}/corpora/{corpus_hashid}/corpus_follower_associations/{ressource.hashid}'
+#     jsonpatch = [{'op': 'add', 'path': jsonpatch_path, 'value': value}]
+#     room = f'/users/{corpus_owner_hashid}'
+#     socketio.emit('PATCH', jsonpatch, room=room)
+#     # Send a PATCH to the follower
+#     jsonpatch_path = f'/users/{follower_hashid}/corpus_follower_associations/{ressource.hashid}'
+#     jsonpatch = [{'op': 'add', 'path': jsonpatch_path, 'value': value}]
+#     room = f'/users/{follower_hashid}'
+#     socketio.emit('PATCH', jsonpatch, room=room)
+
+
 @db.event.listens_for(Corpus, 'after_update')
 @db.event.listens_for(CorpusFile, 'after_update')
 @db.event.listens_for(Job, 'after_update')
diff --git a/app/static/js/ResourceLists/CorpusFollowerList.js b/app/static/js/ResourceLists/CorpusFollowerList.js
new file mode 100644
index 0000000000000000000000000000000000000000..616f37931e5eb3e651ddf580d2d231482f31beb8
--- /dev/null
+++ b/app/static/js/ResourceLists/CorpusFollowerList.js
@@ -0,0 +1,204 @@
+class CorpusFollowerList extends ResourceList {
+  static autoInit() {
+    for (let corpusFollowerListElement of document.querySelectorAll('.corpus-follower-list:not(.no-autoinit)')) {
+      new CorpusFollowerList(corpusFollowerListElement);
+    }
+  }
+
+  constructor(listContainerElement, options = {}) {
+    super(listContainerElement, options);
+    this.listjs.list.addEventListener('change', (event) => {this.onChange(event)});
+    this.listjs.list.addEventListener('click', (event) => {this.onClick(event)});
+    this.isInitialized = false;
+    this.userId = listContainerElement.dataset.userId;
+    this.corpusId = listContainerElement.dataset.corpusId;
+    if (this.userId === undefined || this.corpusId === undefined) {return;}
+    app.subscribeUser(this.userId).then((response) => {
+      app.socket.on('PATCH', (patch) => {
+        if (this.isInitialized) {this.onPatch(patch);}
+      });
+    });
+    app.getUser(this.userId).then((user) => {
+      this.add(Object.values(user.corpora[this.corpusId].corpus_follower_associations));
+      this.isInitialized = true;
+    });
+  }
+
+  get item() {
+    return (values) => {
+      return `
+        <tr class="list-item clickable hoverable">
+          <td><img alt="user-image" class="circle responsive-img avatar" style="width:50%"></td>
+          <td><b class="username"><b></td>
+          <td><span class="full-name"></span><br><i class="about-me"></i></td>
+          <td>
+            <span class="disable-on-click">
+              <label>
+                <input ${values['permission-can-VIEW'] ? 'checked' : ''} class="permission-can-VIEW list-action-trigger" data-list-action="toggle-permission" data-permission="VIEW" type="checkbox">
+                <span>View</span>
+              </label>
+            </span>
+            <br>
+            <span class="disable-on-click">
+              <label>
+                <input ${values['permission-can-CONTRIBUTE'] ? 'checked' : ''} class="permission-can-CONTRIBUTE list-action-trigger" data-list-action="toggle-permission" data-permission="CONTRIBUTE" type="checkbox">
+                <span>Contribute</span>
+              </label>
+            </span>
+            <br>
+            <span class="disable-on-click">
+              <label>
+                <input ${values['permission-can-ADMINISTRATE'] ? 'checked' : ''} class="permission-can-ADMINISTRATE list-action-trigger" data-list-action="toggle-permission" data-permission="ADMINISTRATE" type="checkbox">
+                <span>Administrate</span>
+              </label>
+            </span>
+          </td>
+          <td class="right-align">
+            <a class="list-action-trigger btn-floating red waves-effect waves-light" data-list-action="unfollow-request"><i class="material-icons">delete</i></a>
+            <a class="list-action-trigger btn-floating darken waves-effect waves-light" data-list-action="view"><i class="material-icons">send</i></a>
+          </td>
+        </tr>
+      `.trim();
+    }
+  }
+
+  get valueNames() {
+    return [
+      {data: ['id']},
+      {data: ['follower-id']},
+      {name: 'avatar', attr: 'src'},
+      'username',
+      'about-me',
+      'full-name'
+    ];
+  }
+
+  initListContainerElement() {
+    if (!this.listContainerElement.hasAttribute('id')) {
+      this.listContainerElement.id = Utils.generateElementId('corpus-follower-list-');
+    }
+    let listSearchElementId = Utils.generateElementId(`${this.listContainerElement.id}-search-`);
+    this.listContainerElement.innerHTML = `
+      <div class="input-field">
+        <i class="material-icons prefix">search</i>
+        <input id="${listSearchElementId}" class="search" type="text"></input>
+        <label for="${listSearchElementId}">Search corpus follower</label>
+      </div>
+      <table>
+        <thead>
+          <tr>
+            <th style="width:15%;"></th>
+            <th>Username</th>
+            <th>User details</th>
+            <th>Permissions</th>
+            <th></th>
+          </tr>
+        </thead>
+        <tbody class="list"></tbody>
+      </table>
+      <ul class="pagination"></ul>
+    `.trim();
+  }
+
+  mapResourceToValue(corpusFollowerAssociation) {
+    return {
+      'id': corpusFollowerAssociation.id,
+      'follower-id': corpusFollowerAssociation.follower.id,
+      'avatar': corpusFollowerAssociation.follower.avatar ? `/users/${corpusFollowerAssociation.follower.id}/avatar` : '/static/images/user_avatar.png',
+      'username': corpusFollowerAssociation.follower.username,
+      'full-name': corpusFollowerAssociation.follower.full_name ? corpusFollowerAssociation.follower.full_name : '',
+      'about-me': corpusFollowerAssociation.follower.about_me ? corpusFollowerAssociation.follower.about_me : '',
+      'permission-can-VIEW': corpusFollowerAssociation.permissions.includes('VIEW'),
+      'permission-can-CONTRIBUTE': corpusFollowerAssociation.permissions.includes('CONTRIBUTE'),
+      'permission-can-ADMINISTRATE': corpusFollowerAssociation.permissions.includes('ADMINISTRATE')
+    };
+  }
+
+  sort() {
+    this.listjs.sort('username', {order: 'desc'});
+  }
+
+  onChange(event) {
+    if (event.target.tagName !== 'INPUT') {return;}
+    let listItemElement = event.target.closest('.list-item[data-id]');
+    if (listItemElement === null) {return;}
+    let itemId = listItemElement.dataset.id;
+    let listActionElement = event.target.closest('.list-action-trigger[data-list-action]');
+    if (listActionElement === null) {return;}
+    let listAction = listActionElement.dataset.listAction;
+    switch (listAction) {
+      case 'toggle-permission': {
+        let followerId = listItemElement.dataset.followerId;
+        let permission = listActionElement.dataset.permission;
+        if (event.target.checked) {
+          Utils.addCorpusFollowerPermissionRequest(this.corpusId, followerId, permission)
+            .catch((error) => {event.target.checked = !event.target.checked;});
+        } else {
+          Utils.removeCorpusFollowerPermissionRequest(this.corpusId, followerId, permission)
+            .catch((error) => {event.target.checked = !event.target.checked;});
+        }
+        break;
+      }
+      default: {
+        break;
+      }
+    }
+  }
+  
+  onClick(event) {
+    if (event.target.closest('.disable-on-click') !== null) {return;}
+    let listItemElement = event.target.closest('.list-item[data-id]');
+    if (listItemElement === null) {return;}
+    let itemId = listItemElement.dataset.id;
+    let listActionElement = event.target.closest('.list-action-trigger[data-list-action]');
+    let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
+    switch (listAction) {
+      case 'unfollow-request': {
+        let followerId = listItemElement.dataset.followerId;
+        Utils.unfollowCorpusRequest(this.corpusId, followerId);
+        break;
+      }
+      case 'view': {
+        let followerId = listItemElement.dataset.followerId;
+        window.location.href = `/users/${followerId}`;
+        break;
+      }
+      default: {
+        break;
+      }
+    }
+  }
+
+  onPatch(patch) {
+    let re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/corpus_follower_associations/([A-Za-z0-9]*)`);
+    let filteredPatch = patch.filter(operation => re.test(operation.path));
+    for (let operation of filteredPatch) {
+      switch(operation.op) {
+        case 'add': {
+          // let re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/corpus_follower_associations/([A-Za-z0-9]*)$`);
+          // if (re.test(operation.path)) {this.add(operation.value);}
+          break;
+        }
+        case 'remove': {
+          let re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/corpus_follower_associations/([A-Za-z0-9]*)$`);
+          if (re.test(operation.path)) {
+            let [match, jobId] = operation.path.match(re);
+            this.remove(jobId);
+          }
+          break;
+        }
+        case 'replace': {
+          // let re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/corpus_follower_associations/([A-Za-z0-9]*)/(service|status|description|title)$`);
+          // if (re.test(operation.path)) {
+          //   let [match, jobId, valueName] = operation.path.match(re);
+          //   this.replace(jobId, valueName, operation.value);
+          // }
+          break;
+        }
+        default: {
+          break;
+        }
+      }
+    }
+  }
+}
diff --git a/app/static/js/ResourceLists/ResourceList.js b/app/static/js/ResourceLists/ResourceList.js
index 7fe7dec6da3e52b96ddb6e394a198dde4522e7db..b7445553ab8f92ffe00df0e2a8137422f0be7ddd 100644
--- a/app/static/js/ResourceLists/ResourceList.js
+++ b/app/static/js/ResourceLists/ResourceList.js
@@ -14,6 +14,7 @@ class ResourceList {
     TesseractOCRPipelineModelList.autoInit();
     UserList.autoInit();
     AdminUserList.autoInit();
+    CorpusFollowerList.autoInit();
   }
 
   static defaultOptions = {
diff --git a/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js b/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js
index b90fb06bf09545dbdc462eecf7533dd8e6cfc921..f7901528b1666cd0a06d6397d7d289773d5c4a27 100644
--- a/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js
+++ b/app/static/js/ResourceLists/SpacyNLPPipelineModelList.js
@@ -30,14 +30,12 @@ class SpaCyNLPPipelineModelList extends ResourceList {
           <td><b><span class="title"></span> <span class="version"></span></b><br><i><span class="description"></span></i></td>
           <td><a class="publisher-url"><span class="publisher"></span></a> (<span class="publishing-year"></span>)<br><a class="publishing-url publishing-url-2"></a></td>
           <td>
-            <div class="list-action-trigger switch center-align" data-list-action="share-request">
-              <span class="share"></span>
+            <span class="disable-on-click">
               <label>
-                <input class="is-public" ${values['is-public'] ? 'checked' : ''} type="checkbox">
-                <span class="lever"></span>
-                public
+                <input ${values['is-public'] ? 'checked' : ''} class="is-public list-action-trigger" data-list-action="toggle-is-public" type="checkbox">
+                <span>Public</span>
               </label>
-            </div>
+            </span>
           </td>
           <td class="right-align">
             <a class="list-action-trigger btn-floating red waves-effect waves-light" data-list-action="delete-request"><i class="material-icons">delete</i></a>
@@ -80,6 +78,7 @@ class SpaCyNLPPipelineModelList extends ResourceList {
           <tr>
             <th>Title and Description</th>
             <th>Publisher</th>
+            <th>Availability</th>
             <th></th>
           </tr>
         </thead>
@@ -111,6 +110,7 @@ class SpaCyNLPPipelineModelList extends ResourceList {
   }
 
   onChange(event) {
+    if (event.target.tagName !== 'INPUT') {return;}
     let listItemElement = event.target.closest('.list-item[data-id]');
     if (listItemElement === null) {return;}
     let itemId = listItemElement.dataset.id;
@@ -118,7 +118,7 @@ class SpaCyNLPPipelineModelList extends ResourceList {
     if (listActionElement === null) {return;}
     let listAction = listActionElement.dataset.listAction;
     switch (listAction) {
-      case 'share-request': {
+      case 'toggle-is-public': {
         Utils.spaCyNLPPipelineModelToggleIsPublicRequest(this.userId, itemId);
         break;
       }
@@ -129,15 +129,11 @@ class SpaCyNLPPipelineModelList extends ResourceList {
   }
 
   onClick(event) {
+    if (event.target.closest('.disable-on-click') !== null) {return;}
     let listItemElement = event.target.closest('.list-item[data-id]');
     if (listItemElement === null) {return;}
     let itemId = listItemElement.dataset.id;
     let listActionElement = event.target.closest('.list-action-trigger[data-list-action]');
-    // ignore switch clicks, handle them by the onChange method instead
-    if (listActionElement.classList.contains('switch')) {
-      event.preventDefault();
-      this.onChange(event);
-    }
     let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
     switch (listAction) {
       case 'delete-request': {
diff --git a/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js b/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js
index 4ad3f5b143327f29b827f9a6936ce0204f33e85f..c5e08b1d64233ba87f3b81bd88d07501c78b3653 100644
--- a/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js
+++ b/app/static/js/ResourceLists/TesseractOCRPipelineModelList.js
@@ -38,14 +38,12 @@ class TesseractOCRPipelineModelList extends ResourceList {
           <td><b><span class="title"></span> <span class="version"></span></b><br><i><span class="description"></span></i></td>
           <td><a class="publisher-url"><span class="publisher"></span></a> (<span class="publishing-year"></span>)<br><a class="publishing-url"><span class="publishing-url-2"></span></a></td>
           <td>
-            <div class="list-action-trigger switch center-align" data-list-action="share-request">
-              <span class="share"></span>
+            <span class="disable-on-click">
               <label>
-                <input ${values['is-public'] ? 'checked' : ''} class="is-public" type="checkbox">
-                <span class="lever"></span>
-                public
+                <input ${values['is-public'] ? 'checked' : ''} class="is-public list-action-trigger" data-list-action="toggle-is-public" type="checkbox">
+                <span>Public</span>
               </label>
-            </div>
+            </span>
           </td>
           <td class="right-align">
             <a class="list-action-trigger btn-floating red waves-effect waves-light" data-list-action="delete-request"><i class="material-icons">delete</i></a>
@@ -89,6 +87,7 @@ class TesseractOCRPipelineModelList extends ResourceList {
           <tr>
             <th>Title and Description</th>
             <th>Publisher</th>
+            <th>Availability</th>
             <th></th>
           </tr>
         </thead>
@@ -120,6 +119,7 @@ class TesseractOCRPipelineModelList extends ResourceList {
   }
 
   onChange(event) {
+    if (event.target.tagName !== 'INPUT') {return;}
     let listItemElement = event.target.closest('.list-item[data-id]');
     if (listItemElement === null) {return;}
     let itemId = listItemElement.dataset.id;
@@ -127,7 +127,7 @@ class TesseractOCRPipelineModelList extends ResourceList {
     if (listActionElement === null) {return;}
     let listAction = listActionElement.dataset.listAction;
     switch (listAction) {
-      case 'share-request': {
+      case 'toggle-is-public': {
         Utils.tesseractOCRPipelineModelToggleIsPublicRequest(this.userId, itemId);
         break;
       }
@@ -138,6 +138,7 @@ class TesseractOCRPipelineModelList extends ResourceList {
   }
 
   onClick(event) {
+    if (event.target.closest('.disable-on-click') !== null) {return;}
     let listItemElement = event.target.closest('.list-item[data-id]');
     if (listItemElement === null) {return;}
     let itemId = listItemElement.dataset.id;
diff --git a/app/static/js/Utils.js b/app/static/js/Utils.js
index b3da4ca1a87665fa81050270f8bcf8f0579db44e..80340d6c764dd6fc3714434cf2b6eb96e5407145 100644
--- a/app/static/js/Utils.js
+++ b/app/static/js/Utils.js
@@ -71,14 +71,17 @@ class Utils {
 
   static addCorpusFollowerPermissionRequest(corpusId, followerId, permission) {
     return new Promise((resolve, reject) => {
-      fetch(`/corpora/${corpusId}/followers/${followerId}/add_permission?permission=${permission}`, {method: 'POST', headers: {Accept: 'application/json'}})
+      fetch(`/corpora/${corpusId}/followers/${followerId}/permissions/${permission}/add`, {method: 'POST', headers: {Accept: 'application/json'}})
         .then(
           (response) => {
-            if (response.status === 400) {app.flash('Bad Request', 'error'); reject(response);}
-            if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);}
-            if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);}
-            app.flash(`Permission added`, 'corpus');
-            resolve(response);
+            if (response.ok) {
+              app.flash(`Permission added`, 'corpus');
+              resolve(response);
+              return;
+            } else {
+              app.flash(`${response.statusText}`, 'error');
+              reject(response);
+            }
           },
           (response) => {
             app.flash('Something went wrong', 'error');
@@ -90,21 +93,23 @@ class Utils {
 
   static removeCorpusFollowerPermissionRequest(corpusId, followerId, permission) {
     return new Promise((resolve, reject) => {
-      fetch(`/corpora/${corpusId}/followers/${followerId}/remove_permission?permission=${permission}`, {method: 'POST', headers: {Accept: 'application/json'}})
+      fetch(`/corpora/${corpusId}/followers/${followerId}/permissions/${permission}/remove`, {method: 'POST', headers: {Accept: 'application/json'}})
         .then(
           (response) => {
-            if (response.status === 400) {app.flash('Bad Request', 'error'); reject(response);}
-            if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);}
-            if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);}
-            app.flash(`Permission removed`, 'corpus');
-            resolve(response);
+            if (response.ok) {
+              app.flash(`Permission removed`, 'corpus');
+              resolve(response);
+            } else {
+              app.flash(`${response.statusText}`, 'error');
+              reject(response);
+            }
           },
           (response) => {
             app.flash('Something went wrong', 'error');
             reject(response);
           }
         );
-    });
+    }); 
   }
 
   static enableCorpusIsPublicRequest(userId, corpusId) {
@@ -145,7 +150,7 @@ class Utils {
       let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
       confirmElement.addEventListener('click', (event) => {
         let corpusTitle = corpus?.title;
-        fetch(`/corpora/${corpusId}/enable_is_public`, {method: 'POST', headers: {Accept: 'application/json'}})
+        fetch(`/corpora/${corpusId}/is_public/enable`, {method: 'POST', headers: {Accept: 'application/json'}})
           .then(
             (response) => {
               if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);}
@@ -173,7 +178,7 @@ class Utils {
       }
 
       let corpusTitle = corpus?.title;
-      fetch(`/corpora/${corpusId}/disable_is_public`, {method: 'POST', headers: {Accept: 'application/json'}})
+      fetch(`/corpora/${corpusId}/is_public/disable`, {method: 'POST', headers: {Accept: 'application/json'}})
         .then(
           (response) => {
             if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);}
@@ -215,6 +220,27 @@ class Utils {
     });
   }
 
+  static unfollowCorpusRequest(corpusId, followerId) {
+    return new Promise((resolve, reject) => {
+      fetch(`/corpora/${corpusId}/followers/${followerId}/unfollow`, {method: 'POST', headers: {Accept: 'application/json'}})
+        .then(
+          (response) => {
+            if (response.ok) {
+              app.flash(`User unfollowed from Corpus`, 'corpus');
+              resolve(response);
+            } else {
+              app.flash(`${response.statusText}`, 'error');
+              reject(response);
+            }
+          },
+          (response) => {
+            app.flash('Something went wrong', 'error');
+            reject(response);
+          }
+        );
+    });
+  }
+
   static deleteCorpusRequest(userId, corpusId) {
     return new Promise((resolve, reject) => {
       let corpus;
diff --git a/app/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2
index 53a0469c245863507c04000d2dc186dba7a16646..2a3626abb478b46a894e0a5b24fce7f5bee69d45 100644
--- a/app/templates/_scripts.html.j2
+++ b/app/templates/_scripts.html.j2
@@ -30,6 +30,7 @@
   'js/ResourceLists/TesseractOCRPipelineModelList.js',
   'js/ResourceLists/UserList.js',
   'js/ResourceLists/AdminUserList.js',
+  'js/ResourceLists/CorpusFollowerList.js',
   'js/XMLtoObject.js'
 %}
 <script src="{{ ASSET_URL }}"></script>
diff --git a/app/templates/corpora/corpus.html.j2 b/app/templates/corpora/corpus.html.j2
index c84d6c0eab36c1a1aa3227e046fb2fb19d6e8aab..0e982d70beaec70c4b40492ea6cbf1e937cc5423 100644
--- a/app/templates/corpora/corpus.html.j2
+++ b/app/templates/corpora/corpus.html.j2
@@ -6,8 +6,8 @@
 
 {% block page_content %}
 <div class="container">
-  <div class="row">
-    <div class="col s12" data-corpus-id="{{ corpus.hashid }}" data-user-id="{{ corpus.user.hashid }}" id="corpus-display">
+  <div class="row" data-corpus-id="{{ corpus.hashid }}" data-user-id="{{ corpus.user.hashid }}" id="corpus-display">
+    <div class="col s12">
       <div class="row">
         <div class="col s8 m9 l10">
           <h1 id="title"><span class="corpus-title"></span></h1>
@@ -89,7 +89,7 @@
           <div class="action-switch switch" data-action="toggle-is-public">
             <span class="share"></span>
             <label>
-              <input class="corpus-is-public" {% if corpus.is_public %}checked{% endif %} type="checkbox">
+              <input {% if corpus.is_public %}checked{% endif %} type="checkbox">
               <span class="lever"></span>
               public
             </label>
@@ -130,7 +130,7 @@
       <div class="card">
         <div class="card-content">
           <span class="card-title" id="files">Corpus followers</span>
-          <div class="user-list no-autoinit"></div>
+          <div class="corpus-follower-list" data-user-id="{{ corpus.user.hashid }}" data-corpus-id="{{ corpus.hashid }}"></div>
         </div>
       </div>
     </div>
diff --git a/migrations/versions/03c7211f089d_.py b/migrations/versions/03c7211f089d_.py
new file mode 100644
index 0000000000000000000000000000000000000000..2f73e3aacd97d11d957290746b7964121dfe3a1d
--- /dev/null
+++ b/migrations/versions/03c7211f089d_.py
@@ -0,0 +1,42 @@
+"""empty message
+
+Revision ID: 03c7211f089d
+Revises: 5fe6a6c7870c
+Create Date: 2023-02-21 09:57:22.005883
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '03c7211f089d'
+down_revision = '5fe6a6c7870c'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.alter_column(
+        'corpus_follower_associations',
+        'followed_corpus_id',
+        new_column_name='corpus_id'
+    )
+    op.alter_column(
+        'corpus_follower_associations',
+        'following_user_id',
+        new_column_name='follower_id'
+    )
+
+
+def downgrade():
+    op.alter_column(
+        'corpus_follower_associations',
+        'corpus_id',
+        new_column_name='followed_corpus_id'
+    )
+    op.alter_column(
+        'corpus_follower_associations',
+        'follower_id',
+        new_column_name='following_user_id'
+    )