diff --git a/.gitmodules b/.gitmodules
index 9436955b1cedc29130e4c4e33352ea14fe327101..c6af8543b7869b2a26a1772b937ee3aef8cdacbb 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -16,3 +16,6 @@
 [submodule "vendor/file-upload"]
 	path = vendor/file-upload
 	url = git@gitlab.tugraz.at:VPU/WebComponents/FileUpload.git
+[submodule "vendor/app-shell"]
+	path = vendor/app-shell
+	url = git@gitlab.tugraz.at:VPU/Apps/AppShell.git
diff --git a/package-lock.json b/package-lock.json
index 3addabec0d31ebd735150a86e017da1ba26ec284..2e78241b592bbfcb1a3dd5fa9289042c3a9f4ec4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5173,6 +5173,15 @@
       "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=",
       "dev": true
     },
+    "vpu-app-shell": {
+      "version": "file:vendor/app-shell",
+      "requires": {
+        "i18next": "^19.1.0",
+        "lit-element": "^2.2.1",
+        "lit-html": "^1.1.2",
+        "universal-router": "^8.3.0"
+      }
+    },
     "vpu-auth": {
       "version": "file:vendor/auth",
       "requires": {
diff --git a/package.json b/package.json
index 6c4aba564b7738bb919043993f9f626e4d4eb8f9..837d85bfd75bab911d71b34cbf864694da7a954c 100644
--- a/package.json
+++ b/package.json
@@ -50,13 +50,13 @@
     "select2": "^4.0.13",
     "source-sans-pro": "^2.45.0",
     "suggestions": "^1.7.0",
-    "universal-router": "^8.3.0",
     "vpu-auth": "file:./vendor/auth",
     "vpu-common": "file:./vendor/common",
     "vpu-file-upload": "file:./vendor/file-upload",
     "vpu-language-select": "file:./vendor/language-select",
     "vpu-notification": "file:./vendor/notification",
-    "vpu-person-profile": "file:./vendor/person-profile"
+    "vpu-person-profile": "file:./vendor/person-profile",
+    "vpu-app-shell": "file:./vendor/app-shell"
   },
   "scripts": {
     "clean": "rm dist/* -R",
diff --git a/src/app/build-info.js b/src/app/build-info.js
deleted file mode 100644
index 6be385b6538a539de5ebe711de3c165676bfd4e4..0000000000000000000000000000000000000000
--- a/src/app/build-info.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import {html, LitElement, css} from 'lit-element';
-import * as commonUtils from 'vpu-common/utils';
-import * as commonStyles from 'vpu-common/styles';
-import buildinfo from 'consts:buildinfo';
-
-class VPUBuildInfo extends LitElement {
-
-    constructor() {
-        super();
-    }
-
-    static get styles() {
-        return css`
-            ${commonStyles.getThemeCSS()}
-            ${commonStyles.getGeneralCSS()}
-            ${commonStyles.getTagCSS()}
-
-            :host {
-                display: inline-block;
-            }
-        `;
-    } 
-
-    render() {
-        const date = new Date(buildinfo.time);
-
-        return html`
-            <a href="${buildinfo.url}" style="float: right">
-                <div class="tags has-addons" title="Build Time: ${date.toString()}">
-                    <span class="tag is-light">build</span>
-                    <span class="tag is-dark">${buildinfo.info} (${buildinfo.env})</span>
-                </div>
-            </a>
-        `;
-    }
-}
-
-commonUtils.defineCustomElement('vpu-build-info', VPUBuildInfo);
diff --git a/src/app/index.js b/src/app/index.js
deleted file mode 100644
index 5b2f9eee669f90b08c10fa0ef7497321cc5c7641..0000000000000000000000000000000000000000
--- a/src/app/index.js
+++ /dev/null
@@ -1,715 +0,0 @@
-import {createI18nInstance} from '../i18n.js';
-import {html, css} from 'lit-element';
-import VPULitElement from 'vpu-common/vpu-lit-element'
-import 'vpu-language-select';
-import 'vpu-common/vpu-button.js';
-import 'vpu-auth';
-import 'vpu-notification';
-import * as commonUtils from 'vpu-common/utils';
-import * as commonStyles from 'vpu-common/styles';
-import buildinfo from 'consts:buildinfo';
-import {classMap} from 'lit-html/directives/class-map.js';
-// import * as errorreport from 'vpu-common/errorreport';
-import {Router} from './router.js';
-import * as events from 'vpu-common/events.js';
-import './build-info.js';
-import './tugraz-logo.js';
-import {send as notify} from 'vpu-notification';
-import environment from 'consts:environment';
-
-// errorreport.init({release: 'vpi-signature-app@' + buildinfo.info});
-
-
-const i18n = createI18nInstance();
-
-/**
- * In case the application gets updated future dynamic imports might fail.
- * This sends a notification suggesting the user to reload the page.
- *
- * uage: importNotify(import('<path>'));
- *
- * @param {Promise} promise
- */
-const importNotify = async (promise) => {
-    try {
-        return await promise;
-    } catch (error) {
-        console.log(error);
-        notify({
-            "body": i18n.t('page-updated-needs-reload'),
-            "type": "info",
-            "icon": "warning"
-        });
-        throw error;
-    }
-};
-
-
-class VPUApp extends VPULitElement {
-    constructor() {
-        super();
-        this.lang = i18n.language;
-        this.activeView = '';
-        this.entryPointUrl = commonUtils.getAPiUrl();
-        this.subtitle = '';
-        this.description = '';
-        this.routes = [];
-        this.metadata = {};
-        this.topic = {};
-        this.basePath = '';
-
-        this._updateAuth = this._updateAuth.bind(this);
-        this._loginStatus = 'unknown';
-        this._subscriber = new events.EventSubscriber('vpu-auth-update', 'vpu-auth-update-request');
-
-        this._attrObserver = new MutationObserver(this.onAttributeObserved);
-    }
-
-    onAttributeObserved(mutationsList, observer) {
-        for(let mutation of mutationsList) {
-            if (mutation.type === 'attributes') {
-                const key = mutation.attributeName;
-                const value = mutation.target.getAttribute(key);
-                sessionStorage.setItem('vpu-attr-' + key, value);
-            }
-        }
-    }
-
-    /**
-     * Fetches the metadata of the components we want to use in the menu, dynamically imports the js modules for them,
-     * then triggers a rebuilding of the menu and resolves the current route
-     *
-     * @param {string} topicURL The topic metadata URL or relative path to load things from
-     */
-    async fetchMetadata(topicURL) {
-        const metadata = {};
-        const routes = [];
-
-        const result = await (await fetch(topicURL, {
-            headers: {'Content-Type': 'application/json'}
-        })).json();
-
-        this.topic = result;
-
-        const fetchOne = async (url) => {
-              const result = await fetch(url, {
-                headers: {'Content-Type': 'application/json'}
-            });
-            if (!result.ok)
-                throw result;
-
-            const jsondata = await result.json();
-            if (jsondata["element"] === undefined)
-                throw new Error("no element defined in metadata");
-
-            return jsondata;
-        };
-
-        let promises = [];
-        for (const activity of result.activities) {
-            const actURL = new URL(activity.path, new URL(topicURL, window.location).href).href;
-            promises.push([activity.visible === undefined || activity.visible, actURL, fetchOne(actURL)]);
-        }
-
-        for (const [visible, actURL, p] of promises) {
-            try {
-                const activity = await p;
-                activity.visible = visible;
-                // Resolve module_src relative to the location of the json file
-                activity.module_src = new URL(activity.module_src, actURL).href;
-                if (activity.routing_name === 'order-list' && environment === 'production') {
-                    console.warn('NOTE: order-list disabled in production!');
-                    continue;
-                }
-                metadata[activity.routing_name] = activity;
-                routes.push(activity.routing_name);
-            } catch (error) {
-                console.log(error);
-            }
-        }
-        // this also triggers a rebuilding of the menu
-        this.metadata = metadata;
-        this.routes = routes;
-
-        // Switch to the first route if none is selected
-        if (!this.activeView)
-            this.switchComponent(routes[0]);
-        else
-            this.switchComponent(this.activeView);
-
-    }
-
-    initRouter() {
-        const routes = [
-            {
-                path: '',
-                action: (context) => {
-                    return {
-                        lang: this.lang,
-                        component: '',
-                    };
-                }
-            },
-            {
-                path: '/:lang',
-                children: [
-                    {
-                        path: '',
-                        action: (context, params) => {
-                            return {
-                                lang: params.lang,
-                                component: '',
-                            };
-                        }
-                    },
-                    {
-                        name: 'mainRoute',
-                        path: '/:component',
-                        action: (context, params) => {
-                            // remove the additional parameters added by Keycloak
-                            let componentTag = params.component.toLowerCase().replace(/&.+/,"");
-                            return {
-                                lang: params.lang,
-                                component: componentTag,
-                            };
-                        },
-                    },
-                ],
-            },
-        ];
-
-        this.router = new Router(routes, {
-            routeName: 'mainRoute',
-            getState: () => {
-                return {
-                    component: this.activeView,
-                    lang: this.lang,
-                };
-            },
-            setState: (state) => {
-                this.updateLangIfChanged(state.lang);
-                this.switchComponent(state.component);
-            }
-        }, {
-            baseUrl: new URL(this.basePath, window.location).pathname.replace(/\/$/, ''),
-        });
-
-        this.router.setStateFromCurrentLocation();
-    }
-
-    static get properties() {
-        return {
-            lang: { type: String, reflect: true },
-            src: { type: String },
-            basePath: { type: String, attribute: 'base-path' },
-            activeView: { type: String, attribute: false},
-            entryPointUrl: { type: String, attribute: 'entry-point-url' },
-            metadata: { type: Object, attribute: false },
-            topic: { type: Object, attribute: false },
-            subtitle: { type: String, attribute: false },
-            description: { type: String, attribute: false },
-            _loginStatus: { type: Boolean, attribute: false },
-        };
-    }
-
-    _updateAuth(login) {
-        if (login.status != this._loginStatus) {
-            console.log('Login status: ' + login.status);
-        }
-
-        this._loginStatus = login.status;
-
-        // Clear the session storage when the user logs out
-        if (this._loginStatus === 'logging-out') {
-            sessionStorage.clear();
-        }
-    }
-
-    connectedCallback() {
-        super.connectedCallback();
-
-        if (this.src)
-            this.fetchMetadata(this.src);
-        this.initRouter();
-
-        // listen to the vpu-auth-profile event to switch to the person profile
-        window.addEventListener("vpu-auth-profile", () => {
-            this.switchComponent('person-profile');
-        });
-
-        this._subscriber.subscribe(this._updateAuth);
-    }
-
-    disconnectedCallback() {
-        this._subscriber.unsubscribe(this._updateAuth);
-        super.disconnectedCallback();
-    }
-
-    /**
-     * Switches language if another language is requested
-     *
-     * @param {string} lang
-     */
-    updateLangIfChanged(lang) {
-        if (this.lang !== lang) {
-            this.lang = lang;
-            this.router.update();
-
-            const event = new CustomEvent("vpu-language-changed", {
-                bubbles: true,
-                detail: {'lang': lang}
-            });
-
-            this.dispatchEvent(event);
-        }
-    }
-
-    update(changedProperties) {
-        changedProperties.forEach((oldValue, propName) => {
-            if (propName === "lang") {
-                // For screen readers
-                document.documentElement.setAttribute("lang", this.lang);
-                i18n.changeLanguage(this.lang);
-            }
-        });
-
-        super.update(changedProperties);
-    }
-
-    onMenuItemClick(e) {
-        e.preventDefault();
-        const link = e.composedPath()[0];
-        const location = link.getAttribute('href');
-        this.router.updateFromPathname(location);
-    }
-
-    onLanguageChanged(e) {
-        const newLang = e.detail.lang;
-        const changed = (this.lang !== newLang);
-        this.lang = newLang;
-        if (changed) {
-            this.router.update();
-            this.subtitle = this.activeMetaDataText("short_name");
-            this.description = this.activeMetaDataText("description");
-        }
-    }
-
-    switchComponent(componentTag) {
-        const changed = (componentTag !== this.activeView);
-        this.activeView = componentTag;
-        if (changed)
-            this.router.update();
-        const metadata = this.metadata[componentTag];
-
-        if (metadata === undefined) {
-            return;
-        }
-
-        importNotify(import(metadata.module_src)).then(() => {
-            this.updatePageTitle();
-            this.subtitle = this.activeMetaDataText("short_name");
-            this.description = this.activeMetaDataText("description");
-        }).catch((e) => {
-            console.error(`Error loading ${ metadata.element }`);
-            throw e;
-        });
-    }
-
-    metaDataText(routingName, key) {
-        const metadata = this.metadata[routingName];
-        return metadata !== undefined && metadata[key] !== undefined ? metadata[key][this.lang] : '';
-    }
-
-    topicMetaDataText(key) {
-        return (this.topic[key] !== undefined) ? this.topic[key][this.lang] : '';
-    }
-
-    activeMetaDataText(key) {
-        return this.metaDataText(this.activeView, key);
-    }
-
-    updatePageTitle() {
-        document.title = `${this.topicMetaDataText('name')} - ${this.activeMetaDataText("short_name")}`;
-    }
-
-    toggleMenu() {
-        const menu = this.shadowRoot.querySelector("ul.menu");
-
-        if (menu === null) {
-            return;
-        }
-
-        menu.classList.toggle('hidden');
-
-        const chevron = this.shadowRoot.querySelector("#menu-chevron-icon");
-        if (chevron !== null) {
-            chevron.name = menu.classList.contains('hidden') ? 'chevron-down' : 'chevron-up';
-        }
-    }
-
-    static get styles() {
-        // language=css
-        return css`
-            ${commonStyles.getThemeCSS()}
-            ${commonStyles.getGeneralCSS()}
-
-            .hidden {display: none}
-
-            h1.title {
-                margin-bottom: 0;
-                font-weight: 300;
-            }
-
-            #main {
-                display: grid;
-                grid-template-columns: minmax(180px, 17%) minmax(0, auto);
-                grid-template-rows: min-content min-content 1fr min-content;
-                grid-template-areas: "header header" "headline headline" "sidebar main" "footer footer";
-                max-width: 1400px;
-                margin: auto;
-                min-height: 100vh;
-            }
-
-            #main-logo {
-                padding: 0 50px 0 0;
-            }
-
-            header {
-                grid-area: header;
-                display: grid;
-                grid-template-columns: 50% 1px auto;
-                grid-template-rows: 60px 60px;
-                grid-template-areas: "hd1-left hd1-middle hd1-right" "hd2-left . hd2-right";
-                width: 100%;
-                max-width: 1060px;
-                margin: 0 auto;
-            }
-
-            aside { grid-area: sidebar; margin: 30px 15px; }
-            #headline { grid-area: headline; margin: 30px; text-align: center; }
-            main { grid-area: main; margin: 30px }
-            footer { grid-area: footer; margin: 30px; text-align: right; }
-
-            header .hd1-left {
-                display: flex;
-                flex-direction: column;
-                justify-content: center;
-                grid-area: hd1-left;
-                text-align: right;
-                padding-right: 20px;
-            }
-
-            header .hd1-middle {
-                grid-area: hd1-middle;
-                background-color: #000;
-                background: linear-gradient(180deg, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 85%, rgba(0,0,0,0) 90%);
-            }
-
-            header .hd1-right {
-                grid-area: hd1-right;
-                display: flex;
-                flex-direction: column;
-                justify-content: center;
-                padding-left: 20px;
-            }
-
-            header .hd2-left {
-                grid-area: hd2-left;
-                display: flex;
-                flex-direction: column;
-                white-space: nowrap;
-            }
-
-            header .hd2-left .header {
-                margin-left: 50px;
-            }
-
-            header .hd2-left a:hover {
-                color: #fff;
-                background-color: #000;
-            }
-
-            header .hd2-right {
-                grid-area: hd2-right;
-                display: flex;
-                flex-direction: column;
-                justify-content: center;
-                text-align: right;
-            }
-
-            header a {
-                color: black;
-                display: inline;
-            }
-
-            aside ul.menu, footer ul.menu {
-                list-style: none;
-            }
-
-            ul.menu li.close {
-                display: none;
-            }
-
-            footer {
-                display: grid;
-                grid-gap: 1em;
-                grid-template-columns: 1fr max-content max-content;
-            }
-
-            footer a {
-                border-bottom: 1px solid rgba(0,0,0,0.3);
-                padding: 0;
-            }
-
-            footer a:hover {
-                color: #fff;
-                background-color: #000;
-            }
-
-            /* We don't allown inline-svg */
-            /*
-            footer .int-link-external::after {
-                content: "\\00a0\\00a0\\00a0\\00a0";
-                background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3Ardf%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2F02%2F22-rdf-syntax-ns%23%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%225.6842mm%22%20width%3D%225.6873mm%22%20version%3D%221.1%22%20xmlns%3Acc%3D%22http%3A%2F%2Fcreativecommons.org%2Fns%23%22%20xmlns%3Adc%3D%22http%3A%2F%2Fpurl.org%2Fdc%2Felements%2F1.1%2F%22%20viewBox%3D%220%200%2020.151879%2020.141083%22%3E%3Cg%20transform%3D%22translate(-258.5%20-425.15)%22%3E%3Cpath%20style%3D%22stroke-linejoin%3Around%3Bstroke%3A%23000%3Bstroke-linecap%3Around%3Bstroke-width%3A1.2%3Bfill%3Anone%22%20d%3D%22m266.7%20429.59h-7.5029v15.002h15.002v-7.4634%22%2F%3E%3Cpath%20style%3D%22stroke-linejoin%3Around%3Bstroke%3A%23000%3Bstroke-linecap%3Around%3Bstroke-width%3A1.2%3Bfill%3Anone%22%20d%3D%22m262.94%20440.86%2015.002-15.002%22%2F%3E%3Cpath%20style%3D%22stroke-linejoin%3Around%3Bstroke%3A%23000%3Bstroke-linecap%3Around%3Bstroke-width%3A1.2%3Bfill%3Anone%22%20d%3D%22m270.44%20425.86h7.499v7.499%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E');
-                background-size:contain;
-                background-repeat: no-repeat;
-                background-position:center center;
-                margin: 0 0.5% 0 1.5%;
-                font-size:94%;
-            } 
-            */
-
-            .menu a {
-                padding: 0.3em;
-                font-weight: 300;
-                color: #000;
-                display: block;
-            }
-
-            .menu a:hover {
-                color: #E4154B;
-            }
-
-            .menu a.selected {
-                color: var(--vpu-light);
-                background-color: var(--vpu-dark);
-            }
-
-            aside .subtitle {
-                display: none;
-                color: #4a4a4a;
-                font-size: 1.25rem;
-                font-weight: 300;
-                line-height: 1.25;
-                cursor: pointer;
-                text-align: center;
-            }
-
-            ul.menu.hidden {
-                display: block;
-            }
-
-            a { transition: background-color 0.15s ease 0s, color 0.15s ease 0s; }
-
-            .description {
-                text-align: left;
-                margin-bottom: 1rem;
-                display: none;
-            }
-
-            @media (max-width: 680px) {
-                #main {
-                    grid-template-columns: minmax(0, auto);
-                    grid-template-rows: min-content min-content min-content 1fr min-content;
-                    grid-template-areas: "header" "headline" "sidebar" "main" "footer";
-                }
-
-                header {
-                    grid-template-rows: 40px;
-                    grid-template-areas: "hd1-left hd1-middle hd1-right";
-                }
-
-                header .hd2-left, header .hd2-right {
-                    display: none;
-                }
-
-                aside {
-                    margin: 0 15px;
-                }
-
-                aside h2.subtitle {
-                    display: block;
-                    margin-bottom: 0.5em;
-                }
-
-                aside h2.subtitle:not(:last-child) {
-                    margin-bottom: 0.5em;
-                }
-
-                aside .menu {
-                    border: black 1px solid;
-                }
-
-                .menu li {
-                    padding: 7px;
-                }
-
-                .menu a {
-                    padding: 8px;
-                }
-
-                ul.menu li.close {
-                    display: block;
-                    padding: 0 15px 15px 15px;
-                    text-align: right;
-                    cursor: pointer;
-                }
-
-                ul.menu.hidden {
-                    display: none;
-                }
-            }
-        `;
-    }
-
-    _createActivityElement(activity) {
-        // We have to create elements dynamically based on a tag name which isn't possible with lit-html.
-        // This means we pass the finished element to lit-html and have to handle element caching and
-        // event binding ourselves.
-
-        if (this._lastElm !== undefined) {
-            if (this._lastElm.tagName.toLowerCase() == activity.element.toLowerCase()) {
-                return this._lastElm;
-            } else {
-                this._attrObserver.disconnect();
-                this._lastElm = undefined;
-            }
-        }
-
-        const elm = document.createElement(activity.element);
-
-        for(const key of this.topic.attributes) {
-            let value = sessionStorage.getItem('vpu-attr-' + key);
-            if (value !== null) {
-                elm.setAttribute(key, value);
-            }
-        }
-
-        this._attrObserver.observe(elm, {attributes: true, attributeFilter: this.topic.attributes});
-
-        this._lastElm = elm;
-        return elm;
-    }
-
-    _renderActivity() {
-        const act = this.metadata[this.activeView];
-        if (act === undefined)
-            return html``;
-
-        const elm =  this._createActivityElement(act);
-        elm.setAttribute("entry-point-url", this.entryPointUrl);
-        elm.setAttribute("lang", this.lang);
-        return elm;
-    }
-
-    render() {
-        const silentCheckSsoUri = commonUtils.getAssetURL('silent-check-sso.html');
-
-        const getSelectClasses = (name => {
-            return classMap({selected: this.activeView === name});
-        });
-
-        // We hide the app until we are either fully logged in or logged out
-        // At the same time when we hide the main app we show the main slot (e.g. a loading spinner)
-        const appHidden = (this._loginStatus == 'unknown' || this._loginStatus == 'logging-in');
-        const mainClassMap = classMap({hidden: appHidden});
-        const slotClassMap = classMap({hidden: !appHidden});
-
-        // XXX: Safari doesn't like CSS being applied to slots or via HTML,
-        // so we have to remove the slow instead of hiding it
-        if (!appHidden) {
-            this.updateComplete.then(() => {
-                const slot = this.shadowRoot.querySelector("slot");
-                if (slot) {
-                    slot.remove();
-                }
-
-                const welcomeActivity = this._("vpu-welcome");
-
-                if (welcomeActivity) {
-                    welcomeActivity.setMetaData(this.metadata, i18n.t('welcome.headline'), i18n.t('welcome.description'));
-                }
-            });
-        }
-
-        const prodClassMap = classMap({hidden: buildinfo.env === 'production'});
-
-        this.updatePageTitle();
-
-        // build the menu
-        let menuTemplates = [];
-        for (let routingName of this.routes) {
-            const data = this.metadata[routingName];
-
-            if (data['visible']) {
-                menuTemplates.push(html`<li><a @click="${(e) => this.onMenuItemClick(e)}" href="${this.router.getPathname({component: routingName})}" data-nav class="${getSelectClasses(routingName)}" title="${this.metaDataText(routingName, "description")}">${this.metaDataText(routingName, "short_name")}</a></li>`);
-            }
-        }
-
-        return html`
-            <slot class="${slotClassMap}"></slot>
-            <div class="${mainClassMap}">
-            <div id="main">
-                <vpu-notification lang="${this.lang}"></vpu-notification>
-                <header>
-                    <div class="hd1-left">
-                        <vpu-language-select @vpu-language-changed=${this.onLanguageChanged.bind(this)}></vpu-language-select>
-                    </div>
-                    <div class="hd1-middle">
-                    </div>
-                    <div class="hd1-right">
-                        <vpu-auth lang="${this.lang}" show-profile keycloak-config='{"clientId": "${commonUtils.setting('keyCloakClientId')}", "silentCheckSsoRedirectUri": "${silentCheckSsoUri}"}' load-person try-login></vpu-auth>
-                    </div>
-                    <div class="hd2-left">
-                        <div class="header">
-                            <a href="https://www.tugraz.at/" title="TU Graz Home" target="_blank" rel="noopener">TU Graz<br>Graz University of Technology</a>
-                        </div>
-                    </div>
-                    <div class="hd2-right">
-                        <vpu-tugraz-logo id="main-logo" lang="${this.lang}"></vpu-tugraz-logo>
-                    </div>
-                </header>
-
-                <div id="headline">
-                    <h1 class="title">${this.topicMetaDataText('name')}</h1>
-                </div>
-
-                <aside>
-                    <h2 class="subtitle" @click="${this.toggleMenu}">
-                        ${this.subtitle}
-                        <vpu-icon name="chevron-down" style="color: red" id="menu-chevron-icon"></vpu-icon>
-                    </h2>
-                    <ul class="menu hidden">
-                        ${menuTemplates}
-                        <li class="close" @click="${this.toggleMenu}"><vpu-icon name="close" style="color: red"></vpu-icon></li>
-                    </ul>
-                </aside>
-
-                <main>
-                    <p class="description">${this.description}</p>
-                    ${ this._renderActivity() }
-                </main>
-
-                <footer>
-                    <div></div>
-                    <a target="_blank" rel="noopener" class="int-link-external" href="https://datenschutz.tugraz.at/erklaerung/">${i18n.t('privacy-policy')}</a>
-                    <vpu-build-info class="${prodClassMap}"></vpu-build-info>
-                </footer>
-            </div>
-            </div>
-        `;
-    }
-}
-
-commonUtils.initAssetBaseURL('vpu-app-src');
-commonUtils.defineCustomElement('vpu-app', VPUApp);
diff --git a/src/app/router.js b/src/app/router.js
deleted file mode 100644
index 2ee466094ed5d6e7531e11ecd9ea7b9c354feb26..0000000000000000000000000000000000000000
--- a/src/app/router.js
+++ /dev/null
@@ -1,117 +0,0 @@
-import UniversalRouter from 'universal-router';
-import generateUrls from 'universal-router/generateUrls';
-
-/**
- * A wrapper around UniversalRouter which adds history integration
- */
-export class Router {
-
-    /**
-     * @param {Array} routes The routes passed to UniversalRouter
-     * @param {object} options Options
-     * @param {string} options.routeName The main route name
-     * @param {Function} options.getState Function which should return the current state
-     * @param {Function} options.setState Function which gets passed the new state based on the route
-     * @param {object} unioptions options passed to UniversalRouter
-     */
-    constructor(routes, options, unioptions) {
-        this.getState = options.getState;
-        this.setState = options.setState;
-        // XXX: We only have one route atm
-        // If we need more we need to pass the route name to each function
-        this.routeName = options.routeName;
-
-        console.assert(this.getState);
-        console.assert(this.setState);
-        console.assert(this.routeName);
-
-        // https://github.com/kriasoft/universal-router
-        this.router = new UniversalRouter(routes, unioptions);
-
-        window.addEventListener('popstate', (event) => {
-            this.setStateFromCurrentLocation();
-            this.dispatchLocationChanged();
-        });
-    }
-
-    /**
-     * In case something else has changed the location, update the app state accordingly.
-     */
-    setStateFromCurrentLocation() {
-        const oldPathName = location.pathname;
-        this.router.resolve({pathname: oldPathName}).then(page => {
-            const newPathname = this.getPathname(page);
-            // In case of a router redirect, set the new location
-            if (newPathname !== oldPathName) {
-                const referrerUrl = location.href;
-                window.history.replaceState({}, '', newPathname);
-                this.dispatchLocationChanged(referrerUrl);
-            }
-            this.setState(page);
-        }).catch((e) => {
-            // In case we can't resolve the location, just leave things as is.
-            // This happens when a user enters a wrong URL or when testing with karma.
-        });
-    }
-
-    /**
-     * Update the router after some internal state change.
-     */
-    update() {
-        // Queue updates so we can call this multiple times when changing state
-        // without it resulting in multiple location changes
-        setTimeout(() => {
-            const newPathname = this.getPathname();
-            const oldPathname = location.pathname;
-            if (newPathname === oldPathname)
-                return;
-            const referrerUrl = location.href;
-            window.history.pushState({}, '', newPathname);
-            this.dispatchLocationChanged(referrerUrl);
-        });
-    }
-
-    /**
-     * Given a new routing path set the location and the app state.
-     *
-     * @param {string} pathname
-     */
-    updateFromPathname(pathname) {
-        this.router.resolve({pathname: pathname}).then(page => {
-            if (location.pathname === pathname)
-                return;
-            const referrerUrl = location.href;
-            window.history.pushState({}, '', pathname);
-            this.setState(page);
-            this.dispatchLocationChanged(referrerUrl);
-        }).catch((err) => {
-            throw new Error(`Route not found: ${pathname}: ${err}`);
-        });
-    }
-
-    /**
-     * Pass some new router state to get a new router path that can
-     * be passed to updateFromPathname() later on. If nothing is
-     * passed the current state is used.
-     *
-     * @param {object} [partialState] The optional partial new state
-     * @returns {string} The new path
-     */
-    getPathname(partialState) {
-        const currentState = this.getState();
-        if (partialState === undefined)
-            partialState = {};
-        let combined = {...currentState, ...partialState};
-        return generateUrls(this.router)(this.routeName, combined);
-    }
-
-    dispatchLocationChanged(referrerUrl = "") {
-        // fire a locationchanged event
-        window.dispatchEvent(new CustomEvent('locationchanged', {
-            detail: {
-                referrerUrl: referrerUrl,
-            },
-            bubbles: true
-        }));
-    }
-}
diff --git a/src/app/tugraz-logo.js b/src/app/tugraz-logo.js
deleted file mode 100644
index 607869f3a839ab179bb4364cad4c954f8a91b36a..0000000000000000000000000000000000000000
--- a/src/app/tugraz-logo.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import {html, LitElement, css} from 'lit-element';
-import * as commonUtils from 'vpu-common/utils';
-import * as commonStyles from 'vpu-common/styles';
-import {createI18nInstance} from '../i18n.js';
-
-const i18n = createI18nInstance();
-
-class VPUTUGrazLogo extends LitElement {
-
-    constructor() {
-        super();
-
-        this.lang = i18n.language;
-    }
-
-    static get properties() {
-        return {
-            lang: { type: String }
-        };
-    }
-
-    update(changedProperties) {
-        changedProperties.forEach((oldValue, propName) => {
-            if (propName === "lang") {
-                i18n.changeLanguage(this.lang);
-            }
-        });
-        super.update(changedProperties);
-    }
-
-    static get styles() {
-        return css`
-            ${commonStyles.getThemeCSS()}
-            ${commonStyles.getGeneralCSS()}
-
-            :host {
-                display: inline-block;
-            }
-
-            #claim
-            {
-                font-size: 12px;
-                text-align: right;
-                padding: 0 17px 0 0;
-                line-height: 17px;
-                letter-spacing: 2px;
-                vertical-align: top;
-                text-transform: uppercase;
-                display: inline-block;
-                white-space: nowrap;
-            }
-
-            #img {
-                overflow: visible;
-            }
-
-            a:hover path, a:focus path {
-                fill:#000 !important;
-                transition:none;
-            }
-
-             * {
-                transition:fill 0.15s, stroke 0.15s;
-            }
-        `;
-    } 
-
-    render() {
-        return html`
-            <a href="https://www.tugraz.at" title="TU Graz Home" target="_blank" rel="noopener">
-                <div id="claim">
-                    <div class="int-header-logo-claim-single">${i18n.t('logo.word1')}</div>
-                    <div class="int-header-logo-claim-single">${i18n.t('logo.word2')}</div>
-                    <div class="int-header-logo-claim-single">${i18n.t('logo.word3')}</div>
-                </div>
-                <svg id="img" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" height="51.862" width="141.1" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" viewBox="0 0 141.10001 51.862499"><g transform="matrix(1.25 0 0 -1.25 0 51.862)"><g transform="scale(.1)"><path style="fill:#e4154b" d="m0 103.73h207.45v207.46l-207.45 0.01v-207.47z"></path><path style="fill:#e4154b" d="m228.19 103.73h207.46v207.46h-207.46v-207.46z"></path><path style="fill:#e4154b" d="m456.41 103.73h207.44v207.46h-207.44v-207.46z"></path><path style="fill:#e4154b" d="m103.72 0h207.47v207.46h-207.47v-207.46z"></path><path style="fill:#e4154b" d="m352.68 207.46h207.44v207.46h-207.44v-207.46z"></path><path style="fill:#231f20" d="m751.04 277.91h-66.426v33.195h171.19v-33.195h-66.407v-173.73h-38.359v173.73"></path><path style="fill:#231f20" d="m1048.3 180.22c0-12.461-2.25-23.711-6.72-33.75-4.5-10.039-10.61-18.555-18.36-25.567-7.76-7.031-16.9-12.421-27.503-16.21-10.605-3.809-22.109-5.7036-34.551-5.7036-12.422 0-23.945 1.8946-34.551 5.7036-10.605 3.789-19.824 9.179-27.656 16.21-7.851 7.012-13.984 15.528-18.34 25.567-4.394 10.039-6.582 21.289-6.582 33.75v130.89h38.379v-129.59c0-5.039 0.801-10.351 2.442-15.898 1.64-5.547 4.336-10.664 8.125-15.332s8.789-8.516 15.039-11.523c6.211-3.008 13.926-4.512 23.144-4.512 9.199 0 16.914 1.504 23.145 4.512 6.23 3.007 11.25 6.855 15.039 11.523 3.77 4.668 6.48 9.785 8.12 15.332 1.63 5.547 2.45 10.859 2.45 15.898v129.59h38.38v-130.89"></path><path style="fill:#231f20" d="m832.56 75.664c-7.597 3.2812-17.46 4.8632-25.332 4.8632-22.929 0-35.605-14.434-35.605-33.184 0-18.613 12.383-32.637 33.34-32.637 5.351 0 9.59 0.5274 12.969 1.3086v23.867h-20.84v14.414h39.687v-49.297c-10.41-2.6172-21.25-4.707-31.816-4.707-31.797 0-53.906 14.805-53.906 45.742 0 31.348 20.566 48.906 53.906 48.906 11.406 0 20.41-1.4453 28.867-3.8086l-1.27-15.469"></path><path style="fill:#231f20" d="m856.2 69.375h16.758v-15.332h0.293c0.84 6.289 8.574 16.914 19.824 16.914 1.836 0 3.828 0 5.782-0.5273v-17.715c-1.68 0.918-5.059 1.4454-8.457 1.4454-15.333 0-15.333-17.832-15.333-27.52v-24.785h-18.867v67.52"></path><path style="fill:#231f20" d="m913.75 65.84c7.324 3.1446 17.187 5.1172 25.215 5.1172 22.09 0 31.23-8.5351 31.23-28.457v-8.6523c0-6.8165 0.156-11.934 0.293-16.914 0.137-5.1172 0.41-9.8242 0.84-15.078h-16.602c-0.703 3.5352-0.703 8.0078-0.839 10.098h-0.293c-4.36-7.4618-13.81-11.661-22.38-11.661-12.793 0-25.332 7.207-25.332 20.059 0 10.078 5.195 15.976 12.383 19.258 7.187 3.2812 16.464 3.9453 24.355 3.9453h10.41c0 10.879-5.195 14.551-16.328 14.551-8.008 0-16.035-2.8907-22.363-7.3438l-0.586 15.078zm22.11-52.715c5.782 0 10.274 2.3633 13.223 6.0352 3.105 3.8086 3.945 8.6523 3.945 13.906h-8.164c-8.437 0-20.957-1.3086-20.957-11.68 0-5.7617 5.195-8.2617 11.953-8.2617"></path><path style="fill:#231f20" d="m985.69 69.375h57.422v-14.414l-36.04-39.473h37.31v-13.633h-60.235v14.297l36.715 39.59h-35.172v13.633"></path><path style="fill:#e4154b" d="m1059.6 0h69.102v69.121h-69.102v-69.121z"></path></g></g></svg>
-            </a>
-        `;
-    }
-}
-
-commonUtils.defineCustomElement('vpu-tugraz-logo', VPUTUGrazLogo);
diff --git a/src/i18n/de/translation.json b/src/i18n/de/translation.json
index aa88e156de4e6a34b46c26208307c954025c85bd..df1513fb4999b63c9aff6cfe782206de72431eaf 100644
--- a/src/i18n/de/translation.json
+++ b/src/i18n/de/translation.json
@@ -1,9 +1,4 @@
 {
-  "logo": {
-    "word1": "Wissen",
-    "word2": "Technik",
-    "word3": "Leidenschaft"
-  },
   "pdf-upload": {
     "upload-field-label": "PDF Dateien zum Signieren hochladen",
     "upload-area-text": "Sie können in diesem Bereich PDF Dateien per Drag & Drop oder per Direktauswahl hochladen",
@@ -24,7 +19,5 @@
   },
   "error-summary": "Ein Fehler ist aufgetreten",
   "error-permission-message": "Sie müssen das Recht auf Amtssignaturen besitzen um diese Funktion nutzen zu können!",
-  "error-login-message": "Sie müssen eingeloggt sein um diese Funktion nutzen zu können!",
-  "privacy-policy": "Datenschutz",
-  "page-updated-needs-reload": "Die Applikation wurde aktualisiert. Bitte laden Sie die Seite neu."
+  "error-login-message": "Sie müssen eingeloggt sein um diese Funktion nutzen zu können!"
 }
diff --git a/src/i18n/en/translation.json b/src/i18n/en/translation.json
index cbced032189a0ae1a7c16f9a33e3d851f147e827..ba9d45350df947103128eb678f8dae89a9896ae3 100644
--- a/src/i18n/en/translation.json
+++ b/src/i18n/en/translation.json
@@ -1,9 +1,4 @@
 {
-  "logo": {
-    "word1": "Science",
-    "word2": "Passion",
-    "word3": "Technology"
-  },
   "pdf-upload": {
     "upload-field-label": "Upload PDF files to sign",
     "upload-area-text": "In this area you can upload PDF files via Drag & Drop or by selecting them directly",
@@ -24,7 +19,5 @@
   },
   "error-summary": "An error occurred",
   "error-permission-message": "You need have permissions to use the official signature to use this function!",
-  "error-login-message": "You need to be logged in to use this function!",
-  "privacy-policy": "Privacy Policy",
-  "page-updated-needs-reload": "The application has been updated. Please reload the page."
+  "error-login-message": "You need to be logged in to use this function!"
 }
diff --git a/src/vpu-signature.js b/src/vpu-signature.js
index 36c3352e9fe9d7fa40d067da73cdccfa8c2fdbd5..b463f4be36e6455ff979940eb38557b769c8f9c1 100644
--- a/src/vpu-signature.js
+++ b/src/vpu-signature.js
@@ -1 +1 @@
-import './app';
+import 'vpu-app-shell';
diff --git a/test/unit.js b/test/unit.js
index 969d0933029738da5e9186aa962082e0ba8951b7..654fe70cef03c99f6a32b5658edcc2abaedf2a1a 100644
--- a/test/unit.js
+++ b/test/unit.js
@@ -2,7 +2,6 @@ import {assert} from 'chai';
 
 import '../src/vpu-signature-pdf-upload';
 import '../src/vpu-signature.js';
-import {Router} from '../src/app/router.js';
 
 suite('vpu-signature-pdf-upload basics', () => {
   let node;
@@ -40,28 +39,3 @@ suite('vpu-signature-app basics', () => {
   });
 });
 
-suite('router', () => {
-
-  test('basics', () => {
-    const routes = [
-      {
-          name: 'foo',
-          path: '',
-          action: (context) => {
-              return {};
-          }
-      },
-    ];
-
-    const router = new Router(routes, {
-      routeName: 'foo',
-      getState: () => { return {}; },
-      setState: (state) => { },
-    });
-
-    router.setStateFromCurrentLocation();
-    router.update();
-    router.updateFromPathname("/");
-    assert.equal(router.getPathname(), '/');
-  });
-});
diff --git a/vendor/app-shell b/vendor/app-shell
new file mode 160000
index 0000000000000000000000000000000000000000..0fdb2d6ba4f06ae9bc5bd45915698bd608e06fab
--- /dev/null
+++ b/vendor/app-shell
@@ -0,0 +1 @@
+Subproject commit 0fdb2d6ba4f06ae9bc5bd45915698bd608e06fab