diff --git a/app/corpora/routes.py b/app/corpora/routes.py
index 3a3c76e39721f7203b849bb5f69df81dadba1b73..532f0249e197a1da837ed334aaceea2f357dd28c 100644
--- a/app/corpora/routes.py
+++ b/app/corpora/routes.py
@@ -11,8 +11,6 @@ from flask import (
     send_from_directory
 )
 from flask_login import current_user, login_required
-from werkzeug.utils import secure_filename
-from zipfile import ZipFile
 from . import bp
 from . import tasks
 from .forms import (
@@ -24,7 +22,6 @@ from .forms import (
 import os
 import shutil
 import tempfile
-import glob
 import xml.etree.ElementTree as ET
 
 
@@ -350,4 +347,4 @@ def download_corpus_file(corpus_id, corpus_file_id):
         attachment_filename=corpus_file.filename,
         directory=os.path.dirname(corpus_file.path),
         filename=os.path.basename(corpus_file.path)
-    )
\ No newline at end of file
+    )
diff --git a/app/static/js/App.js b/app/static/js/App.js
index 349eaedf7df5beec75b0230fe8b559a345864f09..27ddb0eb4077600d23193ca956172f932688a2dc 100644
--- a/app/static/js/App.js
+++ b/app/static/js/App.js
@@ -1,30 +1,53 @@
 class App {
   constructor() {
-    this.data = {users: {}};
-    this.promises = {users: {}};
+    this.data = {
+      promises: {getUser: {}, subscribeUser: {}},
+      users: {},
+    };
     this.socket = io({transports: ['websocket'], upgrade: false});
-    this.socket.on('PATCH', (patch) => {this.data = jsonpatch.applyPatch(this.data, patch).newDocument;});
+    this.socket.on('PATCH', (patch) => {
+      const re = new RegExp(`^/users/(${Object.keys(this.data.users).join('|')})`);
+      const filteredPatch = patch.filter(operation => re.test(operation.path));
+  
+      jsonpatch.applyPatch(this.data, filteredPatch);
+    });
   }
 
-  get users() {
-    return this.data.users;
+  getUser(userId) {
+    if (userId in this.data.promises.getUser) {
+      return this.data.promises.getUser[userId];
+    }
+
+    this.data.promises.getUser[userId] = new Promise((resolve, reject) => {
+      this.socket.emit('GET /users/<user_id>', userId, (response) => {
+        if (response.code === 200) {
+          this.data.users[userId] = response.payload;
+          resolve(this.data.users[userId]);
+        } else {
+          reject(response);
+        }
+      });
+    });
+
+    return this.data.promises.getUser[userId];
   }
 
   subscribeUser(userId) {
-    if (userId in this.promises.users) {
-      return this.promises.users[userId];
+    if (userId in this.data.promises.subscribeUser) {
+      return this.data.promises.subscribeUser[userId];
     }
-    this.promises.users[userId] = new Promise((resolve, reject) => {
-      this.socket.emit('SUBSCRIBE /users/<user_id>', userId, response => {
+
+    this.data.promises.subscribeUser[userId] = new Promise((resolve, reject) => {
+      this.socket.emit('SUBSCRIBE /users/<user_id>', userId, (response) => {
         if (response.code === 200) {
-          this.data.users[userId] = response.payload;
-          resolve(this.data.users[userId]);
+          resolve(response);
         } else {
           reject(response);
         }
       });
     });
-    return this.promises.users[userId];
+
+    return this.data.promises.subscribeUser[userId];
   }
 
   flash(message, category) {
diff --git a/app/static/js/JobStatusNotifier.js b/app/static/js/JobStatusNotifier.js
index 57ca0135a944316b10ace6f1cee18eb4fa9fa84e..bef2e5c7204c12759fb9e4cb1bf01fa66c170bc0 100644
--- a/app/static/js/JobStatusNotifier.js
+++ b/app/static/js/JobStatusNotifier.js
@@ -1,10 +1,18 @@
 class JobStatusNotifier {
   constructor(userId) {
     this.userId = userId;
-    app.socket.on('PATCH', (patch) => {this.onPATCH(patch);});
+    this.isInitialized = false;
+    app.subscribeUser(this.userId).then((response) => {
+      app.socket.on('PATCH', (patch) => {this.onPATCH(patch);});
+    });
+    app.getUser(this.userId).then((user) => {
+      this.isInitialized = true;
+    });
   }
 
   onPATCH(patch) {
+    if (!this.isInitialized) {return;}
+
     let filteredPatch;
     let jobId;
     let match;
@@ -13,11 +21,11 @@ class JobStatusNotifier {
 
     re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)/status$`);
     filteredPatch = patch
-      .filter(operation => operation.op === 'replace')
-      .filter(operation => re.test(operation.path));
+      .filter((operation) => {return operation.op === 'replace';})
+      .filter((operation) => {return re.test(operation.path);});
     for (operation of filteredPatch) {
       [match, jobId] = operation.path.match(re);
-      app.flash(`[<a href="/jobs/${jobId}">${app.users[this.userId].jobs[jobId].title}</a>] New status: <span class="job-status-text" data-job-status="${operation.value}"></span>`, 'job');
+      app.flash(`[<a href="/jobs/${jobId}">${app.data.users[this.userId].jobs[jobId].title}</a>] New status: <span class="job-status-text" data-job-status="${operation.value}"></span>`, 'job');
     }
   }
 }
diff --git a/app/static/js/RessourceDisplays/CorpusDisplay.js b/app/static/js/RessourceDisplays/CorpusDisplay.js
index 39d30e897b823a17408b5d546a9a1051ec6afe3d..3fe7e96eb17c59f400fb5306702e6fa13c4fbb05 100644
--- a/app/static/js/RessourceDisplays/CorpusDisplay.js
+++ b/app/static/js/RessourceDisplays/CorpusDisplay.js
@@ -5,9 +5,8 @@ class CorpusDisplay extends RessourceDisplay {
   }
 
   init(user) {
-    let corpus;
+    const corpus = user.corpora[this.corpusId];
 
-    corpus = user.corpora[this.corpusId];
     this.setCreationDate(corpus.creation_date);
     this.setDescription(corpus.description);
     this.setLastEditedDate(corpus.last_edited_date);
@@ -17,12 +16,15 @@ class CorpusDisplay extends RessourceDisplay {
   }
 
   onPATCH(patch) {
+    if (!this.isInitialized) {return;}
+
     let filteredPatch;
     let operation;
     let re;
 
     re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}`);
     filteredPatch = patch.filter(operation => re.test(operation.path));
+
     for (operation of filteredPatch) {
       switch(operation.op) {
         case 'replace':
@@ -55,7 +57,7 @@ class CorpusDisplay extends RessourceDisplay {
   setNumTokens(numTokens) {
     this.setElements(
       this.displayElement.querySelectorAll('.corpus-token-ratio'),
-      `${numTokens}/${app.users[this.userId].corpora[this.corpusId].max_num_tokens}`
+      `${numTokens}/${app.data.users[this.userId].corpora[this.corpusId].max_num_tokens}`
     );
   }
 
@@ -77,7 +79,7 @@ class CorpusDisplay extends RessourceDisplay {
     }
     elements = this.displayElement.querySelectorAll('.corpus-build-trigger');
     for (element of elements) {
-      if (['UNPREPARED', 'FAILED'].includes(status) && Object.values(app.users[this.userId].corpora[this.corpusId].files).length > 0) {
+      if (['UNPREPARED', 'FAILED'].includes(status) && Object.values(app.data.users[this.userId].corpora[this.corpusId].files).length > 0) {
         element.classList.remove('disabled');
       } else {
         element.classList.add('disabled');
diff --git a/app/static/js/RessourceDisplays/JobDisplay.js b/app/static/js/RessourceDisplays/JobDisplay.js
index 98c1691311bb6a2b42e27d6739693205f50dd6ed..dc1bd7779b87426ac3b08d64992675fa6b2d0c23 100644
--- a/app/static/js/RessourceDisplays/JobDisplay.js
+++ b/app/static/js/RessourceDisplays/JobDisplay.js
@@ -5,9 +5,8 @@ class JobDisplay extends RessourceDisplay {
   }
 
   init(user) {
-    let job;
+    const job = user.jobs[this.jobId];
 
-    job = user.jobs[this.jobId];
     this.setCreationDate(job.creation_date);
     this.setEndDate(job.creation_date);
     this.setDescription(job.description);
@@ -19,12 +18,15 @@ class JobDisplay extends RessourceDisplay {
   }
 
   onPATCH(patch) {
+    if (!this.isInitialized) {return;}
+
     let filteredPatch;
     let operation;
     let re;
 
     re = new RegExp(`^/users/${this.userId}/jobs/${this.jobId}`);
     filteredPatch = patch.filter(operation => re.test(operation.path));
+
     for (operation of filteredPatch) {
       switch(operation.op) {
         case 'replace':
diff --git a/app/static/js/RessourceDisplays/RessourceDisplay.js b/app/static/js/RessourceDisplays/RessourceDisplay.js
index d98a9635d595d4a85c82b64e2c5491a9b8d91311..0fde4640dde7350961636b85e85acc9566f3b204 100644
--- a/app/static/js/RessourceDisplays/RessourceDisplay.js
+++ b/app/static/js/RessourceDisplays/RessourceDisplay.js
@@ -2,8 +2,16 @@ class RessourceDisplay {
   constructor(displayElement) {
     this.displayElement = displayElement;
     this.userId = this.displayElement.dataset.userId;
-    app.socket.on('PATCH', (patch) => {this.onPATCH(patch);});
-    app.subscribeUser(this.userId).then((user) => {this.init(user);});
+    this.isInitialized = false;
+    if (this.userId) {
+      app.subscribeUser(this.userId).then((response) => {
+        app.socket.on('PATCH', (patch) => {this.onPATCH(patch);});
+      });
+      app.getUser(this.userId).then((user) => {
+        this.init(user);
+        this.isInitialized = true;
+      });
+    }
   }
 
   init(user) {throw 'Not implemented';}
diff --git a/app/static/js/RessourceLists/CorpusFileList.js b/app/static/js/RessourceLists/CorpusFileList.js
index b61d624baf65ba5b6254a84360bb82e86ec9f8ee..7ffd18b6ab421a2aa3bd5c53fbae9c66ec6dd3c1 100644
--- a/app/static/js/RessourceLists/CorpusFileList.js
+++ b/app/static/js/RessourceLists/CorpusFileList.js
@@ -65,7 +65,7 @@ class CorpusFileList extends RessourceList {
           <div class="modal">
             <div class="modal-content">
               <h4>Confirm corpus deletion</h4>
-              <p>Do you really want to delete the corpus file <b>${app.users[this.userId].corpora[this.corpusId].files[corpusFileId].filename}</b>? It will be permanently deleted!</p>
+              <p>Do you really want to delete the corpus file <b>${app.data.users[this.userId].corpora[this.corpusId].files[corpusFileId].filename}</b>? It will be permanently deleted!</p>
             </div>
             <div class="modal-footer">
               <a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
@@ -97,6 +97,8 @@ class CorpusFileList extends RessourceList {
   }
 
   onPATCH(patch) {
+    if (!this.isInitialized) {return;}
+
     let corpusFileId;
     let filteredPatch;
     let match;
diff --git a/app/static/js/RessourceLists/CorpusList.js b/app/static/js/RessourceLists/CorpusList.js
index d6b75bec5992a0c02006f76a0ebd9e1a1bda4fae..b2727737e4289b0ebe79419d63146ee809c214c2 100644
--- a/app/static/js/RessourceLists/CorpusList.js
+++ b/app/static/js/RessourceLists/CorpusList.js
@@ -60,7 +60,7 @@ class CorpusList extends RessourceList {
           <div class="modal">
             <div class="modal-content">
               <h4>Confirm corpus deletion</h4>
-              <p>Do you really want to delete the corpus <b>${app.users[this.userId].corpora[corpusId].title}</b>? All files will be permanently deleted!</p>
+              <p>Do you really want to delete the corpus <b>${app.data.users[this.userId].corpora[corpusId].title}</b>? All files will be permanently deleted!</p>
             </div>
             <div class="modal-footer">
               <a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
@@ -89,6 +89,8 @@ class CorpusList extends RessourceList {
   }
 
   onPATCH(patch) {
+    if (!this.isInitialized) {return;}
+
     let corpusId;
     let filteredPatch;
     let match;
diff --git a/app/static/js/RessourceLists/JobList.js b/app/static/js/RessourceLists/JobList.js
index ef027b0e5881034386144477589b3f73d0f758fe..97895a0eddd78d69f8883abf77b2cc19bcb3bb93 100644
--- a/app/static/js/RessourceLists/JobList.js
+++ b/app/static/js/RessourceLists/JobList.js
@@ -36,7 +36,6 @@ class JobList extends RessourceList {
     ]
   };
 
-
   constructor(listElement, options = {}) {
     super(listElement, {...JobList.options, ...options});
   }
@@ -66,7 +65,7 @@ class JobList extends RessourceList {
           <div class="modal">
             <div class="modal-content">
               <h4>Confirm job deletion</h4>
-              <p>Do you really want to delete the job <b>${app.users[this.userId].jobs[jobId].title}</b>? All files will be permanently deleted!</p>
+              <p>Do you really want to delete the job <b>${app.data.users[this.userId].jobs[jobId].title}</b>? All files will be permanently deleted!</p>
             </div>
             <div class="modal-footer">
               <a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
@@ -95,6 +94,8 @@ class JobList extends RessourceList {
   }
 
   onPATCH(patch) {
+    if (!this.isInitialized) {return;}
+
     let filteredPatch;
     let jobId;
     let match;
diff --git a/app/static/js/RessourceLists/JobResultList.js b/app/static/js/RessourceLists/JobResultList.js
index 85c3c865f238a9b001a04b5004c44f2751372a0a..16c390df119691018f32d9efc5cf6db51792d2b3 100644
--- a/app/static/js/RessourceLists/JobResultList.js
+++ b/app/static/js/RessourceLists/JobResultList.js
@@ -58,6 +58,8 @@ class JobResultList extends RessourceList {
   }
 
   onPATCH(patch) {
+    if (!this.isInitialized) {return;}
+
     let filteredPatch;
     let operation;
     let re;
diff --git a/app/static/js/RessourceLists/RessourceList.js b/app/static/js/RessourceLists/RessourceList.js
index b36b4d921a25aaf4860aa4fcd00425dec2e08adc..29a946631cc8b2a6d4dcdd872f6c5e18eae7f626 100644
--- a/app/static/js/RessourceLists/RessourceList.js
+++ b/app/static/js/RessourceLists/RessourceList.js
@@ -90,12 +90,15 @@ class RessourceList {
     this.listjs.list.style.cursor = 'pointer';
     this.userId = this.listjs.listContainer.dataset.userId;
     this.listjs.list.addEventListener('click', event => this.onclick(event));
+    this.isInitialized = false;
     if (this.userId) {
-      app.socket.on('PATCH', (patch) => {this.onPATCH(patch);});
-      app.subscribeUser(this.userId).then(
-        (user) => {this.init(user);},
-        (error) => {throw JSON.stringify(error);}
-      );
+      app.subscribeUser(this.userId).then((response) => {
+        app.socket.on('PATCH', (patch) => {this.onPATCH(patch);});
+      });
+      app.getUser(this.userId).then((user) => {
+        this.init(user);
+        this.isInitialized = true;
+      });
     }
   }
 
diff --git a/app/static/js/RessourceLists/UserList.js b/app/static/js/RessourceLists/UserList.js
index 5f488f47ef3e5ef897b812e7d5efcf00640cee60..f1f7e42afe2e27a0e66e69829d10728403ecbb0b 100644
--- a/app/static/js/RessourceLists/UserList.js
+++ b/app/static/js/RessourceLists/UserList.js
@@ -20,7 +20,7 @@ class UserList extends RessourceList {
         'id-1': user.id,
         'username': user.username,
         'email': user.email,
-        'last-seen': new Date(user.last_seen).toLocaleString("en-US"),
+        'last-seen': new Date(user.last_seen).toLocaleString('en-US'),
         'member-since': user.member_since,
         'role': user.role.name
       };
diff --git a/app/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2
index 9a98ee9b708a6359a3bc4bc44719ff8ac00e09fc..1956d6e7b269efefc52bcaee9f7cded6507cc451 100644
--- a/app/templates/_scripts.html.j2
+++ b/app/templates/_scripts.html.j2
@@ -32,11 +32,7 @@
   const jobStatusNotifier = new JobStatusNotifier(currentUserId);
 
   // Initialize components for current user
-  app.subscribeUser(currentUserId)
-    .then(
-      (user) => {return;},
-      (error) => {throw JSON.stringify(error);}
-    );
+  app.subscribeUser(currentUserId).catch((error) => {throw JSON.stringify(error);});
   {%- endif %}
 
   // Disable all option elements with no value
diff --git a/app/templates/jobs/_breadcrumbs.html.j2 b/app/templates/jobs/_breadcrumbs.html.j2
index 5bad7de317888574269990d0fb1e94b5a701c471..e3de43f313d7d460f3196d2889f10136803b0a82 100644
--- a/app/templates/jobs/_breadcrumbs.html.j2
+++ b/app/templates/jobs/_breadcrumbs.html.j2
@@ -1,8 +1,8 @@
 {% set breadcrumbs %}
 <li class="tab disabled"><i class="material-icons">navigate_next</i></li>
 <li class="tab"><a href="{{ url_for('main.dashboard', _anchor='jobs') }}" target="_self">My jobs</a></li>
-<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
 {% if request.path == url_for('.job', job_id=job.id) %}
+<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
 <li class="tab"><a class="active" href="{{ url_for('.job', job_id=job.id) }}" target="_self">{{ job.title }}</a></li>
 {% endif %}
 {% endset %}
diff --git a/app/templates/users/users.html.j2 b/app/templates/users/users.html.j2
index ff9b27ca6719b82ecba69b381393c6a62da30c76..56713948a8c4a3928b5438124a60817c6b2ccdd6 100644
--- a/app/templates/users/users.html.j2
+++ b/app/templates/users/users.html.j2
@@ -7,10 +7,28 @@
       <h1 id="title">{{ title }}</h1>
     </div>
 
-    <div class="col s12">
+    <div class="col s12 nopaque-ressource-list no-autoinit" data-ressource-type="User" id="users">
       <div class="card">
         <div class="card-content">
-          <table class="" id="users"></table>
+          <div class="input-field">
+            <i class="material-icons prefix">search</i>
+            <input id="search-user" class="search" type="text"></input>
+            <label for="search-user">Search user</label>
+          </div>
+          <table>
+            <thead>
+              <tr>
+                <th>Id</th>
+                <th>Username</th>
+                <th>Email</th>
+                <th>Last seen</th>
+                <th>Role</th>
+                <th></th>
+              </tr>
+            </thead>
+            <tbody class="list"></tbody>
+          </table>
+          <ul class="pagination"></ul>
         </div>
       </div>
     </div>
@@ -18,52 +36,10 @@
 </div>
 {% endblock page_content %}
 
-
 {% block scripts %}
 {{ super() }}
-<script src="https://unpkg.com/gridjs/dist/gridjs.umd.js"></script>
 <script>
-  const updateUrl = (prev, query) => {
-    return prev + (prev.indexOf('?') >= 0 ? '&' : '?') + new URLSearchParams(query).toString();
-  };
-
-  new gridjs.Grid({
-    columns: [
-      { id: 'username', name: 'Username' },
-      { id: 'email', name: 'Email' },
-    ],
-    server: {
-      url: '/users/api_users',
-      then: results => results.data,
-      total: results => results.total,
-    },
-    search: {
-      enabled: true,
-      server: {
-        url: (prev, search) => {
-          return updateUrl(prev, {search});
-        },
-      },
-    },
-    sort: {
-      enabled: true,
-      multiColumn: true,
-      server: {
-        url: (prev, columns) => {
-          const columnIds = ['username', 'email'];
-          const sort = columns.map(col => (col.direction === 1 ? '+' : '-') + columnIds[col.index]);
-          return updateUrl(prev, {sort});
-        },
-      },
-    },
-    pagination: {
-      enabled: true,
-      server: {
-        url: (prev, page, limit) => {
-          return updateUrl(prev, {offset: page * limit, limit: limit});
-        },
-      },
-    }
-  }).render(document.getElementById('users'));
+  let userList = new UserList(document.querySelector('#users'));
+  userList.init({{ dict_users|tojson }});
 </script>
 {% endblock scripts %}
diff --git a/app/users/events.py b/app/users/events.py
index 1bda2b7b933f5a1a3293bf3e03384cca2e46636d..4b6f2976a3bc1450428b315a6cfb7ba90e88b963 100644
--- a/app/users/events.py
+++ b/app/users/events.py
@@ -5,7 +5,7 @@ from flask_login import current_user
 from flask_socketio import join_room, leave_room
 
 
-@socketio.on('SUBSCRIBE /users/<user_id>')
+@socketio.on('GET /users/<user_id>')
 @socketio_login_required
 def subscribe_user(user_hashid):
     user_id = hashids.decode(user_hashid)
@@ -15,10 +15,24 @@ def subscribe_user(user_hashid):
     if not (user == current_user or current_user.is_administrator):
         return {'code': 403, 'msg': 'Forbidden'}
     dict_user = user.to_dict(backrefs=True, relationships=True)
-    join_room(f'/users/{user.hashid}')
     return {'code': 200, 'msg': 'OK', 'payload': dict_user}
 
 
+@socketio.on('SUBSCRIBE /users/<user_id>')
+@socketio_login_required
+def subscribe_user(user_hashid):
+    user_id = hashids.decode(user_hashid)
+    user = User.query.get(user_id)
+    if user is None:
+        return {'code': 404, 'msg': 'Not found'}
+    if not (user == current_user or current_user.is_administrator):
+        return {'code': 403, 'msg': 'Forbidden'}
+    # dict_user = user.to_dict(backrefs=True, relationships=True)
+    join_room(f'/users/{user.hashid}')
+    # return {'code': 200, 'msg': 'OK', 'payload': dict_user}
+    return {'code': 200, 'msg': 'OK'}
+
+
 @socketio.on('UNSUBSCRIBE /users/<user_id>')
 @socketio_login_required
 def subscribe_user(user_hashid):
diff --git a/app/users/routes.py b/app/users/routes.py
index 6813aa96b1f9001831657d280ab8df40e47d0794..b657107dcaa689a389abe54aebfad9602b624bc1 100644
--- a/app/users/routes.py
+++ b/app/users/routes.py
@@ -1,50 +1,17 @@
+from app.decorators import admin_required
 from app.models import User
-from flask import render_template, request, url_for
+from flask import render_template, request
+from flask_login import login_required
 from . import bp
 
 
 @bp.route('/')
+@login_required
+@admin_required
 def users():
+    dict_users = [u.to_dict(backrefs=True, relationships=False) for u in User.query.all()]
     return render_template(
         'users/users.html.j2',
-        title='Users'
+        title='Users',
+        dict_users=dict_users
     )
-
-
-@bp.route('/api_users')
-def api_users():
-    query = User.query
-
-    # search filter
-    search = request.args.get('search')
-    if search:
-        query = query.filter(User.username.like(f'%{search}%') | User.email.like(f'%{search}%'))
-    total = query.count()
-
-    # sorting
-    sort = request.args.get('sort')
-    if sort:
-        order = []
-        for s in sort.split(','):
-            direction = s[0]
-            name = s[1:]
-            if name not in ['username', 'email']:
-                name = 'username'
-            col = getattr(User, name)
-            if direction == '-':
-                col = col.desc()
-            order.append(col)
-        if order:
-            query = query.order_by(*order)
-
-    # pagination
-    offset = request.args.get('offset', type=int, default=-1)
-    limit = request.args.get('limit', type=int, default=-1)
-    if offset != -1 and limit != -1:
-        query = query.offset(offset).limit(limit)
-
-    # response
-    return {
-        'data': [user.to_dict() for user in query],
-        'total': total
-    }