diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7fb02fb13033502fc84f850986c9caef6ad28b24..29ef97112b217feef7e25e1f5a1b6e6af2a4c0c9 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -24,7 +24,7 @@ test:
   stage: test
   script:
     - yarn install
-    - yarn run build-dev
+    - APP_ENV=development yarn run build
     - yarn run test-full
 
 linting:
diff --git a/app.config.js b/app.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..5fbba00401416f23d8be33e09be5507b443dc64d
--- /dev/null
+++ b/app.config.js
@@ -0,0 +1,52 @@
+export default {
+    local: {
+        basePath: '/dist/',
+        entryPointURL: 'http://127.0.0.1:8000',
+        keyCloakBaseURL: 'https://auth-dev.tugraz.at/auth',
+        keyCloakClientId: 'auth-dev-mw-frontend-local',
+        matomoUrl: 'https://analytics.tugraz.at/',
+        matomoSiteId: 131,
+        nextcloudBaseURL: 'http://localhost:8081',
+        pdfAsQualifiedlySigningServer: 'https://sig-dev.tugraz.at',
+    },
+    bs: {
+        basePath: '/dist/',
+        entryPointURL: 'http://bs-local.com:8000',
+        keyCloakBaseURL: 'https://auth-test.tugraz.at/auth',
+        keyCloakClientId: 'auth-dev-mw-frontend-local',
+        matomoUrl: 'https://analytics.tugraz.at/',
+        matomoSiteId: 131,
+        nextcloudBaseURL: 'http://bs-local.com:8081',
+        pdfAsQualifiedlySigningServer: 'https://sig-dev.tugraz.at',
+    },
+    development: {
+        basePath: '/apps/signature/',
+        entryPointURL: 'https://mw-dev.tugraz.at',
+        keyCloakBaseURL: 'https://auth-dev.tugraz.at/auth',
+        keyCloakClientId: 'esign-dev_tugraz_at-ESIGN',
+        matomoUrl: 'https://analytics.tugraz.at/',
+        matomoSiteId: 131,
+        nextcloudBaseURL: 'https://nc-dev.tugraz.at/pers',
+        pdfAsQualifiedlySigningServer: 'https://sig-dev.tugraz.at',
+    },
+    demo: {
+        basePath: '/apps/signature/',
+        entryPointURL: 'https://api-demo.tugraz.at',
+        keyCloakBaseURL: 'https://auth-test.tugraz.at/auth',
+        keyCloakClientId: 'esig-demo_tugraz_at-ESIG',
+        matomoUrl: 'https://analytics.tugraz.at/',
+        matomoSiteId: 131,
+        nextcloudBaseURL: 'https://cloud.tugraz.at',
+        pdfAsQualifiedlySigningServer: 'https://sig-test.tugraz.at',
+    },
+    production: {
+        basePath: '/',
+        entryPointURL: 'https://api.tugraz.at',
+        keyCloakBaseURL: 'https://auth.tugraz.at/auth',
+        keyCloakClientId: 'esig_tugraz_at',
+        matomoUrl: 'https://analytics.tugraz.at/',
+        matomoSiteId: 137,
+        nextcloudBaseURL: '',
+        pdfAsQualifiedlySigningServer: 'https://sig.tugraz.at',
+    },
+};
\ No newline at end of file
diff --git a/assets/.htaccess.ejs b/assets/.htaccess.ejs
index f2512eea05f890b3a3137ceedee5ea2830069238..be754ead3713e5f2eb1605c6d29c1ba34202a68e 100644
--- a/assets/.htaccess.ejs
+++ b/assets/.htaccess.ejs
@@ -4,7 +4,7 @@ DirectoryIndex <%= getUrl(name + '.html') %>
 </FilesMatch>
 
 Header set Cache-Control "must-revalidate, max-age=60"
-Header set Content-Security-Policy "default-src 'self' 'unsafe-eval' 'unsafe-inline' analytics.tugraz.at <%= keyCloakServer %> <%= entryPointURL %> httpbin.org <%= nextcloudBaseURL %> www.handy-signatur.at <%= pdfAsQualifiedlySigningServer %>; img-src * blob: data:"
+Header set Content-Security-Policy "default-src 'self' 'unsafe-eval' 'unsafe-inline' <%= matomoUrl %> <%= keyCloakServer %> <%= entryPointURL %> httpbin.org <%= nextcloudBaseURL %> www.handy-signatur.at <%= pdfAsQualifiedlySigningServer %>; img-src * blob: data:"
 
 # Apache adds a "-gzip" suffix to the etag when it uses gzip but doesn't
 # take that into account when receiving requests.
diff --git a/assets/dbp-signature.html.ejs b/assets/dbp-signature.html.ejs
index 5a2e4fa91268f33ad9d1249dbfa566b40638fdfc..8f48cefa9d3b5983664f44e5d655c51f7b71075c 100644
--- a/assets/dbp-signature.html.ejs
+++ b/assets/dbp-signature.html.ejs
@@ -65,7 +65,7 @@
         src="<%= getUrl(name + '.topic.metadata.json') %>"
         base-path="<%= getUrl('') %>"
         keycloak-config='{"url": "<%= keyCloakBaseURL %>", "realm": "tugraz", "clientId": "<%= keyCloakClientId %>", "silentCheckSsoRedirectUri": "<%= getUrl('silent-check-sso.html') %>"}'
-        matomo-url='https://analytics.tugraz.at/'
+        matomo-url='<%= matomoUrl %>'
         matomo-site-id='<%= matomoSiteId %>'
     ><dbp-loading-spinner></dbp-loading-spinner></<%= name %>>
 </dbp-provider>
diff --git a/deploy.php b/deploy.php
index 763d282d8671b7d4f53c51993107e056842e090c..f45bcee4fdab7608865a47d5ddb198b05c329acd 100644
--- a/deploy.php
+++ b/deploy.php
@@ -42,30 +42,16 @@ host('production')
     ->hostname('mw@mw01-prod.tugraz.at')
     ->set('deploy_path', '/home/mw/prod_esig/deploy');
 
-// Demo build task
-task('build-demo', function () {
+task('build', function () {
+    $stage = get('stage');
     runLocally("yarn install");
-    runLocally("yarn run build-demo");
-})->onStage('demo');
-
-// Demo dev task
-task('build-development', function () {
-    runLocally("yarn install");
-    runLocally("yarn run build-dev");
-})->onStage('development');
-
-//Production task
-task('build-production', function () {
-    runLocally("yarn install");
-    runLocally("yarn run build-prod");
-})->onStage('production');
+    runLocally("APP_ENV=$stage yarn run build");
+});
 
 // Deploy task
 task('deploy', [
     'deploy:info',
-    'build-demo',
-    'build-development',
-    'build-production',
+    'build',
     'deploy:prepare',
     'deploy:lock',
     'deploy:release',
diff --git a/package.json b/package.json
index 459f80cf060042f186bdbadcc148f97611e4680e..5620c21a96fd9d4dd3c9ccb0c271072797bb2ffd 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,6 @@
     "@rollup/plugin-commonjs": "^17.0.0",
     "@rollup/plugin-json": "^4.1.0",
     "@rollup/plugin-node-resolve": "^11.0.0",
-    "@rollup/plugin-replace": "^2.3.3",
     "@rollup/plugin-url": "^6.0.0",
     "babel-eslint": "^10.0.3",
     "chai": "^4.2.0",
@@ -41,8 +40,8 @@
     "@dbp-toolkit/app-shell": "^0.1.0",
     "@dbp-toolkit/auth": "^0.1.0",
     "@dbp-toolkit/common": "^0.1.0",
-    "@dbp-toolkit/font-source-sans-pro": "^0.1.0",
     "@dbp-toolkit/file-handling": "^0.1.0",
+    "@dbp-toolkit/font-source-sans-pro": "^0.1.0",
     "@dbp-toolkit/language-select": "^0.1.0",
     "@dbp-toolkit/notification": "^0.1.0",
     "@dbp-toolkit/person-profile": "^0.1.0",
@@ -58,16 +57,14 @@
     "webdav": "^3.3.0"
   },
   "scripts": {
-    "build-dev": "rollup -c --environment BUILD:development",
-    "build-prod": "rollup -c --environment BUILD:production",
-    "build-demo": "rollup -c --environment BUILD:demo",
+    "build": "rollup -c",
     "i18next": "i18next-scanner",
     "watch": "rollup -c --watch",
     "watch-local": "yarn run watch",
     "watch-full": "rollup -c --watch --environment FORCE_FULL",
-    "watch-bs": "rollup -c --watch --environment BUILD:bs",
-    "test": "rollup -c --environment BUILD:test && karma start --singleRun",
-    "test-full": "rollup -c --environment FORCE_FULL,BUILD:test && karma start --singleRun",
+    "watch-bs": "rollup -c --watch --environment APP_ENV:bs",
+    "test": "rollup -c --environment APP_ENV:test && karma start --singleRun",
+    "test-full": "rollup -c --environment FORCE_FULL,APP_ENV:test && karma start --singleRun",
     "lint": "eslint ."
   }
 }
diff --git a/rollup.config.js b/rollup.config.js
index 1b7dad105edf7ebba405a5eec1beafd53e2d9458..cd895e20660288fe2b299a939168b6c768dcf26f 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -7,7 +7,6 @@ import commonjs from '@rollup/plugin-commonjs';
 import copy from 'rollup-plugin-copy';
 import {terser} from "rollup-plugin-terser";
 import json from '@rollup/plugin-json';
-import replace from "@rollup/plugin-replace";
 import serve from 'rollup-plugin-serve';
 import urlPlugin from "@rollup/plugin-url";
 import consts from 'rollup-plugin-consts';
@@ -16,6 +15,7 @@ import del from 'rollup-plugin-delete';
 import emitEJS from 'rollup-plugin-emit-ejs'
 import {getBabelOutputPlugin} from '@rollup/plugin-babel';
 import selfsigned from 'selfsigned';
+import appConfig from './app.config.js';
 
 // -------------------------------
 
@@ -27,103 +27,48 @@ const USE_HTTPS = false;
 // -------------------------------
 
 const pkg = require('./package.json');
-const build = (typeof process.env.BUILD !== 'undefined') ? process.env.BUILD : 'local';
+const appEnv = (typeof process.env.APP_ENV !== 'undefined') ? process.env.APP_ENV : 'local';
 const watch = process.env.ROLLUP_WATCH === 'true';
-const buildFull = (!watch && build !== 'test') || (process.env.FORCE_FULL !== undefined);
-
-console.log("build: " + build);
-let basePath = '';
-let entryPointURL = '';
-let nextcloudBaseURL = 'https://cloud.tugraz.at';
-let nextcloudWebAppPasswordURL = nextcloudBaseURL + '/apps/webapppassword';
-let nextcloudWebDavURL = nextcloudBaseURL + '/remote.php/dav/files';
-let nextcloudName = 'TU Graz cloud';
-let keyCloakServer = '';
-let keyCloakBaseURL = '';
-let keyCloakClientId = '';
-let pdfAsQualifiedlySigningServer = '';
-let matomoSiteId = 131;
+const buildFull = (!watch && appEnv !== 'test') || (process.env.FORCE_FULL !== undefined);
 let useTerser = buildFull;
 let useBabel = buildFull;
 let checkLicenses = buildFull;
 
-switch (build) {
-  case 'local':
-    basePath = '/dist/';
-    entryPointURL = 'http://127.0.0.1:8000';
-    nextcloudBaseURL = 'http://localhost:8081';
-    nextcloudWebAppPasswordURL = nextcloudBaseURL + '/index.php/apps/webapppassword';
-    nextcloudWebDavURL = nextcloudBaseURL + '/remote.php/dav/files';
-    keyCloakServer = 'auth-dev.tugraz.at';
-    keyCloakBaseURL = 'https://' + keyCloakServer + '/auth';
-    keyCloakClientId = 'auth-dev-mw-frontend-local';
-    pdfAsQualifiedlySigningServer = 'sig-dev.tugraz.at';
-    break;
-  case 'development':
-    basePath = '/apps/signature/';
-    entryPointURL = 'https://mw-dev.tugraz.at';
-    // "/pers" can't go here because it's not allowed in the "Content-Security-Policy"
-    nextcloudBaseURL = 'https://nc-dev.tugraz.at';
-    // "/index.php" is needed to don't get a "This origin is not allowed!" because the "target-origin" get parameter can't be read
-    nextcloudWebAppPasswordURL = nextcloudBaseURL + '/pers/index.php/apps/webapppassword';
-    nextcloudWebDavURL = nextcloudBaseURL + '/pers/remote.php/dav/files';
-    keyCloakServer = 'auth-dev.tugraz.at';
-    keyCloakBaseURL = 'https://' + keyCloakServer + '/auth';
-    keyCloakClientId = 'esign-dev_tugraz_at-ESIGN';
-    pdfAsQualifiedlySigningServer = 'sig-dev.tugraz.at';
-    break;
-  case 'demo':
-    basePath = '/apps/signature/';
-    entryPointURL = 'https://api-demo.tugraz.at';
-    // "/pers" can't go here because it's not allowed in the "Content-Security-Policy"
-    nextcloudBaseURL = 'https://cloud.tugraz.at';
-    // "/index.php" is needed to don't get a "This origin is not allowed!" because the "target-origin" get parameter can't be read
-    nextcloudWebAppPasswordURL = nextcloudBaseURL + '/index.php/apps/webapppassword';
-    nextcloudWebDavURL = nextcloudBaseURL + '/remote.php/dav/files';
-    keyCloakServer = 'auth-test.tugraz.at';
-    keyCloakBaseURL = 'https://' + keyCloakServer + '/auth';
-    keyCloakClientId = 'esig-demo_tugraz_at-ESIG';
-    pdfAsQualifiedlySigningServer = 'sig-test.tugraz.at';
-    break;
-  case 'production':
-    basePath = '/';
-    entryPointURL = 'https://api.tugraz.at';
-    nextcloudBaseURL = '';
-    nextcloudWebAppPasswordURL = nextcloudBaseURL + '';
-    nextcloudWebDavURL = nextcloudBaseURL + '';
-    keyCloakServer = 'auth.tugraz.at';
-    keyCloakBaseURL = 'https://' + keyCloakServer + '/auth';
-    keyCloakClientId = 'esig_tugraz_at';
-    pdfAsQualifiedlySigningServer = 'sig.tugraz.at';
-    matomoSiteId = 137;
-    break;
-  case 'test':
-    basePath = '/apps/signature/';
-    entryPointURL = '';
-    nextcloudBaseURL = '';
-    nextcloudWebAppPasswordURL = '';
-    keyCloakServer = '';
-    keyCloakBaseURL = '';
-    keyCloakClientId = '';
-    pdfAsQualifiedlySigningServer = '';
-    break;
-  case 'bs':
-    basePath = '/dist/';
-    entryPointURL = 'http://bs-local.com:8000';
-    nextcloudBaseURL = 'http://bs-local.com:8081';
-    nextcloudWebAppPasswordURL = nextcloudBaseURL + '/index.php/apps/webapppassword';
-    nextcloudWebDavURL = nextcloudBaseURL + '/remote.php/dav/files';
-    keyCloakServer = 'auth-dev.tugraz.at';
-    keyCloakBaseURL = 'https://' + keyCloakServer + '/auth';
-    keyCloakClientId = 'auth-dev-mw-frontend-local';
-    pdfAsQualifiedlySigningServer = 'sig-dev.tugraz.at';
-    break;
-  default:
-    console.error('Unknown build environment: ' + build);
+console.log("APP_ENV: " + appEnv);
+
+let config;
+if (appEnv in appConfig) {
+    config = appConfig[appEnv];
+} else if (appEnv === 'test') {
+    config = {
+        basePath: '/',
+        entryPointURL: 'https://test',
+        keyCloakBaseURL: 'https://test',
+        keyCloakClientId: '',
+        matomoUrl: '',
+        matomoSiteId: -1,
+        nextcloudBaseURL: 'https://test',
+        pdfAsQualifiedlySigningServer: 'https://test'
+    };
+} else {
+    console.error(`Unknown build environment: '${appEnv}', use one of '${Object.keys(appConfig)}'`);
     process.exit(1);
 }
 
-let nextcloudFileURL = nextcloudBaseURL + '/apps/files/?dir=';
+config.keyCloakServer = new URL(config.keyCloakBaseURL).origin;
+config.nextcloudName = 'TU Graz cloud';
+
+if (config.nextcloudBaseURL) {
+    config.nextcloudFileURL = config.nextcloudBaseURL + '/index.php/apps/files/?dir=';
+    config.nextcloudOrigin = new URL(config.nextcloudBaseURL).origin;
+    config.nextcloudWebAppPasswordURL = config.nextcloudBaseURL + '/index.php/apps/webapppassword';
+    config.nextcloudWebDavURL = config.nextcloudBaseURL + '/remote.php/dav/files';
+} else {
+    config.nextcloudFileURL = '';
+    config.nextcloudOrigin = '';
+    config.nextcloudWebAppPasswordURL = '';
+    config.nextcloudWebDavURL = '';
+}
 
 /**
  * Creates a server certificate and caches it in the .cert directory
@@ -164,7 +109,7 @@ function getBuildInfo() {
         info: commit,
         url: newUrl,
         time: new Date().toISOString(),
-        env: build
+        env: appEnv
     }
 }
 
@@ -184,7 +129,7 @@ export async function getPackagePath(packageName, assetPath) {
 }
 
 export default (async () => {return {
-    input: (build != 'test') ? [
+    input: (appEnv != 'test') ? [
       'src/' + pkg.name + '.js',
       'vendor/toolkit/packages/provider/src/dbp-provider.js',
       'src/dbp-official-signature-pdf-upload.js',
@@ -221,33 +166,34 @@ export default (async () => {return {
           targets: 'dist/*'
         }),
         consts({
-          environment: build,
+          environment: appEnv,
           buildinfo: getBuildInfo(),
-          nextcloudBaseURL: nextcloudBaseURL,
+          nextcloudBaseURL: config.nextcloudBaseURL,
         }),
         emitEJS({
           src: 'assets',
           include: ['**/*.ejs', '**/.*.ejs'],
           data: {
             getUrl: (p) => {
-              return url.resolve(basePath, p);
+              return url.resolve(config.basePath, p);
             },
             getPrivateUrl: (p) => {
-                return url.resolve(`${basePath}local/${pkg.name}/`, p);
+                return url.resolve(`${config.basePath}local/${pkg.name}/`, p);
             },
             name: pkg.name,
-            entryPointURL: entryPointURL,
-            nextcloudWebAppPasswordURL: nextcloudWebAppPasswordURL,
-            nextcloudWebDavURL: nextcloudWebDavURL,
-            nextcloudBaseURL: nextcloudBaseURL,
-            nextcloudFileURL: nextcloudFileURL,
-            nextcloudName: nextcloudName,
-            keyCloakServer: keyCloakServer,
-            keyCloakBaseURL: keyCloakBaseURL,
-            keyCloakClientId: keyCloakClientId,
-            pdfAsQualifiedlySigningServer: pdfAsQualifiedlySigningServer,
-            environment: build,
-            matomoSiteId: matomoSiteId,
+            entryPointURL: config.entryPointURL,
+            nextcloudWebAppPasswordURL: config.nextcloudWebAppPasswordURL,
+            nextcloudWebDavURL: config.nextcloudWebDavURL,
+            nextcloudBaseURL: config.nextcloudBaseURL,
+            nextcloudFileURL: config.nextcloudFileURL,
+            nextcloudName: config.nextcloudName,
+            keyCloakServer: config.keyCloakServer,
+            keyCloakBaseURL: config.keyCloakBaseURL,
+            keyCloakClientId: config.keyCloakClientId,
+            pdfAsQualifiedlySigningServer: config.pdfAsQualifiedlySigningServer,
+            environment: appEnv,
+            matomoUrl: config.matomoUrl,
+            matomoSiteId: config.matomoSiteId,
             buildInfo: getBuildInfo()
           }
         }),
@@ -287,9 +233,6 @@ Dependencies:
           emitFiles: true,
           fileName: 'shared/[name].[hash][extname]'
         }),
-        replace({
-            "process.env.BUILD": '"' + build + '"',
-        }),
         copy({
             targets: [
                 {src: 'assets/silent-check-sso.html', dest:'dist'},
@@ -334,10 +277,10 @@ Dependencies:
           contentBase: '.',
           host: '127.0.0.1',
           port: 8001,
-          historyApiFallback: basePath + pkg.name + '.html',
+          historyApiFallback: config.basePath + pkg.name + '.html',
           https: USE_HTTPS ? generateTLSConfig() : false,
           headers: {
-              'Content-Security-Policy': `default-src 'self' 'unsafe-eval' 'unsafe-inline' analytics.tugraz.at ${keyCloakServer} ${entryPointURL} httpbin.org ${nextcloudBaseURL} www.handy-signatur.at ${pdfAsQualifiedlySigningServer} ; img-src * blob: data:`
+              'Content-Security-Policy': `default-src 'self' 'unsafe-eval' 'unsafe-inline' ${config.matomoUrl} ${config.keyCloakServer} ${config.entryPointURL} httpbin.org ${config.nextcloudOrigin} www.handy-signatur.at ${config.pdfAsQualifiedlySigningServer} ; img-src * blob: data:`
           },
         }) : false
     ]