diff --git a/app/__init__.py b/app/__init__.py
index 37dc1a4b457d922a462ad484307cebae5e3b1621..c6df42939037fa067d81c769619c5e6d12069444 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -6,11 +6,13 @@ from flask_migrate import Migrate
 from flask_paranoid import Paranoid
 from flask_socketio import SocketIO
 from flask_sqlalchemy import SQLAlchemy
+from hashids import Hashids
 import flask_assets
 
 
 assets = flask_assets.Environment()
 db = SQLAlchemy()
+hashids = Hashids(min_length=32)  # , salt=current_app.config.get('SECRET_KEY')
 login = LoginManager()
 login.login_view = 'auth.login'
 login.login_message = 'Please log in to access this page.'
@@ -37,6 +39,9 @@ def create_app(config_class=Config):
         message_queue=app.config.get('NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI')
     )
 
+    from .utils import HashidConverter
+    app.url_map.converters['hashid'] = HashidConverter
+
     from .events import socketio as socketio_events
     from .events import sqlalchemy as sqlalchemy_events
 
diff --git a/app/admin/routes.py b/app/admin/routes.py
index 3d7de6fb437713b8ff1692e6eb5a3744facf1f59..902f8207058a8e4c85d7373eb4552465b166d559 100644
--- a/app/admin/routes.py
+++ b/app/admin/routes.py
@@ -19,12 +19,13 @@ def index():
 @login_required
 @admin_required
 def users():
-    # users = [user.to_dict() for user in User.query.all()]
-    users = {user.id: user.to_dict() for user in User.query.all()}
-    return render_template('admin/users.html.j2', title='Users', users=users)
+    dict_users = {user.id: user.to_dict(backrefs=True, relationships=False)
+                  for user in User.query.all()}
+    return render_template(
+        'admin/users.html.j2', title='Users', dict_users=dict_users)
 
 
-@bp.route('/users/<int:user_id>')
+@bp.route('/users/<hashid:user_id>')
 @login_required
 @admin_required
 def user(user_id):
@@ -32,7 +33,7 @@ def user(user_id):
     return render_template('admin/user.html.j2', title='User', user=user)
 
 
-@bp.route('/users/<int:user_id>/delete')
+@bp.route('/users/<hashid:user_id>/delete')
 @login_required
 @admin_required
 def delete_user(user_id):
@@ -41,7 +42,7 @@ def delete_user(user_id):
     return redirect(url_for('.users'))
 
 
-@bp.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])  # noqa
+@bp.route('/users/<hashid:user_id>/edit', methods=['GET', 'POST'])  # noqa
 @login_required
 @admin_required
 def edit_user(user_id):
diff --git a/app/api/jobs.py b/app/api/jobs.py
index c22de0ed651edc1899be00a04691844df756c8f4..153d5060d6269d8c47305a7018e16eb8e882025b 100644
--- a/app/api/jobs.py
+++ b/app/api/jobs.py
@@ -27,7 +27,7 @@ class API_Jobs(Resource):
         pass
 
 
-@ns.route('/<int:id>')
+@ns.route('/<hashid:id>')
 class API_Job(Resource):
     '''Show a single job and lets you delete it'''
 
diff --git a/app/corpora/cqi_over_socketio/__init__.py b/app/corpora/cqi_over_socketio/__init__.py
index 2cce78344970738524168e0918cccc21b009d8be..d914973c88b906c592fb9f140ed34c5620b69dfe 100644
--- a/app/corpora/cqi_over_socketio/__init__.py
+++ b/app/corpora/cqi_over_socketio/__init__.py
@@ -62,7 +62,7 @@ def connect(auth):
     if corpus is None:
         # return {'code': 404, 'msg': 'Not Found'}
         raise ConnectionRefusedError('Not Found')
-    if not (corpus.creator == current_user or current_user.is_administrator()):
+    if not (corpus.user == current_user or current_user.is_administrator()):
         # return {'code': 403, 'msg': 'Forbidden'}
         raise ConnectionRefusedError('Forbidden')
     if corpus.status not in ['prepared', 'start analysis', 'analysing', 'stop analysis']:
diff --git a/app/corpora/query_results_routes.py b/app/corpora/query_results_routes.py
index 1ccc477eed14ecb746f514d4eea716d7d337b005..478b6fe1ab3e63e3a7cbdf181a573b9c2224f9ea 100644
--- a/app/corpora/query_results_routes.py
+++ b/app/corpora/query_results_routes.py
@@ -22,7 +22,7 @@ def add_query_result():
     if form.is_submitted():
         if not form.validate():
             return make_response(form.errors, 400)
-        query_result = QueryResult(creator=current_user,
+        query_result = QueryResult(user=current_user,
                                    description=form.description.data,
                                    filename=form.file.data.filename,
                                    title=form.title.data)
@@ -65,19 +65,19 @@ def add_query_result():
                            form=form, title='Add query result')
 
 
-@bp.route('/result/<int:query_result_id>')
+@bp.route('/result/<hashid:query_result_id>')
 @login_required
 def query_result(query_result_id):
     abort(503)
     query_result = QueryResult.query.get_or_404(query_result_id)
-    if not (query_result.creator == current_user
+    if not (query_result.user == current_user
             or current_user.is_administrator()):
         abort(403)
     return render_template('corpora/query_results/query_result.html.j2',
                            query_result=query_result, title='Query result')
 
 
-@bp.route('/result/<int:query_result_id>/inspect')
+@bp.route('/result/<hashid:query_result_id>/inspect')
 @login_required
 def inspect_query_result(query_result_id):
     '''
@@ -86,7 +86,7 @@ def inspect_query_result(query_result_id):
     abort(503)
     query_result = QueryResult.query.get_or_404(query_result_id)
     query_metadata = query_result.query_metadata
-    if not (query_result.creator == current_user
+    if not (query_result.user == current_user
             or current_user.is_administrator()):
         abort(403)
     display_options_form = DisplayOptionsForm(
@@ -108,12 +108,12 @@ def inspect_query_result(query_result_id):
                            title='Inspect query result')
 
 
-@bp.route('/result/<int:query_result_id>/delete')
+@bp.route('/result/<hashid:query_result_id>/delete')
 @login_required
 def delete_query_result(query_result_id):
     abort(503)
     query_result = QueryResult.query.get_or_404(query_result_id)
-    if not (query_result.creator == current_user
+    if not (query_result.user == current_user
             or current_user.is_administrator()):
         abort(403)
     flash('Query result "{}" has been marked for deletion!'.format(query_result), 'result')  # noqa
@@ -121,12 +121,12 @@ def delete_query_result(query_result_id):
     return redirect(url_for('services.service', service="corpus_analysis"))
 
 
-@bp.route('/result/<int:query_result_id>/download')
+@bp.route('/result/<hashid:query_result_id>/download')
 @login_required
 def download_query_result(query_result_id):
     abort(503)
     query_result = QueryResult.query.get_or_404(query_result_id)
-    if not (query_result.creator == current_user
+    if not (query_result.user == current_user
             or current_user.is_administrator()):
         abort(403)
     return send_from_directory(as_attachment=True,
diff --git a/app/corpora/routes.py b/app/corpora/routes.py
index f700a540152a6beab1636112eb73bbf353ad9126..5af9ea92386ec1114313707fd96927408d8b2931 100644
--- a/app/corpora/routes.py
+++ b/app/corpora/routes.py
@@ -21,7 +21,7 @@ def add_corpus():
     form = AddCorpusForm(prefix='add-corpus-form')
     if form.validate_on_submit():
         corpus = Corpus(
-            creator=current_user,
+            user=current_user,
             description=form.description.data,
             title=form.title.data
         )
@@ -52,7 +52,7 @@ def import_corpus():
         if not form.validate():
             return make_response(form.errors, 400)
         corpus = Corpus(
-            creator=current_user,
+            user=current_user,
             description=form.description.data,
             title=form.title.data
         )
@@ -115,18 +115,18 @@ def import_corpus():
                            title='Import Corpus')
 
 
-@bp.route('/<int:corpus_id>')
+@bp.route('/<hashid:corpus_id>')
 @login_required
 def corpus(corpus_id):
     corpus = Corpus.query.get_or_404(corpus_id)
-    if not (corpus.creator == current_user or current_user.is_administrator()):
+    if not (corpus.user == current_user or current_user.is_administrator()):
         abort(403)
     corpus_files = [corpus_file.to_dict() for corpus_file in corpus.files]
     return render_template('corpora/corpus.html.j2', corpus=corpus,
                            corpus_files=corpus_files, title='Corpus')
 
 
-@bp.route('/<int:corpus_id>/analyse')
+@bp.route('/<hashid:corpus_id>/analyse')
 @login_required
 def analyse_corpus(corpus_id):
     corpus = Corpus.query.get_or_404(corpus_id)
@@ -137,37 +137,37 @@ def analyse_corpus(corpus_id):
     )
 
 
-@bp.route('/<int:corpus_id>/download')
+@bp.route('/<hashid:corpus_id>/download')
 @login_required
 def download_corpus(corpus_id):
     abort(503)
     corpus = Corpus.query.get_or_404(corpus_id)
-    if not (corpus.creator == current_user or current_user.is_administrator()):
+    if not (corpus.user == current_user or current_user.is_administrator()):
         abort(403)
     return send_from_directory(
         as_attachment=True,
-        directory=os.path.join(corpus.creator.path, 'corpora'),
+        directory=os.path.join(corpus.user.path, 'corpora'),
         filename=corpus.archive_file,
         mimetype='zip'
     )
 
 
-@bp.route('/<int:corpus_id>/delete')
+@bp.route('/<hashid:corpus_id>/delete')
 @login_required
 def delete_corpus(corpus_id):
     corpus = Corpus.query.get_or_404(corpus_id)
-    if not (corpus.creator == current_user or current_user.is_administrator()):
+    if not (corpus.user == current_user or current_user.is_administrator()):
         abort(403)
     flash('Corpus "{}" marked for deletion!'.format(corpus.title), 'corpus')
     tasks.delete_corpus(corpus_id)
     return redirect(url_for('main.dashboard'))
 
 
-@bp.route('/<int:corpus_id>/files/add', methods=['GET', 'POST'])
+@bp.route('/<hashid:corpus_id>/files/add', methods=['GET', 'POST'])
 @login_required
 def add_corpus_file(corpus_id):
     corpus = Corpus.query.get_or_404(corpus_id)
-    if not (corpus.creator == current_user or current_user.is_administrator()):
+    if not (corpus.user == current_user or current_user.is_administrator()):
         abort(403)
     form = AddCorpusFileForm(corpus, prefix='add-corpus-file-form')
     if form.is_submitted():
@@ -200,13 +200,13 @@ def add_corpus_file(corpus_id):
                            form=form, title='Add corpus file')
 
 
-@bp.route('/<int:corpus_id>/files/<int:corpus_file_id>/delete')
+@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>/delete')
 @login_required
 def delete_corpus_file(corpus_id, corpus_file_id):
     corpus_file = CorpusFile.query.get_or_404(corpus_file_id)
     if not corpus_file.corpus_id == corpus_id:
         abort(404)
-    if not (corpus_file.corpus.creator == current_user
+    if not (corpus_file.corpus.user == current_user
             or current_user.is_administrator()):
         abort(403)
     flash('Corpus file "{}" marked for deletion!'.format(corpus_file.filename), 'corpus')  # noqa
@@ -214,13 +214,13 @@ def delete_corpus_file(corpus_id, corpus_file_id):
     return redirect(url_for('.corpus', corpus_id=corpus_id))
 
 
-@bp.route('/<int:corpus_id>/files/<int:corpus_file_id>/download')
+@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>/download')
 @login_required
 def download_corpus_file(corpus_id, corpus_file_id):
     corpus_file = CorpusFile.query.get_or_404(corpus_file_id)
     if not corpus_file.corpus_id == corpus_id:
         abort(404)
-    if not (corpus_file.corpus.creator == current_user
+    if not (corpus_file.corpus.user == current_user
             or current_user.is_administrator()):
         abort(403)
     return send_from_directory(as_attachment=True,
@@ -228,11 +228,11 @@ def download_corpus_file(corpus_id, corpus_file_id):
                                filename=corpus_file.filename)
 
 
-@bp.route('/<int:corpus_id>/files/<int:corpus_file_id>', methods=['GET', 'POST'])
+@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['GET', 'POST'])
 @login_required
 def corpus_file(corpus_id, corpus_file_id):
     corpus = Corpus.query.get_or_404(corpus_id)
-    if not (corpus.creator == current_user or current_user.is_administrator()):
+    if not (corpus.user == current_user or current_user.is_administrator()):
         abort(403)
     corpus_file = CorpusFile.query.get_or_404(corpus_file_id)
     if corpus_file.corpus != corpus:
@@ -273,11 +273,11 @@ def corpus_file(corpus_id, corpus_file_id):
                            title='Edit corpus file')
 
 
-@bp.route('/<int:corpus_id>/prepare')
+@bp.route('/<hashid:corpus_id>/prepare')
 @login_required
 def prepare_corpus(corpus_id):
     corpus = Corpus.query.get_or_404(corpus_id)
-    if not (corpus.creator == current_user or current_user.is_administrator()):
+    if not (corpus.user == current_user or current_user.is_administrator()):
         abort(403)
     if corpus.files.all():
         tasks.build_corpus(corpus_id)
diff --git a/app/daemon/__init__.py b/app/daemon/__init__.py
index 60adcf2a56cf82bc44ae141cb9620b9356291df5..009774562c51aa633f80106ad8e22828cdfdbb38 100644
--- a/app/daemon/__init__.py
+++ b/app/daemon/__init__.py
@@ -21,6 +21,7 @@ class Daemon(CheckCorporaMixin, CheckJobsMixin):
                 self.check_corpora()
                 self.check_jobs()
                 db.session.commit()
-            except:
+            except Exception as e:
+                current_app.logger.warning(e)
                 pass
             sleep(1.5)
diff --git a/app/events/socketio.py b/app/events/socketio.py
index 81f40533761a3f9ff26f296badb5de139194aeb9..ceb43a4f8a551e28f23d64aedb61cfcaed8cce3d 100644
--- a/app/events/socketio.py
+++ b/app/events/socketio.py
@@ -1,4 +1,4 @@
-from flask import request
+from app import hashids
 from flask_login import current_user
 from flask_socketio import join_room
 from .. import socketio
@@ -6,68 +6,23 @@ from ..decorators import socketio_login_required
 from ..models import User
 
 
-'''
-' A list containing session ids of Socket.IO sessions, to keep track
-' of all connected sessions, which can be used to determine the runtimes of
-' associated background tasks.
-'''
-sessions = []
-
-
 ###############################################################################
 # Socket.IO event handlers                                                    #
 ###############################################################################
-@socketio.on('connect')
-@socketio_login_required
-def socketio_connect():
-    '''
-    ' The Socket.IO module creates a session id (sid) for each request.
-    ' On connect the sid is saved in the sessions list.
-    '''
-    sessions.append(request.sid)
-    # return {'code': 200, 'msg': 'OK'}
-
-
-@socketio.on('disconnect')
-def socketio_disconnect():
-    '''
-    ' On disconnect the session id gets removed from the sessions list.
-    '''
-    try:
-        sessions.remove(request.sid)
-    except ValueError:
-        pass
-    # return {'code': 200, 'msg': 'OK'}
-
-
-@socketio.on('start_user_session')
-@socketio_login_required
-def socketio_start_user_session(user_id):
-    user = User.query.get(user_id)
-    if user is None:
-        response = {'code': 404, 'msg': 'Not found'}
-        socketio.emit('start_user_session', response, room=request.sid)
-    elif not (user == current_user or current_user.is_administrator):
-        response = {'code': 403, 'msg': 'Forbidden'}
-        socketio.emit('start_user_session', response, room=request.sid)
-    else:
-        response = {'code': 200, 'msg': 'OK'}
-        socketio.emit('start_user_session', response, room=request.sid)
-        socketio.emit('user_{}_init'.format(user.id), user.to_dict(),
-                      room=request.sid)
-        room = 'user_{}'.format(user.id)
-        join_room(room)
-
-
-@socketio.on('users.request')
+@socketio.on('users.user.get')
 @socketio_login_required
-def socketio_start_session(user_id):
+def users_user_get(user_hashid):
+    user_id = hashids.decode(user_hashid)[0]
     user = User.query.get(user_id)
     if user is None:
         response = {'code': 404, 'msg': 'Not found'}
     elif not (user == current_user or current_user.is_administrator):
         response = {'code': 403, 'msg': 'Forbidden'}
     else:
-        response = {'code': 200, 'msg': 'OK', 'payload': user.to_dict()}
-        join_room('users.{}'.format(user.id))
+        response = {
+            'code': 200,
+            'msg': 'OK',
+            'payload': user.to_dict(backrefs=True, relationships=True)
+        }
+        join_room(f'users.{user.hashid}')
     return response
diff --git a/app/events/sqlalchemy.py b/app/events/sqlalchemy.py
index 9356ccedfdf27a75ba1b2ef42cbd00b2fb75db3a..457a2bde8b245ead1b4c324e06359189025256a9 100644
--- a/app/events/sqlalchemy.py
+++ b/app/events/sqlalchemy.py
@@ -1,4 +1,5 @@
 from datetime import datetime
+from flask import current_app
 from .. import db, mail, socketio
 from ..email import create_message
 from ..models import Corpus, CorpusFile, Job, JobInput, JobResult, QueryResult
@@ -14,10 +15,9 @@ from ..models import Corpus, CorpusFile, Job, JobInput, JobResult, QueryResult
 @db.event.listens_for(JobResult, 'after_delete')
 @db.event.listens_for(QueryResult, 'after_delete')
 def ressource_after_delete(mapper, connection, ressource):
-    event = 'user_{}_patch'.format(ressource.user_id)
     jsonpatch = [{'op': 'remove', 'path': ressource.jsonpatch_path}]
-    room = 'user_{}'.format(ressource.user_id)
-    socketio.emit(event, jsonpatch, room=room)
+    room = f'users.{ressource.user_hashid}'
+    socketio.emit('users.patch', jsonpatch, room=room)
 
 
 @db.event.listens_for(Corpus, 'after_insert')
@@ -27,16 +27,12 @@ def ressource_after_delete(mapper, connection, ressource):
 @db.event.listens_for(JobResult, 'after_insert')
 @db.event.listens_for(QueryResult, 'after_insert')
 def ressource_after_insert_handler(mapper, connection, ressource):
-    event = 'user_{}_patch'.format(ressource.user_id)
+    value = ressource.to_dict(backrefs=False, relationships=False)
     jsonpatch = [
-        {
-            'op': 'add',
-            'path': ressource.jsonpatch_path,
-            'value': ressource.to_dict(include_relationships=False)
-        }
+        {'op': 'add', 'path': ressource.jsonpatch_path, 'value': value}
     ]
-    room = 'user_{}'.format(ressource.user_id)
-    socketio.emit(event, jsonpatch, room=room)
+    room = f'users.{ressource.user_hashid}'
+    socketio.emit('users.patch', jsonpatch, room=room)
 
 
 @db.event.listens_for(Corpus, 'after_update')
@@ -63,26 +59,25 @@ def ressource_after_update_handler(mapper, connection, ressource):
         jsonpatch.append(
             {
                 'op': 'replace',
-                'path': '{}/{}'.format(ressource.jsonpatch_path, attr.key),
+                'path': f'{ressource.jsonpatch_path}/{attr.key}',
                 'value': new_value
             }
         )
         # Job status update notification if it changed and wanted by the user
         if isinstance(ressource, Job) and attr.key == 'status':
-            if ressource.creator.setting_job_status_mail_notifications == 'none':  # noqa
+            if ressource.user.setting_job_status_mail_notifications == 'none':  # noqa
                 pass
-            elif (ressource.creator.setting_job_status_mail_notifications == 'end'  # noqa
+            elif (ressource.user.setting_job_status_mail_notifications == 'end'  # noqa
                   and ressource.status not in ['complete', 'failed']):
                 pass
             else:
                 msg = create_message(
-                    ressource.creator.email,
-                    'Status update for your Job "{}"'.format(ressource.title),
+                    ressource.user.email,
+                    f'Status update for your Job "{ressource.title}"',
                     'tasks/email/notification',
                     job=ressource
                 )
                 mail.send(msg)
     if jsonpatch:
-        event = 'user_{}_patch'.format(ressource.user_id)
-        room = 'user_{}'.format(ressource.user_id)
-        socketio.emit(event, jsonpatch, room=room)
+        room = f'users.{ressource.user_hashid}'
+        socketio.emit('users.patch', jsonpatch, room=room)
diff --git a/app/jobs/routes.py b/app/jobs/routes.py
index 5db5692f7c4b3f30877bbcfbedabea99140a5997..5138a28f5b6881c4841793b29fa99d8dd0894acb 100644
--- a/app/jobs/routes.py
+++ b/app/jobs/routes.py
@@ -8,33 +8,33 @@ from ..models import Job, JobInput, JobResult
 import os
 
 
-@bp.route('/<int:job_id>')
+@bp.route('/<hashid:job_id>')
 @login_required
 def job(job_id):
     job = Job.query.get_or_404(job_id)
-    if not (job.creator == current_user or current_user.is_administrator()):
+    if not (job.user == current_user or current_user.is_administrator()):
         abort(403)
     job_inputs = [job_input.to_dict() for job_input in job.inputs]
     return render_template('jobs/job.html.j2', job=job, job_inputs=job_inputs,
                            title='Job')
 
 
-@bp.route('/<int:job_id>/delete')
+@bp.route('/<hashid:job_id>/delete')
 @login_required
 def delete_job(job_id):
     job = Job.query.get_or_404(job_id)
-    if not (job.creator == current_user or current_user.is_administrator()):
+    if not (job.user == current_user or current_user.is_administrator()):
         abort(403)
     tasks.delete_job(job_id)
     flash('Job has been marked for deletion!', 'job')
     return redirect(url_for('main.dashboard'))
 
 
-@bp.route('/<int:job_id>/inputs/<int:job_input_id>/download')
+@bp.route('/<hashid:job_id>/inputs/<hashid:job_input_id>/download')
 @login_required
 def download_job_input(job_id, job_input_id):
     job_input = JobInput.query.filter(JobInput.job_id == job_id, JobInput.id == job_input_id).first_or_404()  # noqa
-    if not (job_input.job.creator == current_user
+    if not (job_input.job.user == current_user
             or current_user.is_administrator()):
         abort(403)
     return send_from_directory(as_attachment=True,
@@ -42,7 +42,7 @@ def download_job_input(job_id, job_input_id):
                                filename=job_input.filename)
 
 
-@bp.route('/<int:job_id>/restart')
+@bp.route('/<hashid:job_id>/restart')
 @login_required
 @admin_required
 def restart(job_id):
@@ -55,11 +55,11 @@ def restart(job_id):
     return redirect(url_for('.job', job_id=job_id))
 
 
-@bp.route('/<int:job_id>/results/<int:job_result_id>/download')
+@bp.route('/<hashid:job_id>/results/<hashid:job_result_id>/download')
 @login_required
 def download_job_result(job_id, job_result_id):
     job_result = JobResult.query.filter(JobResult.job_id == job_id, JobResult.id == job_result_id).first_or_404()  # noqa
-    if not (job_result.job.creator == current_user
+    if not (job_result.job.user == current_user
             or current_user.is_administrator()):
         abort(403)
     return send_from_directory(as_attachment=True,
diff --git a/app/models.py b/app/models.py
index 0cc4e83cf01e728fa97c6d09df22fe8bad6c5d47..23b91cf31ab7620c503419734be89c61eeac05cc 100644
--- a/app/models.py
+++ b/app/models.py
@@ -5,30 +5,44 @@ from itsdangerous import BadSignature, TimedJSONWebSignatureSerializer
 from time import sleep
 from werkzeug.security import generate_password_hash, check_password_hash
 import xml.etree.ElementTree as ET
-from . import db, login
+from . import db, hashids, login
 import base64
+import enum
 import os
 import shutil
 
 
-class Permission:
+class HashidMixin:
+    @property
+    def hashid(self):
+        return hashids.encode(self.id)
+
+
+class FileMixin:
+    creation_date = db.Column(db.DateTime, default=datetime.utcnow)
+    filename = db.Column(db.String(256))
+    last_edited_date = db.Column(db.DateTime, default=datetime.utcnow)
+    mimetype = db.Column(db.String(255))
+
+    def file_mixin_to_dict(self, backrefs=False, relationships=False):
+        return {
+            'creation_date': self.creation_date.isoformat() + 'Z',
+            'filename': self.filename,
+            'last_edited_date': self.last_edited_date.isoformat() + 'Z',
+            'mimetype': self.mimetype
+        }
+
+
+class Permission(enum.IntEnum):
     '''
     Defines User permissions as integers by the power of 2. User permission
-    can be evaluated using the bitwise operator &. 3 equals to CREATE_JOB and
-    DELETE_JOB and so on.
+    can be evaluated using the bitwise operator &.
     '''
-    MANAGE_CORPORA = 1
-    MANAGE_JOBS = 2
-    # PERMISSION_NAME = 4
-    # PERMISSION_NAME = 8
-    ADMIN = 16
+    ADMINISTRATE = 1
+    USE_API = 2
 
 
-class Role(db.Model):
-    '''
-    Model for the different roles Users can have. Is a one-to-many
-    relationship. A Role can be associated with many User rows.
-    '''
+class Role(HashidMixin, db.Model):
     __tablename__ = 'roles'
     # Primary key
     id = db.Column(db.Integer, primary_key=True)
@@ -39,78 +53,62 @@ class Role(db.Model):
     # Relationships
     users = db.relationship('User', backref='role', lazy='dynamic')
 
-    def to_dict(self, include_relationships=True):
-        return {'id': self.id,
-                'default': self.default,
-                'name': self.name,
-                'permissions': self.permissions}
-
     def __init__(self, **kwargs):
-        super(Role, self).__init__(**kwargs)
+        super().__init__(**kwargs)
         if self.permissions is None:
             self.permissions = 0
 
     def __repr__(self):
-        '''
-        String representation of the Role. For human readability.
-        '''
-        return '<Role {}>'.format(self.name)
+        return f'<Role {self.name}>'
 
-    def add_permission(self, perm):
-        '''
-        Add new permission to Role. Input is a Permission.
-        '''
-        if not self.has_permission(perm):
-            self.permissions += perm
+    def add_permission(self, permission):
+        if not self.has_permission(permission):
+            self.permissions += permission
 
-    def remove_permission(self, perm):
-        '''
-        Removes permission from a Role. Input a Permission.
-        '''
-        if self.has_permission(perm):
-            self.permissions -= perm
+    def has_permission(self, permission):
+        return self.permissions & permission == permission
+
+    def remove_permission(self, permission):
+        if self.has_permission(permission):
+            self.permissions -= permission
 
     def reset_permissions(self):
-        '''
-        Resets permissions to zero. Zero equals no permissions at all.
-        '''
         self.permissions = 0
 
-    def has_permission(self, perm):
-        '''
-        Checks if a Role has a specific Permission. Does this with the bitwise
-        operator.
-        '''
-        return self.permissions & perm == perm
+    def to_dict(self, backrefs=False, relationships=False):
+        dict_role = {
+            'id': self.hashid,
+            'default': self.default,
+            'name': self.name,
+            'permissions': self.permissions
+        }
+        if relationships:
+            dict_role['users']: {
+                x.to_dict(backrefs=False, relationships=True)
+                for x in self.users
+            }
+        return dict_role
 
     @staticmethod
     def insert_roles():
-        '''
-        Inserts roles into the database. This has to be executed befor Users
-        are added to the database. Otherwiese Users will not have a Role
-        assigned to them. Order of the roles dictionary determines the ID of
-        each role. Users have the ID 1 and Administrators have the ID 2.
-        '''
-        roles = {'User': [Permission.MANAGE_CORPORA, Permission.MANAGE_JOBS],
-                 'Administrator': [Permission.MANAGE_CORPORA,
-                                   Permission.MANAGE_JOBS, Permission.ADMIN]}
-        default_role = 'User'
-        for r in roles:
-            role = Role.query.filter_by(name=r).first()
+        roles = {
+            'User': [],
+            'Administrator': [Permission.USE_API, Permission.ADMINISTRATE]
+        }
+        default_role_name = 'User'
+        for role_name, permissions in roles.items():
+            role = Role.query.filter_by(name=role_name).first()
             if role is None:
-                role = Role(name=r)
+                role = Role(name=role_name)
             role.reset_permissions()
-            for perm in roles[r]:
-                role.add_permission(perm)
-            role.default = (role.name == default_role)
+            for permission in permissions:
+                role.add_permission(permission)
+            role.default = role.name == default_role_name
             db.session.add(role)
         db.session.commit()
 
 
-class User(UserMixin, db.Model):
-    '''
-    Model for Users that are registered to Opaque.
-    '''
+class User(HashidMixin, UserMixin, db.Model):
     __tablename__ = 'users'
     # Primary key
     id = db.Column(db.Integer, primary_key=True)
@@ -122,28 +120,50 @@ class User(UserMixin, db.Model):
     last_seen = db.Column(db.DateTime(), default=datetime.utcnow)
     member_since = db.Column(db.DateTime(), default=datetime.utcnow)
     password_hash = db.Column(db.String(128))
-    setting_dark_mode = db.Column(db.Boolean, default=False)
-    setting_job_status_mail_notifications = db.Column(db.String(16),
-                                                      default='end')
-    setting_job_status_site_notifications = db.Column(db.String(16),
-                                                      default='all')
     token = db.Column(db.String(32), index=True, unique=True)
     token_expiration = db.Column(db.DateTime)
     username = db.Column(db.String(64), unique=True, index=True)
+    setting_dark_mode = db.Column(db.Boolean, default=False)
+    setting_job_status_mail_notifications = db.Column(
+        db.String(16), default='end')
+    setting_job_status_site_notifications = db.Column(
+        db.String(16), default='all')
+    # Backrefs: role: Role
     # Relationships
-    corpora = db.relationship('Corpus', backref='creator', lazy='dynamic',
-                              cascade='save-update, merge, delete')
-    jobs = db.relationship('Job', backref='creator', lazy='dynamic',
-                           cascade='save-update, merge, delete')
-    query_results = db.relationship('QueryResult',
-                                    backref='creator',
-                                    cascade='save-update, merge, delete',
-                                    lazy='dynamic')
+    corpora = db.relationship(
+        'Corpus',
+        backref='user',
+        cascade='all, delete-orphan',
+        lazy='dynamic'
+    )
+    jobs = db.relationship(
+        'Job',
+        backref='user',
+        cascade='all, delete-orphan',
+        lazy='dynamic'
+    )
+    query_results = db.relationship(
+        'QueryResult',
+        backref='user',
+        cascade='all, delete-orphan',
+        lazy='dynamic'
+    )
+
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+        if self.role is not None:
+            return
+        if self.email == current_app.config['NOPAQUE_ADMIN']:
+            self.role = Role.query.filter_by(name='Administrator').first()
+        else:
+            self.role = Role.query.filter_by(default=True).first()
+
+    def __repr__(self):
+        return f'<User {self.username}>'
 
     @property
-    def path(self):
-        return os.path.join(current_app.config['NOPAQUE_DATA_DIR'],
-                            str(self.id))
+    def jsonpatch_path(self):
+        return f'/users/{self.hashid}'
 
     @property
     def password(self):
@@ -153,117 +173,40 @@ class User(UserMixin, db.Model):
     def password(self, password):
         self.password_hash = generate_password_hash(password)
 
-    def to_dict(self, include_relationships=True):
-        dict_user = {
-            'id': self.id,
-            'role_id': self.role_id,
-            'confirmed': self.confirmed,
-            'email': self.email,
-            'last_seen': self.last_seen.isoformat() + 'Z',
-            'member_since': self.member_since.isoformat() + 'Z',
-            'settings': {'dark_mode': self.setting_dark_mode,
-                         'job_status_mail_notifications':
-                             self.setting_job_status_mail_notifications,
-                         'job_status_site_notifications':
-                             self.setting_job_status_site_notifications},
-            'username': self.username,
-            'role': self.role.to_dict()
-        }
-        if include_relationships:
-            dict_user['corpora'] = {corpus.id: corpus.to_dict()
-                                    for corpus in self.corpora}
-            dict_user['jobs'] = {job.id: job.to_dict() for job in self.jobs}
-            dict_user['query_results'] = {
-                query_result.id: query_result.to_dict()
-                for query_result in self.query_results
-            }
-        return dict_user
-
-    def __repr__(self):
-        '''
-        String representation of the User. For human readability.
-        '''
-        return '<User {}>'.format(self.username)
-
-    def __init__(self, **kwargs):
-        super(User, self).__init__(**kwargs)
-        if self.role is None:
-            if self.email == current_app.config['NOPAQUE_ADMIN']:
-                self.role = Role.query.filter_by(name='Administrator').first()
-            if self.role is None:
-                self.role = Role.query.filter_by(default=True).first()
-
-    def generate_confirmation_token(self, expiration=3600):
-        '''
-        Generates a confirmation token for user confirmation via email.
-        '''
-        s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'],
-                                            expiration)
-        return s.dumps({'confirm': self.id}).decode('utf-8')
+    @property
+    def path(self):
+        return os.path.join(
+            current_app.config.get('NOPAQUE_DATA_DIR'), str(self.id))
 
-    def generate_reset_token(self, expiration=3600):
-        '''
-        Generates a reset token for password reset via email.
-        '''
-        s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'],
-                                            expiration)
-        return s.dumps({'reset': self.id}).decode('utf-8')
+    def can(self, permission):
+        return self.role.has_permission(permission)
 
     def confirm(self, token):
-        '''
-        Confirms User if the given token is valid and not expired.
-        '''
         s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'])
         try:
             data = s.loads(token.encode('utf-8'))
         except BadSignature:
             return False
-        if data.get('confirm') != self.id:
+        if data.get('confirm') != self.hashid:
             return False
         self.confirmed = True
         db.session.add(self)
         return True
 
-    @staticmethod
-    def reset_password(token, new_password):
-        '''
-        Resets password for User if the given token is valid and not expired.
-        '''
-        s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'])
-        try:
-            data = s.loads(token.encode('utf-8'))
-        except BadSignature:
-            return False
-        user = User.query.get(data.get('reset'))
-        if user is None:
-            return False
-        user.password = new_password
-        db.session.add(user)
-        return True
-
-    def verify_password(self, password):
-        return check_password_hash(self.password_hash, password)
-
-    def can(self, perm):
-        '''
-        Checks if a User with its current role can doe something. Checks if the
-        associated role actually has the needed Permission.
-        '''
-        return self.role is not None and self.role.has_permission(perm)
-
-    def is_administrator(self):
-        '''
-        Checks if User has Admin permissions.
-        '''
-        return self.can(Permission.ADMIN)
-
     def delete(self):
-        '''
-        Delete the user and its corpora and jobs from database and filesystem.
-        '''
         shutil.rmtree(self.path, ignore_errors=True)
         db.session.delete(self)
 
+    def generate_confirmation_token(self, expiration=3600):
+        s = TimedJSONWebSignatureSerializer(
+            current_app.config['SECRET_KEY'], expiration)
+        return s.dumps({'confirm': self.hashid}).decode('utf-8')
+
+    def generate_reset_token(self, expiration=3600):
+        s = TimedJSONWebSignatureSerializer(
+            current_app.config['SECRET_KEY'], expiration)
+        return s.dumps({'reset': self.hashid}).decode('utf-8')
+
     def get_token(self, expires_in=3600):
         now = datetime.utcnow()
         if self.token and self.token_expiration > now + timedelta(seconds=60):
@@ -273,9 +216,50 @@ class User(UserMixin, db.Model):
         db.session.add(self)
         return self.token
 
+    def is_administrator(self):
+        return self.can(Permission.ADMINISTRATE)
+
     def revoke_token(self):
         self.token_expiration = datetime.utcnow() - timedelta(seconds=1)
 
+    def to_dict(self, backrefs=False, relationships=False):
+        dict_user = {
+            'id': self.hashid,
+            'role_id': self.role.hashid,
+            'confirmed': self.confirmed,
+            'email': self.email,
+            'last_seen': self.last_seen.isoformat() + 'Z',
+            'member_since': self.member_since.isoformat() + 'Z',
+            'username': self.username,
+            'settings': {
+                'dark_mode': self.setting_dark_mode,
+                'job_status_mail_notifications':
+                    self.setting_job_status_mail_notifications,
+                'job_status_site_notifications':
+                    self.setting_job_status_site_notifications
+            }
+        }
+        if backrefs:
+            dict_user['role'] = self.role.to_dict(
+                backrefs=True, relationships=False)
+        if relationships:
+            dict_user['corpora'] = {
+                x.hashid: x.to_dict(backrefs=False, relationships=True)
+                for x in self.corpora
+            }
+            dict_user['jobs'] = {
+                x.hashid: x.to_dict(backrefs=False, relationships=True)
+                for x in self.jobs
+            }
+            dict_user['query_results'] = {
+                x.hashid: x.to_dict(backrefs=False, relationships=True)
+                for x in self.query_results
+            }
+        return dict_user
+
+    def verify_password(self, password):
+        return check_password_hash(self.password_hash, password)
+
     @staticmethod
     def check_token(token):
         user = User.query.filter_by(token=token).first()
@@ -283,104 +267,137 @@ class User(UserMixin, db.Model):
             return None
         return user
 
+    @staticmethod
+    def reset_password(token, new_password):
+        s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'])
+        try:
+            data = s.loads(token.encode('utf-8'))
+        except BadSignature:
+            return False
+        user = User.query.get(data.get('reset'))
+        if user is None:
+            return False
+        user.password = new_password
+        db.session.add(user)
+        return True
 
-class JobInput(db.Model):
-    '''
-    Class to define JobInputs.
-    '''
+
+class JobInput(FileMixin, HashidMixin, db.Model):
     __tablename__ = 'job_inputs'
     # Primary key
     id = db.Column(db.Integer, primary_key=True)
     # Foreign keys
     job_id = db.Column(db.Integer, db.ForeignKey('jobs.id'))
-    # Fields
-    filename = db.Column(db.String(255))
+    # Backrefs: job: Job
+
+    def __repr__(self):
+        return f'<JobInput {self.filename}>'
 
     @property
     def download_url(self):
-        return url_for('jobs.download_job_input', job_id=self.job_id,
-                       job_input_id=self.id)
+        return url_for(
+            'jobs.download_job_input',
+            job_id=self.job.id,
+            job_input_id=self.id
+        )
 
     @property
     def jsonpatch_path(self):
-        return '/jobs/{}/inputs/{}'.format(self.job_id, self.id)
+        return f'{self.job.jsonpatch_path}/inputs/{self.hashid}'
 
     @property
     def path(self):
         return os.path.join(self.job.path, self.filename)
 
+    def to_dict(self, backrefs=False, relationships=False):
+        dict_job_input = {
+            'id': self.hashid,
+            'job_id': self.job.hashid,
+            'download_url': self.download_url,
+            'url': self.url,
+            **self.file_mixin_to_dict()
+        }
+        if backrefs:
+            dict_job_input['job'] = self.job.to_dict(
+                backrefs=True, relationships=False)
+        return dict_job_input
+
     @property
     def url(self):
-        return url_for('jobs.job', job_id=self.job_id,
-                       _anchor='job-{}-input-{}'.format(self.job_id, self.id))
+        return url_for(
+            'jobs.job',
+            job_id=self.job_id,
+            _anchor=f'job-{self.job.hashid}-input-{self.hashid}'
+        )
+
+    @property
+    def user_hashid(self):
+        return self.job.user.hashid
 
     @property
     def user_id(self):
         return self.job.user_id
 
-    def __repr__(self):
-        '''
-        String representation of the JobInput. For human readability.
-        '''
-        return '<JobInput {}>'.format(self.filename)
-
-    def to_dict(self, include_relationships=True):
-        return {'download_url': self.download_url,
-                'url': self.url,
-                'id': self.id,
-                'job_id': self.job_id,
-                'filename': self.filename}
-
 
-class JobResult(db.Model):
-    '''
-    Class to define JobResults.
-    '''
+class JobResult(FileMixin, HashidMixin, db.Model):
     __tablename__ = 'job_results'
     # Primary key
     id = db.Column(db.Integer, primary_key=True)
     # Foreign keys
     job_id = db.Column(db.Integer, db.ForeignKey('jobs.id'))
-    # Fields
-    filename = db.Column(db.String(255))
+    # Backrefs: job: Job
+
+    def __repr__(self):
+        return f'<JobResult {self.filename}>'
 
     @property
     def download_url(self):
-        return url_for('jobs.download_job_result', job_id=self.job_id,
-                       job_result_id=self.id)
+        return url_for(
+            'jobs.download_job_result',
+            job_id=self.job_id,
+            job_result_id=self.id
+        )
 
     @property
     def jsonpatch_path(self):
-        return '/jobs/{}/results/{}'.format(self.job_id, self.id)
+        return f'{self.job.jsonpatch_path}/results/{self.hashid}'
 
     @property
     def path(self):
         return os.path.join(self.job.path, 'output', self.filename)
 
+    def to_dict(self, backrefs=False, relationships=False):
+        dict_job_result = {
+            'id': self.hashid,
+            'job_id': self.job.hashid,
+            'download_url': self.download_url,
+            'url': self.url,
+            **self.file_mixin_to_dict(
+                backrefs=backrefs, relationships=relationships)
+        }
+        if backrefs:
+            dict_job_result['job'] = self.job.to_dict(
+                backrefs=True, relationships=False)
+        return dict_job_result
+
     @property
     def url(self):
-        return url_for('jobs.job', job_id=self.job_id,
-                       _anchor='job-{}-result-{}'.format(self.job_id, self.id))
+        return url_for(
+            'jobs.job',
+            job_id=self.job_id,
+            _anchor=f'job-{self.job.hashid}-result-{self.hashid}'
+        )
+
+    @property
+    def user_hashid(self):
+        return self.job.user.hashid
 
     @property
     def user_id(self):
         return self.job.user_id
 
-    def __repr__(self):
-        '''
-        String representation of the JobResult. For human readability.
-        '''
-        return '<JobResult {}>'.format(self.filename)
 
-    def to_dict(self, include_relationships=True):
-        return {'download_url': self.download_url,
-                'url': self.url,
-                'id': self.id,
-                'job_id': self.job_id,
-                'filename': self.filename}
-
-
-class Job(db.Model):
+class Job(HashidMixin, db.Model):
     '''
     Class to define Jobs.
     '''
@@ -402,29 +419,39 @@ class Job(db.Model):
     service_version = db.Column(db.String(16))
     status = db.Column(db.String(16))
     title = db.Column(db.String(32))
+    # Backrefs: user: User
     # Relationships
-    inputs = db.relationship('JobInput', backref='job', lazy='dynamic',
-                             cascade='save-update, merge, delete')
-    results = db.relationship('JobResult', backref='job', lazy='dynamic',
-                              cascade='save-update, merge, delete')
+    inputs = db.relationship(
+        'JobInput',
+        backref='job',
+        cascade='all, delete-orphan',
+        lazy='dynamic'
+    )
+    results = db.relationship(
+        'JobResult',
+        backref='job',
+        cascade='all, delete-orphan',
+        lazy='dynamic'
+    )
+
+    def __repr__(self):
+        return f'<Job {self.title}>'
 
     @property
     def jsonpatch_path(self):
-        return '/jobs/{}'.format(self.id)
+        return f'{self.user.jsonpatch_path}/jobs/{self.hashid}'
 
     @property
     def path(self):
-        return os.path.join(self.creator.path, 'jobs', str(self.id))
+        return os.path.join(self.user.path, 'jobs', str(self.id))
 
     @property
     def url(self):
         return url_for('jobs.job', job_id=self.id)
 
-    def __repr__(self):
-        '''
-        String representation of the Job. For human readability.
-        '''
-        return '<Job {}>'.format(self.title)
+    @property
+    def user_hashid(self):
+        return self.user.hashid
 
     def delete(self):
         '''
@@ -457,32 +484,36 @@ class Job(db.Model):
         self.end_date = None
         self.status = 'submitted'
 
-    def to_dict(self, include_relationships=True):
+    def to_dict(self, backrefs=False, relationships=False):
         dict_job = {
-            'url': self.url,
-            'id': self.id,
-            'user_id': self.user_id,
+            'id': self.hashid,
+            'user_id': self.user.hashid,
             'creation_date': self.creation_date.isoformat() + 'Z',
             'description': self.description,
-            'end_date': self.end_date.isoformat() + 'Z' if self.end_date else None,
+            'end_date': None if self.end_date is None else f'{self.end_date.isoformat()}Z',  # noqa
             'service': self.service,
             'service_args': self.service_args,
             'service_version': self.service_version,
             'status': self.status,
             'title': self.title,
+            'url': self.url
         }
-        if include_relationships:
-            dict_job['inputs'] = {input.id: input.to_dict()
-                                  for input in self.inputs}
-            dict_job['results'] = {result.id: result.to_dict()
-                                   for result in self.results}
+        if backrefs:
+            dict_job['user'] = self.user.to_dict(
+                backrefs=True, relationships=False)
+        if relationships:
+            dict_job['inputs'] = {
+                x.hashid: x.to_dict(backrefs=False, relationships=True)
+                for x in self.inputs
+            }
+            dict_job['results'] = {
+                x.hashid: x.to_dict(backrefs=False, relationships=True)
+                for x in self.results
+            }
         return dict_job
 
 
-class CorpusFile(db.Model):
-    '''
-    Class to define Files.
-    '''
+class CorpusFile(FileMixin, HashidMixin, db.Model):
     __tablename__ = 'corpus_files'
     # Primary key
     id = db.Column(db.Integer, primary_key=True)
@@ -494,7 +525,6 @@ class CorpusFile(db.Model):
     booktitle = db.Column(db.String(255))
     chapter = db.Column(db.String(255))
     editor = db.Column(db.String(255))
-    filename = db.Column(db.String(255))
     institution = db.Column(db.String(255))
     journal = db.Column(db.String(255))
     pages = db.Column(db.String(255))
@@ -502,15 +532,19 @@ class CorpusFile(db.Model):
     publishing_year = db.Column(db.Integer)
     school = db.Column(db.String(255))
     title = db.Column(db.String(255))
+    # Backrefs: corpus: Corpus
 
     @property
     def download_url(self):
-        return url_for('corpora.download_corpus_file',
-                       corpus_id=self.corpus_id, corpus_file_id=self.id)
+        return url_for(
+            'corpora.download_corpus_file',
+            corpus_id=self.corpus_id,
+            corpus_file_id=self.id
+        )
 
     @property
     def jsonpatch_path(self):
-        return '/corpora/{}/files/{}'.format(self.corpus_id, self.id)
+        return f'/{self.corpus.jsonpatch_path}/files/{self.hashid}'
 
     @property
     def path(self):
@@ -518,8 +552,15 @@ class CorpusFile(db.Model):
 
     @property
     def url(self):
-        return url_for('corpora.corpus_file', corpus_id=self.corpus_id,
-                       corpus_file_id=self.id)
+        return url_for(
+            'corpora.corpus_file',
+            corpus_id=self.corpus_id,
+            corpus_file_id=self.id
+        )
+
+    @property
+    def user_hashid(self):
+        return self.corpus.user.hashid
 
     @property
     def user_id(self):
@@ -536,27 +577,33 @@ class CorpusFile(db.Model):
         db.session.delete(self)
         self.corpus.status = 'unprepared'
 
-    def to_dict(self, include_relationships=True):
-        return {'download_url': self.download_url,
-                'url': self.url,
-                'id': self.id,
-                'corpus_id': self.corpus_id,
-                'address': self.address,
-                'author': self.author,
-                'booktitle': self.booktitle,
-                'chapter': self.chapter,
-                'editor': self.editor,
-                'filename': self.filename,
-                'institution': self.institution,
-                'journal': self.journal,
-                'pages': self.pages,
-                'publisher': self.publisher,
-                'publishing_year': self.publishing_year,
-                'school': self.school,
-                'title': self.title}
-
-
-class Corpus(db.Model):
+    def to_dict(self, backrefs=False, relationships=False):
+        dict_corpus_file = {
+            'id': self.hashid,
+            'corpus_id': self.corpus.hashid,
+            'download_url': self.download_url,
+            'url': self.url,
+            'address': self.address,
+            'author': self.author,
+            'booktitle': self.booktitle,
+            'chapter': self.chapter,
+            'editor': self.editor,
+            'institution': self.institution,
+            'journal': self.journal,
+            'pages': self.pages,
+            'publisher': self.publisher,
+            'publishing_year': self.publishing_year,
+            'school': self.school,
+            'title': self.title,
+            **self.file_mixin_to_dict(
+                backrefs=backrefs, relationships=relationships)
+        }
+        if backrefs:
+            dict_corpus_file['corpus'] = self.corpus.to_dict(
+                backrefs=True, relationships=False)
+
+
+class Corpus(HashidMixin, db.Model):
     '''
     Class to define a corpus.
     '''
@@ -574,47 +621,39 @@ class Corpus(db.Model):
     num_analysis_sessions = db.Column(db.Integer, default=0)
     num_tokens = db.Column(db.Integer, default=0)
     archive_file = db.Column(db.String(255))
+    # Backrefs: user: User
     # Relationships
-    files = db.relationship('CorpusFile', backref='corpus', lazy='dynamic',
-                            cascade='save-update, merge, delete')
+    files = db.relationship(
+        'CorpusFile',
+        backref='corpus',
+        lazy='dynamic',
+        cascade='all, delete-orphan'
+    )
     # Python class variables
     max_num_tokens = 2147483647
 
+    def __repr__(self):
+        return f'<Corpus {self.title}>'
+
     @property
     def analysis_url(self):
         return url_for('corpora.analyse_corpus', corpus_id=self.id)
 
     @property
     def jsonpatch_path(self):
-        return '/corpora/{}'.format(self.id)
+        return f'{self.user.jsonpatch_path}/corpora/{self.hashid}'
 
     @property
     def path(self):
-        return os.path.join(self.creator.path, 'corpora', str(self.id))
+        return os.path.join(self.user.path, 'corpora', str(self.id))
 
     @property
     def url(self):
         return url_for('corpora.corpus', corpus_id=self.id)
 
-    def to_dict(self, include_relationships=True):
-        dict_corpus = {
-            'analysis_url': self.analysis_url,
-            'url': self.url,
-            'id': self.id,
-            'user_id': self.user_id,
-            'creation_date': self.creation_date.isoformat() + 'Z',
-            'description': self.description,
-            'max_num_tokens': self.max_num_tokens,
-            'num_analysis_sessions': self.num_analysis_sessions,
-            'num_tokens': self.num_tokens,
-            'status': self.status,
-            'last_edited_date': self.last_edited_date.isoformat() + 'Z',
-            'title': self.title
-        }
-        if include_relationships:
-            dict_corpus['files'] = {file.id: file.to_dict()
-                                    for file in self.files}
-        return dict_corpus
+    @property
+    def user_hashid(self):
+        return self.user.hashid
 
     def build(self):
         output_dir = os.path.join(self.path, 'merged')
@@ -646,17 +685,33 @@ class Corpus(db.Model):
         shutil.rmtree(self.path, ignore_errors=True)
         db.session.delete(self)
 
-    def __repr__(self):
-        '''
-        String representation of the corpus. For human readability.
-        '''
-        return '<Corpus {}>'.format(self.title)
+    def to_dict(self, backrefs=False, relationships=False):
+        dict_corpus = {
+            'id': self.hashid,
+            'user_id': self.user.hashid,
+            'analysis_url': self.analysis_url,
+            'url': self.url,
+            'creation_date': self.creation_date.isoformat() + 'Z',
+            'description': self.description,
+            'max_num_tokens': self.max_num_tokens,
+            'num_analysis_sessions': self.num_analysis_sessions,
+            'num_tokens': self.num_tokens,
+            'status': self.status,
+            'last_edited_date': self.last_edited_date.isoformat() + 'Z',
+            'title': self.title
+        }
+        if backrefs:
+            dict_corpus['user'] = self.user.to_dict(
+                backrefs=True, relationships=False)
+        if relationships:
+            dict_corpus['files'] = {
+                x.id: x.to_dict(backrefs=False, relationships=True)
+                for x in self.files
+            }
+        return dict_corpus
 
 
-class QueryResult(db.Model):
-    '''
-    Class to define a corpus analysis result.
-    '''
+class QueryResult(FileMixin, HashidMixin, db.Model):
     __tablename__ = 'query_results'
     # Primary key
     id = db.Column(db.Integer, primary_key=True)
@@ -664,49 +719,60 @@ class QueryResult(db.Model):
     user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
     # Fields
     description = db.Column(db.String(255))
-    filename = db.Column(db.String(255))
     query_metadata = db.Column(db.JSON())
     title = db.Column(db.String(32))
+    # Backrefs: user: User
+
+    def __repr__(self):
+        '''
+        String representation of the QueryResult. For human readability.
+        '''
+        return f'<QueryResult {self.title}>'
 
     @property
     def download_url(self):
-        return url_for('corpora.download_query_result',
-                       query_result_id=self.id)
+        return url_for(
+            'corpora.download_query_result', query_result_id=self.id)
 
     @property
     def jsonpatch_path(self):
-        return '/query_results/{}'.format(self.id)
+        return f'{self.user.jsonpatch_path}/query_results/{self.hashid}'
 
     @property
     def path(self):
         return os.path.join(
-            self.creator.path, 'query_results', str(self.id), self.filename)
+            self.user.path, 'query_results', str(self.id), self.filename)
 
     @property
     def url(self):
         return url_for('corpora.query_result', query_result_id=self.id)
 
+    @property
+    def user_hashid(self):
+        return self.user.hashid
+
     def delete(self):
         shutil.rmtree(self.path, ignore_errors=True)
         db.session.delete(self)
 
-    def to_dict(self, include_relationships=True):
-        return {'download_url': self.download_url,
-                'url': self.url,
-                'id': self.id,
-                'user_id': self.user_id,
-                'corpus_title': self.query_metadata['corpus_name'],
-                'description': self.description,
-                'filename': self.filename,
-                'query': self.query_metadata['query'],
-                'query_metadata': self.query_metadata,
-                'title': self.title}
-
-    def __repr__(self):
-        '''
-        String representation of the QueryResult. For human readability.
-        '''
-        return '<QueryResult {}>'.format(self.title)
+    def to_dict(self, backrefs=False, relationships=False):
+        dict_query_result = {
+            'id': self.hashid,
+            'user_id': self.user.hashid,
+            'download_url': self.download_url,
+            'url': self.url,
+            'corpus_title': self.query_metadata['corpus_name'],
+            'description': self.description,
+            'filename': self.filename,
+            'query': self.query_metadata['query'],
+            'query_metadata': self.query_metadata,
+            'title': self.title,
+            **self.file_mixin_to_dict(
+                backrefs=backrefs, relationships=relationships)
+        }
+        if backrefs:
+            dict_query_result['user'] = self.user.to_dict(
+                backrefs=True, relationships=False)
 
 
 @login.user_loader
diff --git a/app/services/routes.py b/app/services/routes.py
index fc7b0aa7966b243156f6305d80c44228443a4756..26218226bcccfa28c27d881dca0f2c7f30f13575 100644
--- a/app/services/routes.py
+++ b/app/services/routes.py
@@ -24,8 +24,8 @@ def service(service):
     # Check if the requested service exist
     if service not in SERVICES or service not in AddJobForms:
         abort(404)
-    version = request.args.get('version',
-                               SERVICES[service]['versions']['latest'])
+    version = request.args.get(
+        'version', SERVICES[service]['versions']['latest'])
     if version not in SERVICES[service]['versions']:
         abort(404)
     form = AddJobForms[service](prefix='add-job-form', version=version)
@@ -44,7 +44,7 @@ def service(service):
             service_args.append('-l {}'.format(form.language.data))
             if form.binarization.data:
                 service_args.append('--binarize')
-        job = Job(creator=current_user,
+        job = Job(user=current_user,
                   description=form.description.data,
                   service=service, service_args=json.dumps(service_args),
                   service_version=form.version.data,
@@ -65,7 +65,8 @@ def service(service):
         else:
             for file in form.files.data:
                 filename = secure_filename(file.filename)
-                job_input = JobInput(filename=filename, job=job)
+                job_input = JobInput(
+                    filename=filename, job=job, mimetype=file.mimetype)
                 file.save(job_input.path)
                 db.session.add(job_input)
             job.status = 'submitted'
diff --git a/app/static/js/nopaque/App.js b/app/static/js/nopaque/App.js
new file mode 100644
index 0000000000000000000000000000000000000000..9162640af719ae26b3a97c6d8da56eb0c2c215af
--- /dev/null
+++ b/app/static/js/nopaque/App.js
@@ -0,0 +1,80 @@
+class App {
+  constructor() {
+    this.data = {users: {}};
+    this.eventListeners = {'users.patch': []};
+    this.socket = io({transports: ['websocket'], upgrade: false});
+    this.socket.on('users.patch', patch => this.usersPatchHandler(patch));
+  }
+
+  get users() {return this.data.users;}
+
+  addEventListener(type, listener) {
+    if (!(type in this.eventListeners)) {throw `Unknown event type: ${type}`;}
+    this.eventListeners[type].push(listener);
+  }
+
+  flash(message, category) {
+    let toast, toastCloseActionElement;
+    switch (category) {
+      case "corpus":
+        message = `<i class="left material-icons">book</i>${message}`;
+        break;
+      case "error":
+        message = `<i class="left material-icons error-color-text">error</i>${message}`;
+        break;
+      case "job":
+        message = `<i class="left nopaque-icons">J</i>${message}`;
+        break;
+      default:
+        message = `<i class="left material-icons">notifications</i>${message}`;
+    }
+    toast = M.toast({
+      html: `
+        <span>${message}</span>
+        <button class="btn-flat toast-action white-text" data-action="close">
+          <i class="material-icons">close</i>
+        </button>
+      `.trim()
+    });
+    toastCloseActionElement = toast.el.querySelector('.toast-action[data-action="close"]');
+    toastCloseActionElement.addEventListener('click', () => {toast.dismiss();});
+  }
+
+  getUserById(userId) {
+    return new Promise((resolve, reject) => {
+      if (userId in this.data.users) {resolve(this.users[userId]);}
+      this.socket.emit('users.user.get', userId, response => {
+        if (response.code === 200) {
+          this.data.users[userId] = response.payload;
+          resolve(this.data.users[userId]);
+        } else {
+          reject(response);
+        }
+      });
+    });
+  }
+
+  usersPatchHandler(patch) {
+    let re, match, userId, ressourceId, jobId, relationship;
+    for (let operation of patch.filter(operation => operation.op === 'add')) {
+      re = new RegExp(`^/users/([A-Za-z0-9]*)/corpora/([A-Za-z0-9]*)/(files)`);
+      if (re.test(operation.path)) {
+        [match, userId, ressourceId, relationship] = operation.path.match(re);
+        if (!(relationship in this.users[userId].corpora[ressourceId])) {
+          this.users[userId].corpora[ressourceId][relationship] = {};
+        }
+        continue;
+      }
+      re = new RegExp(`^/users/([A-Za-z0-9]*)/jobs/([A-Za-z0-9]*)/(inputs|results)`);
+      if (re.test(operation.path)) {
+        [match, userId, ressourceId, relationship] = operation.path.match(re);
+        if (!(relationship in this.users[userId].jobs[ressourceId])) {
+          this.users[userId].jobs[ressourceId][relationship] = {};
+        }
+        continue;
+      }
+    }
+    this.data = jsonpatch.apply_patch(this.data, patch);
+    for (let listener of this.eventListeners['users.patch']) {listener(patch);}
+  }
+}
diff --git a/app/static/js/nopaque/JobStatusNotifier.js b/app/static/js/nopaque/JobStatusNotifier.js
new file mode 100644
index 0000000000000000000000000000000000000000..51adf06e4e98088b629d9035261b539435ce84b4
--- /dev/null
+++ b/app/static/js/nopaque/JobStatusNotifier.js
@@ -0,0 +1,17 @@
+class JobStatusNotifier {
+  constructor(userId) {
+    this.userId = userId;
+  }
+
+  usersPatchHandler(patch) {
+    let re, filteredPatch, match, jobId;
+    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));
+    for (let 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: ${operation.value}`, 'job');
+    }
+  }
+}
diff --git a/app/static/js/nopaque/RessourceDisplays/CorpusDisplay.js b/app/static/js/nopaque/RessourceDisplays/CorpusDisplay.js
index d6038ef6c2e12b273cb6b3a87cd50e82484c0526..739dde2f0f43feb7c353c681ebf05ba46b8dfeca 100644
--- a/app/static/js/nopaque/RessourceDisplays/CorpusDisplay.js
+++ b/app/static/js/nopaque/RessourceDisplays/CorpusDisplay.js
@@ -2,31 +2,34 @@ class CorpusDisplay extends RessourceDisplay {
   constructor(displayElement) {
     super(displayElement);
     this.corpusId = displayElement.dataset.corpusId;
-    this.user.eventListeners.corpus.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.corpusId);
+    for (let exportCorpusTriggerElement of this.displayElement.querySelectorAll('.export-corpus-trigger')) {
+      exportCorpusTriggerElement.addEventListener('click', () => this.requestCorpusExport());
+    }
+    app.socket.on(`export_corpus_${this.corpusId}`, () => this.downloadCorpus());
   }
 
-  init() {
-    for (let exportCorpusTriggerElement of this.displayElement.querySelectorAll('.export-corpus-trigger')) {exportCorpusTriggerElement.addEventListener('click', () => this.requestCorpusExport());}
-    nopaque.appClient.socket.on(`export_corpus_${this.user.data.corpora[this.corpusId].id}`, () => this.downloadCorpus());
-    this.setCreationDate(this.user.data.corpora[this.corpusId].creation_date);
-    this.setDescription(this.user.data.corpora[this.corpusId].description);
-    this.setLastEditedDate(this.user.data.corpora[this.corpusId].last_edited_date);
-    this.setStatus(this.user.data.corpora[this.corpusId].status);
-    this.setTitle(this.user.data.corpora[this.corpusId].title);
-    this.setTokenRatio(this.user.data.corpora[this.corpusId].num_tokens, this.user.data.corpora[this.corpusId].max_num_tokens);
+  init(user) {
+    let corpus;
+    corpus = user.corpora[this.corpusId];
+    this.setCreationDate(corpus.creation_date);
+    this.setDescription(corpus.description);
+    this.setLastEditedDate(corpus.last_edited_date);
+    this.setStatus(corpus.status);
+    this.setTitle(corpus.title);
+    this.setTokenRatio(corpus.num_tokens, corpus.max_num_tokens);
   }
 
   patch(patch) {
-    let re;
-    for (let operation of patch) {
+    let re, filteredPatch;
+    re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}`);
+    filteredPatch = patch.filter(operation => re.test(operation.path));
+    for (let operation of filteredPatch) {
       switch(operation.op) {
         case 'replace':
-          // Matches: /jobs/{this.job.id}/status
-          re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/last_edited_date');
+          re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/last_edited_date$`);
           if (re.test(operation.path)) {this.setLastEditedDate(operation.value); break;}
-          // Matches: /jobs/{this.job.id}/status
-          re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/status$');
-          if (re.test(operation.path)) {this.setStatus(operation.value); break;}
+          re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/status$`);
+          if (re.test(operation.path)) {this.status$(operation.value); break;}
           break;
         default:
           break;
@@ -35,18 +38,19 @@ class CorpusDisplay extends RessourceDisplay {
   }
 
   requestCorpusExport() {
-    nopaque.appClient.socket.emit('export_corpus', this.user.data.corpora[this.corpusId].id);
-    nopaque.appClient.flash('Preparing your corpus export...', 'corpus');
+    app.socket.emit('export_corpus', app.users[this.userId].corpora[this.corpusId]);
+    app.flash('Preparing your corpus export...', 'corpus');
     for (let exportCorpusTriggerElement of this.displayElement.querySelectorAll('.export-corpus-trigger')) {exportCorpusTriggerElement.classList.toggle('disabled', true);}
   }
 
   downloadCorpus() {
-    nopaque.appClient.flash('Corpus export is done. Your corpus download is ready!', 'corpus');
+    let downloadButton;
+    app.flash('Corpus download is ready!', 'corpus');
     for (let exportCorpusTriggerElement of this.displayElement.querySelectorAll('.export-corpus-trigger')) {exportCorpusTriggerElement.classList.toggle('disabled', false);}
     // Little trick to call the download view after ziping has finished
-    let fakeBtn = document.createElement('a');
-    fakeBtn.href = `/corpora/${this.user.data.corpora[this.corpusId].id}/download`;
-    fakeBtn.click();
+    downloadButton = document.createElement('a');
+    downloadButton.href = `/corpora/${app.users[this.userId].corpora[this.corpusId]}/download`;
+    downloadButton.click();
   }
 
   setTitle(title) {
@@ -70,7 +74,7 @@ class CorpusDisplay extends RessourceDisplay {
       }
     }
     for (let element of this.displayElement.querySelectorAll('.build-corpus-trigger')) {
-      if (status === 'unprepared' && Object.values(this.user.data.corpora[this.corpusId].files).length > 0) {
+      if (status === 'unprepared' && Object.values(app.users[this.userId].corpora[this.corpusId].files).length > 0) {
         element.classList.remove('disabled');
       } else {
         element.classList.add('disabled');
@@ -90,13 +94,15 @@ class CorpusDisplay extends RessourceDisplay {
     }
   }
 
-  setCreationDate(iso8601CreationDate) {
-    let creationDate = new Date(iso8601CreationDate).toLocaleString("en-US");
-    for (let element of this.displayElement.querySelectorAll('.corpus-creation-date')) {this.setElement(element, creationDate);}
+  setCreationDate(creationDate) {
+    for (let element of this.displayElement.querySelectorAll('.corpus-creation-date')) {
+      this.setElement(element, creationDate.toLocaleString("en-US"));
+    }
   }
 
-  setLastEditedDate(iso8601LastEditedDate) {
-    let endDate = new Date(iso8601LastEditedDate).toLocaleString("en-US");
-    for (let element of this.displayElement.querySelectorAll('.corpus-end-date')) {this.setElement(element, endDate);}
+  setLastEditedDate(lastEditedDate) {
+    for (let element of this.displayElement.querySelectorAll('.corpus-end-date')) {
+      this.setElement(element, lastEditedDate.toLocaleString("en-US"));
+    }
   }
 }
diff --git a/app/static/js/nopaque/RessourceDisplays/JobDisplay.js b/app/static/js/nopaque/RessourceDisplays/JobDisplay.js
index dfe4290420f50cef35af3068f4d1a5596702da34..ac1815090b19525ced2a0c90758f689f484dd444 100644
--- a/app/static/js/nopaque/RessourceDisplays/JobDisplay.js
+++ b/app/static/js/nopaque/RessourceDisplays/JobDisplay.js
@@ -1,32 +1,37 @@
 class JobDisplay extends RessourceDisplay {
   constructor(displayElement) {
     super(displayElement);
-    this.jobId = displayElement.dataset.jobId;
-    this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.jobId);
+    this.jobId = this.displayElement.dataset.jobId;
   }
 
-  init(job) {
-    this.setCreationDate(this.user.data.jobs[this.jobId].creation_date);
-    this.setEndDate(this.user.data.jobs[this.jobId].creation_date);
-    this.setDescription(this.user.data.jobs[this.jobId].description);
-    this.setService(this.user.data.jobs[this.jobId].service);
-    this.setServiceArgs(this.user.data.jobs[this.jobId].service_args);
-    this.setServiceVersion(this.user.data.jobs[this.jobId].service_version);
-    this.setStatus(this.user.data.jobs[this.jobId].status);
-    this.setTitle(this.user.data.jobs[this.jobId].title);
+  init(user) {
+    let job = user.jobs[this.jobId];
+    this.setCreationDate(job.creation_date);
+    this.setEndDate(job.creation_date);
+    this.setDescription(job.description);
+    this.setService(job.service);
+    this.setServiceArgs(job.service_args);
+    this.setServiceVersion(job.service_version);
+    this.setStatus(job.status);
+    this.setTitle(job.title);
   }
 
-  patch(patch) {
-    let re;
-    for (let operation of patch) {
+  usersPatchHandler(patch) {
+    let re = new RegExp(`^/users/${this.userId}/jobs/${this.jobId}`);
+    let filteredPatch = patch.filter(operation => re.test(operation.path));
+    for (let operation of filteredPatch) {
       switch(operation.op) {
         case 'replace':
-          // Matches: /jobs/{this.user.data.jobs[this.jobId].id}/status
-          re = new RegExp('^/jobs/' + this.user.data.jobs[this.jobId].id + '/end_date');
-          if (re.test(operation.path)) {this.setEndDate(operation.value); break;}
-          // Matches: /jobs/{this.user.data.jobs[this.jobId].id}/status
-          re = new RegExp('^/jobs/' + this.user.data.jobs[this.jobId].id + '/status$');
-          if (re.test(operation.path)) {this.setStatus(operation.value); break;}
+          re = new RegExp(`^/users/${this.userId}/jobs/${this.jobId}/end_date$`);
+          if (re.test(operation.path)) {
+            this.setEndDate(operation.value);
+            break;
+          }
+          re = new RegExp(`^/users/${this.userId}/jobs/${this.jobId}/status$`);
+          if (re.test(operation.path)) {
+            this.setStatus(operation.value);
+            break;
+          }
           break;
         default:
           break;
@@ -63,14 +68,16 @@ class JobDisplay extends RessourceDisplay {
     }
   }
 
-  setCreationDate(iso8601CreationDate) {
-    let creationDate = new Date(iso8601CreationDate).toLocaleString("en-US");
-    for (let element of this.displayElement.querySelectorAll('.job-creation-date')) {this.setElement(element, creationDate);}
+  setCreationDate(creationDate) {
+    for (let element of this.displayElement.querySelectorAll('.job-creation-date')) {
+      this.setElement(element, creationDate.toLocaleString('en-US'));
+    }
   }
 
-  setEndDate(iso8601EndDate) {
-    let endDate = new Date(iso8601EndDate).toLocaleString("en-US");
-    for (let element of this.displayElement.querySelectorAll('.job-end-date')) {this.setElement(element, endDate);}
+  setEndDate(endDate) {
+    for (let element of this.displayElement.querySelectorAll('.job-end-date')) {
+      this.setElement(element, endDate.toLocaleString('en-US'));
+    }
   }
 
   setService(service) {
diff --git a/app/static/js/nopaque/RessourceDisplays/RessourceDisplay.js b/app/static/js/nopaque/RessourceDisplays/RessourceDisplay.js
index 13ca95d08653093c2d2b4c94389272ba79819769..c7aebe75dd417516ee3def4d935cf6f03f52b63c 100644
--- a/app/static/js/nopaque/RessourceDisplays/RessourceDisplay.js
+++ b/app/static/js/nopaque/RessourceDisplays/RessourceDisplay.js
@@ -1,35 +1,14 @@
 class RessourceDisplay {
   constructor(displayElement) {
-    if (displayElement.dataset.userId) {
-      if (displayElement.dataset.userId in nopaque.appClient.users) {
-        this.user = nopaque.appClient.users[displayElement.dataset.userId];
-      } else {
-        console.error(`User not found: ${displayElement.dataset.userId}`);
-        return;
-      }
-    } else {
-      this.user = nopaque.appClient.users.self;
-    }
     this.displayElement = displayElement;
+    this.userId = this.displayElement.dataset.userId;
+    app.addEventListener('users.patch', patch => this.usersPatchHandler(patch));
+    app.getUserById(this.userId).then(user => this.init(user), error => {throw JSON.stringify(error);});
   }
 
-  eventHandler(eventType, payload) {
-    switch (eventType) {
-      case 'init':
-        this.init(payload);
-        break;
-      case 'patch':
-        this.patch(payload);
-        break;
-      default:
-        console.error(`Unknown event type: ${eventType}`);
-        break;
-    }
-  }
-
-  init() {console.error('init method not implemented!');}
+  init(user) {throw 'Not implemented';}
 
-  patch() {console.error('patch method not implemented!');}
+  usersPatchHandler(patch) {throw 'Not implemented';}
 
   setElement(element, value) {
     switch (element.tagName) {
diff --git a/app/static/js/nopaque/RessourceLists/CorpusFileList.js b/app/static/js/nopaque/RessourceLists/CorpusFileList.js
index b5b636fa4cd842731c51194e8b582fab32b80d8d..fdd26fcda09e582a1d02b66035eb374eb7466c27 100644
--- a/app/static/js/nopaque/RessourceLists/CorpusFileList.js
+++ b/app/static/js/nopaque/RessourceLists/CorpusFileList.js
@@ -2,32 +2,32 @@ class CorpusFileList extends RessourceList {
   constructor(listElement, options = {}) {
     super(listElement, {...CorpusFileList.options, ...options});
     this.corpusId = listElement.dataset.corpusId;
-    this.user.eventListeners.corpus.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.corpusId);
   }
 
-  init() {
-    super.init(this.user.data.corpora[this.corpusId].files);
+  init(user) {
+    this._init(user.corpora[this.corpusId].files);
   }
 
   onclick(event) {
-    let ressourceElement = event.target.closest('tr');
-    if (ressourceElement === null) {return;}
-    let corpusFileId = ressourceElement.dataset.id;
-    let actionButtonElement = event.target.closest('.action-button');
-    if (actionButtonElement === null) {return;}
+    let corpusFileElement = event.target.closest('tr[data-id]');
+    if (corpusFileElement === null) {throw 'Could not locate corpus file element';}
+    let corpusFileId = corpusFileElement.dataset.id;
+    let actionButtonElement = event.target.closest('.action-button[data-action]');
     let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
     switch (action) {
       case 'delete':
-        let deleteModalHTML = `<div class="modal">
-                                 <div class="modal-content">
-                                   <h4>Confirm corpus deletion</h4>
-                                   <p>Do you really want to delete the corpus file <b>${this.user.data.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>
-                                   <a class="btn modal-close red waves-effect waves-light" href="${this.user.data.corpora[this.corpusId].files[corpusFileId].url}/delete"><i class="material-icons left">delete</i>Delete</a>
-                                 </div>
-                               </div>`;
+        let deleteModalHTML = `
+          <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>
+            </div>
+            <div class="modal-footer">
+              <a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
+              <a class="btn modal-close red waves-effect waves-light" href="/corpora/${this.corpusId}/files/${corpusFileId}/delete"><i class="material-icons left">delete</i>Delete</a>
+            </div>
+          </div>
+        `.trim();
         let deleteModalParentElement = document.querySelector('#modals');
         deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
         let deleteModalElement = deleteModalParentElement.lastChild;
@@ -35,40 +35,37 @@ class CorpusFileList extends RessourceList {
         deleteModal.open();
         break;
       case 'download':
-        window.location.href = this.user.data.corpora[this.corpusId].files[corpusFileId].download_url;
+        window.location.href = `/corpora/${this.corpusId}/files/${corpusFileId}/download`;
         break;
       case 'view':
-        if (corpusFileId !== '-1') {window.location.href = this.user.data.corpora[this.corpusId].files[corpusFileId].url;}
+        window.location.href = `/corpora/${this.corpusId}/files/${corpusFileId}`;
         break;
       default:
-        console.error(`Unknown action: "${action}"`);
         break;
     }
   }
 
-  patch(patch) {
-    let id, match, re, valueName;
-    for (let operation of patch) {
+  usersPatchHandler(patch) {
+    let re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/files/([A-Za-z0-9]*)`);
+    let filteredPatch = patch.filter(operation => re.test(operation.path));
+    for (let operation of filteredPatch) {
       switch(operation.op) {
         case 'add':
-          // Matches the only paths that should be handled here: /corpora/{this.user.data.corpora[this.corpusId].id}/files/{corpusFileId}
-          re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/files/(\\d+)$');
+          re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/files/([A-Za-z0-9]*)$`);
           if (re.test(operation.path)) {this.add(operation.value);}
           break;
         case 'remove':
-          // See case add ;)
-          re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/files/(\\d+)$');
+          re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/files/([A-Za-z0-9]*)$`);
           if (re.test(operation.path)) {
-            [match, id] = operation.path.match(re);
-            this.remove(id);
+            [match, corpusFileId] = operation.path.match(re);
+            this.remove(corpusFileId);
           }
           break;
         case 'replace':
-          // Matches the only paths that should be handled here: /corpora/{corpusId}/{status || description || title}
-          re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/files/(\\d+)/(author|filename|publishing_year|title)$');
+          re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/files/([A-Za-z0-9]*)/(author|filename|publishing_year|title)$`);
           if (re.test(operation.path)) {
-            [match, id, valueName] = operation.path.match(re);
-            this.replace(id, valueName, operation.value);
+            [match, corpusFileId, valueName] = operation.path.match(re);
+            this.replace(corpusFileId, valueName, operation.value);
           }
           break;
         default:
@@ -78,20 +75,29 @@ class CorpusFileList extends RessourceList {
   }
 
   preprocessRessource(corpusFile) {
-    return {id: corpusFile.id, author: corpusFile.author, filename: corpusFile.filename, publishing_year: corpusFile.publishing_year, title: corpusFile.title};
+    return {
+      id: corpusFile.id,
+      author: corpusFile.author,
+      creationDate: corpusFile.creation_date,
+      filename: corpusFile.filename,
+      publishing_year: corpusFile.publishing_year,
+      title: corpusFile.title
+    };
   }
 }
 CorpusFileList.options = {
-  item: `<tr>
-           <td><span class="filename"></span></td>
-           <td><span class="author"></span></td>
-           <td><span class="title"></span></td>
-           <td><span class="publishing_year"></span></td>
-           <td class="right-align">
-             <a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
-             <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
-             <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
-           </td>
-         </tr>`,
+  item: `
+    <tr>
+      <td><span class="filename"></span></td>
+      <td><span class="author"></span></td>
+      <td><span class="title"></span></td>
+      <td><span class="publishing_year"></span></td>
+      <td class="right-align">
+        <a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
+        <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
+        <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
+      </td>
+    </tr>
+  `.trim(),
   valueNames: [{data: ['id']}, 'author', 'filename', 'publishing_year', 'title']
 };
diff --git a/app/static/js/nopaque/RessourceLists/CorpusList.js b/app/static/js/nopaque/RessourceLists/CorpusList.js
index ffe555cf7a7a31e5cff8dc4329daedb1d4b71d32..f9c5fead0ca36fdf1a140360d65268fccdd2c591 100644
--- a/app/static/js/nopaque/RessourceLists/CorpusList.js
+++ b/app/static/js/nopaque/RessourceLists/CorpusList.js
@@ -1,31 +1,32 @@
 class CorpusList extends RessourceList {
   constructor(listElement, options = {}) {
     super(listElement, {...CorpusList.options, ...options});
-    this.user.eventListeners.corpus.addEventListener((eventType, payload) => this.eventHandler(eventType, payload));
   }
 
-  init() {
-    super.init(this.user.data.corpora);
+  init(user) {
+    super._init(user.corpora);
   }
 
   onclick(event) {
-    let ressourceElement = event.target.closest('tr');
-    if (ressourceElement === null) {return;}
-    let corpusId = ressourceElement.dataset.id;
-    let actionButtonElement = event.target.closest('.action-button');
-    let action = (actionButtonElement === null) ? 'view' : actionButtonElement.dataset.action;
+    let corpusElement = event.target.closest('tr[data-id]');
+    if (corpusElement === null) {throw 'Could not locate corpus element';}
+    let corpusId = corpusElement.dataset.id;
+    let actionButtonElement = event.target.closest('.action-button[data-action]');
+    let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
     switch (action) {
       case 'delete':
-        let deleteModalHTML = `<div class="modal">
-                                 <div class="modal-content">
-                                   <h4>Confirm corpus deletion</h4>
-                                   <p>Do you really want to delete the corpus <b>${this.user.data.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>
-                                   <a class="btn modal-close red waves-effect waves-light" href="${this.user.data.corpora[corpusId].url}/delete"><i class="material-icons left">delete</i>Delete</a>
-                                 </div>
-                               </div>`;
+        let deleteModalHTML = `
+          <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>
+            </div>
+            <div class="modal-footer">
+              <a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
+              <a class="btn modal-close red waves-effect waves-light" href="/corpora/${corpusId}/delete"><i class="material-icons left">delete</i>Delete</a>
+            </div>
+          </div>
+        `.trim();
         let deleteModalParentElement = document.querySelector('#modals');
         deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
         let deleteModalElement = deleteModalParentElement.lastChild;
@@ -33,37 +34,34 @@ class CorpusList extends RessourceList {
         deleteModal.open();
         break;
       case 'view':
-        if (corpusId !== '-1') {window.location.href = this.user.data.corpora[corpusId].url;}
+        window.location.href = `/corpora/${corpusId}`;
         break;
       default:
-        console.error(`Unknown action: ${action}`);
         break;
     }
   }
 
-  patch(patch) {
-    let id, match, re, valueName;
-    for (let operation of patch) {
+  usersPatchHandler(patch) {
+    let re = new RegExp(`^/users/${this.userId}/corpora/([A-Za-z0-9]*)`);
+    let filteredPatch = patch.filter(operation => re.test(operation.path));
+    for (let operation of filteredPatch) {
       switch(operation.op) {
         case 'add':
-          // Matches the only paths that should be handled here: /corpora/{corpusId}
-          re = /^\/corpora\/(\d+)$/;
+          re = new RegExp(`^/users/${this.userId}/corpora/([A-Za-z0-9]*)$`);
           if (re.test(operation.path)) {this.add(operation.value);}
           break;
         case 'remove':
-          // See case 'add' ;)
-          re = /^\/corpora\/(\d+)$/;
+          re = new RegExp(`^/users/${this.userId}/corpora/([A-Za-z0-9]*)$`);
           if (re.test(operation.path)) {
-            [match, id] = operation.path.match(re);
-            this.remove(id);
+            let [match, corpusId] = operation.path.match(re);
+            this.remove(corpusId);
           }
           break;
         case 'replace':
-          // Matches the only paths that should be handled here: /corpora/{corpusId}/{status || description || title}
-          re = /^\/corpora\/(\d+)\/(status|description|title)$/;
+          re = new RegExp(`^/users/${this.userId}/corpora/([A-Za-z0-9]*)/(status|description|title)$`);
           if (re.test(operation.path)) {
-            [match, id, valueName] = operation.path.match(re);
-            this.replace(id, valueName, operation.value);
+            let [match, corpusId, valueName] = operation.path.match(re);
+            this.replace(corpusId, valueName, operation.value);
           }
           break;
         default:
@@ -73,21 +71,26 @@ class CorpusList extends RessourceList {
   }
 
   preprocessRessource(corpus) {
-    return {id: corpus.id,
-            status: corpus.status,
-            description: corpus.description,
-            title: corpus.title};
+    return {
+      id: corpus.id,
+      creationDate: corpus.creation_date,
+      description: corpus.description,
+      status: corpus.status,
+      title: corpus.title
+    };
   }
 }
 CorpusList.options = {
-  item: `<tr>
-           <td><a class="btn-floating disabled"><i class="material-icons service-color darken" data-service="corpus-analysis">book</i></a></td>
-           <td><b class="title"></b><br><i class="description"></i></td>
-           <td><span class="badge new status status-color status-text" data-badge-caption=""></span></td>
-           <td class="right-align">
-             <a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
-             <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
-           </td>
-         </tr>`,
+  item: `
+    <tr>
+      <td><a class="btn-floating disabled"><i class="material-icons service-color darken" data-service="corpus-analysis">book</i></a></td>
+      <td><b class="title"></b><br><i class="description"></i></td>
+      <td><span class="badge new status status-color status-text" data-badge-caption=""></span></td>
+      <td class="right-align">
+        <a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
+        <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
+      </td>
+    </tr>
+  `.trim(),
   valueNames: [{data: ['id']}, {name: 'status', attr: 'data-status'}, 'description', 'title']
 };
diff --git a/app/static/js/nopaque/RessourceLists/JobInputList.js b/app/static/js/nopaque/RessourceLists/JobInputList.js
index 6d3dcd4fccf663975dfcbd59c38f5155dc392561..1a9195523779e693b3c919d5f2c22b153d1b3ab3 100644
--- a/app/static/js/nopaque/RessourceLists/JobInputList.js
+++ b/app/static/js/nopaque/RessourceLists/JobInputList.js
@@ -2,40 +2,46 @@ class JobInputList extends RessourceList {
   constructor(listElement, options = {}) {
     super(listElement, {...JobInputList.options, ...options});
     this.jobId = listElement.dataset.jobId;
-    this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.jobId);
   }
 
-  init() {
-    super.init(this.user.data.jobs[this.jobId].inputs);
+  init(user) {
+    this._init(user.jobs[this.jobId].inputs);
   }
 
   onclick(event) {
-    let ressourceElement = event.target.closest('tr');
-    if (ressourceElement === null) {return;}
-    let jobInputId = ressourceElement.dataset.id;
-    let actionButtonElement = event.target.closest('.action-button');
+    let jobInputElement = event.target.closest('tr[data-id]');
+    if (jobInputElement === null) {return;}
+    let jobInputId = jobInputElement.dataset.id;
+    let actionButtonElement = event.target.closest('.action-button[data-action]');
     if (actionButtonElement === null) {return;}
     let action = actionButtonElement.dataset.action;
     switch (action) {
       case 'download':
-        window.location.href = this.user.data.jobs[this.jobId].inputs[jobInputId].download_url;
+        window.location.href = `/jobs/${this.jobId}/inputs/${jobInputId}/download`;
         break;
       default:
-        console.error(`Unknown action: "${action}"`);
         break;
     }
   }
 
+  usersPatchHandler(patch) {return;}
+
   preprocessRessource(jobInput) {
-    return {id: jobInput.id, filename: jobInput.filename};
+    return {
+      id: jobInput.id,
+      creationDate: jobInput.creation_date,
+      filename: jobInput.filename
+    };
   }
 }
 JobInputList.options = {
-  item: `<tr>
-           <td><span class="filename"></span></td>
-           <td class="right-align">
-             <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
-           </td>
-         </tr>`,
+  item: `
+    <tr>
+      <td><span class="filename"></span></td>
+      <td class="right-align">
+        <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
+      </td>
+    </tr>
+  `.trim(),
   valueNames: [{data: ['id']}, 'filename']
 };
diff --git a/app/static/js/nopaque/RessourceLists/JobList.js b/app/static/js/nopaque/RessourceLists/JobList.js
index d0311928e95f2260d1a0af7b9873c4c4e830f9bf..3c21abcb5fcc9676a1964ad31ae7ddbf15d7b639 100644
--- a/app/static/js/nopaque/RessourceLists/JobList.js
+++ b/app/static/js/nopaque/RessourceLists/JobList.js
@@ -1,31 +1,32 @@
 class JobList extends RessourceList {
   constructor(listElement, options = {}) {
     super(listElement, {...JobList.options, ...options});
-    this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload));
   }
 
-  init() {
-    super.init(this.user.data.jobs);
+  init(user) {
+    this._init(user.jobs);
   }
 
   onclick(event) {
-    let ressourceElement = event.target.closest('tr');
-    if (ressourceElement === null) {return;}
-    let jobId = ressourceElement.dataset.id;
-    let actionButtonElement = event.target.closest('.action-button');
+    let jobElement = event.target.closest('tr[data-id]');
+    if (jobElement === null) {throw 'Could not locate job element';}
+    let jobId = jobElement.dataset.id;
+    let actionButtonElement = event.target.closest('.action-button[data-action]');
     let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
     switch (action) {
       case 'delete':
-        let deleteModalHTML = `<div class="modal">
-                                 <div class="modal-content">
-                                   <h4>Confirm job deletion</h4>
-                                   <p>Do you really want to delete the job <b>${this.user.data.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>
-                                   <a class="btn modal-close red waves-effect waves-light" href="${this.user.data.jobs[jobId].url}/delete"><i class="material-icons left">delete</i>Delete</a>
-                                 </div>
-                               </div>`;
+        let deleteModalHTML = `
+          <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>
+            </div>
+            <div class="modal-footer">
+              <a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
+              <a class="btn modal-close red waves-effect waves-light" href="/jobs/${jobId}/delete"><i class="material-icons left">delete</i>Delete</a>
+            </div>
+          </div>
+        `.trim();
         let deleteModalParentElement = document.querySelector('#modals');
         deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
         let deleteModalElement = deleteModalParentElement.lastChild;
@@ -33,37 +34,34 @@ class JobList extends RessourceList {
         deleteModal.open();
         break;
       case 'view':
-        if (jobId !== '-1') {window.location.href = this.user.data.jobs[jobId].url;}
+        window.location.href = `/jobs/${jobId}`;
         break;
       default:
-        console.error(`Unknown action: "${action}"`);
         break;
     }
   }
 
-  patch(patch) {
-    let id, match, re, valueName;
-    for (let operation of patch) {
+  usersPatchHandler(patch) {
+    let re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)`);
+    let filteredPatch = patch.filter(operation => re.test(operation.path));
+    for (let operation of filteredPatch) {
       switch(operation.op) {
         case 'add':
-          // Matches the only paths that should be handled here: /jobs/{jobId}
-          re = /^\/jobs\/(\d+)$/;
+          re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)$`);
           if (re.test(operation.path)) {this.add(operation.value);}
           break;
         case 'remove':
-          // See case add ;)
-          re = /^\/jobs\/(\d+)$/;
+          re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)$`);
           if (re.test(operation.path)) {
-            [match, id] = operation.path.match(re);
-            this.remove(id);
+            let [match, jobId] = operation.path.match(re);
+            this.remove(jobId);
           }
           break;
         case 'replace':
-          // Matches the only paths that should be handled here: /jobs/{jobId}/{service || status || description || title}
-          re = /^\/jobs\/(\d+)\/(service|status|description|title)$/;
+          re = new RegExp(`^/users/${this.userId}/jobs/([A-Za-z0-9]*)/(service|status|description|title)$`);
           if (re.test(operation.path)) {
-            [match, id, valueName] = operation.path.match(re);
-            this.replace(id, valueName, operation.value);
+            let [match, jobId, valueName] = operation.path.match(re);
+            this.replace(jobId, valueName, operation.value);
           }
           break;
         default:
@@ -73,22 +71,27 @@ class JobList extends RessourceList {
   }
 
   preprocessRessource(job) {
-    return {id: job.id,
-            service: job.service,
-            status: job.status,
-            description: job.description,
-            title: job.title};
+    return {
+      id: job.id,
+      creationDate: job.creation_date,
+      description: job.description,
+      service: job.service,
+      status: job.status,
+      title: job.title
+    };
   }
 }
 JobList.options = {
-  item: `<tr>
-           <td><a class="btn-floating disabled"><i class="nopaque-icons service service-color darken service-icon"></i></a></td>
-           <td><b class="title"></b><br><i class="description"></i></td>
-           <td><span class="badge new status status-color status-text" data-badge-caption=""></span></td>
-           <td class="right-align">
-             <a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
-             <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
-           </td>
-         </tr>`,
+  item: `
+    <tr>
+      <td><a class="btn-floating disabled"><i class="nopaque-icons service service-color darken service-icon"></i></a></td>
+      <td><b class="title"></b><br><i class="description"></i></td>
+      <td><span class="badge new status status-color status-text" data-badge-caption=""></span></td>
+      <td class="right-align">
+        <a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
+        <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
+      </td>
+    </tr>
+  `.trim(),
   valueNames: [{data: ['id']}, {name: 'service', attr: 'data-service'}, {name: 'status', attr: 'data-status'}, 'description', 'title']
 };
diff --git a/app/static/js/nopaque/RessourceLists/JobResultList.js b/app/static/js/nopaque/RessourceLists/JobResultList.js
index 44b3567526def86572df31a71adbfe3dda0de972..e741015774962745e846010c0f4dca09f26a2149 100644
--- a/app/static/js/nopaque/RessourceLists/JobResultList.js
+++ b/app/static/js/nopaque/RessourceLists/JobResultList.js
@@ -2,37 +2,35 @@ class JobResultList extends RessourceList {
   constructor(listElement, options = {}) {
     super(listElement, {...JobResultList.options, ...options});
     this.jobId = listElement.dataset.jobId;
-    this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.jobId);
   }
 
-  init() {
-    super.init(this.user.data.jobs[this.jobId].results);
+  init(user) {
+    super._init(user.jobs[this.jobId].results);
   }
 
   onclick(event) {
-    let ressourceElement = event.target.closest('tr');
-    if (ressourceElement === null) {return;}
-    let jobResultId = ressourceElement.dataset.id;
-    let actionButtonElement = event.target.closest('.action-button');
+    let jobResultElement = event.target.closest('tr[data-id]');
+    if (jobResultElement === null) {return;}
+    let jobResultId = jobResultElement.dataset.id;
+    let actionButtonElement = event.target.closest('.action-button[data-action]');
     if (actionButtonElement === null) {return;}
     let action = actionButtonElement.dataset.action;
     switch (action) {
       case 'download':
-        window.location.href = this.user.data.jobs[this.jobId].results[jobResultId].download_url;
+        window.location.href = `/jobs/${this.jobId}/results/${jobResultId}`;
         break;
       default:
-        console.error(`Unknown action: "${action}"`);
         break;
     }
   }
 
-  patch(patch) {
-    let re;
-    for (let operation of patch) {
+  usersPatchHandler(patch) {
+    let re = new RegExp(`^/users/${this.userId}/jobs/${this.jobId}/results/([A-Za-z0-9]*)`);
+    let filteredPatch = patch.filter(operation => re.test(operation.path));
+    for (let operation of filteredPatch) {
       switch(operation.op) {
         case 'add':
-          // Matches the only paths that should be handled here: /jobs/{this.user.data.jobs[this.jobId].id}/results/{jobResultId}
-          re = new RegExp('^/jobs/' + this.user.data.jobs[this.jobId].id + '/results/(\\d+)$');
+          re = new RegExp(`^/users/${this.userId}/jobs/${this.jobId}/results/([A-Za-z0-9]*)$`);
           if (re.test(operation.path)) {this.add(operation.value);}
           break;
         default:
@@ -56,16 +54,23 @@ class JobResultList extends RessourceList {
     } else {
       description = 'All result files created during this job';
     }
-    return {id: jobResult.id, description: description, filename: jobResult.filename};
+    return {
+      id: jobResult.id,
+      creationDate: jobResult.creation_date,
+      description: description,
+      filename: jobResult.filename
+    };
   }
 }
 JobResultList.options = {
-  item: `<tr>
-           <td><span class="description"></span></td>
-           <td><span class="filename"></span></td>
-           <td class="right-align">
-             <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
-           </td>
-         </tr>`,
+  item: `
+    <tr>
+      <td><span class="description"></span></td>
+      <td><span class="filename"></span></td>
+      <td class="right-align">
+        <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
+      </td>
+    </tr>
+  `.trim(),
   valueNames: [{data: ['id']}, 'description', 'filename']
 };
diff --git a/app/static/js/nopaque/RessourceLists/QueryResultList.js b/app/static/js/nopaque/RessourceLists/QueryResultList.js
index 4fb7b44b96b0a3d5e9c676ce675a1b2409c0d960..c64361b5f61b5bee8fba098fb3678da51ec9ad08 100644
--- a/app/static/js/nopaque/RessourceLists/QueryResultList.js
+++ b/app/static/js/nopaque/RessourceLists/QueryResultList.js
@@ -1,31 +1,32 @@
 class QueryResultList extends RessourceList {
   constructor(listElement, options = {}) {
     super(listElement, {...QueryResultList.options, ...options});
-    this.user.eventListeners.queryResult.addEventListener((eventType, payload) => this.eventHandler(eventType, payload));
   }
 
-  init() {
-    super.init(this.user.data.query_results);
+  init(user) {
+    super.init(user.query_results);
   }
 
   onclick(event) {
-    let ressourceElement = event.target.closest('tr');
-    if (ressourceElement === null) {return;}
-    let queryResultId = ressourceElement.dataset.id;
-    let actionButtonElement = event.target.closest('.action-button');
+    let queryResultElement = event.target.closest('tr[data-id]');
+    if (queryResultElement === null) {return;}
+    let queryResultId = queryResultElement.dataset.id;
+    let actionButtonElement = event.target.closest('.action-button[data-action]');
     let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
     switch (action) {
       case 'delete':
-        let deleteModalHTML = `<div class="modal">
-                                 <div class="modal-content">
-                                   <h4>Confirm query result deletion</h4>
-                                   <p>Do you really want to delete the query result <b>${this.user.data.query_results[queryResultId].title}</b>? It will be permanently deleted!</p>
-                                 </div>
-                                 <div class="modal-footer">
-                                   <a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
-                                   <a class="btn modal-close red waves-effect waves-light" href="${this.user.data.query_results[queryResultId].url}/delete"><i class="material-icons left">delete</i>Delete</a>
-                                 </div>
-                               </div>`;
+        let deleteModalHTML = `
+          <div class="modal">
+            <div class="modal-content">
+              <h4>Confirm query result deletion</h4>
+              <p>Do you really want to delete the query result <b>${app.users[this.userId].query_results[queryResultId].title}</b>? It will be permanently deleted!</p>
+            </div>
+            <div class="modal-footer">
+              <a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
+              <a class="btn modal-close red waves-effect waves-light" href="/query_results/${queryResultId}/delete"><i class="material-icons left">delete</i>Delete</a>
+            </div>
+          </div>
+        `.trim();
         let deleteModalParentElement = document.querySelector('#modals');
         deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
         let deleteModalElement = deleteModalParentElement.lastChild;
@@ -33,37 +34,34 @@ class QueryResultList extends RessourceList {
         deleteModal.open();
         break;
       case 'view':
-        if (queryResultId !== '-1') {window.location.href = this.user.data.query_results[queryResultId].url;}
+        window.location.href = `/query_results/${queryResultId}`;
         break;
       default:
-        console.error(`Unknown action: "${action}"`);
         break;
     }
   }
 
-  patch(patch) {
-    let id, match, re, valueName;
-    for (let operation of patch) {
+  usersPatchHandler(patch) {
+    let re = new RegExp(`^/users/${this.userId}/query_results/([A-Za-z0-9]*)`);
+    let filteredPatch = patch.filter(operation => re.test(operation.path));
+    for (let operation of filteredPatch) {
       switch(operation.op) {
         case 'add':
-          // Matches the only paths that should be handled here: /jobs/{jobId}
-          re = /^\/query_results\/(\d+)$/;
+          re = new RegExp(`^/users/${this.userId}/query_results/([A-Za-z0-9]*)$`);
           if (re.test(operation.path)) {this.add(operation.value);}
           break;
         case 'remove':
-          // See case add ;)
-          re = /^\/query_results\/(\d+)$/;
+          re = new RegExp(`^/users/${this.userId}/query_results/([A-Za-z0-9]*)$`);
           if (re.test(operation.path)) {
-            [match, id] = operation.path.match(re);
-            this.remove(id);
+            let [match, queryResultId] = operation.path.match(re);
+            this.remove(queryResultId);
           }
           break;
         case 'replace':
-          // Matches the only paths that should be handled here: /jobs/{jobId}/{service || status || description || title}
-          re = /^\/query_results\/(\d+)\/(corpus_title|description|query|title)$/;
+          re = new RegExp(`^/users/${this.userId}/query_results/([A-Za-z0-9]*)/(corpus_title|description|query|title)$`);
           if (re.test(operation.path)) {
-            [match, id, valueName] = operation.path.match(re);
-            this.replace(id, valueName, operation.value);
+            let [match, queryResultId, valueName] = operation.path.match(re);
+            this.replace(queryResultId, valueName, operation.value);
           }
           break;
         default:
@@ -73,21 +71,26 @@ class QueryResultList extends RessourceList {
   }
 
   preprocessRessource(queryResult) {
-    return {id: queryResult.id,
-            corpus_title: queryResult.corpus_title,
-            description: queryResult.description,
-            query: queryResult.query,
-            title: queryResult.title};
+    return {
+      id: queryResult.id,
+      corpus_title: queryResult.corpus_title,
+      creationDate: queryResult.creation_date,
+      description: queryResult.description,
+      query: queryResult.query,
+      title: queryResult.title
+    };
   }
 }
 QueryResultList.options = {
-  item: `<tr>
-           <td><b class="title"></b><br><i class="description"></i><br></td>
-           <td><span class="corpus_title"></span><br><span class="query"></span></td>
-           <td class="right-align">
-             <a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
-             <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
-           </td>
-         </tr>`,
+  item: `
+    <tr>
+      <td><b class="title"></b><br><i class="description"></i><br></td>
+      <td><span class="corpus_title"></span><br><span class="query"></span></td>
+      <td class="right-align">
+        <a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
+        <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
+      </td>
+    </tr>
+  `.trim(),
   valueNames: [{data: ['id']}, 'corpus_title', 'description', 'query', 'title']
 };
diff --git a/app/static/js/nopaque/RessourceLists/RessourceList.js b/app/static/js/nopaque/RessourceLists/RessourceList.js
index ec289861fd6920df9bc3132b215b473651210ba9..81b9133dd1d8e3a8728a85a7cb057092183fd7ed 100644
--- a/app/static/js/nopaque/RessourceLists/RessourceList.js
+++ b/app/static/js/nopaque/RessourceLists/RessourceList.js
@@ -4,87 +4,68 @@ class RessourceList {
    * a base class for concrete ressource list implementations.
    */
   constructor(listElement, options = {}) {
-    if (listElement.dataset.userId) {
-      if (listElement.dataset.userId in nopaque.appClient.users) {
-        this.user = nopaque.appClient.users[listElement.dataset.userId];
-      } else {
-        console.error(`User not found: ${listElement.dataset.userId}`);
-        return;
-      }
-    } else {
-      this.user = nopaque.appClient.users.self;
-    }
     this.list = new List(listElement, {...RessourceList.options, ...options});
-    this.list.list.innerHTML = `<tr>
-                                  <td class="row" colspan="100%">
-                                    <div class="col s12">&nbsp;</div>
-                                    <div class="col s3 m2 xl1">
-                                      <div class="preloader-wrapper active">
-                                        <div class="spinner-layer spinner-green-only">
-                                          <div class="circle-clipper left">
-                                            <div class="circle"></div>
-                                          </div>
-                                          <div class="gap-patch">
-                                            <div class="circle"></div>
-                                          </div>
-                                          <div class="circle-clipper right">
-                                            <div class="circle"></div>
-                                          </div>
-                                        </div>
-                                      </div>
-                                    </div>
-                                    <div class="col s9 m6 xl5">
-                                      <span class="card-title">Waiting for data...</span>
-                                      <p>This list is not initialized yet.</p>
-                                    </div>
-                                  </td>
-                                </tr>`;
+    this.list.list.innerHTML = `
+      <tr>
+        <td class="row" colspan="100%">
+          <div class="col s12">&nbsp;</div>
+          <div class="col s3 m2 xl1">
+            <div class="preloader-wrapper active">
+              <div class="spinner-layer spinner-green-only">
+                <div class="circle-clipper left">
+                  <div class="circle"></div>
+                </div>
+                <div class="gap-patch">
+                  <div class="circle"></div>
+                </div>
+                <div class="circle-clipper right">
+                  <div class="circle"></div>
+                </div>
+              </div>
+            </div>
+          </div>
+          <div class="col s9 m6 xl5">
+            <span class="card-title">Waiting for data...</span>
+            <p>This list is not initialized yet.</p>
+          </div>
+        </td>
+      </tr>
+    `.trim();
     this.list.list.style.cursor = 'pointer';
+    this.userId = listElement.dataset.userId;
     if (typeof this.onclick === 'function') {this.list.list.addEventListener('click', event => this.onclick(event));}
-  }
-
-  eventHandler(eventType, payload) {
-    switch (eventType) {
-      case 'init':
-        this.init();
-        break;
-      case 'patch':
-        this.patch(payload);
-        break;
-      default:
-        console.error(`Unknown event type: ${eventType}`);
-        break;
+    if (this.userId) {
+      app.addEventListener('users.patch', patch => this.usersPatchHandler(patch));
+      app.getUserById(this.userId).then(
+        user => this.init(user),
+        error => {throw JSON.stringify(error);}
+      );
     }
   }
 
-  init(ressources) {
+  _init(ressources) {
     this.list.clear();
     this.add(Object.values(ressources));
-    this.list.sort('id', {order: 'desc'});
-    let emptyListElementHTML = `<tr class="show-if-only-child" data-id="-1">
-                                  <td colspan="100%">
-                                    <span class="card-title"><i class="left material-icons" style="font-size: inherit;">file_download</i>Nothing here...</span>
-                                    <p>No ressource available.</p>
-                                  </td>
-                                </tr>`;
+    let emptyListElementHTML = `
+      <tr class="show-if-only-child">
+        <td colspan="100%">
+          <span class="card-title"><i class="left material-icons" style="font-size: inherit;">file_download</i>Nothing here...</span>
+          <p>No ressource available.</p>
+        </td>
+      </tr>
+    `.trim();
     this.list.list.insertAdjacentHTML('afterbegin', emptyListElementHTML);
   }
 
-  patch(patch) {
-    /*
-     * It's not possible to generalize a patch Handler for all type of
-     * ressources. So this method is meant to be an interface.
-     */
-    console.error('patch method not implemented!');
-  }
+  init(user) {throw 'Not implemented';}
+
+  usersPatchHandler(patch) {throw 'Not implemented';}
+
+  preprocessRessource() {throw 'Not implemented'}
 
   add(values) {
     let ressources = Array.isArray(values) ? values : [values];
-    if (typeof this.preprocessRessource === 'function') {
-      ressources = ressources.map(ressource => this.preprocessRessource(ressource));
-    }
-    // Set a callback function ('() => {return;}') to force List.js perform the
-    // add method asynchronous: https://listjs.com/api/#add
+    ressources = ressources.map(ressource => this.preprocessRessource(ressource));
     this.list.add(ressources, () => {
       this.list.sort('id', {order: 'desc'});
     });
diff --git a/app/static/js/nopaque/RessourceLists/UserList.js b/app/static/js/nopaque/RessourceLists/UserList.js
index 60c6ad025cd2e5b469af94bcc011b3ce509c5d18..04100d2531f05a8f515f5afd9750b35e571aea34 100644
--- a/app/static/js/nopaque/RessourceLists/UserList.js
+++ b/app/static/js/nopaque/RessourceLists/UserList.js
@@ -1,32 +1,32 @@
 class UserList extends RessourceList {
   constructor(listElement, options = {}) {
     super(listElement, {...UserList.options, ...options});
-    users = undefined;
   }
 
   init(users) {
-    this.users = users;
-    super.init(users);
+    super._init(Object.values(users));
   }
 
   onclick(event) {
-    let ressourceElement = event.target.closest('tr');
-    if (ressourceElement === null) {return;}
-    let userId = ressourceElement.dataset.id;
-    let actionButtonElement = event.target.closest('.action-button');
+    let userElement = event.target.closest('tr[data-id]');
+    if (userElement === null) {return;}
+    let userId = userElement.dataset.id;
+    let actionButtonElement = event.target.closest('.action-button[data-action]');
     let action = (actionButtonElement === null) ? 'view' : actionButtonElement.dataset.action;
     switch (action) {
       case 'delete':
-        let deleteModalHTML = `<div class="modal">
-                                 <div class="modal-content">
-                                   <h4>Confirm user deletion</h4>
-                                   <p>Do you really want to delete the corpus <b>${this.users[userId].username}</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>
-                                   <a class="btn modal-close red waves-effect waves-light" href="/admin/users/${userId}/delete"><i class="material-icons left">delete</i>Delete</a>
-                                 </div>
-                               </div>`;
+        let deleteModalHTML = `
+          <div class="modal">
+            <div class="modal-content">
+              <h4>Confirm user deletion</h4>
+              <p>Do you really want to delete user <b>${userId}</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>
+              <a class="btn modal-close red waves-effect waves-light" href="/admin/users/${userId}/delete"><i class="material-icons left">delete</i>Delete</a>
+            </div>
+          </div>
+        `.trim();
         let deleteModalParentElement = document.querySelector('#modals');
         deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
         let deleteModalElement = deleteModalParentElement.lastChild;
@@ -37,35 +37,38 @@ class UserList extends RessourceList {
         window.location.href = `/admin/users/${userId}/edit`;
         break;
       case 'view':
-        if (userId !== '-1') {window.location.href = `/admin/users/${userId}`;}
+        window.location.href = `/admin/users/${userId}`;
         break;
       default:
-        console.error(`Unknown action: ${action}`);
         break;
     }
   }
 
   preprocessRessource(user) {
-    return {id: user.id,
-            id_: user.id,
-            username: user.username,
-            email: user.email,
-            last_seen: new Date(user.last_seen).toLocaleString("en-US"),
-            role: user.role.name};
+    return {
+      id: user.id,
+      id_: user.id,
+      username: user.username,
+      email: user.email,
+      last_seen: user.last_seen.toLocaleString("en-US"),
+      role: user.role.name
+    };
   }
 }
 UserList.options = {
-  item: `<tr>
-           <td><span class="id_"></span></td>
-           <td><span class="username"></span></td>
-           <td><span class="email"></span></td>
-           <td><span class="last_seen"></span></td>
-           <td><span class="role"></span></td>
-           <td class="right-align">
-             <a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
-             <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="edit" data-position="top" data-tooltip="Edit"><i class="material-icons">edit</i></a>
-             <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
-           </td>
-         </tr>`,
+  item: `
+    <tr>
+      <td><span class="id_"></span></td>
+      <td><span class="username"></span></td>
+      <td><span class="email"></span></td>
+      <td><span class="last_seen"></span></td>
+      <td><span class="role"></span></td>
+      <td class="right-align">
+        <a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
+        <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="edit" data-position="top" data-tooltip="Edit"><i class="material-icons">edit</i></a>
+        <a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
+      </td>
+    </tr>
+  `.trim(),
   valueNames: [{data: ['id']}, 'id_', 'username', 'email', 'last_seen', 'role']
 };
diff --git a/app/static/js/nopaque/main.js b/app/static/js/nopaque/main.js
index 3ae00ee7fb0e321f0f49e926703db4db7841421a..118af40020bd6f0e203dcd1a2bf2b10c882c3c12 100644
--- a/app/static/js/nopaque/main.js
+++ b/app/static/js/nopaque/main.js
@@ -1,177 +1,3 @@
-class AppClient {
-  constructor(currentUserId) {
-    if (currentUserId) {
-      this.socket = io({transports: ['websocket'], upgrade: false});
-      this.users = {};
-      this.users.self = this.loadUser(currentUserId);
-      this.users.self.eventListeners.job.addEventListener((eventType, payload) => this.jobEventHandler(eventType, payload));
-    }
-  }
-
-  flash(message, category) {
-    let toast;
-    let toastCloseActionElement;
-
-    switch (category) {
-      case "corpus":
-        message = `<i class="left material-icons">book</i>${message}`;
-        break;
-      case "error":
-        message = `<i class="left material-icons error-color-text">error</i>${message}`;
-        break;
-      case "job":
-        message = `<i class="left nopaque-icons">J</i>${message}`;
-        break;
-      default:
-        message = `<i class="left material-icons">notifications</i>${message}`;
-    }
-
-    toast = M.toast({html: `<span>${message}</span>
-                            <button data-action="close" class="btn-flat toast-action white-text">
-                              <i class="material-icons">close</i>
-                            </button>`});
-    toastCloseActionElement = toast.el.querySelector('.toast-action[data-action="close"]');
-    toastCloseActionElement.addEventListener('click', () => {toast.dismiss();});
-  }
-
-  jobEventHandler(eventType, payload) {
-    switch (eventType) {
-      case 'init':
-        break;
-      case 'patch':
-        this.jobPatch(payload);
-        break;
-      default:
-        console.error(`[AppClient.jobEventHandler] Unknown event type: ${eventType}`);
-        break;
-    }
-  }
-
-  loadUser(userId) {
-    if (userId in this.users) {return this.users[userId];}
-    let user = new User();
-    this.users[userId] = user;
-    this.socket.on(`user_${userId}_init`, msg => user.init(msg));
-    this.socket.on(`user_${userId}_patch`, msg => user.patch(msg));
-    this.socket.emit('start_user_session', userId);
-    return user;
-  }
-
-  jobPatch(patch) {
-    if (this.users.self.data.settings.job_status_site_notifications === 'none') {return;}
-    let jobStatusPatches = patch.filter(operation => operation.op === 'replace' && /^\/jobs\/(\d+)\/status$/.test(operation.path));
-    for (let operation of jobStatusPatches) {
-      let [match, jobId] = operation.path.match(/^\/jobs\/(\d+)\/status$/);
-      if (this.users.self.data.settings.job_status_site_notifications === "end" && !['complete', 'failed'].includes(operation.value)) {continue;}
-      this.flash(`[<a href="/jobs/${jobId}">${this.users.self.data.jobs[jobId].title}</a>] New status: ${operation.value}`, 'job');
-    }
-  }
-}
-
-class User {
-  constructor() {
-    this.data = undefined;
-    this.eventListeners = {
-      corpus: {
-        addEventListener(listener, corpusId='*') {
-          if (corpusId in this) {this[corpusId].push(listener);} else {this[corpusId] = [listener];}
-        }
-      },
-      job: {
-        addEventListener(listener, jobId='*') {
-          if (jobId in this) {this[jobId].push(listener);} else {this[jobId] = [listener];}
-        }
-      },
-      queryResult: {
-        addEventListener(listener, queryResultId='*') {
-          if (queryResultId in this) {this[queryResultId].push(listener);} else {this[queryResultId] = [listener];}
-        }
-      }
-    };
-  }
-
-  init(data) {
-    this.data = data;
-
-    for (let [corpusId, eventListeners] of Object.entries(this.eventListeners.corpus)) {
-      if (corpusId === '*') {
-        for (let eventListener of eventListeners) {eventListener('init');}
-      } else {
-        if (corpusId in this.data.corpora) {
-          for (let eventListener of eventListeners) {eventListener('init');}
-        }
-      }
-    }
-
-    for (let [jobId, eventListeners] of Object.entries(this.eventListeners.job)) {
-      if (jobId === '*') {
-        for (let eventListener of eventListeners) {eventListener('init');}
-      } else {
-        if (jobId in this.data.jobs) {
-          for (let eventListener of eventListeners) {eventListener('init');}
-        }
-      }
-    }
-
-    for (let [queryResultId, eventListeners] of Object.entries(this.eventListeners.queryResult)) {
-      if (queryResultId === '*') {
-        for (let eventListener of eventListeners) {eventListener('init');}
-      } else {
-        if (queryResultId in this.data.query_results) {
-          for (let eventListener of eventListeners) {eventListener('init');}
-        }
-      }
-    }
-  }
-
-  patch(patch) {
-    this.data = jsonpatch.apply_patch(this.data, patch);
-
-    let corporaPatch = patch.filter(operation => operation.path.startsWith("/corpora"));
-    if (corporaPatch.length > 0) {
-      for (let [corpusId, eventListeners] of Object.entries(this.eventListeners.corpus)) {
-        if (corpusId === '*') {
-          for (let eventListener of eventListeners) {eventListener('patch', corporaPatch);}
-        } else {
-          let corpusPatch = corporaPatch.filter(operation => operation.path.startsWith(`/corpora/${corpusId}`));
-          if (corpusPatch.length > 0) {
-            for (let eventListener of eventListeners) {eventListener('patch', corpusPatch);}
-          }
-        }
-      }
-    }
-
-    let jobsPatch = patch.filter(operation => operation.path.startsWith("/jobs"));
-    if (jobsPatch.length > 0) {
-      for (let [jobId, eventListeners] of Object.entries(this.eventListeners.job)) {
-        if (jobId === '*') {
-          for (let eventListener of eventListeners) {eventListener('patch', jobsPatch);}
-        } else {
-          let jobPatch = jobsPatch.filter(operation => operation.path.startsWith(`/jobs/${jobId}`));
-          if (jobPatch.length > 0) {
-            for (let eventListener of eventListeners) {eventListener('patch', jobPatch);}
-          }
-        }
-      }
-    }
-
-    let queryResultsPatch = patch.filter(operation => operation.path.startsWith("/query_results"));
-    if (queryResultsPatch.length > 0) {
-      for (let [queryResultId, eventListeners] of Object.entries(this.eventListeners.queryResult)) {
-        if (queryResultId === '*') {
-          for (let eventListener of eventListeners) {eventListener('patch', queryResultsPatch);}
-        } else {
-          let queryResultPatch = queryResultsPatch.filter(operation => operation.path.startsWith(`/query_results/${queryResultId}`));
-          if (queryResultPatch.length > 0) {
-            for (let eventListener of eventListeners) {eventListener('patch', queryResultPatch);}
-          }
-        }
-      }
-    }
-  }
-}
-
-
 /*
  * The nopaque object is used as a namespace for nopaque specific functions and
  * variables.
diff --git a/app/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2
index 49796bccc86379fda07e04de4c5bfdf50f703469..a86e3cfb487849ac1385fe5f2585989f6bdd8f9f 100644
--- a/app/templates/_scripts.html.j2
+++ b/app/templates/_scripts.html.j2
@@ -7,6 +7,8 @@
 <script src="{{ url_for('static', filename='js/jsonpatch.min.js') }}"></script>
 <script src="{{ url_for('static', filename='js/list.min.js') }}"></script>
 <script src="{{ url_for('static', filename='js/socket.io.min.js') }}"></script>
+<script src="{{ url_for('static', filename='js/nopaque/App.js') }}"></script>
+<script src="{{ url_for('static', filename='js/nopaque/JobStatusNotifier.js') }}"></script>
 <script src="{{ url_for('static', filename='js/nopaque/main.js') }}"></script>
 {% assets filters='rjsmin', output="js/nopaque/RessourceDisplays.min.bundle.js",
           "js/nopaque/RessourceDisplays/RessourceDisplay.js",
@@ -14,7 +16,7 @@
           "js/nopaque/RessourceDisplays/JobDisplay.js" %}
 <script src="{{ ASSET_URL }}"></script>
 {% endassets %}
-{% assets filters='rjsmin', output="js/nopaque/RessourceLists.min.bundle.js",
+{% assets output="js/nopaque/RessourceLists.min.bundle.js",
           "js/nopaque/RessourceLists/RessourceList.js",
           "js/nopaque/RessourceLists/CorpusList.js",
           "js/nopaque/RessourceLists/CorpusFileList.js",
@@ -31,7 +33,13 @@
   M.AutoInit();
   M.CharacterCounter.init(document.querySelectorAll('input[data-length][type="email"], input[data-length][type="password"], input[data-length][type="text"], textarea[data-length]'));
   M.Dropdown.init(document.querySelectorAll('#nav-more-dropdown-trigger'), {alignment: 'right', constrainWidth: false, coverTrigger: false});
-  nopaque.appClient = new AppClient({% if current_user.is_authenticated %}{{ current_user.id }}{% endif %});
+  var app = new App();
+  {% if current_user.is_authenticated %}
+  var currentUserId = '{{ current_user.hashid }}';
+  let jobStatusNotifier = new JobStatusNotifier(currentUserId);
+  app.addEventListener('users.patch', patch => jobStatusNotifier.usersPatchHandler(patch));
+  app.getUserById(currentUserId).then(user => {}, error => {throw JSON.stringify(error)});
+  {% endif %}
   nopaque.Forms.init();
-  for (let flashedMessage of {{ get_flashed_messages(with_categories=True)|tojson }}) {nopaque.appClient.flash(flashedMessage[1], flashedMessage[0]);}
+  for (let flashedMessage of {{ get_flashed_messages(with_categories=True)|tojson }}) {app.flash(flashedMessage[1], flashedMessage[0]);}
 </script>
diff --git a/app/templates/admin/edit_user.html.j2 b/app/templates/admin/edit_user.html.j2
index bd75147f8bb8d0afb693c9dfec31cf62b1e7b593..89a73d035dab74669c23a30411ef2c34addf0e6d 100644
--- a/app/templates/admin/edit_user.html.j2
+++ b/app/templates/admin/edit_user.html.j2
@@ -12,7 +12,7 @@
     <div class="col s12 m4">
       <h2>{{ user.username }}</h2>
       <p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren,</p>
-      <a class="waves-effect waves-light btn" href="{{ url_for('.user', user_id=user.id) }}"><i class="material-icons left">arrow_back</i>Back to user administration</a>
+      <a class="waves-effect waves-light btn" href="{{ url_for('.user', user_id=user.hashid) }}"><i class="material-icons left">arrow_back</i>Back to user administration</a>
     </div>
 
     <div class="col s12 m8">
diff --git a/app/templates/admin/user.html.j2 b/app/templates/admin/user.html.j2
index ada3a55cfda9b3e0cb2d0bc3705670066b46e86b..7122e58a62670a15a92d91d899cf31c113353115 100644
--- a/app/templates/admin/user.html.j2
+++ b/app/templates/admin/user.html.j2
@@ -21,11 +21,11 @@
           <ul>
             <li>Username: {{ user.username }}</li>
             <li>Email: {{ user.email }}</li>
-            <li>ID: {{ user.id }}</li>
-            <li>Member since: {{ user.member_since.strftime('%m/%d/%Y, %H:%M:%S %p') }}</li>
+            <li>Id: {{ user.id }}</li>
+            <li>Hashid: {{ user.hashid }}</li>
+            <li>Member since: {{ user.member_since }}</li>
             <li>Confirmed status: {{ user.confirmed }}</li>
-            <li>Last seen: {{ user.last_seen.strftime('%m/%d/%Y, %H:%M:%S %p') }}</li>
-            <li>Role ID: {{ user.role_id }}</li>
+            <li>Last seen: {{ user.last_seen }}</li>
             <li>Permissions as Int: {{ user.role.permissions }}</li>
             <li>Role name: {{ user.role.name }}</li>
           </ul>
@@ -37,7 +37,7 @@
       </div>
     </div>
 
-<div class="col s12 l6" id="corpora" data-user-id="{{ user.id }}">
+<div class="col s12 l6" id="corpora" data-user-id="{{ user.hashid }}">
   <h3>Corpora</h3>
   <div class="card">
     <div class="card-content">
@@ -65,7 +65,7 @@
   </div>
 </div>
 
-<div class="col s12 l6" id="jobs" data-user-id="{{ user.id }}">
+<div class="col s12 l6" id="jobs" data-user-id="{{ user.hashid }}">
   <h3>Jobs</h3>
   <div class="card">
     <div class="card-content">
@@ -111,7 +111,6 @@
 {% block scripts %}
 {{ super() }}
 <script>
-  nopaque.appClient.loadUser({{ user.id }});
   let corpusList = new CorpusList(document.querySelector('#corpora'));
   let jobList = new JobList(document.querySelector('#jobs'));
 </script>
diff --git a/app/templates/admin/users.html.j2 b/app/templates/admin/users.html.j2
index 19991a51aff82eca5da440795d424d648ca5c981..e8ffac293e61a3f78b5ab9c582f37c1a178882b3 100644
--- a/app/templates/admin/users.html.j2
+++ b/app/templates/admin/users.html.j2
@@ -41,6 +41,6 @@
 {{ super() }}
 <script>
   let userList = new UserList(document.querySelector('#users'), {page: 10});
-  userList.init({{ users|tojson }});
+  userList.init({{ dict_users|tojson }});
 </script>
 {% endblock scripts %}
diff --git a/app/templates/corpora/corpus.html.j2 b/app/templates/corpora/corpus.html.j2
index c1ac05a1b3dd8b1d6b9e8c0979b1263885512c78..c6c7702fc6fc18c70900a0790117325bd1dc291b 100644
--- a/app/templates/corpora/corpus.html.j2
+++ b/app/templates/corpora/corpus.html.j2
@@ -6,7 +6,7 @@
 {% block page_content %}
 <div class="container">
   <div class="row">
-    <div class="col s12" data-corpus-id="{{ corpus.id }}" data-user-id="{{ corpus.creator.id }}" id="corpus-display">
+    <div class="col s12" data-corpus-id="{{ corpus.hashid }}" data-user-id="{{ corpus.user.hashid }}" id="corpus-display">
       <div class="row">
         <div class="col s8 m9 l10">
           <h1 id="title"><span class="corpus-title"></span></h1>
@@ -83,7 +83,7 @@
       </div>
     </div>
 
-    <div class="col s12" id="corpus-files" data-corpus-id="{{ corpus.id }}" data-user-id="{{ corpus.creator.id }}">
+    <div class="col s12" id="corpus-files" data-corpus-id="{{ corpus.hashid }}" data-user-id="{{ corpus.user.hashid }}">
       <div class="card">
         <div class="card-content">
           <span class="card-title" id="files">Corpus files</span>
@@ -118,7 +118,6 @@
 {% block scripts %}
 {{ super() }}
 <script>
-  nopaque.appClient.loadUser({{ corpus.creator.id }});
   let corpusDisplay = new CorpusDisplay(document.querySelector('#corpus-display'));
   let corpusFileList = new CorpusFileList(document.querySelector('#corpus-files'));
 </script>
diff --git a/app/templates/jobs/job.html.j2 b/app/templates/jobs/job.html.j2
index 53eed62f3b9e5b43b7704c96f075798352f0a06a..344794bdaabfe6661c7c49b5b37cf8ff408a066c 100644
--- a/app/templates/jobs/job.html.j2
+++ b/app/templates/jobs/job.html.j2
@@ -6,7 +6,7 @@
 {% block page_content %}
 <div class="container">
   <div class="row">
-    <div class="col s12" data-job-id="{{ job.id }}" data-user-id="{{ job.creator.id }}" id="job-display">
+    <div class="col s12" data-job-id="{{ job.hashid }}" data-user-id="{{ job.user.hashid }}" id="job-display">
       <div class="row">
         <div class="col s8 m9 l10">
           <h1 id="title"><i style="font-size: inherit;" class="nopaque-icons service-icon" data-service="{{ job.service }}"></i> <span class="job-title"></span></h1>
@@ -111,7 +111,7 @@
       {% endif %}
     </div>
 
-    <div class="col s12" id="job-inputs" data-job-id="{{ job.id }}" data-user-id="{{ job.creator.id }}">
+    <div class="col s12" id="job-inputs" data-job-id="{{ job.hashid }}" data-user-id="{{ job.user.hashid }}">
       <div class="card">
         <div class="card-content">
           <div class="row">
@@ -136,7 +136,7 @@
       </div>
     </div>
 
-    <div class="col s12" id="job-results" data-job-id="{{ job.id }}" data-user-id="{{ job.creator.id }}">
+    <div class="col s12" id="job-results" data-job-id="{{ job.hashid }}" data-user-id="{{ job.user.hashid }}">
       <div class="card">
         <div class="card-content">
           <div class="row">
@@ -168,7 +168,6 @@
 {% block scripts %}
 {{ super() }}
 <script>
-  nopaque.appClient.loadUser({{ job.creator.id }});
   let jobDisplay = new JobDisplay(document.querySelector('#job-display'));
   let jobInputList = new JobInputList(document.querySelector('#job-inputs'));
   let jobResultList = new JobResultList(document.querySelector('#job-results'));
diff --git a/app/templates/main/dashboard.html.j2 b/app/templates/main/dashboard.html.j2
index b7ece5330992d06f4c14449c68918e685c46d5bb..1492caf7579e8bdbf62cb75cd8854ac3878e4b95 100644
--- a/app/templates/main/dashboard.html.j2
+++ b/app/templates/main/dashboard.html.j2
@@ -18,7 +18,7 @@
             <li class="tab col s6"><a href="#query-results">Query results</a></li>
           </ul>
         </div>
-        <div class="col s12" id="corpora">
+        <div class="col s12" data-user-id="{{ current_user.hashid }}" id="corpora">
           <div class="card">
             <div class="card-content">
               <div class="input-field">
@@ -89,7 +89,7 @@
       </div>
     </div>
 
-    <div class="col s12" id="jobs">
+    <div class="col s12" data-user-id="{{ current_user.hashid }}" id="jobs">
       <h3>My Jobs</h3>
       <p>A job is the execution of a service provided by nopaque. You can create any number of jobs and let them be processed simultaneously.</p>
       <div class="card">
@@ -177,6 +177,6 @@
 <script>
   let corpusList = new CorpusList(document.querySelector('#corpora'));
   let jobList = new JobList(document.querySelector('#jobs'));
-  let queryResultList = new QueryResultList(document.querySelector('#query-results'));
+  //let queryResultList = new QueryResultList(document.querySelector('#query-results'));
 </script>
 {% endblock scripts %}
diff --git a/app/templates/tasks/email/notification.html.j2 b/app/templates/tasks/email/notification.html.j2
index 1aac0bf712b706d5c491449333562cd8dd6cb5a2..019dd45614ccf3329be1538f068df0d293719d25 100644
--- a/app/templates/tasks/email/notification.html.j2
+++ b/app/templates/tasks/email/notification.html.j2
@@ -1,4 +1,4 @@
-<p>Dear <b>{{ job.creator.username }}</b>,</p>
+<p>Dear <b>{{ job.user.username }}</b>,</p>
 
 <p>The status of your Job "<b>{{ job.title }}</b>" has changed!</p>
 <p>It is now <b>{{ job.status }}</b>!</p>
diff --git a/app/templates/tasks/email/notification.txt.j2 b/app/templates/tasks/email/notification.txt.j2
index 03012b3eceb871ef5cba9b26e3008524007047f3..be746b9c2d1dc2779713ed1a3bfc0861800d2da6 100644
--- a/app/templates/tasks/email/notification.txt.j2
+++ b/app/templates/tasks/email/notification.txt.j2
@@ -1,4 +1,4 @@
-Dear {{ job.creator.username }},
+Dear {{ job.user.username }},
 
 The status of your Job "{{ job.title }}" has changed!
 It is now {{ job.status }}!
diff --git a/app/utils.py b/app/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..a320d4bb7a1b389fd6a30a3e46126aedcd608d30
--- /dev/null
+++ b/app/utils.py
@@ -0,0 +1,10 @@
+from app import hashids
+from werkzeug.routing import BaseConverter
+
+
+class HashidConverter(BaseConverter):
+    def to_python(self, value):
+        return hashids.decode(value)[0]
+
+    def to_url(self, value):
+        return hashids.encode(value)
diff --git a/config.py b/config.py
index 06c481739c8f0126c0a7887bdbfcc1aa4d280e41..79f7f9b60ce2fc741dd4d92da80fa301c518a33e 100644
--- a/config.py
+++ b/config.py
@@ -45,7 +45,7 @@ class Config:
     NOPAQUE_ADMIN = os.environ.get('NOPAQUE_ADMIN')
     NOPAQUE_DAEMON_ENABLED = \
         os.environ.get('NOPAQUE_DAEMON_ENABLED', 'true').lower() == 'true'
-    NOPAQUE_DATA_DIR = os.environ.get('NOPAQUE_DATA_DIR', '/mnt/nopaque')
+    NOPAQUE_DATA_DIR = os.path.abspath(os.environ.get('NOPAQUE_DATA_DIR'))
     NOPAQUE_DOCKER_REGISTRY = 'gitlab.ub.uni-bielefeld.de:4567'
     NOPAQUE_DOCKER_IMAGE_PREFIX = f'{NOPAQUE_DOCKER_REGISTRY}/sfb1288inf/'
     NOPAQUE_DOCKER_REGISTRY_USERNAME = \
diff --git a/migrations/versions/68ed092ffe5e_.py b/migrations/versions/68ed092ffe5e_.py
new file mode 100644
index 0000000000000000000000000000000000000000..bbd8b9678bf0e01116c1ac9b23295822f70489f5
--- /dev/null
+++ b/migrations/versions/68ed092ffe5e_.py
@@ -0,0 +1,50 @@
+"""empty message
+
+Revision ID: 68ed092ffe5e
+Revises: be010d5d708d
+Create Date: 2021-11-24 15:33:16.258600
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '68ed092ffe5e'
+down_revision = 'be010d5d708d'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('corpus_files', sa.Column('creation_date', sa.DateTime(), nullable=True))
+    op.add_column('corpus_files', sa.Column('last_edited_date', sa.DateTime(), nullable=True))
+    op.add_column('corpus_files', sa.Column('mimetype', sa.String(length=255), nullable=True))
+    op.add_column('job_inputs', sa.Column('creation_date', sa.DateTime(), nullable=True))
+    op.add_column('job_inputs', sa.Column('last_edited_date', sa.DateTime(), nullable=True))
+    op.add_column('job_inputs', sa.Column('mimetype', sa.String(length=255), nullable=True))
+    op.add_column('job_results', sa.Column('creation_date', sa.DateTime(), nullable=True))
+    op.add_column('job_results', sa.Column('last_edited_date', sa.DateTime(), nullable=True))
+    op.add_column('job_results', sa.Column('mimetype', sa.String(length=255), nullable=True))
+    op.add_column('query_results', sa.Column('creation_date', sa.DateTime(), nullable=True))
+    op.add_column('query_results', sa.Column('last_edited_date', sa.DateTime(), nullable=True))
+    op.add_column('query_results', sa.Column('mimetype', sa.String(length=255), nullable=True))
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('query_results', 'mimetype')
+    op.drop_column('query_results', 'last_edited_date')
+    op.drop_column('query_results', 'creation_date')
+    op.drop_column('job_results', 'mimetype')
+    op.drop_column('job_results', 'last_edited_date')
+    op.drop_column('job_results', 'creation_date')
+    op.drop_column('job_inputs', 'mimetype')
+    op.drop_column('job_inputs', 'last_edited_date')
+    op.drop_column('job_inputs', 'creation_date')
+    op.drop_column('corpus_files', 'mimetype')
+    op.drop_column('corpus_files', 'last_edited_date')
+    op.drop_column('corpus_files', 'creation_date')
+    # ### end Alembic commands ###
diff --git a/requirements.txt b/requirements.txt
index f1afb44abeedf07bedcf9ae2d7a67c60db8479ee..fbff2916cfa393d56c87ccdaeb1b010eb4c7f058 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,6 +13,7 @@ Flask-SocketIO~=5.1
 Flask-SQLAlchemy
 Flask-WTF
 gunicorn
+hashids
 hiredis
 jsonpatch
 jsonschema