Repository: thelounge/thelounge
Pull request #4558 (2022-05-01 13:56:58)

Config
This is a smaller change than it looks like... all it does really is pull out the config from the Helper namespace... it doesn't belong there.

igalic: funny how often Helper was used to mean Config.

Patch:
diff --git a/.eslintrc.yml b/.eslintrc.yml
index b297f929..0c36982a 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -2,7 +2,7 @@
 root: true
 
 parserOptions:
-  ecmaVersion: 2020
+  ecmaVersion: 2022
 
 env:
   es6: true
diff --git a/.prettierignore b/.prettierignore
index 2119df94..aa316e17 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,6 +1,7 @@
 coverage/
 public/
 test/fixtures/.thelounge/logs/
+test/fixtures/.thelounge/certificates/
 test/fixtures/.thelounge/storage/
 
 *.log
diff --git a/src/client.js b/src/client.js
index a4729415..b19f9db6 100644
--- a/src/client.js
+++ b/src/client.js
@@ -7,7 +7,7 @@ const Chan = require("./models/chan");
 const crypto = require("crypto");
 const Msg = require("./models/msg");
 const Network = require("./models/network");
-const Helper = require("./helper");
+const Config = require("./config");
 const UAParser = require("ua-parser-js");
 const {v4: uuidv4} = require("uuid");
 const escapeRegExp = require("lodash/escapeRegExp");
@@ -72,13 +72,13 @@ function Client(manager, name, config = {}) {
 	client.config.log = Boolean(client.config.log);
 	client.config.password = String(client.config.password);
 
-	if (!Helper.config.public && client.config.log) {
-		if (Helper.config.messageStorage.includes("sqlite")) {
+	if (!Config.values.public && client.config.log) {
+		if (Config.values.messageStorage.includes("sqlite")) {
 			client.messageProvider = new MessageStorage(client);
 			client.messageStorage.push(client.messageProvider);
 		}
 
-		if (Helper.config.messageStorage.includes("text")) {
+		if (Config.values.messageStorage.includes("text")) {
 			client.messageStorage.push(new TextFileMessageStorage(client));
 		}
 
@@ -236,7 +236,7 @@ Client.prototype.connect = function (args, isStartup = false) {
 	const network = new Network({
 		uuid: args.uuid,
 		name: String(
-			args.name || (Helper.config.lockNetwork ? Helper.config.defaults.name : "") || ""
+			args.name || (Config.values.lockNetwork ? Config.values.defaults.name : "") || ""
 		),
 		host: String(args.host || ""),
 		port: parseInt(args.port, 10),
@@ -759,7 +759,7 @@ Client.prototype.unregisterPushSubscription = function (token) {
 
 Client.prototype.save = _.debounce(
 	function SaveClient() {
-		if (Helper.config.public) {
+		if (Config.values.public) {
 			return;
 		}
 
diff --git a/src/clientManager.js b/src/clientManager.js
index e90489ea..dc31317c 100644
--- a/src/clientManager.js
+++ b/src/clientManager.js
@@ -8,7 +8,7 @@ const fs = require("fs");
 const path = require("path");
 const Auth = require("./plugins/auth");
 const Client = require("./client");
-const Helper = require("./helper");
+const Config = require("./config");
 const WebPush = require("./plugins/webpush");
 
 module.exports = ClientManager;
@@ -22,12 +22,12 @@ ClientManager.prototype.init = function (identHandler, sockets) {
 	this.identHandler = identHandler;
 	this.webPush = new WebPush();
 
-	if (!Helper.config.public) {
+	if (!Config.values.public) {
 		this.loadUsers();
 
 		// LDAP does not have user commands, and users are dynamically
 		// created upon logon, so we don't need to watch for new files
-		if (!Helper.config.ldap.enable) {
+		if (!Config.values.ldap.enable) {
 			this.autoloadUsers();
 		}
 	}
@@ -81,7 +81,7 @@ ClientManager.prototype.loadUsers = function () {
 
 ClientManager.prototype.autoloadUsers = function () {
 	fs.watch(
-		Helper.getUsersPath(),
+		Config.getUsersPath(),
 		_.debounce(
 			() => {
 				const loaded = this.clients.map((c) => c.name);
@@ -145,12 +145,12 @@ ClientManager.prototype.loadUser = function (name) {
 };
 
 ClientManager.prototype.getUsers = function () {
-	if (!fs.existsSync(Helper.getUsersPath())) {
+	if (!fs.existsSync(Config.getUsersPath())) {
 		return [];
 	}
 
 	return fs
-		.readdirSync(Helper.getUsersPath())
+		.readdirSync(Config.getUsersPath())
 		.filter((file) => file.endsWith(".json"))
 		.map((file) => file.slice(0, -5));
 };
@@ -160,7 +160,7 @@ ClientManager.prototype.addUser = function (name, password, enableLog) {
 		throw new Error(`${name} is an invalid username.`);
 	}
 
-	const userPath = Helper.getUserConfigPath(name);
+	const userPath = Config.getUserConfigPath(name);
 
 	if (fs.existsSync(userPath)) {
 		log.error(`User ${colors.green(name)} already exists.`);
@@ -182,7 +182,7 @@ ClientManager.prototype.addUser = function (name, password, enableLog) {
 	}
 
 	try {
-		const userFolderStat = fs.statSync(Helper.getUsersPath());
+		const userFolderStat = fs.statSync(Config.getUsersPath());
 		const userFileStat = fs.statSync(userPath);
 
 		if (
@@ -231,7 +231,7 @@ ClientManager.prototype.saveUser = function (client, callback) {
 		return;
 	}
 
-	const pathReal = Helper.getUserConfigPath(client.name);
+	const pathReal = Config.getUserConfigPath(client.name);
 	const pathTemp = pathReal + ".tmp";
 
 	try {
@@ -253,7 +253,7 @@ ClientManager.prototype.saveUser = function (client, callback) {
 };
 
 ClientManager.prototype.removeUser = function (name) {
-	const userPath = Helper.getUserConfigPath(name);
+	const userPath = Config.getUserConfigPath(name);
 
 	if (!fs.existsSync(userPath)) {
 		log.error(`Tried to remove non-existing user ${colors.green(name)}.`);
@@ -266,7 +266,7 @@ ClientManager.prototype.removeUser = function (name) {
 };
 
 function readUserConfig(name) {
-	const userPath = Helper.getUserConfigPath(name);
+	const userPath = Config.getUserConfigPath(name);
 
 	if (!fs.existsSync(userPath)) {
 		log.error(`Tried to read non-existing user ${colors.green(name)}`);
diff --git a/src/command-line/index.js b/src/command-line/index.js
index 0ad1a71d..cfea187e 100644
--- a/src/command-line/index.js
+++ b/src/command-line/index.js
@@ -6,6 +6,7 @@ const path = require("path");
 const colors = require("chalk");
 const program = require("commander");
 const Helper = require("../helper");
+const Config = require("../config");
 const Utils = require("./utils");
 
 program
@@ -20,7 +21,7 @@ program
 // Parse options from `argv` returning `argv` void of these options.
 const argvWithoutOptions = program.parseOptions(process.argv);
 
-Helper.setHome(process.env.THELOUNGE_HOME || Utils.defaultHome());
+Config.setHome(process.env.THELOUNGE_HOME || Utils.defaultHome());
 
 // Check config file owner and warn if we're running under a different user
 try {
@@ -34,11 +35,11 @@ try {
 createPackagesFolder();
 
 // Merge config key-values passed as CLI options into the main config
-Helper.mergeConfig(Helper.config, program.opts().config);
+Config.merge(program.opts().config);
 
 require("./start");
 
-if (!Helper.config.public) {
+if (!Config.values.public) {
 	require("./users");
 }
 
@@ -56,7 +57,7 @@ require("./outdated");
 program.parse(argvWithoutOptions.operands.concat(argvWithoutOptions.unknown));
 
 function createPackagesFolder() {
-	const packagesPath = Helper.getPackagesPath();
+	const packagesPath = Config.getPackagesPath();
 	const packagesConfig = path.join(packagesPath, "package.json");
 
 	// Create node_modules folder, otherwise yarn will start walking upwards to find one
@@ -95,7 +96,7 @@ function verifyFileOwner() {
 		);
 	}
 
-	const configStat = fs.statSync(path.join(Helper.getHomePath(), "config.js"));
+	const configStat = fs.statSync(path.join(Config.getHomePath(), "config.js"));
 
 	if (configStat && configStat.uid !== uid) {
 		log.warn(
diff --git a/src/command-line/install.js b/src/command-line/install.js
index bc30a851..e85e8fed 100644
--- a/src/command-line/install.js
+++ b/src/command-line/install.js
@@ -5,6 +5,7 @@ const colors = require("chalk");
 const semver = require("semver");
 const program = require("commander");
 const Helper = require("../helper");
+const Config = require("../config");
 const Utils = require("./utils");
 
 program
@@ -17,8 +18,8 @@ program
 		const path = require("path");
 		const packageJson = require("package-json");
 
-		if (!fs.existsSync(Helper.getConfigPath())) {
-			log.error(`${Helper.getConfigPath()} does not exist.`);
+		if (!fs.existsSync(Config.getConfigPath())) {
+			log.error(`${Config.getConfigPath()} does not exist.`);
 			return;
 		}
 
diff --git a/src/command-line/start.js b/src/command-line/start.js
index 524b6503..2ccb3a3a 100644
--- a/src/command-line/start.js
+++ b/src/command-line/start.js
@@ -5,7 +5,7 @@ const colors = require("chalk");
 const fs = require("fs");
 const path = require("path");
 const program = require("commander");
-const Helper = require("../helper");
+const Config = require("../config");
 const Utils = require("./utils");
 
 program
@@ -21,15 +21,15 @@ program
 	});
 
 function initalizeConfig() {
-	if (!fs.existsSync(Helper.getConfigPath())) {
-		fs.mkdirSync(Helper.getHomePath(), {recursive: true});
-		fs.chmodSync(Helper.getHomePath(), "0700");
+	if (!fs.existsSync(Config.getConfigPath())) {
+		fs.mkdirSync(Config.getHomePath(), {recursive: true});
+		fs.chmodSync(Config.getHomePath(), "0700");
 		fs.copyFileSync(
 			path.resolve(path.join(__dirname, "..", "..", "defaults", "config.js")),
-			Helper.getConfigPath()
+			Config.getConfigPath()
 		);
-		log.info(`Configuration file created at ${colors.green(Helper.getConfigPath())}.`);
+		log.info(`Configuration file created at ${colors.green(Config.getConfigPath())}.`);
 	}
 
-	fs.mkdirSync(Helper.getUsersPath(), {recursive: true, mode: 0o700});
+	fs.mkdirSync(Config.getUsersPath(), {recursive: true, mode: 0o700});
 }
diff --git a/src/command-line/uninstall.js b/src/command-line/uninstall.js
index 8686e668..a7565bc9 100644
--- a/src/command-line/uninstall.js
+++ b/src/command-line/uninstall.js
@@ -3,7 +3,7 @@
 const log = require("../log");
 const colors = require("chalk");
 const program = require("commander");
-const Helper = require("../helper");
+const Config = require("../config");
 const Utils = require("./utils");
 
 program
@@ -14,7 +14,7 @@ program
 		const fs = require("fs");
 		const path = require("path");
 
-		const packagesConfig = path.join(Helper.getPackagesPath(), "package.json");
+		const packagesConfig = path.join(Config.getPackagesPath(), "package.json");
 		const packages = JSON.parse(fs.readFileSync(packagesConfig, "utf-8"));
 
 		if (
diff --git a/src/command-line/upgrade.js b/src/command-line/upgrade.js
index 6911ad72..57cc65b4 100644
--- a/src/command-line/upgrade.js
+++ b/src/command-line/upgrade.js
@@ -3,7 +3,7 @@
 const log = require("../log");
 const colors = require("chalk");
 const program = require("commander");
-const Helper = require("../helper");
+const Config = require("../config");
 const Utils = require("./utils");
 
 program
@@ -15,7 +15,7 @@ program
 		const path = require("path");
 
 		// Get paths to the location of packages directory
-		const packagesConfig = path.join(Helper.getPackagesPath(), "package.json");
+		const packagesConfig = path.join(Config.getPackagesPath(), "package.json");
 		const packagesList = JSON.parse(fs.readFileSync(packagesConfig, "utf-8")).dependencies;
 		const argsList = ["upgrade", "--latest"];
 
diff --git a/src/command-line/users/add.js b/src/command-line/users/add.js
index 4f3b9ba7..98140e16 100644
--- a/src/command-line/users/add.js
+++ b/src/command-line/users/add.js
@@ -5,6 +5,7 @@ const colors = require("chalk");
 const program = require("commander");
 const fs = require("fs");
 const Helper = require("../../helper");
+const Config = require("../../config");
 const Utils = require("../utils");
 
 program
@@ -14,8 +15,8 @@ program
 	.option("--password [password]", "new password, will be prompted if not specified")
 	.option("--save-logs", "if password is specified, this enables saving logs to disk")
 	.action(function (name, cmdObj) {
-		if (!fs.existsSync(Helper.getUsersPath())) {
-			log.error(`${Helper.getUsersPath()} does not exist.`);
+		if (!fs.existsSync(Config.getUsersPath())) {
+			log.error(`${Config.getUsersPath()} does not exist.`);
 			return;
 		}
 
@@ -76,5 +77,5 @@ function add(manager, name, password, enableLog) {
 	manager.addUser(name, hash, enableLog);
 
 	log.info(`User ${colors.bold(name)} created.`);
-	log.info(`User file located at ${colors.green(Helper.getUserConfigPath(name))}.`);
+	log.info(`User file located at ${colors.green(Config.getUserConfigPath(name))}.`);
 }
diff --git a/src/command-line/users/edit.js b/src/command-line/users/edit.js
index fc28f35d..0e9f4705 100644
--- a/src/command-line/users/edit.js
+++ b/src/command-line/users/edit.js
@@ -5,16 +5,16 @@ const program = require("commander");
 const child = require("child_process");
 const colors = require("chalk");
 const fs = require("fs");
-const Helper = require("../../helper");
+const Config = require("../../config");
 const Utils = require("../utils");
 
 program
 	.command("edit <name>")
-	.description(`Edit user file located at ${colors.green(Helper.getUserConfigPath("<name>"))}`)
+	.description(`Edit user file located at ${colors.green(Config.getUserConfigPath("<name>"))}`)
 	.on("--help", Utils.extraHelp)
 	.action(function (name) {
-		if (!fs.existsSync(Helper.getUsersPath())) {
-			log.error(`${Helper.getUsersPath()} does not exist.`);
+		if (!fs.existsSync(Config.getUsersPath())) {
+			log.error(`${Config.getUsersPath()} does not exist.`);
 			return;
 		}
 
@@ -33,12 +33,12 @@ program
 
 		const child_spawn = child.spawn(
 			process.env.EDITOR || "vi",
-			[Helper.getUserConfigPath(name)],
+			[Config.getUserConfigPath(name)],
 			{stdio: "inherit"}
 		);
 		child_spawn.on("error", function () {
 			log.error(
-				`Unable to open ${colors.green(Helper.getUserConfigPath(name))}. ${colors.bold(
+				`Unable to open ${colors.green(Config.getUserConfigPath(name))}. ${colors.bold(
 					"$EDITOR"
 				)} is not set, and ${colors.bold("vi")} was not found.`
 			);
diff --git a/src/command-line/users/index.js b/src/command-line/users/index.js
index b4784963..76ed999f 100644
--- a/src/command-line/users/index.js
+++ b/src/command-line/users/index.js
@@ -1,6 +1,6 @@
 "use strict";
 
-if (!require("../../helper").config.ldap.enable) {
+if (!require("../../config").values.ldap.enable) {
 	require("./add");
 	require("./reset");
 }
diff --git a/src/command-line/users/remove.js b/src/command-line/users/remove.js
index 4b47c407..840b69a8 100644
--- a/src/command-line/users/remove.js
+++ b/src/command-line/users/remove.js
@@ -4,7 +4,7 @@ const log = require("../../log");
 const colors = require("chalk");
 const program = require("commander");
 const fs = require("fs");
-const Helper = require("../../helper");
+const Config = require("../../config");
 const Utils = require("../utils");
 
 program
@@ -12,8 +12,8 @@ program
 	.description("Remove an existing user")
 	.on("--help", Utils.extraHelp)
 	.action(function (name) {
-		if (!fs.existsSync(Helper.getUsersPath())) {
-			log.error(`${Helper.getUsersPath()} does not exist.`);
+		if (!fs.existsSync(Config.getUsersPath())) {
+			log.error(`${Config.getUsersPath()} does not exist.`);
 			return;
 		}
 
diff --git a/src/command-line/users/reset.js b/src/command-line/users/reset.js
index bcf9ea76..89a7f2f5 100644
--- a/src/command-line/users/reset.js
+++ b/src/command-line/users/reset.js
@@ -5,6 +5,7 @@ const colors = require("chalk");
 const program = require("commander");
 const fs = require("fs");
 const Helper = require("../../helper");
+const Config = require("../../config");
 const Utils = require("../utils");
 
 program
@@ -13,8 +14,8 @@ program
 	.on("--help", Utils.extraHelp)
 	.option("--password [password]", "new password, will be prompted if not specified")
 	.action(function (name, cmdObj) {
-		if (!fs.existsSync(Helper.getUsersPath())) {
-			log.error(`${Helper.getUsersPath()} does not exist.`);
+		if (!fs.existsSync(Config.getUsersPath())) {
+			log.error(`${Config.getUsersPath()} does not exist.`);
 			return;
 		}
 
@@ -52,7 +53,7 @@ program
 	});
 
 function change(name, password) {
-	const pathReal = Helper.getUserConfigPath(name);
+	const pathReal = Config.getUserConfigPath(name);
 	const pathTemp = pathReal + ".tmp";
 	const user = JSON.parse(fs.readFileSync(pathReal, "utf-8"));
 
diff --git a/src/command-line/utils.js b/src/command-line/utils.js
index 859d2991..c6cec2e3 100644
--- a/src/command-line/utils.js
+++ b/src/command-line/utils.js
@@ -5,6 +5,7 @@ const log = require("../log");
 const colors = require("chalk");
 const fs = require("fs");
 const Helper = require("../helper");
+const Config = require("../config");
 const path = require("path");
 
 let home;
@@ -91,7 +92,7 @@ class Utils {
 
 	static executeYarnCommand(command, ...parameters) {
 		const yarn = require.resolve("yarn/bin/yarn.js");
-		const packagesPath = Helper.getPackagesPath();
+		const packagesPath = Config.getPackagesPath();
 		const cachePath = path.join(packagesPath, "package_manager_cache");
 
 		const staticParameters = [
diff --git a/src/config.js b/src/config.js
new file mode 100644
index 00000000..e7860f8f
--- /dev/null
+++ b/src/config.js
@@ -0,0 +1,182 @@
+"use strict";
+
+const path = require("path");
+const fs = require("fs");
+const os = require("os");
+const _ = require("lodash");
+const colors = require("chalk");
+const log = require("./log");
+const Helper = require("./helper");
+
+class Config {
+	values = require(path.resolve(path.join(__dirname, "..", "defaults", "config.js")));
+	#homePath;
+
+	getHomePath() {
+		return this.#homePath;
+	}
+
+	getConfigPath() {
+		return path.join(this.#homePath, "config.js");
+	}
+
+	getUserLogsPath() {
+		return path.join(this.#homePath, "logs");
+	}
+
+	getStoragePath() {
+		return path.join(this.#homePath, "storage");
+	}
+
+	getFileUploadPath() {
+		return path.join(this.#homePath, "uploads");
+	}
+
+	getUsersPath() {
+		return path.join(this.#homePath, "users");
+	}
+
+	getUserConfigPath(name) {
+		return path.join(this.getUsersPath(), `${name}.json`);
+	}
+
+	getClientCertificatesPath() {
+		return path.join(this.#homePath, "certificates");
+	}
+
+	getPackagesPath() {
+		return path.join(this.#homePath, "packages");
+	}
+
+	getPackageModulePath(packageName) {
+		return path.join(this.getPackagesPath(), "node_modules", packageName);
+	}
+
+	getDefaultNick() {
+		if (!this.values.defaults.nick) {
+			return "thelounge";
+		}
+
+		return this.values.defaults.nick.replace(/%/g, () => Math.floor(Math.random() * 10));
+	}
+
+	merge(newConfig) {
+		this._merge_config_objects(this.values, newConfig);
+	}
+
+	_merge_config_objects(oldConfig, newConfig) {
+		// semi exposed function so that we can test it
+		// it mutates the oldConfig, but returns it as a convenience for testing
+
+		for (const key in newConfig) {
+			if (!Object.prototype.hasOwnProperty.call(oldConfig, key)) {
+				log.warn(`Unknown key "${colors.bold(key)}", please verify your config.`);
+			}
+		}
+
+		return _.mergeWith(oldConfig, newConfig, (objValue, srcValue, key) => {
+			// Do not override config variables if the type is incorrect (e.g. object changed into a string)
+			if (
+				typeof objValue !== "undefined" &&
+				objValue !== null &&
+				typeof objValue !== typeof srcValue
+			) {
+				log.warn(`Incorrect type for "${colors.bold(key)}", please verify your config.`);
+
+				return objValue;
+			}
+
+			// For arrays, simply override the value with user provided one.
+			if (_.isArray(objValue)) {
+				return srcValue;
+			}
+		});
+	}
+
+	setHome(newPath) {
+		this.#homePath = Helper.expandHome(newPath);
+
+		// Reload config from new home location
+		const configPath = this.getConfigPath();
+
+		if (fs.existsSync(configPath)) {
+			const userConfig = require(configPath);
+
+			if (_.isEmpty(userConfig)) {
+				log.warn(
+					`The file located at ${colors.green(
+						configPath
+					)} does not appear to expose anything.`
+				);
+				log.warn(
+					`Make sure it is non-empty and the configuration is exported using ${colors.bold(
+						"module.exports = { ... }"
+					)}.`
+				);
+				log.warn("Using default configuration...");
+			}
+
+			this.merge(userConfig);
+		}
+
+		if (this.values.fileUpload.baseUrl) {
+			try {
+				new URL("test/file.png", this.values.fileUpload.baseUrl);
+			} catch (e) {
+				this.values.fileUpload.baseUrl = null;
+
+				log.warn(`The ${colors.bold("fileUpload.baseUrl")} you specified is invalid: ${e}`);
+			}
+		}
+
+		const manifestPath = path.resolve(
+			path.join(__dirname, "..", "public", "thelounge.webmanifest")
+		);
+
+		// Check if manifest exists, if not, the app most likely was not built
+		if (!fs.existsSync(manifestPath)) {
+			log.error(
+				`The client application was not built. Run ${colors.bold(
+					"NODE_ENV=production yarn build"
+				)} to resolve this.`
+			);
+			process.exit(1);
+		}
+
+		// Load theme color from the web manifest
+		const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
+		this.values.themeColor = manifest.theme_color;
+
+		// log dir probably shouldn't be world accessible.
+		// Create it with the desired permission bits if it doesn't exist yet.
+		let logsStat = undefined;
+
+		const userLogsPath = this.getUserLogsPath();
+
+		try {
+			logsStat = fs.statSync(userLogsPath);
+		} catch {
+			// ignored on purpose, node v14.17.0 will give us {throwIfNoEntry: false}
+		}
+
+		if (!logsStat) {
+			try {
+				fs.mkdirSync(userLogsPath, {recursive: true, mode: 0o750});
+			} catch (e) {
+				log.error("Unable to create logs directory", e);
+			}
+		} else if (logsStat && logsStat.mode & 0o001) {
+			log.warn(
+				"contents of",
+				userLogsPath,
+				"can be accessed by any user, the log files may be exposed"
+			);
+
+			if (os.platform() !== "win32") {
+				log.warn(`run \`chmod o-x ${userLogsPath}\` to correct it`);
+			}
+		}
+	}
+}
+
+module.exports = new Config();
diff --git a/src/helper.js b/src/helper.js
index 58316b97..4777a10c 100644
--- a/src/helper.js
+++ b/src/helper.js
@@ -2,45 +2,20 @@
 
 const pkg = require("../package.json");
 const _ = require("lodash");
-const log = require("./log");
 const path = require("path");
 const os = require("os");
 const fs = require("fs");
 const net = require("net");
 const bcrypt = require("bcryptjs");
-const colors = require("chalk");
 const crypto = require("crypto");
 
-let homePath;
-let configPath;
-let usersPath;
-let storagePath;
-let packagesPath;
-let fileUploadPath;
-let userLogsPath;
-let clientCertificatesPath;
-
 const Helper = {
-	config: null,
 	expandHome,
-	getHomePath,
-	getPackagesPath,
-	getPackageModulePath,
-	getStoragePath,
-	getConfigPath,
-	getFileUploadPath,
-	getUsersPath,
-	getUserConfigPath,
-	getUserLogsPath,
-	getClientCertificatesPath,
-	setHome,
 	getVersion,
 	getVersionCacheBust,
 	getVersionNumber,
 	getGitCommit,
 	ip2hex,
-	mergeConfig,
-	getDefaultNick,
 	parseHostmask,
 	compareHostmask,
 	compareWithWildcard,
@@ -54,8 +29,6 @@ const Helper = {
 
 module.exports = Helper;
 
-Helper.config = require(path.resolve(path.join(__dirname, "..", "defaults", "config.js")));
-
 function getVersion() {
 	const gitCommit = getGitCommit();
 	const version = `v${pkg.version}`;
@@ -100,134 +73,6 @@ function getVersionCacheBust() {
 	return hash.substring(0, 10);
 }
 
-function setHome(newPath) {
-	homePath = expandHome(newPath);
-	configPath = path.join(homePath, "config.js");
-	usersPath = path.join(homePath, "users");
-	storagePath = path.join(homePath, "storage");
-	fileUploadPath = path.join(homePath, "uploads");
-	packagesPath = path.join(homePath, "packages");
-	userLogsPath = path.join(homePath, "logs");
-	clientCertificatesPath = path.join(homePath, "certificates");
-
-	// Reload config from new home location
-	if (fs.existsSync(configPath)) {
-		const userConfig = require(configPath);
-
-		if (_.isEmpty(userConfig)) {
-			log.warn(
-				`The file located at ${colors.green(
-					configPath
-				)} does not appear to expose anything.`
-			);
-			log.warn(
-				`Make sure it is non-empty and the configuration is exported using ${colors.bold(
-					"module.exports = { ... }"
-				)}.`
-			);
-			log.warn("Using default configuration...");
-		}
-
-		mergeConfig(this.config, userConfig);
-	}
-
-	if (this.config.fileUpload.baseUrl) {
-		try {
-			new URL("test/file.png", this.config.fileUpload.baseUrl);
-		} catch (e) {
-			this.config.fileUpload.baseUrl = null;
-
-			log.warn(`The ${colors.bold("fileUpload.baseUrl")} you specified is invalid: ${e}`);
-		}
-	}
-
-	const manifestPath = path.resolve(
-		path.join(__dirname, "..", "public", "thelounge.webmanifest")
-	);
-
-	// Check if manifest exists, if not, the app most likely was not built
-	if (!fs.existsSync(manifestPath)) {
-		log.error(
-			`The client application was not built. Run ${colors.bold(
-				"NODE_ENV=production yarn build"
-			)} to resolve this.`
-		);
-		process.exit(1);
-	}
-
-	// Load theme color from the web manifest
-	const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
-	this.config.themeColor = manifest.theme_color;
-
-	// log dir probably shouldn't be world accessible.
-	// Create it with the desired permission bits if it doesn't exist yet.
-	let logsStat = undefined;
-
-	try {
-		logsStat = fs.statSync(userLogsPath);
-	} catch {
-		// ignored on purpose, node v14.17.0 will give us {throwIfNoEntry: false}
-	}
-
-	if (!logsStat) {
-		try {
-			fs.mkdirSync(userLogsPath, {recursive: true, mode: 0o750});
-		} catch (e) {
-			log.error("Unable to create logs directory", e);
-		}
-	} else if (logsStat && logsStat.mode & 0o001) {
-		log.warn(
-			"contents of",
-			userLogsPath,
-			"can be accessed by any user, the log files may be exposed"
-		);
-
-		if (os.platform() !== "win32") {
-			log.warn(`run \`chmod o-x ${userLogsPath}\` to correct it`);
-		}
-	}
-}
-
-function getHomePath() {
-	return homePath;
-}
-
-function getConfigPath() {
-	return configPath;
-}
-
-function getFileUploadPath() {
-	return fileUploadPath;
-}
-
-function getUsersPath() {
-	return usersPath;
-}
-
-function getUserConfigPath(name) {
-	return path.join(usersPath, name + ".json");
-}
-
-function getUserLogsPath() {
-	return userLogsPath;
-}
-
-function getClientCertificatesPath() {
-	return clientCertificatesPath;
-}
-
-function getStoragePath() {
-	return storagePath;
-}
-
-function getPackagesPath() {
-	return packagesPath;
-}
-
-function getPackageModulePath(packageName) {
-	return path.join(Helper.getPackagesPath(), "node_modules", packageName);
-}
-
 function ip2hex(address) {
 	// no ipv6 support
 	if (!net.isIPv4(address)) {
@@ -271,40 +116,6 @@ function passwordCompare(password, expected) {
 	return bcrypt.compare(password, expected);
 }
 
-function getDefaultNick() {
-	if (!this.config.defaults.nick) {
-		return "thelounge";
-	}
-
-	return this.config.defaults.nick.replace(/%/g, () => Math.floor(Math.random() * 10));
-}
-
-function mergeConfig(oldConfig, newConfig) {
-	for (const key in newConfig) {
-		if (!Object.prototype.hasOwnProperty.call(oldConfig, key)) {
-			log.warn(`Unknown key "${colors.bold(key)}", please verify your config.`);
-		}
-	}
-
-	return _.mergeWith(oldConfig, newConfig, (objValue, srcValue, key) => {
-		// Do not override config variables if the type is incorrect (e.g. object changed into a string)
-		if (
-			typeof objValue !== "undefined" &&
-			objValue !== null &&
-			typeof objValue !== typeof srcValue
-		) {
-			log.warn(`Incorrect type for "${colors.bold(key)}", please verify your config.`);
-
-			return objValue;
-		}
-
-		// For arrays, simply override the value with user provided one.
-		if (_.isArray(objValue)) {
-			return srcValue;
-		}
-	});
-}
-
 function parseHostmask(hostmask) {
 	let nick = "";
 	let ident = "*";
diff --git a/src/identification.js b/src/identification.js
index 5ddc06cf..59ebc590 100644
--- a/src/identification.js
+++ b/src/identification.js
@@ -5,20 +5,21 @@ const fs = require("fs");
 const net = require("net");
 const colors = require("chalk");
 const Helper = require("./helper");
+const Config = require("./config");
 
 class Identification {
 	constructor(startedCallback) {
 		this.connectionId = 0;
 		this.connections = new Map();
 
-		if (typeof Helper.config.oidentd === "string") {
-			this.oidentdFile = Helper.expandHome(Helper.config.oidentd);
+		if (typeof Config.values.oidentd === "string") {
+			this.oidentdFile = Helper.expandHome(Config.values.oidentd);
 			log.info(`Oidentd file: ${colors.green(this.oidentdFile)}`);
 
 			this.refresh();
 		}
 
-		if (Helper.config.identd.enable) {
+		if (Config.values.identd.enable) {
 			if (this.oidentdFile) {
 				log.warn(
 					"Using both identd and oidentd at the same time, this is most likely not intended."
@@ -33,8 +34,8 @@ class Identification {
 
 			server.listen(
 				{
-					port: Helper.config.identd.port || 113,
-					host: Helper.config.bind,
+					port: Config.values.identd.port || 113,
+					host: Config.values.bind,
 				},
 				() => {
 					const address = server.address();
diff --git a/src/models/chan.js b/src/models/chan.js
index 5191d898..451cdc6a 100644
--- a/src/models/chan.js
+++ b/src/models/chan.js
@@ -2,7 +2,7 @@
 
 const _ = require("lodash");
 const log = require("../log");
-const Helper = require("../helper");
+const Config = require("../config");
 const User = require("./user");
 const Msg = require("./msg");
 const storage = require("../plugins/storage");
@@ -81,7 +81,7 @@ Chan.prototype.pushMessage = function (client, msg, increasesUnread) {
 
 	// Never store messages in public mode as the session
 	// is completely destroyed when the page gets closed
-	if (Helper.config.public) {
+	if (Config.values.public) {
 		return;
 	}
 
@@ -92,19 +92,19 @@ Chan.prototype.pushMessage = function (client, msg, increasesUnread) {
 
 	this.writeUserLog(client, msg);
 
-	if (Helper.config.maxHistory >= 0 && this.messages.length > Helper.config.maxHistory) {
-		const deleted = this.messages.splice(0, this.messages.length - Helper.config.maxHistory);
+	if (Config.values.maxHistory >= 0 && this.messages.length > Config.values.maxHistory) {
+		const deleted = this.messages.splice(0, this.messages.length - Config.values.maxHistory);
 
 		// If maxHistory is 0, image would be dereferenced before client had a chance to retrieve it,
 		// so for now, just don't implement dereferencing for this edge case.
-		if (Helper.config.maxHistory > 0) {
+		if (Config.values.maxHistory > 0) {
 			this.dereferencePreviews(deleted);
 		}
 	}
 };
 
 Chan.prototype.dereferencePreviews = function (messages) {
-	if (!Helper.config.prefetch || !Helper.config.prefetchStorage) {
+	if (!Config.values.prefetch || !Config.values.prefetchStorage) {
 		return;
 	}
 
diff --git a/src/models/network.js b/src/models/network.js
index b882b181..1ceee867 100644
--- a/src/models/network.js
+++ b/src/models/network.js
@@ -7,6 +7,7 @@ const Chan = require("./chan");
 const Msg = require("./msg");
 const Prefix = require("./prefix");
 const Helper = require("../helper");
+const Config = require("../config");
 const STSPolicies = require("../plugins/sts");
 const ClientCertificate = require("../plugins/clientCertificate");
 
@@ -92,7 +93,7 @@ Network.prototype.validate = function (client) {
 	// Remove new lines and limit length
 	const cleanString = (str) => str.replace(/[\x00\r\n]/g, "").substring(0, 300);
 
-	this.setNick(cleanNick(String(this.nick || Helper.getDefaultNick())));
+	this.setNick(cleanNick(String(this.nick || Config.getDefaultNick())));
 
 	if (!this.username) {
 		// If username is empty, make one from the provided nick
@@ -133,28 +134,28 @@ Network.prototype.validate = function (client) {
 		this.sasl = "";
 	}
 
-	if (Helper.config.lockNetwork) {
+	if (Config.values.lockNetwork) {
 		// This check is needed to prevent invalid user configurations
 		if (
-			!Helper.config.public &&
+			!Config.values.public &&
 			this.host &&
 			this.host.length > 0 &&
-			this.host !== Helper.config.defaults.host
+			this.host !== Config.values.defaults.host
 		) {
 			error(this, `The hostname you specified (${this.host}) is not allowed.`);
 			return false;
 		}
 
-		if (Helper.config.public) {
-			this.name = Helper.config.defaults.name;
+		if (Config.values.public) {
+			this.name = Config.values.defaults.name;
 			// Sync lobby channel name
-			this.channels[0].name = Helper.config.defaults.name;
+			this.channels[0].name = Config.values.defaults.name;
 		}
 
-		this.host = Helper.config.defaults.host;
-		this.port = Helper.config.defaults.port;
-		this.tls = Helper.config.defaults.tls;
-		this.rejectUnauthorized = Helper.config.defaults.rejectUnauthorized;
+		this.host = Config.values.defaults.host;
+		this.port = Config.values.defaults.port;
+		this.tls = Config.values.defaults.tls;
+		this.rejectUnauthorized = Config.values.defaults.rejectUnauthorized;
 	}
 
 	if (this.host.length === 0) {
@@ -181,7 +182,7 @@ Network.prototype.validate = function (client) {
 Network.prototype.createIrcFramework = function (client) {
 	this.irc = new IrcFramework.Client({
 		version: false, // We handle it ourselves
-		outgoing_addr: Helper.config.bind,
+		outgoing_addr: Config.values.bind,
 		enable_chghost: true,
 		enable_echomessage: true,
 		enable_setname: true,
@@ -205,7 +206,7 @@ Network.prototype.setIrcFrameworkOptions = function (client) {
 	this.irc.options.port = this.port;
 	this.irc.options.password = this.password;
 	this.irc.options.nick = this.nick;
-	this.irc.options.username = Helper.config.useHexIp
+	this.irc.options.username = Config.values.useHexIp
 		? Helper.ip2hex(client.config.browser.ip)
 		: this.username;
 	this.irc.options.gecos = this.realname;
@@ -243,14 +244,14 @@ Network.prototype.setIrcFrameworkOptions = function (client) {
 
 Network.prototype.createWebIrc = function (client) {
 	if (
-		!Helper.config.webirc ||
-		!Object.prototype.hasOwnProperty.call(Helper.config.webirc, this.host)
+		!Config.values.webirc ||
+		!Object.prototype.hasOwnProperty.call(Config.values.webirc, this.host)
 	) {
 		return null;
 	}
 
 	const webircObject = {
-		password: Helper.config.webirc[this.host],
+		password: Config.values.webirc[this.host],
 		username: "thelounge",
 		address: client.config.browser.ip,
 		hostname: client.config.browser.hostname,
@@ -263,9 +264,9 @@ Network.prototype.createWebIrc = function (client) {
 		};
 	}
 
-	if (typeof Helper.config.webirc[this.host] === "function") {
+	if (typeof Config.values.webirc[this.host] === "function") {
 		webircObject.password = null;
-		return Helper.config.webirc[this.host](webircObject, this);
+		return Config.values.webirc[this.host](webircObject, this);
 	}
 
 	return webircObject;
@@ -462,7 +463,7 @@ Network.prototype.quit = function (quitMessage) {
 	// https://ircv3.net/specs/extensions/sts#rescheduling-expiry-on-disconnect
 	STSPolicies.refreshExpiration(this.host);
 
-	this.irc.quit(quitMessage || this.leaveMessage || Helper.config.leaveMessage);
+	this.irc.quit(quitMessage || this.leaveMessage || Config.values.leaveMessage);
 };
 
 Network.prototype.exportForEdit = function () {
@@ -486,7 +487,7 @@ Network.prototype.exportForEdit = function () {
 		"proxyPassword",
 	];
 
-	if (!Helper.config.lockNetwork) {
+	if (!Config.values.lockNetwork) {
 		fieldsToReturn.push("host");
 		fieldsToReturn.push("port");
 		fieldsToReturn.push("tls");
diff --git a/src/plugins/auth/ldap.js b/src/plugins/auth/ldap.js
index 1c93419f..952d9d14 100644
--- a/src/plugins/auth/ldap.js
+++ b/src/plugins/auth/ldap.js
@@ -1,12 +1,12 @@
 "use strict";
 
 const log = require("../../log");
-const Helper = require("../../helper");
+const Config = require("../../config");
 const ldap = require("ldapjs");
 const colors = require("chalk");
 
 function ldapAuthCommon(user, bindDN, password, callback) {
-	const config = Helper.config;
+	const config = Config.values;
 
 	const ldapclient = ldap.createClient({
 		url: config.ldap.url,
@@ -35,7 +35,7 @@ function simpleLdapAuth(user, password, callback) {
 		return callback(false);
 	}
 
-	const config = Helper.config;
+	const config = Config.values;
 
 	const userDN = user.replace(/([,\\/#+<>;"= ])/g, "\\$1");
 	const bindDN = `${config.ldap.primaryKey}=${userDN},${config.ldap.baseDN}`;
@@ -53,7 +53,7 @@ function advancedLdapAuth(user, password, callback) {
 		return callback(false);
 	}
 
-	const config = Helper.config;
+	const config = Config.values;
 	const userDN = user.replace(/([,\\/#+<>;"= ])/g, "\\$1");
 
 	const ldapclient = ldap.createClient({
@@ -132,7 +132,7 @@ function ldapAuth(manager, client, user, password, callback) {
 
 	let auth;
 
-	if ("baseDN" in Helper.config.ldap) {
+	if ("baseDN" in Config.values.ldap) {
 		auth = simpleLdapAuth;
 	} else {
 		auth = advancedLdapAuth;
@@ -147,7 +147,7 @@ function ldapAuth(manager, client, user, password, callback) {
  */
 
 function advancedLdapLoadUsers(users, callbackLoadUser) {
-	const config = Helper.config;
+	const config = Config.values;
 
 	const ldapclient = ldap.createClient({
 		url: config.ldap.url,
@@ -212,7 +212,7 @@ function advancedLdapLoadUsers(users, callbackLoadUser) {
 }
 
 function ldapLoadUsers(users, callbackLoadUser) {
-	if ("baseDN" in Helper.config.ldap) {
+	if ("baseDN" in Config.values.ldap) {
 		// simple LDAP case can't test for user existence without access to the
 		// user's unhashed password, so indicate need to fallback to default
 		// loadUser behaviour by returning false
@@ -223,7 +223,7 @@ function ldapLoadUsers(users, callbackLoadUser) {
 }
 
 function isLdapEnabled() {
-	return !Helper.config.public && Helper.config.ldap.enable;
+	return !Config.values.public && Config.values.ldap.enable;
 }
 
 module.exports = {
diff --git a/src/plugins/clientCertificate.js b/src/plugins/clientCertificate.js
index ba5617c3..eb76d97e 100644
--- a/src/plugins/clientCertificate.js
+++ b/src/plugins/clientCertificate.js
@@ -5,7 +5,7 @@ const fs = require("fs");
 const crypto = require("crypto");
 const {md, pki} = require("node-forge");
 const log = require("../log");
-const Helper = require("../helper");
+const Config = require("../config");
 
 module.exports = {
 	get,
@@ -13,11 +13,11 @@ module.exports = {
 };
 
 function get(uuid) {
-	if (Helper.config.public) {
+	if (Config.values.public) {
 		return null;
 	}
 
-	const folderPath = Helper.getClientCertificatesPath();
+	const folderPath = Config.getClientCertificatesPath();
 	const paths = getPaths(folderPath, uuid);
 
 	if (!fs.existsSync(paths.privateKeyPath) || !fs.existsSync(paths.certificatePath)) {
@@ -37,11 +37,11 @@ function get(uuid) {
 }
 
 function remove(uuid) {
-	if (Helper.config.public) {
+	if (Config.values.public) {
 		return null;
 	}
 
-	const paths = getPaths(Helper.getClientCertificatesPath(), uuid);
+	const paths = getPaths(Config.getClientCertificatesPath(), uuid);
 
 	try {
 		if (fs.existsSync(paths.privateKeyPath)) {
diff --git a/src/plugins/inputs/part.js b/src/plugins/inputs/part.js
index 988dfba5..00c78893 100644
--- a/src/plugins/inputs/part.js
+++ b/src/plugins/inputs/part.js
@@ -2,7 +2,7 @@
 
 const Msg = require("../../models/msg");
 const Chan = require("../../models/chan");
-const Helper = require("../../helper");
+const Config = require("../../config");
 
 exports.commands = ["close", "leave", "part"];
 exports.allowDisconnected = true;
@@ -42,7 +42,7 @@ exports.input = function (network, chan, cmd, args) {
 	) {
 		this.part(network, target);
 	} else {
-		const partMessage = args.join(" ") || network.leaveMessage || Helper.config.leaveMessage;
+		const partMessage = args.join(" ") || network.leaveMessage || Config.values.leaveMessage;
 		network.irc.part(target.name, partMessage);
 	}
 
diff --git a/src/plugins/irc-events/connection.js b/src/plugins/irc-events/connection.js
index da4ba42f..608c6063 100644
--- a/src/plugins/irc-events/connection.js
+++ b/src/plugins/irc-events/connection.js
@@ -5,6 +5,7 @@ const log = require("../../log");
 const Msg = require("../../models/msg");
 const Chan = require("../../models/chan");
 const Helper = require("../../helper");
+const Config = require("../../config");
 
 module.exports = function (irc, network) {
 	const client = this;
@@ -93,7 +94,7 @@ module.exports = function (irc, network) {
 	irc.on("raw socket connected", function (socket) {
 		let ident = client.name || network.username;
 
-		if (Helper.config.useHexIp) {
+		if (Config.values.useHexIp) {
 			ident = Helper.ip2hex(client.config.browser.ip);
 		}
 
@@ -138,7 +139,7 @@ module.exports = function (irc, network) {
 		sendStatus();
 	});
 
-	if (Helper.config.debug.ircFramework) {
+	if (Config.values.debug.ircFramework) {
 		irc.on("debug", function (message) {
 			log.debug(
 				`[${client.name} (${client.id}) on ${network.name} (${network.uuid}]`,
@@ -147,7 +148,7 @@ module.exports = function (irc, network) {
 		});
 	}
 
-	if (Helper.config.debug.raw) {
+	if (Config.values.debug.raw) {
 		irc.on("raw", function (message) {
 			network.channels[0].pushMessage(
 				client,
diff --git a/src/plugins/irc-events/error.js b/src/plugins/irc-events/error.js
index 29f95ab6..8bb49af6 100644
--- a/src/plugins/irc-events/error.js
+++ b/src/plugins/irc-events/error.js
@@ -1,7 +1,7 @@
 "use strict";
 
 const Msg = require("../../models/msg");
-const Helper = require("../../helper");
+const Config = require("../../config");
 
 module.exports = function (irc, network) {
 	const client = this;
@@ -36,7 +36,7 @@ module.exports = function (irc, network) {
 	irc.on("nick in use", function (data) {
 		let message = data.nick + ": " + (data.reason || "Nickname is already in use.");
 
-		if (irc.connection.registered === false && !Helper.config.public) {
+		if (irc.connection.registered === false && !Config.values.public) {
 			message += " An attempt to use it will be made when this nick quits.";
 
 			// Clients usually get nick in use on connect when reconnecting to a network
@@ -81,7 +81,7 @@ module.exports = function (irc, network) {
 		lobby.pushMessage(client, msg, true);
 
 		if (irc.connection.registered === false) {
-			irc.changeNick(Helper.getDefaultNick());
+			irc.changeNick(Config.getDefaultNick());
 		}
 
 		client.emit("nick", {
diff --git a/src/plugins/irc-events/link.js b/src/plugins/irc-events/link.js
index 0d7a5061..6bfa366b 100644
--- a/src/plugins/irc-events/link.js
+++ b/src/plugins/irc-events/link.js
@@ -4,7 +4,7 @@ const cheerio = require("cheerio");
 const got = require("got");
 const URL = require("url").URL;
 const mime = require("mime-types");
-const Helper = require("../../helper");
+const Config = require("../../config");
 const {findLinksWithSchema} = require("../../../client/js/helpers/ircmessageparser/findLinks");
 const storage = require("../storage");
 const currentFetchPromises = new Map();
@@ -13,7 +13,7 @@ const mediaTypeRegex = /^(audio|video)\/.+/;
 const log = require("../../log");
 
 module.exports = function (client, chan, msg, cleanText) {
-	if (!Helper.config.prefetch) {
+	if (!Config.values.prefetch) {
 		return;
 	}
 
@@ -90,7 +90,7 @@ function parseHtml(preview, res, client) {
 					preview.body = preview.body.substr(0, 300);
 				}
 
-				if (!Helper.config.prefetchStorage && Helper.config.disableMediaPreview) {
+				if (!Config.values.prefetchStorage && Config.values.disableMediaPreview) {
 					resolve(res);
 					return;
 				}
@@ -113,7 +113,7 @@ function parseHtml(preview, res, client) {
 							if (
 								resThumb !== null &&
 								imageTypeRegex.test(resThumb.type) &&
-								resThumb.size <= Helper.config.prefetchMaxImageSize * 1024
+								resThumb.size <= Config.values.prefetchMaxImageSize * 1024
 							) {
 								preview.thumbActualUrl = thumb;
 							}
@@ -130,7 +130,7 @@ function parseHtml(preview, res, client) {
 
 function parseHtmlMedia($, preview, client) {
 	return new Promise((resolve, reject) => {
-		if (Helper.config.disableMediaPreview) {
+		if (Config.values.disableMediaPreview) {
 			reject();
 			return;
 		}
@@ -226,14 +226,14 @@ function parse(msg, chan, preview, res, client) {
 		case "image/jxl":
 		case "image/webp":
 		case "image/avif":
-			if (!Helper.config.prefetchStorage && Helper.config.disableMediaPreview) {
+			if (!Config.values.prefetchStorage && Config.values.disableMediaPreview) {
 				return removePreview(msg, preview);
 			}
 
-			if (res.size > Helper.config.prefetchMaxImageSize * 1024) {
+			if (res.size > Config.values.prefetchMaxImageSize * 1024) {
 				preview.type = "error";
 				preview.error = "image-too-big";
-				preview.maxSize = Helper.config.prefetchMaxImageSize * 1024;
+				preview.maxSize = Config.values.prefetchMaxImageSize * 1024;
 			} else {
 				preview.type = "image";
 				preview.thumbActualUrl = preview.link;
@@ -259,7 +259,7 @@ function parse(msg, chan, preview, res, client) {
 				break;
 			}
 
-			if (Helper.config.disableMediaPreview) {
+			if (Config.values.disableMediaPreview) {
 				return removePreview(msg, preview);
 			}
 
@@ -276,7 +276,7 @@ function parse(msg, chan, preview, res, client) {
 				break;
 			}
 
-			if (Helper.config.disableMediaPreview) {
+			if (Config.values.disableMediaPreview) {
 				return removePreview(msg, preview);
 			}
 
@@ -301,7 +301,7 @@ function handlePreview(client, chan, msg, preview, res) {
 	const thumb = preview.thumbActualUrl || "";
 	delete preview.thumbActualUrl;
 
-	if (!thumb.length || !Helper.config.prefetchStorage) {
+	if (!thumb.length || !Config.values.prefetchStorage) {
 		preview.thumb = thumb;
 		return emitPreview(client, chan, msg, preview);
 	}
@@ -382,7 +382,7 @@ function fetch(uri, headers) {
 		return promise;
 	}
 
-	const prefetchTimeout = Helper.config.prefetchTimeout;
+	const prefetchTimeout = Config.values.prefetchTimeout;
 
 	if (!prefetchTimeout) {
 		log.warn(
@@ -394,7 +394,7 @@ function fetch(uri, headers) {
 		let buffer = Buffer.from("");
 		let contentLength = 0;
 		let contentType;
-		let limit = Helper.config.prefetchMaxImageSize * 1024;
+		let limit = Config.values.prefetchMaxImageSize * 1024;
 
 		try {
 			const gotStream = got.stream(uri, {
@@ -415,7 +415,7 @@ function fetch(uri, headers) {
 						// response is an image
 						// if Content-Length header reports a size exceeding the prefetch limit, abort fetch
 						// and if file is not to be stored we don't need to download further either
-						if (contentLength > limit || !Helper.config.prefetchStorage) {
+						if (contentLength > limit || !Config.values.prefetchStorage) {
 							gotStream.destroy();
 						}
 					} else if (mediaTypeRegex.test(contentType)) {
@@ -426,8 +426,8 @@ function fetch(uri, headers) {
 						// twitter.com sends opengraph meta tags within ~20kb of data for individual tweets, the default is set to 50.
 						// for sites like Youtube the og tags are in the first 300K and hence this is configurable by the admin
 						limit =
-							"prefetchMaxSearchSize" in Helper.config
-								? Helper.config.prefetchMaxSearchSize * 1024
+							"prefetchMaxSearchSize" in Config.values
+								? Config.values.prefetchMaxSearchSize * 1024
 								: // set to the previous size if config option is unset
 								  50 * 1024;
 					}
diff --git a/src/plugins/messageStorage/sqlite.js b/src/plugins/messageStorage/sqlite.js
index 6ce93211..243d1ef8 100644
--- a/src/plugins/messageStorage/sqlite.js
+++ b/src/plugins/messageStorage/sqlite.js
@@ -3,7 +3,7 @@
 const log = require("../../log");
 const path = require("path");
 const fs = require("fs");
-const Helper = require("../../helper");
+const Config = require("../../config");
 const Msg = require("../../models/msg");
 
 let sqlite3;
@@ -11,7 +11,7 @@ let sqlite3;
 try {
 	sqlite3 = require("sqlite3");
 } catch (e) {
-	Helper.config.messageStorage = Helper.config.messageStorage.filter((item) => item !== "sqlite");
+	Config.values.messageStorage = Config.values.messageStorage.filter((item) => item !== "sqlite");
 
 	log.error(
 		"Unable to load sqlite3 module. See https://github.com/mapbox/node-sqlite3/wiki/Binaries"
@@ -35,7 +35,7 @@ class MessageStorage {
 	}
 
 	enable() {
-		const logsPath = Helper.getUserLogsPath();
+		const logsPath = Config.getUserLogsPath();
 		const sqlitePath = path.join(logsPath, `${this.client.name}.sqlite3`);
 
 		try {
@@ -165,12 +165,12 @@ class MessageStorage {
 	 * @param Chan channel - Channel object for which to load messages for
 	 */
 	getMessages(network, channel) {
-		if (!this.isEnabled || Helper.config.maxHistory === 0) {
+		if (!this.isEnabled || Config.values.maxHistory === 0) {
 			return Promise.resolve([]);
 		}
 
 		// If unlimited history is specified, load 100k messages
-		const limit = Helper.config.maxHistory < 0 ? 100000 : Helper.config.maxHistory;
+		const limit = Config.values.maxHistory < 0 ? 100000 : Config.values.maxHistory;
 
 		return new Promise((resolve, reject) => {
 			this.database.serialize(() =>
diff --git a/src/plugins/messageStorage/text.js b/src/plugins/messageStorage/text.js
index 2e13aa18..602aeeca 100644
--- a/src/plugins/messageStorage/text.js
+++ b/src/plugins/messageStorage/text.js
@@ -4,7 +4,7 @@ const log = require("../../log");
 const fs = require("fs");
 const path = require("path");
 const filenamify = require("filenamify");
-const Helper = require("../../helper");
+const Config = require("../../config");
 const Msg = require("../../models/msg");
 
 class TextFileMessageStorage {
@@ -31,7 +31,7 @@ class TextFileMessageStorage {
 		}
 
 		const logPath = path.join(
-			Helper.getUserLogsPath(),
+			Config.getUserLogsPath(),
 			this.client.name,
 			TextFileMessageStorage.getNetworkFolderName(network)
 		);
@@ -117,7 +117,7 @@ class TextFileMessageStorage {
 		}
 
 		const logPath = path.join(
-			Helper.getUserLogsPath(),
+			Config.getUserLogsPath(),
 			this.client.name,
 			TextFileMessageStorage.getNetworkFolderName(network),
 			TextFileMessageStorage.getChannelFileName(channel)
diff --git a/src/plugins/packages/index.js b/src/plugins/packages/index.js
index a3d99022..13d5a1be 100644
--- a/src/plugins/packages/index.js
+++ b/src/plugins/packages/index.js
@@ -6,6 +6,7 @@ const colors = require("chalk");
 const path = require("path");
 const semver = require("semver");
 const Helper = require("../../helper");
+const Config = require("../../config");
 const themes = require("./themes");
 const packageMap = new Map();
 const inputs = require("../inputs");
@@ -45,7 +46,7 @@ const packageApis = function (packageInfo) {
 				client.inputLine({target: targetId, text: command}),
 		},
 		Config: {
-			getConfig: () => Helper.config,
+			getConfig: () => Config.values,
 			getPersistentStorageDir: getPersistentStorageDir.bind(this, packageInfo.packageName),
 		},
 		Logger: {
@@ -89,7 +90,7 @@ function getEnabledPackages(packageJson) {
 }
 
 function getPersistentStorageDir(packageName) {
-	const dir = path.join(Helper.getPackagesPath(), packageName);
+	const dir = path.join(Config.getPackagesPath(), packageName);
 	fs.mkdirSync(dir, {recursive: true}); // we don't care if it already exists or not
 	return dir;
 }
@@ -99,7 +100,7 @@ function loadPackage(packageName) {
 	let packageFile;
 
 	try {
-		const packagePath = Helper.getPackageModulePath(packageName);
+		const packagePath = Config.getPackageModulePath(packageName);
 
 		packageInfo = JSON.parse(fs.readFileSync(path.join(packagePath, "package.json"), "utf-8"));
 
@@ -155,7 +156,7 @@ function loadPackage(packageName) {
 }
 
 function loadPackages() {
-	const packageJson = path.join(Helper.getPackagesPath(), "package.json");
+	const packageJson = path.join(Config.getPackagesPath(), "package.json");
 	const packages = getEnabledPackages(packageJson);
 
 	packages.forEach(loadPackage);
@@ -193,7 +194,7 @@ async function outdated(cacheTimeout = TIME_TO_LIVE) {
 	}
 
 	// Get paths to the location of packages directory
-	const packagesPath = Helper.getPackagesPath();
+	const packagesPath = Config.getPackagesPath();
 	const packagesConfig = path.join(packagesPath, "package.json");
 	const packagesList = JSON.parse(fs.readFileSync(packagesConfig, "utf-8")).dependencies;
 	const argsList = [
diff --git a/src/plugins/packages/themes.js b/src/plugins/packages/themes.js
index 24814523..ba422bc3 100644
--- a/src/plugins/packages/themes.js
+++ b/src/plugins/packages/themes.js
@@ -1,7 +1,7 @@
 "use strict";
 
 const fs = require("fs");
-const Helper = require("../../helper");
+const Config = require("../../config");
 const path = require("path");
 const _ = require("lodash");
 const themes = new Map();
@@ -61,7 +61,7 @@ function makePackageThemeObject(moduleName, module) {
 	}
 
 	const themeColor = /^#[0-9A-F]{6}$/i.test(module.themeColor) ? module.themeColor : null;
-	const modulePath = Helper.getPackageModulePath(moduleName);
+	const modulePath = Config.getPackageModulePath(moduleName);
 	return {
 		displayName: module.name || moduleName,
 		filename: path.join(modulePath, module.css),
diff --git a/src/plugins/storage.js b/src/plugins/storage.js
index 3c2d021b..6e9c8dd7 100644
--- a/src/plugins/storage.js
+++ b/src/plugins/storage.js
@@ -4,7 +4,7 @@ const log = require("../log");
 const fs = require("fs");
 const path = require("path");
 const crypto = require("crypto");
-const helper = require("../helper");
+const Config = require("../config");
 
 class Storage {
 	constructor() {
@@ -16,7 +16,7 @@ class Storage {
 		// Deletes directory contents if the directory is not empty.
 		// If the directory does not exist, it is created.
 
-		const dir = helper.getStoragePath();
+		const dir = Config.getStoragePath();
 		let items;
 
 		try {
@@ -44,7 +44,7 @@ class Storage {
 		this.references.delete(url);
 
 		// Drop "storage/" from url and join it with full storage path
-		const filePath = path.join(helper.getStoragePath(), url.substring(8));
+		const filePath = path.join(Config.getStoragePath(), url.substring(8));
 
 		fs.unlink(filePath, (err) => {
 			if (err) {
@@ -57,7 +57,7 @@ class Storage {
 		const hash = crypto.createHash("sha256").update(data).digest("hex");
 		const a = hash.substring(0, 2);
 		const b = hash.substring(2, 4);
-		const folder = path.join(helper.getStoragePath(), a, b);
+		const folder = path.join(Config.getStoragePath(), a, b);
 		const filePath = path.join(folder, `${hash.substring(4)}.${extension}`);
 		const url = `storage/${a}/${b}/${hash.substring(4)}.${extension}`;
 
diff --git a/src/plugins/sts.js b/src/plugins/sts.js
index 25164ee7..697af9ae 100644
--- a/src/plugins/sts.js
+++ b/src/plugins/sts.js
@@ -4,11 +4,11 @@ const _ = require("lodash");
 const fs = require("fs");
 const path = require("path");
 const log = require("../log");
-const Helper = require("../helper");
+const Config = require("../config");
 
 class STSPolicies {
 	constructor() {
-		this.stsFile = path.join(Helper.getHomePath(), "sts-policies.json");
+		this.stsFile = path.join(Config.getHomePath(), "sts-policies.json");
 		this.policies = new Map();
 		this.refresh = _.debounce(this.saveFile, 10000, {maxWait: 60000});
 
diff --git a/src/plugins/uploader.js b/src/plugins/uploader.js
index ca921e21..79f8f195 100644
--- a/src/plugins/uploader.js
+++ b/src/plugins/uploader.js
@@ -1,6 +1,6 @@
 "use strict";
 
-const Helper = require("../helper");
+const Config = require("../config");
 const busboy = require("@fastify/busboy");
 const {v4: uuidv4} = require("uuid");
 const path = require("path");
@@ -86,7 +86,7 @@ class Uploader {
 		}
 
 		const folder = name.substring(0, 2);
-		const uploadPath = Helper.getFileUploadPath();
+		const uploadPath = Config.getFileUploadPath();
 		const filePath = path.join(uploadPath, folder, name);
 		let detectedMimeType = await Uploader.getFileType(filePath);
 
@@ -207,7 +207,7 @@ class Uploader {
 		// that already exists on disk
 		do {
 			randomName = crypto.randomBytes(8).toString("hex");
-			destDir = path.join(Helper.getFileUploadPath(), randomName.substring(0, 2));
+			destDir = path.join(Config.getFileUploadPath(), randomName.substring(0, 2));
 			destPath = path.join(destDir, randomName);
 		} while (fs.existsSync(destPath));
 
@@ -228,8 +228,8 @@ class Uploader {
 		busboyInstance.on("file", (fieldname, fileStream, filename) => {
 			uploadUrl = `${randomName}/${encodeURIComponent(filename)}`;
 
-			if (Helper.config.fileUpload.baseUrl) {
-				uploadUrl = new URL(uploadUrl, Helper.config.fileUpload.baseUrl).toString();
+			if (Config.values.fileUpload.baseUrl) {
+				uploadUrl = new URL(uploadUrl, Config.values.fileUpload.baseUrl).toString();
 			} else {
 				uploadUrl = `uploads/${uploadUrl}`;
 			}
@@ -266,7 +266,7 @@ class Uploader {
 	}
 
 	static getMaxFileSize() {
-		const configOption = Helper.config.fileUpload.maxFileSize;
+		const configOption = Config.values.fileUpload.maxFileSize;
 
 		// Busboy uses Infinity to allow unlimited file size
 		if (configOption < 1) {
diff --git a/src/plugins/webpush.js b/src/plugins/webpush.js
index 09cfb81d..6f3fa3ef 100644
--- a/src/plugins/webpush.js
+++ b/src/plugins/webpush.js
@@ -5,11 +5,11 @@ const log = require("../log");
 const fs = require("fs");
 const path = require("path");
 const WebPushAPI = require("web-push");
-const Helper = require("../helper");
+const Config = require("../config");
 
 class WebPush {
 	constructor() {
-		const vapidPath = path.join(Helper.getHomePath(), "vapid.json");
+		const vapidPath = path.join(Config.getHomePath(), "vapid.json");
 
 		let vapidStat = undefined;
 
diff --git a/src/server.js b/src/server.js
index 640b8742..49c417a3 100644
--- a/src/server.js
+++ b/src/server.js
@@ -12,6 +12,7 @@ const io = require("socket.io");
 const dns = require("dns");
 const Uploader = require("./plugins/uploader");
 const Helper = require("./helper");
+const Config = require("./config");
 const colors = require("chalk");
 const net = require("net");
 const Identification = require("./identification");
@@ -35,7 +36,7 @@ module.exports = function (options = {}) {
 (Node.js ${colors.green(process.versions.node)} on ${colors.green(process.platform)} ${
 		process.arch
 	})`);
-	log.info(`Configuration file: ${colors.green(Helper.getConfigPath())}`);
+	log.info(`Configuration file: ${colors.green(Config.getConfigPath())}`);
 
 	const staticOptions = {
 		redirect: false,
@@ -57,9 +58,9 @@ module.exports = function (options = {}) {
 		.get("/js/bundle.js.map", forceNoCacheRequest)
 		.get("/css/style.css.map", forceNoCacheRequest)
 		.use(express.static(path.join(__dirname, "..", "public"), staticOptions))
-		.use("/storage/", express.static(Helper.getStoragePath(), staticOptions));
+		.use("/storage/", express.static(Config.getStoragePath(), staticOptions));
 
-	if (Helper.config.fileUpload.enable) {
+	if (Config.values.fileUpload.enable) {
 		Uploader.router(app);
 	}
 
@@ -87,25 +88,25 @@ module.exports = function (options = {}) {
 			return res.status(404).send("Not found");
 		}
 
-		const packagePath = Helper.getPackageModulePath(packageName);
+		const packagePath = Config.getPackageModulePath(packageName);
 		return res.sendFile(path.join(packagePath, fileName));
 	});
 
 	let server = null;
 
-	if (Helper.config.public && (Helper.config.ldap || {}).enable) {
+	if (Config.values.public && (Config.values.ldap || {}).enable) {
 		log.warn(
 			"Server is public and set to use LDAP. Set to private mode if trying to use LDAP authentication."
 		);
 	}
 
-	if (!Helper.config.https.enable) {
+	if (!Config.values.https.enable) {
 		server = require("http");
 		server = server.createServer(app);
 	} else {
-		const keyPath = Helper.expandHome(Helper.config.https.key);
-		const certPath = Helper.expandHome(Helper.config.https.certificate);
-		const caPath = Helper.expandHome(Helper.config.https.ca);
+		const keyPath = Helper.expandHome(Config.values.https.key);
+		const certPath = Helper.expandHome(Config.values.https.certificate);
+		const caPath = Helper.expandHome(Config.values.https.ca);
 
 		if (!keyPath.length || !fs.existsSync(keyPath)) {
 			log.error("Path to SSL key is invalid. Stopping server...");
@@ -135,12 +136,12 @@ module.exports = function (options = {}) {
 
 	let listenParams;
 
-	if (typeof Helper.config.host === "string" && Helper.config.host.startsWith("unix:")) {
-		listenParams = Helper.config.host.replace(/^unix:/, "");
+	if (typeof Config.values.host === "string" && Config.values.host.startsWith("unix:")) {
+		listenParams = Config.values.host.replace(/^unix:/, "");
 	} else {
 		listenParams = {
-			port: Helper.config.port,
-			host: Helper.config.host,
+			port: Config.values.port,
+			host: Config.values.host,
 		};
 	}
 
@@ -150,7 +151,7 @@ module.exports = function (options = {}) {
 		if (typeof listenParams === "string") {
 			log.info("Available on socket " + colors.green(listenParams));
 		} else {
-			const protocol = Helper.config.https.enable ? "https" : "http";
+			const protocol = Config.values.https.enable ? "https" : "http";
 			const address = server.address();
 
 			if (address.family === "IPv6") {
@@ -160,7 +161,7 @@ module.exports = function (options = {}) {
 			log.info(
 				"Available at " +
 					colors.green(`${protocol}://${address.address}:${address.port}/`) +
-					` in ${colors.bold(Helper.config.public ? "public" : "private")} mode`
+					` in ${colors.bold(Config.values.public ? "public" : "private")} mode`
 			);
 		}
 
@@ -168,14 +169,14 @@ module.exports = function (options = {}) {
 			wsEngine: require("ws").Server,
 			cookie: false,
 			serveClient: false,
-			transports: Helper.config.transports,
+			transports: Config.values.transports,
 			pingTimeout: 60000,
 		});
 
 		sockets.on("connect", (socket) => {
 			socket.on("error", (err) => log.error(`io socket error: ${err}`));
 
-			if (Helper.config.public) {
+			if (Config.values.public) {
 				performAuthentication.call(socket, {});
 			} else {
 				socket.on("auth:perform", performAuthentication);
@@ -186,17 +187,17 @@ module.exports = function (options = {}) {
 		manager = new ClientManager();
 		packages.loadPackages();
 
-		const defaultTheme = themes.getByName(Helper.config.theme);
+		const defaultTheme = themes.getByName(Config.values.theme);
 
 		if (defaultTheme === undefined) {
 			log.warn(
 				`The specified default theme "${colors.red(
-					Helper.config.theme
+					Config.values.theme
 				)}" does not exist, verify your config.`
 			);
-			Helper.config.theme = "default";
+			Config.values.theme = "default";
 		} else if (defaultTheme.themeColor) {
-			Helper.config.themeColor = defaultTheme.themeColor;
+			Config.values.themeColor = defaultTheme.themeColor;
 		}
 
 		new Identification((identHandler, err) => {
@@ -221,7 +222,7 @@ module.exports = function (options = {}) {
 			// Close all client and IRC connections
 			manager.clients.forEach((client) => client.quit());
 
-			if (Helper.config.prefetchStorage) {
+			if (Config.values.prefetchStorage) {
 				log.info("Clearing prefetch storage folder, this might take a while...");
 
 				require("./plugins/storage").emptyDir();
@@ -241,7 +242,7 @@ module.exports = function (options = {}) {
 		process.on("SIGTERM", exitGracefully);
 
 		// Clear storage folder after server starts successfully
-		if (Helper.config.prefetchStorage) {
+		if (Config.values.prefetchStorage) {
 			require("./plugins/storage").emptyDir();
 		}
 
@@ -265,7 +266,7 @@ function getClientLanguage(socket) {
 function getClientIp(socket) {
 	let ip = socket.handshake.address || "127.0.0.1";
 
-	if (Helper.config.reverseProxy) {
+	if (Config.values.reverseProxy) {
 		const forwarded = (socket.handshake.headers["x-forwarded-for"] || "")
 			.split(/\s*,\s*/)
 			.filter(Boolean);
@@ -281,7 +282,7 @@ function getClientIp(socket) {
 function getClientSecure(socket) {
 	let secure = socket.handshake.secure;
 
-	if (Helper.config.reverseProxy && socket.handshake.headers["x-forwarded-proto"] === "https") {
+	if (Config.values.reverseProxy && socket.handshake.headers["x-forwarded-proto"] === "https") {
 		secure = true;
 	}
 
@@ -310,7 +311,7 @@ function addSecurityHeaders(req, res, next) {
 	// If prefetch is enabled, but storage is not, we have to allow mixed content
 	// - https://user-images.githubusercontent.com is where we currently push our changelog screenshots
 	// - data: is required for the HTML5 video player
-	if (Helper.config.prefetchStorage || !Helper.config.prefetch) {
+	if (Config.values.prefetchStorage || !Config.values.prefetch) {
 		policies.push("img-src 'self' data: https://user-images.githubusercontent.com");
 		policies.unshift("block-all-mixed-content");
 	} else {
@@ -367,7 +368,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
 		openChannel = client.lastActiveChannel;
 	}
 
-	if (Helper.config.fileUpload.enable) {
+	if (Config.values.fileUpload.enable) {
 		new Uploader(socket);
 	}
 
@@ -436,7 +437,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
 		}
 	});
 
-	if (!Helper.config.public && !Helper.config.ldap.enable) {
+	if (!Config.values.public && !Config.values.ldap.enable) {
 		socket.on("change-password", (data) => {
 			if (_.isPlainObject(data)) {
 				const old = data.old_password;
@@ -510,7 +511,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
 
 	// In public mode only one client can be connected,
 	// so there's no need to handle msg:preview:toggle
-	if (!Helper.config.public) {
+	if (!Config.values.public) {
 		socket.on("msg:preview:toggle", (data) => {
 			if (_.isPlainObject(data)) {
 				return;
@@ -571,7 +572,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
 		client.mentions = [];
 	});
 
-	if (!Helper.config.public) {
+	if (!Config.values.public) {
 		socket.on("push:register", (subscription) => {
 			if (!Object.prototype.hasOwnProperty.call(client.config.sessions, token)) {
 				return;
@@ -614,7 +615,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
 
 	socket.on("sessions:get", sendSessionList);
 
-	if (!Helper.config.public) {
+	if (!Config.values.public) {
 		socket.on("setting:set", (newSetting) => {
 			if (!_.isPlainObject(newSetting)) {
 				return;
@@ -739,7 +740,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
 		socket.emit("commands", inputs.getCommands());
 	};
 
-	if (Helper.config.public) {
+	if (Config.values.public) {
 		sendInitEvent(null);
 	} else if (token === null) {
 		client.generateToken((newToken) => {
@@ -757,16 +758,16 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
 }
 
 function getClientConfiguration() {
-	const config = _.pick(Helper.config, ["public", "lockNetwork", "useHexIp", "prefetch"]);
+	const config = _.pick(Config.values, ["public", "lockNetwork", "useHexIp", "prefetch"]);
 
-	config.fileUpload = Helper.config.fileUpload.enable;
-	config.ldapEnabled = Helper.config.ldap.enable;
+	config.fileUpload = Config.values.fileUpload.enable;
+	config.ldapEnabled = Config.values.ldap.enable;
 
 	if (!config.lockNetwork) {
-		config.defaults = _.clone(Helper.config.defaults);
+		config.defaults = _.clone(Config.values.defaults);
 	} else {
 		// Only send defaults that are visible on the client
-		config.defaults = _.pick(Helper.config.defaults, [
+		config.defaults = _.pick(Config.values.defaults, [
 			"name",
 			"nick",
 			"username",
@@ -781,8 +782,8 @@ function getClientConfiguration() {
 	config.version = pkg.version;
 	config.gitCommit = Helper.getGitCommit();
 	config.themes = themes.getAll();
-	config.defaultTheme = Helper.config.theme;
-	config.defaults.nick = Helper.getDefaultNick();
+	config.defaultTheme = Config.values.theme;
+	config.defaults.nick = Config.getDefaultNick();
 	config.defaults.sasl = "";
 	config.defaults.saslAccount = "";
 	config.defaults.saslPassword = "";
@@ -795,7 +796,7 @@ function getClientConfiguration() {
 }
 
 function getServerConfiguration() {
-	const config = _.clone(Helper.config);
+	const config = _.clone(Config.values);
 
 	config.stylesheets = packages.getStylesheets();
 
@@ -833,7 +834,7 @@ function performAuthentication(data) {
 		};
 
 		// If webirc is enabled perform reverse dns lookup
-		if (Helper.config.webirc === null) {
+		if (Config.values.webirc === null) {
 			return finalInit();
 		}
 
@@ -844,7 +845,7 @@ function performAuthentication(data) {
 		});
 	};
 
-	if (Helper.config.public) {
+	if (Config.values.public) {
 		client = new Client(manager);
 		manager.clients.push(client);
 


Test patch:
diff --git a/test/fixtures/env.js b/test/fixtures/env.js
index 57adf7ad..dcdb1a25 100644
--- a/test/fixtures/env.js
+++ b/test/fixtures/env.js
@@ -3,7 +3,7 @@
 const fs = require("fs");
 
 const home = require("path").join(__dirname, ".thelounge");
-require("../../src/helper").setHome(home);
+require("../../src/config").setHome(home);
 
 const STSPolicies = require("../../src/plugins/sts"); // Must be imported *after* setHome
 
diff --git a/test/models/network.js b/test/models/network.js
index 1b5a1ed1..d933439c 100644
--- a/test/models/network.js
+++ b/test/models/network.js
@@ -5,7 +5,7 @@ const Chan = require("../../src/models/chan");
 const Msg = require("../../src/models/msg");
 const User = require("../../src/models/user");
 const Network = require("../../src/models/network");
-const Helper = require("../../src/helper");
+const Config = require("../../src/config");
 const STSPolicies = require("../../src/plugins/sts");
 const ClientCertificate = require("../../src/plugins/clientCertificate");
 
@@ -126,7 +126,7 @@ describe("Network", function () {
 
 	describe("#validate()", function () {
 		it("should set correct defaults", function () {
-			Helper.config.defaults.nick = "";
+			Config.values.defaults.nick = "";
 
 			const network = new Network({
 				host: "localhost",
@@ -147,10 +147,10 @@ describe("Network", function () {
 		});
 
 		it("should enforce lockNetwork", function () {
-			Helper.config.lockNetwork = true;
+			Config.values.lockNetwork = true;
 
 			// Make sure we lock in private mode
-			Helper.config.public = false;
+			Config.values.public = false;
 
 			const network = new Network({
 				host: "",
@@ -165,7 +165,7 @@ describe("Network", function () {
 			expect(network.rejectUnauthorized).to.be.true;
 
 			// Make sure we lock in public mode (also resets public=true for other tests)
-			Helper.config.public = true;
+			Config.values.public = true;
 
 			const network2 = new Network({
 				host: "some.fake.tld",
@@ -173,7 +173,7 @@ describe("Network", function () {
 			expect(network2.validate()).to.be.true;
 			expect(network2.host).to.equal("irc.example.com");
 
-			Helper.config.lockNetwork = false;
+			Config.values.lockNetwork = false;
 		});
 
 		it("should apply STS policies iff they match", function () {
@@ -204,7 +204,7 @@ describe("Network", function () {
 		});
 
 		it("should not remove client certs if TLS is disabled", function () {
-			Helper.config.public = false;
+			Config.values.public = false;
 
 			const client = {idMsg: 1, emit() {}, messageStorage: []};
 
@@ -221,11 +221,11 @@ describe("Network", function () {
 			expect(ClientCertificate.get(network.uuid)).to.deep.equal(client_cert); // Should be unchanged
 
 			ClientCertificate.remove(network.uuid);
-			Helper.config.public = true;
+			Config.values.public = true;
 		});
 
 		it("should not remove client certs if there is a STS policy", function () {
-			Helper.config.public = false;
+			Config.values.public = false;
 
 			const client = {idMsg: 1, emit() {}, messageStorage: []};
 			STSPolicies.update("irc.example.com", 7000, 3600);
@@ -243,13 +243,13 @@ describe("Network", function () {
 			expect(ClientCertificate.get(network.uuid)).to.deep.equal(client_cert); // Should be unchanged
 
 			ClientCertificate.remove(network.uuid);
-			Helper.config.public = true;
+			Config.values.public = true;
 		});
 	});
 
 	describe("#createIrcFramework(client)", function () {
 		it("should generate and use a client certificate when using SASL external", function () {
-			Helper.config.public = false;
+			Config.values.public = false;
 
 			const client = {idMsg: 1, emit() {}};
 			STSPolicies.update("irc.example.com", 7000, 3600);
@@ -265,7 +265,7 @@ describe("Network", function () {
 			expect(network.irc.options.client_certificate).to.not.be.null;
 
 			ClientCertificate.remove(network.uuid);
-			Helper.config.public = true;
+			Config.values.public = true;
 		});
 	});
 
diff --git a/test/plugins/auth/ldap.js b/test/plugins/auth/ldap.js
index 88f76365..da101e66 100644
--- a/test/plugins/auth/ldap.js
+++ b/test/plugins/auth/ldap.js
@@ -2,7 +2,7 @@
 
 const log = require("../../../src/log");
 const ldapAuth = require("../../../src/plugins/auth/ldap");
-const Helper = require("../../../src/helper");
+const Config = require("../../../src/config");
 const ldap = require("ldapjs");
 const expect = require("chai").expect;
 const stub = require("sinon").stub;
@@ -23,7 +23,7 @@ function normalizeDN(dn) {
 function startLdapServer(callback) {
 	const server = ldap.createServer();
 
-	const searchConf = Helper.config.ldap.searchDN;
+	const searchConf = Config.values.ldap.searchDN;
 	const userDN = primaryKey + "=" + user + "," + baseDN;
 
 	// Two users are authorized: john doe and the root user in case of
@@ -143,34 +143,34 @@ describe("LDAP authentication plugin", function () {
 	});
 
 	beforeEach(function () {
-		Helper.config.public = false;
-		Helper.config.ldap.enable = true;
-		Helper.config.ldap.url = "ldap://localhost:" + String(serverPort);
-		Helper.config.ldap.primaryKey = primaryKey;
+		Config.values.public = false;
+		Config.values.ldap.enable = true;
+		Config.values.ldap.url = "ldap://localhost:" + String(serverPort);
+		Config.values.ldap.primaryKey = primaryKey;
 	});
 
 	afterEach(function () {
-		Helper.config.public = true;
-		Helper.config.ldap.enable = false;
+		Config.values.public = true;
+		Config.values.ldap.enable = false;
 	});
 
 	describe("LDAP authentication availability", function () {
 		it("checks that the configuration is correctly tied to isEnabled()", function () {
-			Helper.config.ldap.enable = true;
+			Config.values.ldap.enable = true;
 			expect(ldapAuth.isEnabled()).to.equal(true);
 
-			Helper.config.ldap.enable = false;
+			Config.values.ldap.enable = false;
 			expect(ldapAuth.isEnabled()).to.equal(false);
 		});
 	});
 
 	describe("Simple LDAP authentication (predefined DN pattern)", function () {
-		Helper.config.ldap.baseDN = baseDN;
+		Config.values.ldap.baseDN = baseDN;
 		testLdapAuth();
 	});
 
 	describe("Advanced LDAP authentication (DN found by a prior search query)", function () {
-		delete Helper.config.ldap.baseDN;
+		delete Config.values.ldap.baseDN;
 		testLdapAuth();
 	});
 });
diff --git a/test/plugins/clientCertificate.js b/test/plugins/clientCertificate.js
index 6330b62e..fad096a2 100644
--- a/test/plugins/clientCertificate.js
+++ b/test/plugins/clientCertificate.js
@@ -4,18 +4,18 @@ const fs = require("fs");
 const path = require("path");
 const {expect} = require("chai");
 const ClientCertificate = require("../../src/plugins/clientCertificate");
-const Helper = require("../../src/helper");
+const Config = require("../../src/config");
 
 describe("ClientCertificate", function () {
 	it("should not generate a client certificate in public mode", function () {
-		Helper.config.public = true;
+		Config.values.public = true;
 
 		const certificate = ClientCertificate.get("this-is-test-uuid");
 		expect(certificate).to.be.null;
 	});
 
 	it("should generate a client certificate", function () {
-		Helper.config.public = false;
+		Config.values.public = false;
 		const certificate = ClientCertificate.get("this-is-test-uuid");
 
 		expect(certificate.certificate).to.match(/^-----BEGIN CERTIFICATE-----/);
@@ -25,18 +25,18 @@ describe("ClientCertificate", function () {
 		expect(certificate2.certificate).to.equal(certificate.certificate);
 		expect(certificate2.private_key).to.equal(certificate.private_key);
 
-		Helper.config.public = true;
+		Config.values.public = true;
 	});
 
 	it("should remove the client certificate files", function () {
-		Helper.config.public = false;
+		Config.values.public = false;
 
 		const privateKeyPath = path.join(
-			Helper.getClientCertificatesPath(),
+			Config.getClientCertificatesPath(),
 			`this-is-test-uuid.pem`
 		);
 		const certificatePath = path.join(
-			Helper.getClientCertificatesPath(),
+			Config.getClientCertificatesPath(),
 			`this-is-test-uuid.crt`
 		);
 
@@ -48,6 +48,6 @@ describe("ClientCertificate", function () {
 		expect(fs.existsSync(privateKeyPath)).to.be.false;
 		expect(fs.existsSync(certificatePath)).to.be.false;
 
-		Helper.config.public = true;
+		Config.values.public = true;
 	});
 });
diff --git a/test/plugins/link.js b/test/plugins/link.js
index 9098d5c5..a121cdde 100644
--- a/test/plugins/link.js
+++ b/test/plugins/link.js
@@ -3,7 +3,7 @@
 const path = require("path");
 const expect = require("chai").expect;
 const util = require("../util");
-const Helper = require("../../src/helper");
+const Config = require("../../src/config");
 const link = require("../../src/plugins/irc-events/link.js");
 
 describe("Link plugin", function () {
@@ -36,7 +36,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
 		this.irc = util.createClient();
 		this.network = util.createNetwork();
 
-		Helper.config.prefetchStorage = false;
+		Config.values.prefetchStorage = false;
 	});
 
 	afterEach(function (done) {
@@ -224,11 +224,11 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
 
 	describe("test disableMediaPreview", function () {
 		beforeEach(function (done) {
-			Helper.config.disableMediaPreview = true;
+			Config.values.disableMediaPreview = true;
 			done();
 		});
 		afterEach(function (done) {
-			Helper.config.disableMediaPreview = false;
+			Config.values.disableMediaPreview = false;
 			done();
 		});
 		it("should ignore og:image if disableMediaPreview", function (done) {
diff --git a/test/plugins/sqlite.js b/test/plugins/sqlite.js
index 4dadef14..35016522 100644
--- a/test/plugins/sqlite.js
+++ b/test/plugins/sqlite.js
@@ -5,7 +5,7 @@ const path = require("path");
 const expect = require("chai").expect;
 const util = require("../util");
 const Msg = require("../../src/models/msg");
-const Helper = require("../../src/helper");
+const Config = require("../../src/config");
 const MessageStorage = require("../../src/plugins/messageStorage/sqlite.js");
 
 describe("SQLite Message Storage", function () {
@@ -13,7 +13,7 @@ describe("SQLite Message Storage", function () {
 	this.timeout(util.isRunningOnCI() ? 25000 : 5000);
 	this.slow(300);
 
-	const expectedPath = path.join(Helper.getHomePath(), "logs", "testUser.sqlite3");
+	const expectedPath = path.join(Config.getHomePath(), "logs", "testUser.sqlite3");
 	let store;
 
 	before(function (done) {
@@ -34,7 +34,7 @@ describe("SQLite Message Storage", function () {
 		// After tests run, remove the logs folder
 		// so we return to the clean state
 		fs.unlinkSync(expectedPath);
-		fs.rmdir(path.join(Helper.getHomePath(), "logs"), done);
+		fs.rmdir(path.join(Config.getHomePath(), "logs"), done);
 	});
 
 	it("should resolve an empty array when disabled", function () {
@@ -127,10 +127,10 @@ describe("SQLite Message Storage", function () {
 	});
 
 	it("should retrieve latest LIMIT messages in order", function () {
-		const originalMaxHistory = Helper.config.maxHistory;
+		const originalMaxHistory = Config.values.maxHistory;
 
 		try {
-			Helper.config.maxHistory = 2;
+			Config.values.maxHistory = 2;
 
 			for (let i = 0; i < 200; ++i) {
 				store.index(
@@ -150,15 +150,15 @@ describe("SQLite Message Storage", function () {
 					expect(messages.map((i) => i.text)).to.deep.equal(["msg 198", "msg 199"]);
 				});
 		} finally {
-			Helper.config.maxHistory = originalMaxHistory;
+			Config.values.maxHistory = originalMaxHistory;
 		}
 	});
 
 	it("should search messages", function () {
-		const originalMaxHistory = Helper.config.maxHistory;
+		const originalMaxHistory = Config.values.maxHistory;
 
 		try {
-			Helper.config.maxHistory = 2;
+			Config.values.maxHistory = 2;
 
 			return store
 				.search({
@@ -177,7 +177,7 @@ describe("SQLite Message Storage", function () {
 					expect(messages.results.map((i) => i.text)).to.deep.equal(expectedMessages);
 				});
 		} finally {
-			Helper.config.maxHistory = originalMaxHistory;
+			Config.values.maxHistory = originalMaxHistory;
 		}
 	});
 
@@ -193,10 +193,10 @@ describe("SQLite Message Storage", function () {
 				});
 		}
 
-		const originalMaxHistory = Helper.config.maxHistory;
+		const originalMaxHistory = Config.values.maxHistory;
 
 		try {
-			Helper.config.maxHistory = 3;
+			Config.values.maxHistory = 3;
 
 			store.index(
 				{uuid: "this-is-a-network-guid2"},
@@ -239,7 +239,7 @@ describe("SQLite Message Storage", function () {
 					.then(() => assertResults("@", ["bar @ baz"]))
 			);
 		} finally {
-			Helper.config.maxHistory = originalMaxHistory;
+			Config.values.maxHistory = originalMaxHistory;
 		}
 	});
 
diff --git a/test/plugins/storage.js b/test/plugins/storage.js
index c8c4a14d..802937f9 100644
--- a/test/plugins/storage.js
+++ b/test/plugins/storage.js
@@ -5,7 +5,7 @@ const path = require("path");
 const crypto = require("crypto");
 const expect = require("chai").expect;
 const util = require("../util");
-const Helper = require("../../src/helper");
+const Config = require("../../src/config");
 const storage = require("../../src/plugins/storage");
 const link = require("../../src/plugins/irc-events/link.js");
 
@@ -55,7 +55,7 @@ describe("Image storage", function () {
 	after(function (done) {
 		// After storage tests run, remove the remaining empty
 		// storage folder so we return to the clean state
-		const dir = Helper.getStoragePath();
+		const dir = Config.getStoragePath();
 		fs.rmdir(dir, done);
 	});
 
@@ -63,11 +63,11 @@ describe("Image storage", function () {
 		this.irc = util.createClient();
 		this.network = util.createNetwork();
 
-		Helper.config.prefetchStorage = true;
+		Config.values.prefetchStorage = true;
 	});
 
 	afterEach(function () {
-		Helper.config.prefetchStorage = false;
+		Config.values.prefetchStorage = false;
 	});
 
 	it("should store the thumbnail", function (done) {
@@ -135,7 +135,7 @@ describe("Image storage", function () {
 	});
 
 	it("should clear storage folder", function () {
-		const dir = Helper.getStoragePath();
+		const dir = Config.getStoragePath();
 
 		expect(fs.readdirSync(dir)).to.not.be.empty;
 		storage.emptyDir();
diff --git a/test/server.js b/test/server.js
index e1647bba..28b2304f 100644
--- a/test/server.js
+++ b/test/server.js
@@ -1,7 +1,7 @@
 "use strict";
 
 const log = require("../src/log");
-const Helper = require("../src/helper");
+const Config = require("../src/config");
 const expect = require("chai").expect;
 const stub = require("sinon").stub;
 const got = require("got");
@@ -28,7 +28,7 @@ describe("Server", function () {
 		changelog.checkForUpdates.restore();
 	});
 
-	const webURL = `http://${Helper.config.host}:${Helper.config.port}/`;
+	const webURL = `http://${Config.values.host}:${Config.values.port}/`;
 
 	describe("Express", () => {
 		it("should run a web server on " + webURL, async () => {
@@ -84,7 +84,7 @@ describe("Server", function () {
 					nick: "test-user",
 					join: "#thelounge, #spam",
 					name: "Test Network",
-					host: Helper.config.host,
+					host: Config.values.host,
 					port: 6667,
 				});
 			});
diff --git a/test/tests/mergeConfig.js b/test/tests/mergeConfig.js
index f50d2ffc..7a785402 100644
--- a/test/tests/mergeConfig.js
+++ b/test/tests/mergeConfig.js
@@ -3,7 +3,7 @@
 const log = require("../../src/log");
 const expect = require("chai").expect;
 const stub = require("sinon").stub;
-const mergeConfig = require("../../src/helper").mergeConfig;
+const Config = require("../../src/config");
 const TestUtil = require("../util");
 
 describe("mergeConfig", function () {
@@ -13,7 +13,7 @@ describe("mergeConfig", function () {
 		};
 
 		expect(
-			mergeConfig(config, {
+			Config._merge_config_objects(config, {
 				ip: "overridden",
 			})
 		).to.deep.equal({
@@ -27,7 +27,7 @@ describe("mergeConfig", function () {
 
 	it("should merge new properties", function () {
 		expect(
-			mergeConfig(
+			Config._merge_config_objects(
 				{
 					ip: "default",
 					newProp: "this should appear too",
@@ -44,7 +44,7 @@ describe("mergeConfig", function () {
 
 	it("should extend objects", function () {
 		expect(
-			mergeConfig(
+			Config._merge_config_objects(
 				{
 					tlsOptions: {},
 				},
@@ -68,7 +68,7 @@ describe("mergeConfig", function () {
 		stub(log, "warn").callsFake(TestUtil.sanitizeLog((str) => (warning += str)));
 
 		expect(
-			mergeConfig(
+			Config._merge_config_objects(
 				{
 					optionOne: 123,
 				},
@@ -88,7 +88,7 @@ describe("mergeConfig", function () {
 
 	it("should not warn for unknown second level keys", function () {
 		expect(
-			mergeConfig(
+			Config._merge_config_objects(
 				{
 					optionOne: {
 						subOne: 123,
@@ -111,7 +111,7 @@ describe("mergeConfig", function () {
 
 	it("should allow changing nulls", function () {
 		expect(
-			mergeConfig(
+			Config._merge_config_objects(
 				{
 					oidentd: null,
 				},
@@ -126,7 +126,7 @@ describe("mergeConfig", function () {
 
 	it("should allow changing nulls with objects", function () {
 		expect(
-			mergeConfig(
+			Config._merge_config_objects(
 				{
 					webirc: null,
 				},
@@ -149,7 +149,7 @@ describe("mergeConfig", function () {
 		const callbackFunction = () => ({});
 
 		expect(
-			mergeConfig(
+			Config._merge_config_objects(
 				{
 					webirc: null,
 				},
@@ -168,7 +168,7 @@ describe("mergeConfig", function () {
 
 	it("should keep new properties inside of objects", function () {
 		expect(
-			mergeConfig(
+			Config._merge_config_objects(
 				{
 					nestedOnce: {
 						ip: "default",
@@ -206,7 +206,7 @@ describe("mergeConfig", function () {
 
 	it("should not merge arrays", function () {
 		expect(
-			mergeConfig(
+			Config._merge_config_objects(
 				{
 					test: ["sqlite", "text"],
 				},
@@ -219,7 +219,7 @@ describe("mergeConfig", function () {
 		});
 
 		expect(
-			mergeConfig(
+			Config._merge_config_objects(
 				{
 					test: ["sqlite", "text"],
 				},
@@ -234,7 +234,7 @@ describe("mergeConfig", function () {
 
 	it("should change order in arrays", function () {
 		expect(
-			mergeConfig(
+			Config._merge_config_objects(
 				{
 					test: ["sqlite", "text"],
 				},
@@ -251,7 +251,7 @@ describe("mergeConfig", function () {
 		stub(log, "warn");
 
 		expect(
-			mergeConfig(
+			Config._merge_config_objects(
 				{
 					shouldBeObject: {
 						thing: "yes",
@@ -268,7 +268,7 @@ describe("mergeConfig", function () {
 		});
 
 		expect(
-			mergeConfig(
+			Config._merge_config_objects(
 				{
 					shouldBeString: "string",
 				},