diff --git a/.prettierrc.js b/.prettierrc.js
new file mode 100644
index 0000000000000000000000000000000000000000..e3e0097101444f0b61b000068e4c5e5b7f037399
--- /dev/null
+++ b/.prettierrc.js
@@ -0,0 +1,9 @@
+module.exports = {
+	bracketSpacing: true,
+	jsxBracketSameLine: true,
+	singleQuote: true,
+	trailingComma: 'all',
+	arrowParens: 'avoid',
+	useTabs: true,
+	semi: false,
+}
diff --git a/database/migrations/20000101000000-enable-extensions.js b/database/migrations/20000101000000-enable-extensions.js
index 9ddb56a4008ab7735a764711f2cc08b7beb9186c..c152cf230c7896725adaa7c98f10a6bf91f03321 100644
--- a/database/migrations/20000101000000-enable-extensions.js
+++ b/database/migrations/20000101000000-enable-extensions.js
@@ -1,17 +1,32 @@
 module.exports = {
 	up: (migration, Types) => {
 		return migration.sequelize.transaction(async t => {
-			await migration.sequelize.query('CREATE EXTENSION IF NOT EXISTS postgis;', { transaction: t })
-			await migration.sequelize.query('CREATE EXTENSION IF NOT EXISTS pg_trgm;', { transaction: t })
-			await migration.sequelize.query('CREATE EXTENSION IF NOT EXISTS timescaledb;', { transaction: t })
+			await migration.sequelize.query(
+				'CREATE EXTENSION IF NOT EXISTS postgis;',
+				{ transaction: t },
+			)
+			await migration.sequelize.query(
+				'CREATE EXTENSION IF NOT EXISTS pg_trgm;',
+				{ transaction: t },
+			)
+			await migration.sequelize.query(
+				'CREATE EXTENSION IF NOT EXISTS timescaledb;',
+				{ transaction: t },
+			)
 		})
 	},
 
 	down: (migration, Types) => {
 		return migration.sequelize.transaction(async t => {
-			await migration.sequelize.query('DROP EXTENSION IF EXISTS postgis;', { transaction: t })
-			await migration.sequelize.query('DROP EXTENSION IF EXISTS pg_trgm;', { transaction: t })
-			await migration.sequelize.query('DROP EXTENSION IF EXISTS timescaledb;', { transaction: t })
+			await migration.sequelize.query('DROP EXTENSION IF EXISTS postgis;', {
+				transaction: t,
+			})
+			await migration.sequelize.query('DROP EXTENSION IF EXISTS pg_trgm;', {
+				transaction: t,
+			})
+			await migration.sequelize.query('DROP EXTENSION IF EXISTS timescaledb;', {
+				transaction: t,
+			})
 		})
 	},
-}
\ No newline at end of file
+}
diff --git a/database/migrations/20191117234148-create-users-table.js b/database/migrations/20191117234148-create-users-table.js
index 04ca0110c08c96f92f3798ea3328c4325a69959c..a99e6b39233d953fb474f0af654a8262a9d20fb2 100644
--- a/database/migrations/20191117234148-create-users-table.js
+++ b/database/migrations/20191117234148-create-users-table.js
@@ -49,4 +49,4 @@ module.exports = {
 	down: (migration, Types) => {
 		return migration.dropTable('users')
 	},
-}
\ No newline at end of file
+}
diff --git a/database/migrations/20191118001739-create-metrics-table.js b/database/migrations/20191118001739-create-metrics-table.js
index acfb2aabd95b229dfadbc5c80a4d3f7951b7e17c..7cb5e4258657316d520663e7dab427e25cae6a4f 100644
--- a/database/migrations/20191118001739-create-metrics-table.js
+++ b/database/migrations/20191118001739-create-metrics-table.js
@@ -1,59 +1,70 @@
 module.exports = {
 	up: (migration, Types) => {
 		return migration.sequelize.transaction(async t => {
-			await migration.createTable('metrics', {
-				id: {
-					type: Types.UUID,
-					defaultValue: Types.UUIDV4,
-					allowNull: false,
-				},
-				value: {
-					type: Types.TEXT,
-					allowNull: false,
-				},
-				type: {
-					type: Types.TEXT,
-					allowNull: false,
-				},
-				location: {
-					type: Types.GEOGRAPHY('POINT', 4326),
-					allowNull: false,
-				},
-				author_id: {
-					type: Types.UUID,
-					allowNull: true,
-					references: {
-						model: 'users',
-						key: 'id',
-					},
-					onDelete: 'SET NULL',
-					onUpdate: 'CASCADE',
-				},
-				meta: {
-					type: Types.JSONB,
-					defaultValue: {},
-					allowNull: false,
-				},
-				recorded_at: {
-					type: Types.DATE,
-					defaultValue: Types.fn('now'),
-					allowNull: false,
-				},
-				deleted_at: {
-					type: Types.DATE,
-					defaultValue: null,
-					allowNull: true,
+			await migration.createTable(
+				'metrics',
+				{
+					id: {
+						type: Types.UUID,
+						defaultValue: Types.UUIDV4,
+						allowNull: false,
+					},
+					value: {
+						type: Types.TEXT,
+						allowNull: false,
+					},
+					type: {
+						type: Types.TEXT,
+						allowNull: false,
+					},
+					location: {
+						type: Types.GEOGRAPHY('POINT', 4326),
+						allowNull: false,
+					},
+					author_id: {
+						type: Types.UUID,
+						allowNull: true,
+						references: {
+							model: 'users',
+							key: 'id',
+						},
+						onDelete: 'SET NULL',
+						onUpdate: 'CASCADE',
+					},
+					meta: {
+						type: Types.JSONB,
+						defaultValue: {},
+						allowNull: false,
+					},
+					recorded_at: {
+						type: Types.DATE,
+						defaultValue: Types.fn('now'),
+						allowNull: false,
+					},
+					deleted_at: {
+						type: Types.DATE,
+						defaultValue: null,
+						allowNull: true,
+					},
 				},
-			}, { transaction: t })
-			await migration.sequelize.query('CREATE INDEX metrics_id_idx ON metrics(id)', { transaction: t })
-			await migration.sequelize.query('SELECT create_hypertable(\'metrics\', \'recorded_at\')', { transaction: t })
+				{ transaction: t },
+			)
+			await migration.sequelize.query(
+				'CREATE INDEX metrics_id_idx ON metrics(id)',
+				{ transaction: t },
+			)
+			await migration.sequelize.query(
+				"SELECT create_hypertable('metrics', 'recorded_at')",
+				{ transaction: t },
+			)
 		})
-
 	},
 
 	down: (migration, Types) => {
 		return migration.sequelize.transaction(async t => {
-			await migration.removeIndex('metrics', 'metrics_id_idx', { transaction: t })
+			await migration.removeIndex('metrics', 'metrics_id_idx', {
+				transaction: t,
+			})
 			await migration.dropTable('metrics', { transaction: t })
 		})
 	},
diff --git a/database/migrations/20191118004631-create-oauth-access-tokens-table.js b/database/migrations/20191118004631-create-oauth-access-tokens-table.js
index 619aa0e053dbcaf45c8174d2bb99425fec4aa37b..94bb96357db76ab0ec9b26952e256bbca51c0c2f 100644
--- a/database/migrations/20191118004631-create-oauth-access-tokens-table.js
+++ b/database/migrations/20191118004631-create-oauth-access-tokens-table.js
@@ -66,4 +66,4 @@ module.exports = {
 	down: (migration, Types) => {
 		return migration.dropTable('oauth_access_tokens')
 	},
-}
\ No newline at end of file
+}
diff --git a/database/migrations/20191118005011-create-oauth-refresh-tokens-table.js b/database/migrations/20191118005011-create-oauth-refresh-tokens-table.js
index 16265efa39b55f4653b19d77827e7a79ae24ad59..364331a082b462cd8dd64b9a90734793c4cabc1e 100644
--- a/database/migrations/20191118005011-create-oauth-refresh-tokens-table.js
+++ b/database/migrations/20191118005011-create-oauth-refresh-tokens-table.js
@@ -66,4 +66,4 @@ module.exports = {
 	down: (migration, Types) => {
 		return migration.dropTable('oauth_refresh_tokens')
 	},
-}
\ No newline at end of file
+}
diff --git a/database/migrations/20191118011656-create-files-search-index-table.js b/database/migrations/20191118011656-create-files-search-index-table.js
index 6f0feb3d104512a3ed6d5874d7541e4cdc2382d4..020eb638a5ebb075044fbfe5872a4d5c5099aa07 100644
--- a/database/migrations/20191118011656-create-files-search-index-table.js
+++ b/database/migrations/20191118011656-create-files-search-index-table.js
@@ -1,15 +1,22 @@
 module.exports = {
 	up: (migration, Types) => {
 		return migration.sequelize.transaction(async t => {
-			await migration.sequelize.query(`
+			await migration.sequelize.query(
+				`
 CREATE TABLE IF NOT EXISTS files_search_index (
     file_id UUID UNIQUE PRIMARY KEY NOT NULL REFERENCES files(id) ON DELETE CASCADE ON UPDATE CASCADE,
     search_index TSVECTOR NOT NULL
-);`, { transaction: t })
+);`,
+				{ transaction: t },
+			)
 
-			await migration.sequelize.query(`CREATE INDEX IF NOT EXISTS files_search_index_idx ON files_search_index USING GIN(search_index);`, { transaction: t })
+			await migration.sequelize.query(
+				`CREATE INDEX IF NOT EXISTS files_search_index_idx ON files_search_index USING GIN(search_index);`,
+				{ transaction: t },
+			)
 
-			await migration.sequelize.query(`
+			await migration.sequelize.query(
+				`
 CREATE OR REPLACE FUNCTION update_files_search_index() RETURNS TRIGGER AS $FUNC$
     DECLARE
         indexed_text TSVECTOR := to_tsvector(concat(NEW.comment, array_to_string(NEW.tags, ' ', ''), array_to_string(NEW.labels, ' ', '')));
@@ -20,20 +27,37 @@ CREATE OR REPLACE FUNCTION update_files_search_index() RETURNS TRIGGER AS $FUNC$
             
 		RETURN NEW;
     END;
-$FUNC$ LANGUAGE plpgsql;`, { transaction: t })
+$FUNC$ LANGUAGE plpgsql;`,
+				{ transaction: t },
+			)
 
-			await migration.sequelize.query(`
+			await migration.sequelize.query(
+				`
 CREATE TRIGGER update_files_search_index_trigger AFTER INSERT OR UPDATE ON files
-	FOR EACH ROW EXECUTE FUNCTION update_files_search_index();`, { transaction: t })
+	FOR EACH ROW EXECUTE FUNCTION update_files_search_index();`,
+				{ transaction: t },
+			)
 		})
 	},
 
 	down: (migration, Types) => {
 		return migration.sequelize.transaction(async t => {
-			await migration.sequelize.query(`DROP TRIGGER IF EXISTS update_files_search_index_trigger ON files`, { transaction: t })
-			await migration.sequelize.query(`DROP FUNCTION IF EXISTS update_files_search_index()`, { transaction: t })
-			await migration.sequelize.query(`DROP INDEX IF EXISTS files_search_index_idx`, { transaction: t })
-			await migration.sequelize.query(`DROP TABLE IF EXISTS files_search_index`, { transaction: t })
+			await migration.sequelize.query(
+				`DROP TRIGGER IF EXISTS update_files_search_index_trigger ON files`,
+				{ transaction: t },
+			)
+			await migration.sequelize.query(
+				`DROP FUNCTION IF EXISTS update_files_search_index()`,
+				{ transaction: t },
+			)
+			await migration.sequelize.query(
+				`DROP INDEX IF EXISTS files_search_index_idx`,
+				{ transaction: t },
+			)
+			await migration.sequelize.query(
+				`DROP TABLE IF EXISTS files_search_index`,
+				{ transaction: t },
+			)
 		})
-	}
-}
\ No newline at end of file
+	},
+}
diff --git a/database/migrations/20200112223826-create-files-likes-table.js b/database/migrations/20200112223826-create-files-likes-table.js
index f8122036754e45913b4540a3af8b7d4ddec11cd4..196a7aefc50ef5d3613a88a97cf3e2c156bf1b90 100644
--- a/database/migrations/20200112223826-create-files-likes-table.js
+++ b/database/migrations/20200112223826-create-files-likes-table.js
@@ -36,4 +36,4 @@ module.exports = {
 	down: (migration, Types) => {
 		return migration.dropTable('user_file_likes')
 	},
-}
\ No newline at end of file
+}
diff --git a/database/migrations/20200210015221-add-files-featured-timestamp.js b/database/migrations/20200210015221-add-files-featured-timestamp.js
index 15d4fe0546854bb93d1cf8430a76d97d6475d4e3..bc81b39c3635520240224030a88336c9e58aaf5f 100644
--- a/database/migrations/20200210015221-add-files-featured-timestamp.js
+++ b/database/migrations/20200210015221-add-files-featured-timestamp.js
@@ -9,4 +9,4 @@ module.exports = {
 	down: (migration, Types) => {
 		return migration.removeColumn('files', 'featured_at')
 	},
-}
\ No newline at end of file
+}
diff --git a/database/migrations/20200323000437-create-bundle-codes-table.js b/database/migrations/20200323000437-create-bundle-codes-table.js
index 056e525a787e45646a0c697e636d942da65f3038..d5037b37a2466510c2a9c73861737f42b50f96ec 100644
--- a/database/migrations/20200323000437-create-bundle-codes-table.js
+++ b/database/migrations/20200323000437-create-bundle-codes-table.js
@@ -46,4 +46,4 @@ module.exports = {
 	down: (migration, Types) => {
 		return migration.dropTable('')
 	},
-}
\ No newline at end of file
+}
diff --git a/database/migrations/20200323000553-create-user-bundle-codes-table.js b/database/migrations/20200323000553-create-user-bundle-codes-table.js
index 84b0443be83e4e9c405463d84e5a32162028c6d3..0609c16d0e368be55bcdd7a61fb79ea940504544 100644
--- a/database/migrations/20200323000553-create-user-bundle-codes-table.js
+++ b/database/migrations/20200323000553-create-user-bundle-codes-table.js
@@ -27,4 +27,4 @@ module.exports = {
 	down: (migration, Types) => {
 		return migration.dropTable('user_bundle_codes')
 	},
-}
\ No newline at end of file
+}
diff --git a/database/migrations/20210000000000-create-analytics-table.js b/database/migrations/20210000000000-create-analytics-table.js
new file mode 100644
index 0000000000000000000000000000000000000000..cf5b5a11059450543f1dd99cb48c8e45bd716ea6
--- /dev/null
+++ b/database/migrations/20210000000000-create-analytics-table.js
@@ -0,0 +1,91 @@
+module.exports = {
+	up: (migration, Types) => {
+		return migration.sequelize.transaction(async t => {
+			await migration.createTable(
+				'analytics',
+				{
+					id: {
+						type: Types.UUID,
+						primaryKey: true,
+						defaultValue: Types.UUIDV4,
+						allowNull: false,
+					},
+					session_id: {
+						type: Types.UUID,
+						allowNull: true,
+					},
+					type: {
+						type: Types.TEXT,
+						allowNull: false,
+					},
+					start_time: {
+						type: Types.DATE,
+						defaultValue: Types.fn('now'),
+						allowNull: false,
+					},
+					end_time: {
+						type: Types.DATE,
+						allowNull: true,
+					},
+					parent_id: {
+						type: Types.UUID,
+						allowNull: true,
+						references: {
+							model: 'analytics',
+							key: 'id',
+						},
+						onDelete: 'CASCADE',
+						onUpdate: 'CASCADE',
+					},
+					properties: {
+						type: Types.JSONB,
+						defaultValue: '{}',
+						allowNull: false,
+					},
+					location: {
+						type: Types.GEOGRAPHY('POINT', 4326),
+						allowNull: true,
+					},
+					device: {
+						type: Types.JSONB,
+						allowNull: true,
+					},
+					meta: {
+						type: Types.JSONB,
+						defaultValue: {},
+						allowNull: false,
+					},
+				},
+				{ transaction: t },
+			)
+			// await migration.addColumn(
+			// 	'analytics',
+			// 	'parent_id',
+			// 	{
+			// 		type: Types.UUID,
+			// 		allowNull: true,
+			// 		references: {
+			// 			model: 'analytics',
+			// 			key: 'id',
+			// 		},
+			// 		onDelete: 'CASCADE',
+			// 		onUpdate: 'CASCADE',
+			// 	},
+			// 	{ transaction: t },
+			// )
+			await migration.sequelize.query(
+				'CREATE INDEX analytics_session_id_idx ON analytics(session_id)',
+				{ transaction: t },
+			)
+		})
+	},
+
+	down: (migration, Types) => {
+		return migration.sequelize.transaction(async t => {
+			await migration.removeIndex('analytics', 'analytics_session_id_idx', {
+				transaction: t,
+			})
+			await migration.dropTable('analytics', { transaction: t })
+		})
+	},
+}
diff --git a/package-lock.json b/package-lock.json
index 2adb6e0d1d7bdb5c3676ca357aad9e595bea52ae..87b04fea73525a04f6f05c88dccb2638d1ab7eb1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7393,6 +7393,14 @@
         "xtend": "^4.0.0"
       }
     },
+    "postmark": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/postmark/-/postmark-2.7.1.tgz",
+      "integrity": "sha512-mevTZY8mZ2+DqBQweoUVsjlcsY2wu/WotDKplsFMiOy4mG7euOzmD4pqkWFymMVjeKbU52NZWEkO2Et1X0tdFw==",
+      "requires": {
+        "axios": "^0.21.1"
+      }
+    },
     "prelude-ls": {
       "version": "1.1.2",
       "resolved": "https://npm.lcr.gr/prelude-ls/-/prelude-ls-1.1.2.tgz",
@@ -7405,6 +7413,12 @@
       "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
       "dev": true
     },
+    "prettier": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz",
+      "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==",
+      "dev": true
+    },
     "pretty-format": {
       "version": "26.6.2",
       "resolved": "https://npm.lcr.gr/pretty-format/-/pretty-format-26.6.2.tgz",
diff --git a/package.json b/package.json
index 13d1e659bf5e0630f870966c5bc88af921f894e6..68592072e2e20c38b9881f58e622882e30a85663 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,8 @@
     "start": "NODE_PATH=src node server",
     "cmd": "NODE_PATH=src node run",
     "sql": "NODE_PATH=src node scripts/npx-boot.js sequelize",
-    "repl": "NODE_PATH=src node -e 'Object.entries(require(\"bootstrap\")).forEach(([key, value]) => Object.defineProperty(global, key, { value })); boot().then(() => console.log(\"Booted\"))' -i"
+    "repl": "NODE_PATH=src node -e 'Object.entries(require(\"bootstrap\")).forEach(([key, value]) => Object.defineProperty(global, key, { value })); boot().then(() => console.log(\"Booted\"))' -i",
+	"prettier": "prettier server.js worker.js run.js src database --write"
   },
   "author": "Louis Capitanchik <louis@microhacks.co.uk>",
   "license": "GPL-3.0+",
@@ -56,6 +57,7 @@
     "pg": "^8.3.0",
     "pg-hstore": "^2.3.3",
     "pluralize": "^8.0.0",
+    "postmark": "^2.7.1",
     "redbird": "^0.10.0",
     "remarkable": "^2.0.1",
     "scrypt-kdf": "^2.0.1",
@@ -67,6 +69,7 @@
   "devDependencies": {
     "jest": "^26.6.3",
     "nodemon": "^2.0.4",
+    "prettier": "^2.2.1",
     "supertest": "^6.1.3"
   }
 }
diff --git a/run.js b/run.js
index c2033eae1fb8df6bbb9a6a4bd8ed61de8af03f60..13f1ee6e294cbfc4fd646cc72aeb8317c22eb291 100644
--- a/run.js
+++ b/run.js
@@ -11,8 +11,7 @@ async function runWorker() {
 		.commandDir(bootstrap.fs.path('src', 'console'))
 		.demandCommand()
 		.recommendCommands()
-		.help()
-		.argv
+		.help().argv
 }
 
 async function runMaster() {
@@ -44,9 +43,7 @@ async function run() {
 	}
 }
 
-
-run()
-	.catch(e => {
-		console.error(e)
-		process.exit(1)
-	})
\ No newline at end of file
+run().catch(e => {
+	console.error(e)
+	process.exit(1)
+})
diff --git a/server.js b/server.js
index 339c0902d001a326478040cba144cf7f5e15b7ec..62e95633b796562d6bfd36d925954447eb6ec96e 100644
--- a/server.js
+++ b/server.js
@@ -9,7 +9,7 @@ const debug = require('debug')('server:boot')
 const pkg = require('./package.json')
 
 const Sentry = require('@sentry/node')
-const Tracing = require("@sentry/tracing");
+const Tracing = require('@sentry/tracing')
 
 let server = null
 let worker = null
@@ -20,18 +20,21 @@ function bindSentry(app) {
 
 	debug('Binding sentry to app level errors')
 
-	app.on("error", (err, ctx) => {
+	app.on('error', (err, ctx) => {
 		console.error(err)
-		Sentry.withScope(function(scope) {
-			scope.addEventProcessor(function(event) {
-				return Sentry.Handlers.parseRequest(event, ctx.request);
-			});
-			Sentry.captureException(err);
-		});
-	});
+		Sentry.withScope(function (scope) {
+			scope.addEventProcessor(function (event) {
+				return Sentry.Handlers.parseRequest(event, ctx.request)
+			})
+			Sentry.captureException(err)
+		})
+	})
 }
 
-async function launch(port = 0, host = config('app.host.web', `http://localhost:${ port }`)) {
+async function launch(
+	port = 0,
+	host = config('app.host.web', `http://localhost:${port}`),
+) {
 	const koa = new Koa()
 	const appserver = await app(koa)
 
@@ -41,7 +44,7 @@ async function launch(port = 0, host = config('app.host.web', `http://localhost:
 
 	const httpServer = http.createServer(appserver.callback())
 	httpServer.listen(port)
-	debug(`Listening on ${ host }`)
+	debug(`Listening on ${host}`)
 
 	server = httpServer
 
@@ -62,9 +65,9 @@ async function runProxy() {
 		throw new Error('Failed to start')
 	}
 
-	debug(`Binding hosts [${ hosts.join(', ') }] to server port ${ address.port}`)
+	debug(`Binding hosts [${hosts.join(', ')}] to server port ${address.port}`)
 	for (const host of hosts) {
-		proxy.register(host, `http://127.0.0.1:${ address.port }`)
+		proxy.register(host, `http://127.0.0.1:${address.port}`)
 	}
 }
 
@@ -89,18 +92,12 @@ async function main() {
 	}
 }
 
-main()
-	.catch(e => {
-		console.error(e)
-		process.exit(1)
-	})
-
+main().catch(e => {
+	console.error(e)
+	process.exit(1)
+})
 
-const cleanupsigs = [
-	'SIGINT',
-	'SIGTERM',
-	'SIGUSR2',
-]
+const cleanupsigs = ['SIGINT', 'SIGTERM', 'SIGUSR2']
 
 cleanupsigs.forEach(signal => {
 	process.on(signal, () => {
diff --git a/src/app.js b/src/app.js
index 293f58336bbf62df4324883e4ae5d416e32f03db..b717c76914829b9ac9c84c3404a72dadb30e97f0 100644
--- a/src/app.js
+++ b/src/app.js
@@ -20,20 +20,27 @@ module.exports = async function createApp(app = new Koa()) {
 
 	app.keys = [config('app.key')]
 
-	app.use(hbs(fs.path('views'), {
-		debug: debughbs,
-	}))
+	app.use(
+		hbs(fs.path('views'), {
+			debug: debughbs,
+		}),
+	)
 	app.use(etag({ weak: true }))
 	app.use(bodyparser())
 	app.use(logger(s => requestLog(s)))
 
-	app.use(session({
-		key: config('app.session_key'),
-		renew: true,
-		secure: config('app.secure_sessions'),
-		signed: true,
-		httpOnly: true,
-	}, app))
+	app.use(
+		session(
+			{
+				key: config('app.session_key'),
+				renew: true,
+				secure: config('app.secure_sessions'),
+				signed: true,
+				httpOnly: true,
+			},
+			app,
+		),
+	)
 
 	if (!config('app.secure_sessions')) {
 		debug('Sessions are not restricted to HTTPS')
@@ -44,7 +51,11 @@ module.exports = async function createApp(app = new Koa()) {
 	app.use(serviceProvider.attach)
 
 	Object.values(routers).forEach(router => {
-		debug('[Prefix "%s"] Mounting %d layers', router.opts?.prefix ?? '/', router.stack?.length ?? 0)
+		debug(
+			'[Prefix "%s"] Mounting %d layers',
+			router.opts?.prefix ?? '/',
+			router.stack?.length ?? 0,
+		)
 		app.use(router.routes())
 		app.use(router.allowedMethods())
 	})
diff --git a/src/bootstrap.js b/src/bootstrap.js
index 09bf9daab08924efc1544fe436562933ac11ba93..3d82b02b66c6a0019a4a925019e01280ae888a9c 100644
--- a/src/bootstrap.js
+++ b/src/bootstrap.js
@@ -13,7 +13,7 @@ require('core/events/register')
 
 exports.unset = Symbol('unset')
 
-exports.boot = async function() {
+exports.boot = async function () {
 	const dotenv = require('dotenv')
 	const expand = require('dotenv-expand')
 	const { loadKeys } = require('core/utils/jwt')
@@ -23,7 +23,9 @@ exports.boot = async function() {
 		expand(conf)
 	}
 
-	const envEnv = dotenv.config({ path: `.env.${ process.env.NODE_ENV ?? 'development' }`})
+	const envEnv = dotenv.config({
+		path: `.env.${process.env.NODE_ENV ?? 'development'}`,
+	})
 	expand(envEnv)
 
 	const conf = dotenv.config()
@@ -32,8 +34,7 @@ exports.boot = async function() {
 	if (exports.env('GCS_CREDENTIALS_B64', '').trim() !== '') {
 		await exports.fs.writeAsync(
 			'google-storage.json',
-			Buffer.from(exports.env('GCS_CREDENTIALS_B64'), 'base64')
-				.toString()
+			Buffer.from(exports.env('GCS_CREDENTIALS_B64'), 'base64').toString(),
 		)
 	}
 
@@ -105,14 +106,13 @@ exports.config = function getConfigValue(path, fallback = null) {
 	const [file, ...innerPath] = path.split('.')
 	let conf = null
 	try {
-		conf = require(`./config/${ file }`)
+		conf = require(`./config/${file}`)
 	} catch (e) {
 		console.error(e)
 		// require('services').log.error(e)
 		return fallback
 	}
 
-
 	if (conf.hasOwnProperty('driver')) {
 		if (innerPath.length === 1 && innerPath[0] === 'driver') {
 			return conf.driver
@@ -131,8 +131,8 @@ exports.patchConfig = function setConfigValue(path, value) {
 	const [file, ...innerPath] = path.split('.')
 	let conf = null
 	try {
-		conf = require(`./config/${ file }`)
-	} catch(e) {
+		conf = require(`./config/${file}`)
+	} catch (e) {
 		console.error(e)
 		return null
 	}
@@ -155,21 +155,21 @@ exports.fs = jetpack.cwd(pathUtil.join(__dirname, '..'))
 exports.url = {
 	web(name, params) {
 		const routes = require('./http/routes')
-		return (new URL(
-				routes.web.url(name, params),
-				exports.config('app.host.web'))
+		return new URL(
+			routes.web.url(name, params),
+			exports.config('app.host.web'),
 		).toString()
 	},
 	api(name, params) {
 		const routes = require('./http/routes')
-		return (new URL(
-				pathUtil.join('api', routes.api.url(name, params)),
-				exports.config('app.host.api'))
+		return new URL(
+			pathUtil.join('api', routes.api.url(name, params)),
+			exports.config('app.host.api'),
 		).toString()
 	},
 }
 
-exports.route = function(type, name, params) {
+exports.route = function (type, name, params) {
 	const routers = require('http/routes')
 	if (!routers.hasOwnProperty(type)) {
 		return null
@@ -180,7 +180,9 @@ exports.route = function(type, name, params) {
 }
 
 exports.routes = {
-	api(name, params) { return exports.route('api', name, params) },
+	api(name, params) {
+		return exports.route('api', name, params)
+	},
 }
 
 exports.invoke = async function invokeCommand(command, args = [], sync = true) {
@@ -193,6 +195,6 @@ exports.invoke = async function invokeCommand(command, args = [], sync = true) {
 			...process.env,
 			NODE_PATH: __dirname,
 		},
-		stdio: 'inherit'
+		stdio: 'inherit',
 	})
-}
\ No newline at end of file
+}
diff --git a/src/config/app.js b/src/config/app.js
index 333b95cdc679860f160a76ba64c20687f123fc95..f10841c43820e5f6203cc738610f140462acfee8 100644
--- a/src/config/app.js
+++ b/src/config/app.js
@@ -17,7 +17,10 @@ module.exports = {
 		public_key_b64: env('RSA_PUBLIC_KEY_B64', null),
 		private_key: null,
 		private_key_b64: env('RSA_PRIVATE_KEY_B64', null),
-	}
+	},
 }
 
-module.exports.secure_session = env('SECURE_SESSIONS') != null ? env('SECURE_SESSIONS') !== 'FALSE' : !module.exports.dev
+module.exports.secure_session =
+	env('SECURE_SESSIONS') != null
+		? env('SECURE_SESSIONS') !== 'FALSE'
+		: !module.exports.dev
diff --git a/src/config/client.js b/src/config/client.js
index 88205e6742295c2359669d75d3a39e3e5108c0bd..4bcd6e0813bb3816caf8cf0f0aa32d7d86bcae7c 100644
--- a/src/config/client.js
+++ b/src/config/client.js
@@ -8,5 +8,5 @@ module.exports = {
 		red: '#ff8080',
 		yellow: '#ffdd67',
 		orange: '#ffba92',
-	}
+	},
 }
diff --git a/src/config/database.js b/src/config/database.js
index a200bb95863d4d7f5caa2fc7a5dad83401813c08..3e95b1f85602bbc8d4e08ff3a6f58206ade5a8ba 100644
--- a/src/config/database.js
+++ b/src/config/database.js
@@ -26,12 +26,15 @@ if (url) {
 		dialectOptions: {
 			ssl: {
 				rejectUnauthorized: false,
-				ca: Buffer.from(env('DATABASE_CA_CERT', null) ?? '', 'base64').toString(),
-			}
+				ca: Buffer.from(
+					env('DATABASE_CA_CERT', null) ?? '',
+					'base64',
+				).toString(),
+			},
 		},
 		log_queries: env('LOG_SQL_QUERIES', 'true') === 'true',
 	}
 }
 
 // module.exports.logging = require('debug')('server:database')
-module.exports.logging = false
\ No newline at end of file
+module.exports.logging = false
diff --git a/src/config/mail.js b/src/config/mail.js
index d9735e85b56662572fb9753d6f2e2734d26607c7..5dc0d695b3839a98384deca05bc23ceb5dbb7b23 100644
--- a/src/config/mail.js
+++ b/src/config/mail.js
@@ -1,7 +1,9 @@
 const { env, config } = require('bootstrap')
 
 const fromTo = {
-	from: env('MAIL_FROM', () => { throw new Error('Missing mail from address')}),
+	from: env('MAIL_FROM', () => {
+		throw new Error('Missing mail from address')
+	}),
 	name: env('MAIL_FROM_NAME', 'Autobot'),
 	replyto: env('MAIL_REPLY_TO', () => config('mail.from', null)),
 }
@@ -9,7 +11,28 @@ const fromTo = {
 module.exports = {
 	driver: env('MAIL_DRIVER', 'log'),
 	sendgrid: {
-		key: env('SENDGRID_KEY', () => { throw new Error('Missing Sendgrid Key') }),
+		key: env('SENDGRID_KEY', () => {
+			throw new Error('Missing Sendgrid Key')
+		}),
+		...fromTo,
+		opts: {
+			from: {
+				email: env('MAIL_FROM'),
+				name: env('MAIL_FROM_NAME'),
+			},
+			replyTo: {
+				email: env('MAIL_REPLY_TO'),
+				name: env('MAIL_FROM_NAME'),
+			},
+		},
+		templates: {
+			'reset-password': 'd-dd89d66ad75f40f5b3b0ed6849753cf7',
+		},
+	},
+	postmark: {
+		key: env('POSTMARK_KEY', () => {
+			throw new Error('Missing Postmark Key')
+		}),
 		...fromTo,
 		opts: {
 			from: {
@@ -34,5 +57,5 @@ module.exports = {
 		port: env('SMTP_PORT'),
 		user: env('SMTP_USERNAME'),
 		password: env('SMTP_PASSWORD'),
-	}
+	},
 }
diff --git a/src/config/pusher.js b/src/config/pusher.js
index 99a172b2fea42e9857e28c6edd46b44d7abb6ad3..fc61e5028c46b681d9fabb9df1782efecbd8c758 100644
--- a/src/config/pusher.js
+++ b/src/config/pusher.js
@@ -4,5 +4,5 @@ module.exports = {
 	id: env('PUSHER_APP_ID'),
 	key: env('PUSHER_APP_KEY'),
 	secret: env('PUSHER_APP_SECRET'),
-	region: env('PUSHER_APP_REGION')
+	region: env('PUSHER_APP_REGION'),
 }
diff --git a/src/config/queue.js b/src/config/queue.js
index c7437c81cff3a52a96ce123e3303edc325e9def5..29314227444d2755ecd3450b8da27f9a5c84b089 100644
--- a/src/config/queue.js
+++ b/src/config/queue.js
@@ -8,5 +8,6 @@ module.exports = {
 		user: env('AMQP_USER', 'guest'),
 		pass: env('AMQP_PASSWORD', 'guest'),
 		secure: env('AMQP_SECURE', 'false') === 'true',
-	}
-}
\ No newline at end of file
+		retries: parseInt(env('QUEUE_RETRIES', '5'), 10),
+	},
+}
diff --git a/src/config/sequelize.js b/src/config/sequelize.js
index 585e1b1d90c7e6c391c785e566772251893486fb..99516bffaf2779a73e6cc243a1adb2f597e55599 100644
--- a/src/config/sequelize.js
+++ b/src/config/sequelize.js
@@ -7,13 +7,13 @@ module.exports = {
 		password: 'hackerfest',
 		database: 'hackerfest',
 		host: '127.0.0.1',
-		...(config('database', {})),
+		...config('database', {}),
 		dialect: 'postgres',
 		pool: {
 			max: 5,
 			min: 1,
 			acquire: 30000,
-			idle: 10000
+			idle: 10000,
 		},
 		define: {
 			timestamps: true,
@@ -26,13 +26,13 @@ module.exports = {
 		password: 'hackerfest',
 		database: 'hackerfest',
 		host: '127.0.0.1',
-		...(config('database', {})),
+		...config('database', {}),
 		dialect: 'postgres',
 		pool: {
 			max: 10,
 			min: 1,
 			acquire: 30000,
-			idle: 10000
+			idle: 10000,
 		},
 		define: {
 			timestamps: true,
@@ -45,13 +45,13 @@ module.exports = {
 		password: 'hackerfest',
 		database: 'hackerfest',
 		host: '127.0.0.1',
-		...(config('database', {})),
+		...config('database', {}),
 		dialect: 'postgres',
 		pool: {
 			max: 10,
 			min: 1,
 			acquire: 30000,
-			idle: 10000
+			idle: 10000,
 		},
 		define: {
 			timestamps: true,
diff --git a/src/config/totp.js b/src/config/totp.js
index e0d77cdc0e010fd9373f178af3001d690cb1fc0a..5c518ff40abf9e49d4f0feacead3475c3d125c3f 100644
--- a/src/config/totp.js
+++ b/src/config/totp.js
@@ -2,7 +2,5 @@ const { env } = require('bootstrap')
 
 module.exports = {
 	driver: env('TOTP_DRIVER', 'vault'),
-	vault: {
-
-	}
-}
\ No newline at end of file
+	vault: {},
+}
diff --git a/src/console/CleanTestDatabases.js b/src/console/CleanTestDatabases.js
index 3be7e6e64414441c919867e34f626eba11c79d6d..4477a8a843a59970016fe4300974b797d1cb7a0d 100644
--- a/src/console/CleanTestDatabases.js
+++ b/src/console/CleanTestDatabases.js
@@ -4,9 +4,11 @@ module.exports = {
 	async handler(args) {
 		const { config } = require('bootstrap')
 		const { sequelize } = require('database/models')
-		const [databases] = await sequelize.query(`SELECT datname as name FROM pg_database WHERE datistemplate = false;`)
+		const [databases] = await sequelize.query(
+			`SELECT datname as name FROM pg_database WHERE datistemplate = false;`,
+		)
 
-		const prefix = `${ config('database.database') }_`
+		const prefix = `${config('database.database')}_`
 		const toDelete = []
 
 		for (const entry of databases) {
@@ -16,16 +18,16 @@ module.exports = {
 		}
 
 		if (toDelete.length === 0) {
-			console.log("No databases with the prefix %s", prefix)
+			console.log('No databases with the prefix %s', prefix)
 		} else {
-			console.log("Deleting Databases:")
+			console.log('Deleting Databases:')
 
 			for (const name of toDelete) {
-				console.log(`  ${ name }`)
-				await sequelize.query(`DROP DATABASE ${ name };`)
+				console.log(`  ${name}`)
+				await sequelize.query(`DROP DATABASE ${name};`)
 			}
 		}
 
 		process.exit(0)
-	}
-}
\ No newline at end of file
+	},
+}
diff --git a/src/console/CreateNewDatabase.js b/src/console/CreateNewDatabase.js
index d9ee348fc3fd6a98f166dd063a77c1f478bf2028..7f6a913b1e697f2c953f721dd3f3d2e4f96b2bae 100644
--- a/src/console/CreateNewDatabase.js
+++ b/src/console/CreateNewDatabase.js
@@ -2,47 +2,55 @@ module.exports = {
 	command: 'db:fresh [id]',
 	description: 'Create a fresh database instance with a random name',
 	builder(yargs) {
-		yargs.positional('id', {
-			describe: 'A preset ID to use for the database name',
-			type: 'string',
-		}).option('and-migrate', {
-			demandOption: false,
-			alias: 'migrate',
-			describe: 'Run migrations against the newly created database',
-		})
+		yargs
+			.positional('id', {
+				describe: 'A preset ID to use for the database name',
+				type: 'string',
+			})
+			.option('and-migrate', {
+				demandOption: false,
+				alias: 'migrate',
+				describe: 'Run migrations against the newly created database',
+			})
 	},
 	async handler(args) {
 		const { v4: uuid } = require('uuid')
 		const { config } = require('bootstrap')
 
 		const dbid = args.id ?? uuid().replace(/-/g, '')
-		const newDatabaseName = `${ config('database.database') }_${ dbid }`
+		const newDatabaseName = `${config('database.database')}_${dbid}`
 
 		const { sequelize } = require('database/models')
 
-		const [databases] = await sequelize.query(`SELECT datname as name FROM pg_database WHERE datistemplate = false;`)
+		const [databases] = await sequelize.query(
+			`SELECT datname as name FROM pg_database WHERE datistemplate = false;`,
+		)
 
 		for (const entry of databases) {
 			if (entry.name === newDatabaseName) {
-				throw new Error(`Database with name ${ newDatabaseName } already exists`)
+				throw new Error(`Database with name ${newDatabaseName} already exists`)
 			}
 		}
 
 		console.log('Creating new database with name', newDatabaseName)
-		await sequelize.query(`CREATE DATABASE ${ newDatabaseName };`)
+		await sequelize.query(`CREATE DATABASE ${newDatabaseName};`)
 
 		if (args.migrate) {
 			console.log('Running migrations on database', newDatabaseName)
 			const { exec } = require('core/utils/process')
-			await exec('npm run sql db:migrate', {
-				env: {
-					...process.env,
-					DATABASE_NAME: newDatabaseName,
+			await exec(
+				'npm run sql db:migrate',
+				{
+					env: {
+						...process.env,
+						DATABASE_NAME: newDatabaseName,
+					},
+					stdio: 'inherit',
 				},
-				stdio: 'inherit',
-			}, true)
+				true,
+			)
 		}
 
 		process.exit(0)
-	}
-}
\ No newline at end of file
+	},
+}
diff --git a/src/console/CreateSystemUser.js b/src/console/CreateSystemUser.js
index c1b4982b528bec114fbbefeee470dd8385952572..ac5f406000a59936d263d4fb0e8bc83b75379d4d 100644
--- a/src/console/CreateSystemUser.js
+++ b/src/console/CreateSystemUser.js
@@ -13,5 +13,5 @@ module.exports = {
 		}
 
 		process.exit(0)
-	}
-}
\ No newline at end of file
+	},
+}
diff --git a/src/core/errors/HttpError.js b/src/core/errors/HttpError.js
index b30a5669f8d18bf057ef13c344bef2cd6e14cbe3..bf54766e1a3dea9da5fb4c100379a07a12f2cb16 100644
--- a/src/core/errors/HttpError.js
+++ b/src/core/errors/HttpError.js
@@ -32,4 +32,4 @@ module.exports = class HttpError extends Error {
 			ctx.body = { errors: { general: [this._message] } }
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/src/core/errors/InputValidationError.js b/src/core/errors/InputValidationError.js
index a6234dae6025429a2df5e3e563cb3846b0f648ef..1a97aef27efdefed093e24b1bb7b66e11450238b 100644
--- a/src/core/errors/InputValidationError.js
+++ b/src/core/errors/InputValidationError.js
@@ -4,4 +4,4 @@ module.exports = class InputValidationError extends HttpError {
 	constructor(fields) {
 		super(422, 'The supplied input was not valid', { fields })
 	}
-}
\ No newline at end of file
+}
diff --git a/src/core/errors/UnauthorizedError.js b/src/core/errors/UnauthorizedError.js
index 34a684efc7f9799b56dcd0eb9cefe3fac12265a7..81323dd2dab9d6d1759ffd581fc94dc2c54d2f4b 100644
--- a/src/core/errors/UnauthorizedError.js
+++ b/src/core/errors/UnauthorizedError.js
@@ -1,7 +1,7 @@
 const HttpError = require('./HttpError')
 
-module.exports = class UnauthorizedError extends  HttpError {
+module.exports = class UnauthorizedError extends HttpError {
 	constructor() {
 		super(401, 'You must be logged in to access this resource')
 	}
-}
\ No newline at end of file
+}
diff --git a/src/core/events/bus.js b/src/core/events/bus.js
index cf3958e983301db9dffe5ac339032ecc6bd3e8d5..d41d55dab8f58da01940ce9f9ece17f02609f1ef 100644
--- a/src/core/events/bus.js
+++ b/src/core/events/bus.js
@@ -1,7 +1,7 @@
 const EventEmitter = require('events')
 
 const bus = new EventEmitter()
-bus.on('error', function(error) {
+bus.on('error', function (error) {
 	console.log(error)
 })
 bus.setMaxListeners(250)
diff --git a/src/core/injection/ContextualModule.js b/src/core/injection/ContextualModule.js
index 8af8b5304d8f71d15983c453d11cc791d66a6efd..0fb0279e24b79f36542d5f43bed0049fcebff43b 100644
--- a/src/core/injection/ContextualModule.js
+++ b/src/core/injection/ContextualModule.js
@@ -1,8 +1,14 @@
 module.exports = class ContextualModule {
-	static withContext(ctx) { return new this(ctx) }
-	static withoutContext() { return new this({}) }
+	static withContext(ctx) {
+		return new this(ctx)
+	}
+	static withoutContext() {
+		return new this({})
+	}
 
-	static getServiceName() { throw new Error(`getServiceName Not Implemented for ${ this.name }`) }
+	static getServiceName() {
+		throw new Error(`getServiceName Not Implemented for ${this.name}`)
+	}
 
 	constructor(context = null) {
 		if (context == null) {
diff --git a/src/core/injection/ServiceProvider.js b/src/core/injection/ServiceProvider.js
index 5bbee4177b2521452f51566003393b5a95f518ed..f0d72e7f749132847c50c76c638ec72ea24f9fec 100644
--- a/src/core/injection/ServiceProvider.js
+++ b/src/core/injection/ServiceProvider.js
@@ -32,8 +32,8 @@ function createFakeContext() {
 	res.statusCode = 200
 
 	const context = Object.create(baseCtx)
-	const request = context.request = Object.create(baseReq)
-	const response = context.response = Object.create(baseRes)
+	const request = (context.request = Object.create(baseReq))
+	const response = (context.response = Object.create(baseRes))
 
 	context.app = request.app = response.app = this
 	context.req = request.req = response.req = req
@@ -63,7 +63,9 @@ module.exports = class ServiceProvider {
 		services.forEach(service => {
 			const name = service.getServiceName()
 			if (ctx.services.hasOwnProperty(name)) {
-				console.warn(`Multiple services found for name: ${name}. Using implementation provided by ${service.name}`)
+				console.warn(
+					`Multiple services found for name: ${name}. Using implementation provided by ${service.name}`,
+				)
 			}
 			ctx.services[name] = createServiceWithProfiling(service, ctx)
 		})
@@ -88,4 +90,4 @@ module.exports = class ServiceProvider {
 
 		return ServiceProvider.attach(newContext, () => newContext)
 	}
-}
\ No newline at end of file
+}
diff --git a/src/core/injection/ThreadContext.js b/src/core/injection/ThreadContext.js
index be53ee5c23c268f02bb8caab59a475f5827d5b07..86b3402550f29f97f20d394ce9d1ed6f0d9b898b 100644
--- a/src/core/injection/ThreadContext.js
+++ b/src/core/injection/ThreadContext.js
@@ -53,7 +53,7 @@ class ThreadContext extends AsyncLocalStorage {
 		if (existing) {
 			return existing
 		}
-		const Sentry = require("@sentry/node")
+		const Sentry = require('@sentry/node')
 		const t = Sentry.startTransaction(ctx)
 		this.set('profiling', t)
 		return t
diff --git a/src/core/services/dataloaders.js b/src/core/services/dataloaders.js
index db2f6971bd8e945056cd78f6c9793e9139616f1c..373e012f22e952b01b0c97911649b35ff593a3f1 100644
--- a/src/core/services/dataloaders.js
+++ b/src/core/services/dataloaders.js
@@ -1,40 +1,58 @@
 const Dataloader = require('dataloader')
 
-const createLoaderForModel = exports.createForModel = function (model, singleProperties = ['id'], multipleProperties = []) {
-	const loaders = singleProperties.reduce((set, property) => ({
-		...set,
-		[property]: new Dataloader(keys => Promise.all(keys.map(async key => {
-			const data = await model.findOne({where: {[property]: key}})
-			if (data) {
-				singleProperties.forEach(prop => {
-					if (prop !== property && data[prop] != null) {
-						loaders[prop].prime(data[prop], data)
-					}
-				})
-			}
-			return data
-		}))),
-	}), {})
+const createLoaderForModel = (exports.createForModel = function (
+	model,
+	singleProperties = ['id'],
+	multipleProperties = [],
+) {
+	const loaders = singleProperties.reduce(
+		(set, property) => ({
+			...set,
+			[property]: new Dataloader(keys =>
+				Promise.all(
+					keys.map(async key => {
+						const data = await model.findOne({ where: { [property]: key } })
+						if (data) {
+							singleProperties.forEach(prop => {
+								if (prop !== property && data[prop] != null) {
+									loaders[prop].prime(data[prop], data)
+								}
+							})
+						}
+						return data
+					}),
+				),
+			),
+		}),
+		{},
+	)
 
-	multipleProperties.reduce((set, property) => ({
-		...set,
-		[property]: new Dataloader(keys => Promise.all(keys.map(async key => {
-			const data = await model.find({where: {[property]: key}})
-			if (data && data.length) {
-				singleProperties.forEach(prop => {
-					data.forEach(datum => {
-						if (prop !== property && datum[prop] != null) {
-							loaders[prop].prime(datum[prop], datum)
+	multipleProperties.reduce(
+		(set, property) => ({
+			...set,
+			[property]: new Dataloader(keys =>
+				Promise.all(
+					keys.map(async key => {
+						const data = await model.find({ where: { [property]: key } })
+						if (data && data.length) {
+							singleProperties.forEach(prop => {
+								data.forEach(datum => {
+									if (prop !== property && datum[prop] != null) {
+										loaders[prop].prime(datum[prop], datum)
+									}
+								})
+							})
 						}
-					})
-				})
-			}
-			return data
-		})))
-	}), loaders)
+						return data
+					}),
+				),
+			),
+		}),
+		loaders,
+	)
 
 	return loaders
-}
+})
 
 exports.generateDataloaders = function () {
 	const { User } = require('database/models') // , Ticket, TicketType, Event, Episode, Venue, Address } = require('database/models')
@@ -49,11 +67,15 @@ exports.generateDataloaders = function () {
 	}
 }
 
-exports.DataloaderService = class extends require('core/injection/ContextualModule') {
-	static getServiceName() { return 'dataloaderService' }
+exports.DataloaderService = class extends (
+	require('core/injection/ContextualModule')
+) {
+	static getServiceName() {
+		return 'dataloaderService'
+	}
 
 	constructor(...args) {
-		super(...args);
+		super(...args)
 
 		const dataloader = exports.generateDataloaders()
 		Object.entries(dataloader).forEach(([name, loadermap]) => {
@@ -65,5 +87,4 @@ exports.DataloaderService = class extends require('core/injection/ContextualModu
 			})
 		})
 	}
-
 }
diff --git a/src/core/services/pusher.js b/src/core/services/pusher.js
index 5bf6794c885d9266c00c06724aba78e8956ec0c6..c639ec498c0b066047e80890f8856a02916ca6ac 100644
--- a/src/core/services/pusher.js
+++ b/src/core/services/pusher.js
@@ -1,4 +1,4 @@
-const Pusher = require('pusher');
+const Pusher = require('pusher')
 const { config } = require('bootstrap')
 
 const pusher = new Pusher({
@@ -6,7 +6,7 @@ const pusher = new Pusher({
 	key: config('pusher.key'),
 	secret: config('pusher.secret'),
 	cluster: config('pusher.region'),
-	encrypted: true
-});
+	encrypted: true,
+})
 
 module.exports = pusher
diff --git a/src/core/utils/crypto.js b/src/core/utils/crypto.js
index a89139038250ebc5276b9a61469f5c46205d7b01..942c26393cc2ca98d47320a8a7c735402420023c 100644
--- a/src/core/utils/crypto.js
+++ b/src/core/utils/crypto.js
@@ -36,7 +36,24 @@ exports.secureHexString = function generateSecureHexString(bytes) {
 	return exports.secureBuffer(bytes).then(b => b.toString('hex'))
 }
 
-const hex = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f']
+const hex = [
+	'1',
+	'2',
+	'3',
+	'4',
+	'5',
+	'6',
+	'7',
+	'8',
+	'9',
+	'0',
+	'a',
+	'b',
+	'c',
+	'd',
+	'e',
+	'f',
+]
 
 /**
  * Generate a hexadecimal string of the given length in a synchronous and insecure manner.
@@ -68,9 +85,63 @@ exports.insecureHexString = function generateInsecureHexStringSync(length) {
  * @type {string[]}
  */
 const friendlyAlphabet = [
-	'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
-	'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
-	'1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
+	'a',
+	'b',
+	'c',
+	'd',
+	'e',
+	'f',
+	'g',
+	'h',
+	'j',
+	'k',
+	'm',
+	'n',
+	'p',
+	'q',
+	'r',
+	's',
+	't',
+	'u',
+	'v',
+	'w',
+	'x',
+	'y',
+	'z',
+	'A',
+	'B',
+	'C',
+	'D',
+	'E',
+	'F',
+	'G',
+	'H',
+	'J',
+	'K',
+	'L',
+	'M',
+	'N',
+	'P',
+	'Q',
+	'R',
+	'S',
+	'T',
+	'U',
+	'V',
+	'W',
+	'X',
+	'Y',
+	'Z',
+	'1',
+	'2',
+	'3',
+	'4',
+	'5',
+	'6',
+	'7',
+	'8',
+	'9',
+	'0',
 ]
 exports.friendlyRefString = function generateFriendlyRefString(length) {
 	const buffer = []
@@ -140,8 +211,7 @@ exports.hash = function hashPayload(payload) {
 	const source = stringifyPayload(payload)
 	const inputBuffer = Buffer.from(source, 'utf-8')
 	const params = getScryptParams()
-	return scrypt.kdf(inputBuffer, params)
-		.then(hash => hash.toString('base64'))
+	return scrypt.kdf(inputBuffer, params).then(hash => hash.toString('base64'))
 }
 
 /**
@@ -256,13 +326,21 @@ exports.decryptWith = function decryptWithKey(key, encrypted) {
 	return buffer
 }
 
-exports.hmac = async function createHmacSignature(payload, secret, algorithm = 'sha256') {
+exports.hmac = async function createHmacSignature(
+	payload,
+	secret,
+	algorithm = 'sha256',
+) {
 	const hmac = crypto.createHmac(algorithm, secret)
 	hmac.update(payload)
 	return hmac.digest('base64')
 }
 
-exports.hashFile = function createFileHash(path, algorithm = 'sha384', output = 'base64') {
+exports.hashFile = function createFileHash(
+	path,
+	algorithm = 'sha384',
+	output = 'base64',
+) {
 	const hash = crypto.createHash(algorithm)
 	const stream = fs.createReadStream(path)
 
@@ -281,4 +359,4 @@ exports.hashFile = function createFileHash(path, algorithm = 'sha384', output =
 }
 
 exports.toBase64 = string => Buffer.from(string).toString('base64')
-exports.fromBase64 = string => Buffer.from(string, 'base64').toString()
\ No newline at end of file
+exports.fromBase64 = string => Buffer.from(string, 'base64').toString()
diff --git a/src/core/utils/jwt.js b/src/core/utils/jwt.js
index c3038df10968420c48e73579332c3cd5b651afff..8c4ba3480365c33f395fb4d4cfeabf044a785591 100644
--- a/src/core/utils/jwt.js
+++ b/src/core/utils/jwt.js
@@ -2,25 +2,29 @@ const { generateKeyPair, createPublicKey, createPrivateKey } = require('crypto')
 async function generateRsaKeys() {
 	const { config } = require('bootstrap')
 	return new Promise((resolve, reject) => {
-		generateKeyPair('rsa', {
-			modulusLength: 4096,
-			publicKeyEncoding: {
-				type: 'spki',
-				format: 'pem',
+		generateKeyPair(
+			'rsa',
+			{
+				modulusLength: 4096,
+				publicKeyEncoding: {
+					type: 'spki',
+					format: 'pem',
+				},
+				privateKeyEncoding: {
+					type: 'pkcs8',
+					format: 'pem',
+					cipher: 'aes-256-cbc',
+					passphrase: config('app.key'),
+				},
 			},
-			privateKeyEncoding: {
-				type: 'pkcs8',
-				format: 'pem',
-				cipher: 'aes-256-cbc',
-				passphrase: config('app.key'),
-			}
-		}, (err, pub, priv) => {
-			if (err) {
-				reject(err)
-			} else {
-				resolve({ pub: pub.toString(), priv: priv.toString() })
-			}
-		})
+			(err, pub, priv) => {
+				if (err) {
+					reject(err)
+				} else {
+					resolve({ pub: pub.toString(), priv: priv.toString() })
+				}
+			},
+		)
 	})
 }
 
@@ -42,7 +46,7 @@ exports.loadKeys = async () => {
 	if (pub != null && priv != null) {
 		return {
 			pub,
-			priv
+			priv,
 		}
 	}
 
@@ -56,26 +60,34 @@ exports.loadKeys = async () => {
 	}
 
 	pub = createPublicKey({ key: pub })
-	priv = createPrivateKey({ key: priv,  passphrase: config('app.security.private_key_passphrase', env('RSA_PRIVATE_PASSPHRASE', config('app.key'))) })
+	priv = createPrivateKey({
+		key: priv,
+		passphrase: config(
+			'app.security.private_key_passphrase',
+			env('RSA_PRIVATE_PASSPHRASE', config('app.key')),
+		),
+	})
 
 	patchConfig('app.security.public_key', pub)
 	patchConfig('app.security.private_key', priv)
 	return { pub, priv }
 }
 
-exports.sign = async (payload) => {
+exports.sign = async payload => {
 	const threadContext = require('core/injection/ThreadContext')
 	const { default: SignJWT } = require('jose/jwt/sign')
 	const { priv } = exports.getKeys()
 
-	return await threadContext.profile('jwt.sign', JSON.stringify(payload), () => new SignJWT(payload)
-		.setIssuer(exports.jwtOptions.issuer)
-		.setIssuedAt()
-		.setProtectedHeader({ alg: 'RS256' })
-		.sign(priv))
+	return await threadContext.profile('jwt.sign', JSON.stringify(payload), () =>
+		new SignJWT(payload)
+			.setIssuer(exports.jwtOptions.issuer)
+			.setIssuedAt()
+			.setProtectedHeader({ alg: 'RS256' })
+			.sign(priv),
+	)
 }
 
-exports.verify = async(token) => {
+exports.verify = async token => {
 	const threadContext = require('core/injection/ThreadContext')
 	const { default: jwtVerify } = require('jose/jwt/verify')
 	const { getKeys, jwtOptions } = exports
@@ -89,4 +101,4 @@ exports.verify = async(token) => {
 
 exports.jwtOptions = {
 	issuer: 'urn:jetsam:systems:auth',
-}
\ No newline at end of file
+}
diff --git a/src/core/utils/process.js b/src/core/utils/process.js
index d92c4cd43113eb4d16149c93ab8b6d11b6c140ff..6927d1b106ff49588809ce1a28cb0539608e1d1c 100644
--- a/src/core/utils/process.js
+++ b/src/core/utils/process.js
@@ -1,4 +1,4 @@
-exports.exec = async function(cmd, opts, linkStdio) {
+exports.exec = async function (cmd, opts, linkStdio) {
 	const proc = require('child_process')
 
 	let resolve, reject
@@ -18,4 +18,4 @@ exports.exec = async function(cmd, opts, linkStdio) {
 	child.once('exit', resolve)
 
 	return await defer
-}
\ No newline at end of file
+}
diff --git a/src/core/utils/queue.js b/src/core/utils/queue.js
index 31b162342f2487ad347ce5144eeee32e9f415d8a..c96201aaf208f97175decc160526c05021db73e5 100644
--- a/src/core/utils/queue.js
+++ b/src/core/utils/queue.js
@@ -1,8 +1,11 @@
 const HANDLERS = [
-	['send-user-password-reset', require('domain/auth/handlers/SendUserPasswordReset')]
+	[
+		'send-user-password-reset',
+		require('domain/auth/handlers/SendUserPasswordReset'),
+	],
 ]
 
 module.exports = function bindJobHandlers() {
 	const { queue } = require('services')
 	HANDLERS.forEach(([job, handler]) => queue.bind(job, handler))
-}
\ No newline at end of file
+}
diff --git a/src/core/utils/urls.js b/src/core/utils/urls.js
index 8fe489a36de97fc5c8e4a9a009d8a7fd32b6fdc4..ff3b8329d7a6ddafd418a4522469583dd1dead0b 100644
--- a/src/core/utils/urls.js
+++ b/src/core/utils/urls.js
@@ -10,6 +10,11 @@ exports.createUrl = (host, path, query = unset) => {
 	return url.toString()
 }
 
-exports.appUrl = (path, query = unset) => exports.createUrl(config('app.host.web'), path, query)
+exports.appUrl = (path, query = unset) =>
+	exports.createUrl(config('app.host.web'), path, query)
 
-exports.queryValueToArray = (value = '') => (value || '').split(',').map(s => s.trim()).filter(Boolean)
+exports.queryValueToArray = (value = '') =>
+	(value || '')
+		.split(',')
+		.map(s => s.trim())
+		.filter(Boolean)
diff --git a/src/core/utils/validation.js b/src/core/utils/validation.js
index a4bd295a75e9379c44530714dc6e76182a504807..3ba8c211a5770422d947d3ac3c9beebb853ef6e2 100644
--- a/src/core/utils/validation.js
+++ b/src/core/utils/validation.js
@@ -1,66 +1,57 @@
 const Joi = require('joi')
 
 exports['survey'] = Joi.object({
-	questions: Joi.array().items(Joi.object({
-		question: Joi.string().min(1).required(),
-		type: Joi.any().valid(
-			'text',
-			'number',
-			'checkbox',
-			'choice',
-			'range',
-		).id('survey-type'),
-		required: Joi.boolean().required(),
-		constraints: Joi.alternatives().conditional(Joi.ref('#survey-type'), {
-			switch: [
-				{
-					is: 'text',
-					then: Joi.object({
-						email: Joi.boolean(),
-					}),
-				},
-				{
-					is: 'number',
-					then: Joi.object({
-						min: Joi.number(),
-						max: Joi.number(),
-					}),
-				},
-				{
-					is: 'checkbox',
-					then: Joi.object({
-						checkRequired: Joi.boolean(),
-					}),
-				},
-				{
-					is: 'choice',
-					then: Joi.object({
-						options: Joi.array().items(Joi.string())
-					}),
-				},
-				{
-					is: 'range',
-					then: Joi.object({
-
-					}),
-				},
-				{
-					is: Joi.invalid('text', 'number', 'checkbox', 'choice', 'range'),
-					then: Joi.forbidden(),
-				},
-			],
+	questions: Joi.array().items(
+		Joi.object({
+			question: Joi.string().min(1).required(),
+			type: Joi.any()
+				.valid('text', 'number', 'checkbox', 'choice', 'range')
+				.id('survey-type'),
+			required: Joi.boolean().required(),
+			constraints: Joi.alternatives().conditional(Joi.ref('#survey-type'), {
+				switch: [
+					{
+						is: 'text',
+						then: Joi.object({
+							email: Joi.boolean(),
+						}),
+					},
+					{
+						is: 'number',
+						then: Joi.object({
+							min: Joi.number(),
+							max: Joi.number(),
+						}),
+					},
+					{
+						is: 'checkbox',
+						then: Joi.object({
+							checkRequired: Joi.boolean(),
+						}),
+					},
+					{
+						is: 'choice',
+						then: Joi.object({
+							options: Joi.array().items(Joi.string()),
+						}),
+					},
+					{
+						is: 'range',
+						then: Joi.object({}),
+					},
+					{
+						is: Joi.invalid('text', 'number', 'checkbox', 'choice', 'range'),
+						then: Joi.forbidden(),
+					},
+				],
+			}),
 		}),
-	})),
+	),
 })
 
 exports['events.ticket_types.new'] = Joi.object({
-	name: Joi.string()
-		.min(1)
-		.required(),
+	name: Joi.string().min(1).required(),
 	description: Joi.string(),
-	quantity: Joi.number()
-		.integer()
-		.required()
-		.min(1),
+	quantity: Joi.number().integer().required().min(1),
 	survey: exports['survey'],
 })
diff --git a/src/database/models/AccessToken.js b/src/database/models/AccessToken.js
index 16d060d2e89dc972683d1ff3cca6c20a551a7ed6..dcf414f9a92713669d2d67b2c1e0ececcecc8312 100644
--- a/src/database/models/AccessToken.js
+++ b/src/database/models/AccessToken.js
@@ -3,8 +3,8 @@ const BaseModel = require('./BaseModel')
 
 class AccessToken extends BaseModel {
 	static associate(models) {
-		this.belongsTo(models.User, {foreignKey: 'user_id'})
-		this.belongsTo(models.OAuthClient, {foreignKey: 'client_id'})
+		this.belongsTo(models.User, { foreignKey: 'user_id' })
+		this.belongsTo(models.OAuthClient, { foreignKey: 'client_id' })
 	}
 
 	get scopes() {
@@ -27,13 +27,13 @@ class AccessToken extends BaseModel {
 			accessToken: this.token,
 			accessTokenExpiresAt: this.expires_at,
 			scope: this.scope,
-			client:  client.toOAuthInterface(),
+			client: client.toOAuthInterface(),
 			user,
 		}
 	}
 
 	toJSON() {
-		const user = this.user ? {user: this.user} : {}
+		const user = this.user ? { user: this.user } : {}
 		return {
 			id: this.id,
 			token: this.token,
@@ -48,34 +48,38 @@ class AccessToken extends BaseModel {
 }
 
 module.exports = (sequelize, DataTypes) => {
-	AccessToken.init(Object.assign({
-			id: {
-				type: DataTypes.UUID,
-				primaryKey: true,
-				defaultValue: DataTypes.UUIDV4,
-				validate: {
-					isUUID: 4,
+	AccessToken.init(
+		Object.assign(
+			{
+				id: {
+					type: DataTypes.UUID,
+					primaryKey: true,
+					defaultValue: DataTypes.UUIDV4,
+					validate: {
+						isUUID: 4,
+					},
+				},
+				token: {
+					type: DataTypes.TEXT,
+				},
+				scope: {
+					type: DataTypes.TEXT,
+				},
+				expires_at: {
+					type: DataTypes.DATE,
+				},
+				meta: {
+					type: DataTypes.JSONB,
 				},
 			},
-			token: {
-				type: DataTypes.TEXT,
-			},
-			scope: {
-				type: DataTypes.TEXT,
-			},
-			expires_at: {
-				type: DataTypes.DATE,
-			},
-			meta: {
-				type: DataTypes.JSONB,
-			},
+			timestamps(DataTypes),
+		),
+		{
+			sequelize,
+			paranoid: true,
+			tableName: 'oauth_access_tokens',
 		},
-		timestamps(DataTypes),
-	), {
-		sequelize,
-		paranoid: true,
-		tableName: 'oauth_access_tokens',
-	})
+	)
 
 	return AccessToken
 }
diff --git a/src/database/models/AuthorizationCode.js b/src/database/models/AuthorizationCode.js
index e6b03a4d2e64aa9decba4cb567c1e6ee39057d0a..abccacd695499a28270d6b6521df00cb587d2e11 100644
--- a/src/database/models/AuthorizationCode.js
+++ b/src/database/models/AuthorizationCode.js
@@ -13,12 +13,16 @@ class AuthorizationCode extends BaseModel {
 			expiresAt: this.expires_at,
 			redirectUri: this.redirect_uri,
 			scope: this.scope,
-			client: this.client ? this.client : {
-				id: this.client_id,
-			},
-			user: this.user ? this.user : {
-				id: this.user_id,
-			}
+			client: this.client
+				? this.client
+				: {
+						id: this.client_id,
+				  },
+			user: this.user
+				? this.user
+				: {
+						id: this.user_id,
+				  },
 		}
 	}
 
@@ -35,8 +39,8 @@ class AuthorizationCode extends BaseModel {
 	}
 
 	toJSON() {
-		const user = this.user ? { user: this.user } : { }
-		const client = this.user ? { user: this.user } : { }
+		const user = this.user ? { user: this.user } : {}
+		const client = this.user ? { user: this.user } : {}
 		return {
 			id: this.id,
 			scopes: this.scopes,
@@ -50,31 +54,34 @@ class AuthorizationCode extends BaseModel {
 }
 
 module.exports = (sequelize, DataTypes) => {
-	AuthorizationCode.init(Object.assign(
-		{
-			auth_code: {
-				type: DataTypes.TEXT,
-				primaryKey: true,
-			},
-			scope: {
-				type: DataTypes.TEXT,
+	AuthorizationCode.init(
+		Object.assign(
+			{
+				auth_code: {
+					type: DataTypes.TEXT,
+					primaryKey: true,
+				},
+				scope: {
+					type: DataTypes.TEXT,
+				},
+				redirect_uri: {
+					type: DataTypes.TEXT,
+				},
+				expires_at: {
+					type: DataTypes.DATE,
+				},
+				meta: {
+					type: DataTypes.JSONB,
+				},
 			},
-			redirect_uri: {
-				type: DataTypes.TEXT,
-			},
-			expires_at: {
-				type: DataTypes.DATE,
-			},
-			meta: {
-				type: DataTypes.JSONB,
-			}
+			timestamps(DataTypes),
+		),
+		{
+			sequelize,
+			paranoid: true,
+			tableName: 'oauth_authorization_codes',
 		},
-		timestamps(DataTypes),
-	), {
-		sequelize,
-		paranoid: true,
-		tableName: 'oauth_authorization_codes',
-	})
+	)
 
 	return AuthorizationCode
 }
diff --git a/src/database/models/BaseModel.js b/src/database/models/BaseModel.js
index a1109b4d27224d525e86349affc4f92400bfcc17..6d3840ab90a84988fdb0d3f26a6c1d1759681aa8 100644
--- a/src/database/models/BaseModel.js
+++ b/src/database/models/BaseModel.js
@@ -14,13 +14,9 @@ module.exports = class BaseModel extends Model {
 		return this.name
 	}
 
-	static associate(models) {
+	static associate(models) {}
 
-	}
-
-	async handleIncludes(includes, loaders) {
-
-	}
+	async handleIncludes(includes, loaders) {}
 
 	toJSON() {
 		return {
@@ -30,4 +26,4 @@ module.exports = class BaseModel extends Model {
 			updated_at: this.updated_at,
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/src/database/models/BundleCode.js b/src/database/models/BundleCode.js
index ef315d24363c1c667e2dbacf5dd47be665b9b427..ed9ad0716cf4c1052e7d48a0bfd1525b7c970f46 100644
--- a/src/database/models/BundleCode.js
+++ b/src/database/models/BundleCode.js
@@ -7,7 +7,7 @@ class BundleCode extends BaseModel {
 			through: 'user_bundle_codes',
 			foreignKey: 'bundle_code_id',
 			otherKey: 'user_id',
-			timestamps: false
+			timestamps: false,
 		})
 	}
 	toJSON() {
@@ -24,35 +24,38 @@ class BundleCode extends BaseModel {
 }
 
 module.exports = (sequelize, DataTypes) => {
-	BundleCode.init(Object.assign(
-		{
-			id: {
-				type: DataTypes.UUID,
-				primaryKey: true,
-				defaultValue: DataTypes.UUIDV4,
-				validate: {
-					isUUID: 4,
+	BundleCode.init(
+		Object.assign(
+			{
+				id: {
+					type: DataTypes.UUID,
+					primaryKey: true,
+					defaultValue: DataTypes.UUIDV4,
+					validate: {
+						isUUID: 4,
+					},
+				},
+				name: {
+					type: DataTypes.TEXT,
+				},
+				description: {
+					type: DataTypes.TEXT,
+				},
+				platforms: {
+					type: DataTypes.JSONB,
+				},
+				meta: {
+					type: DataTypes.JSONB,
 				},
 			},
-			name: {
-				type: DataTypes.TEXT,
-			},
-			description: {
-				type: DataTypes.TEXT,
-			},
-			platforms: {
-				type: DataTypes.JSONB,
-			},
-			meta: {
-				type: DataTypes.JSONB,
-			},
+			timestamps(DataTypes),
+		),
+		{
+			sequelize,
+			paranoid: true,
+			tableName: 'bundle_codes',
 		},
-		timestamps(DataTypes),
-	), {
-		sequelize,
-		paranoid: true,
-		tableName: 'bundle_codes',
-	})
+	)
 
 	return BundleCode
-}
\ No newline at end of file
+}
diff --git a/src/database/models/File.js b/src/database/models/File.js
index 8ef0be128fe8e1181b1a11a06ef985aafe9201bf..02fc4babf4c2891611ae3394289d50f370a78a79 100644
--- a/src/database/models/File.js
+++ b/src/database/models/File.js
@@ -25,64 +25,72 @@ class File extends BaseModel {
 }
 
 function generatePublicUri(provider, file) {
-	return ['https://storage.googleapis.com', file.meta.bucket, file.file_root, file.file_name].join('/')
+	return [
+		'https://storage.googleapis.com',
+		file.meta.bucket,
+		file.file_root,
+		file.file_name,
+	].join('/')
 }
 
 module.exports = (sequelize, DataTypes) => {
-	File.init(Object.assign(
-		{
-			id: {
-				type: DataTypes.UUID,
-				defaultValue: DataTypes.UUIDV4,
-				primaryKey: true,
-			},
-			provider: {
-				type: DataTypes.TEXT,
-			},
-			file_root: {
-				type: DataTypes.TEXT,
-			},
-			file_name: {
-				type: DataTypes.TEXT,
-			},
-			comment: {
-				type: DataTypes.TEXT,
-			},
-			tags: {
-				type: DataTypes.ARRAY(DataTypes.TEXT),
-			},
-			labels: {
-				type: DataTypes.ARRAY(DataTypes.TEXT),
-			},
-			public_uri: {
-				type: DataTypes.VIRTUAL,
-				get() {
-					const provider = this.provider
-					return generatePublicUri(provider, this)
-				}
-			},
-			stream: {
-				type: DataTypes.TEXT,
-			},
-			requires_approval: {
-				type: DataTypes.BOOLEAN,
-			},
-			approved: {
-				type: DataTypes.BOOLEAN,
-			},
-			featured: {
-				type: DataTypes.BOOLEAN,
-			},
-			meta: {
-				type: DataTypes.JSONB,
+	File.init(
+		Object.assign(
+			{
+				id: {
+					type: DataTypes.UUID,
+					defaultValue: DataTypes.UUIDV4,
+					primaryKey: true,
+				},
+				provider: {
+					type: DataTypes.TEXT,
+				},
+				file_root: {
+					type: DataTypes.TEXT,
+				},
+				file_name: {
+					type: DataTypes.TEXT,
+				},
+				comment: {
+					type: DataTypes.TEXT,
+				},
+				tags: {
+					type: DataTypes.ARRAY(DataTypes.TEXT),
+				},
+				labels: {
+					type: DataTypes.ARRAY(DataTypes.TEXT),
+				},
+				public_uri: {
+					type: DataTypes.VIRTUAL,
+					get() {
+						const provider = this.provider
+						return generatePublicUri(provider, this)
+					},
+				},
+				stream: {
+					type: DataTypes.TEXT,
+				},
+				requires_approval: {
+					type: DataTypes.BOOLEAN,
+				},
+				approved: {
+					type: DataTypes.BOOLEAN,
+				},
+				featured: {
+					type: DataTypes.BOOLEAN,
+				},
+				meta: {
+					type: DataTypes.JSONB,
+				},
 			},
+			timestamps(DataTypes),
+		),
+		{
+			sequelize,
+			paranoid: true,
+			tableName: 'files',
 		},
-		timestamps(DataTypes),
-	), {
-		sequelize,
-		paranoid: true,
-		tableName: 'files',
-	})
+	)
 
 	return File
 }
diff --git a/src/database/models/Metric.js b/src/database/models/Metric.js
index e975ef2d3ebcd7617edda2f896280318576d924c..a47f28aa6601a9306d12911bff2c0b131d5a1754 100644
--- a/src/database/models/Metric.js
+++ b/src/database/models/Metric.js
@@ -16,11 +16,14 @@ class Metric extends BaseModel {
 	}
 
 	static async findUnassociatedForDevice(deviceId) {
-		return this.findAll({ where:  { author_id: null, meta: { device: { id: deviceId } } } })
+		return this.findAll({
+			where: { author_id: null, meta: { device: { id: deviceId } } },
+		})
 	}
 
 	static async claimNewUnassociatedForDEvice(authorId, deviceId) {
-		return this.sequelize.query(`
+		return this.sequelize.query(
+			`
 			update metrics
 			set author_id = :authorId
 			where meta -> 'device' ->> 'id' = :deviceId::text
@@ -28,10 +31,12 @@ class Metric extends BaseModel {
 				and not exists (
 					select * from metrics where meta -> 'device' ->> 'id' = :deviceId::text and author_id is not null limit 1
 				);
-		`, {
-			replacements: { authorId, deviceId },
-			type: sequelize.QueryTypes.UPDATE,
-		})
+		`,
+			{
+				replacements: { authorId, deviceId },
+				type: sequelize.QueryTypes.UPDATE,
+			},
+		)
 	}
 
 	getNativeValue() {
@@ -57,8 +62,8 @@ class Metric extends BaseModel {
 }
 
 module.exports = (sequelize, DataTypes) => {
-	Metric.init(Object.assign(
-		{
+	Metric.init(
+		Object.assign({
 			fake_pk: {
 				type: DataTypes.VIRTUAL,
 				primaryKey: true,
@@ -86,9 +91,15 @@ module.exports = (sequelize, DataTypes) => {
 				},
 				set(location) {
 					if (Array.isArray(location)) {
-						this.setDataValue('location', { type: 'Point', coordinates: location })
+						this.setDataValue('location', {
+							type: 'Point',
+							coordinates: location,
+						})
 					} else if (location.latitude && location.longitude) {
-						this.setDataValue('location', { type: 'Point', coordinated: [location.longitude, location.latitude] })
+						this.setDataValue('location', {
+							type: 'Point',
+							coordinated: [location.longitude, location.latitude],
+						})
 					} else {
 						this.setDataValue('location', location)
 					}
@@ -103,13 +114,14 @@ module.exports = (sequelize, DataTypes) => {
 			deleted_at: {
 				type: DataTypes.DATE,
 			},
+		}),
+		{
+			sequelize,
+			paranoid: true,
+			timestamps: false,
+			tableName: 'metrics',
 		},
-	), {
-		sequelize,
-		paranoid: true,
-		timestamps: false,
-		tableName: 'metrics',
-	})
+	)
 
 	return Metric
 }
diff --git a/src/database/models/OAuthClient.js b/src/database/models/OAuthClient.js
index 9f35ae27cedf99c15342c33a56ae2a1a7086d742..2cfb54a2859a14affcac9e07a14a2c8cbf6e48bd 100644
--- a/src/database/models/OAuthClient.js
+++ b/src/database/models/OAuthClient.js
@@ -4,7 +4,12 @@ const BaseModel = require('./BaseModel')
 class OAuthClient extends BaseModel {
 	static associate(models) {
 		this.belongsTo(models.User, { as: 'owner', foreignKey: 'owner_id' })
-		this.belongsToMany(models.User, { as: 'users', through: models.AccessToken, otherKey: 'user_id', foreignKey: 'client_id' })
+		this.belongsToMany(models.User, {
+			as: 'users',
+			through: models.AccessToken,
+			otherKey: 'user_id',
+			foreignKey: 'client_id',
+		})
 		this.hasMany(models.AccessToken, { foreignKey: 'client_id' })
 		this.hasMany(models.RefreshToken, { foreignKey: 'client_id' })
 	}
@@ -43,35 +48,38 @@ class OAuthClient extends BaseModel {
 }
 
 module.exports = (sequelize, DataTypes) => {
-	OAuthClient.init(Object.assign(
-		{
-			id: {
-				type: DataTypes.UUID,
-				primaryKey: true,
-				defaultValue: DataTypes.UUIDV4,
-				validate: {
-					isUUID: 4,
+	OAuthClient.init(
+		Object.assign(
+			{
+				id: {
+					type: DataTypes.UUID,
+					primaryKey: true,
+					defaultValue: DataTypes.UUIDV4,
+					validate: {
+						isUUID: 4,
+					},
+				},
+				secret: {
+					type: DataTypes.TEXT,
+				},
+				redirect_uris: {
+					type: DataTypes.ARRAY(DataTypes.TEXT),
+				},
+				grant_types: {
+					type: DataTypes.ARRAY(DataTypes.TEXT),
+				},
+				meta: {
+					type: DataTypes.JSONB,
 				},
 			},
-			secret: {
-				type: DataTypes.TEXT,
-			},
-			redirect_uris: {
-				type: DataTypes.ARRAY(DataTypes.TEXT),
-			},
-			grant_types: {
-				type: DataTypes.ARRAY(DataTypes.TEXT),
-			},
-			meta: {
-				type: DataTypes.JSONB,
-			}
+			timestamps(DataTypes),
+		),
+		{
+			sequelize,
+			paranoid: true,
+			tableName: 'oauth_clients',
 		},
-		timestamps(DataTypes),
-	), {
-		sequelize,
-		paranoid: true,
-		tableName: 'oauth_clients',
-	})
+	)
 
 	return OAuthClient
 }
diff --git a/src/database/models/RefreshToken.js b/src/database/models/RefreshToken.js
index 214edc4471682109bfbc73f4d93531b9fc1429fa..ba315d69f3cf4218c9ab7f72c4aead97baf9c052 100644
--- a/src/database/models/RefreshToken.js
+++ b/src/database/models/RefreshToken.js
@@ -27,13 +27,13 @@ class RefreshToken extends BaseModel {
 			refreshToken: this.token,
 			refreshTokenExpiresAt: this.expires_at,
 			scope: this.scope,
-			client:  client.toOAuthInterface(),
+			client: client.toOAuthInterface(),
 			user,
 		}
 	}
 
 	toJSON() {
-		const user = this.user ? { user: this.user } : { }
+		const user = this.user ? { user: this.user } : {}
 		return {
 			id: this.id,
 			token: this.token,
@@ -48,35 +48,38 @@ class RefreshToken extends BaseModel {
 }
 
 module.exports = (sequelize, DataTypes) => {
-	RefreshToken.init(Object.assign(
-		{
-			id: {
-				type: DataTypes.UUID,
-				primaryKey: true,
-				defaultValue: DataTypes.UUIDV4,
-				validate: {
-					isUUID: 4,
+	RefreshToken.init(
+		Object.assign(
+			{
+				id: {
+					type: DataTypes.UUID,
+					primaryKey: true,
+					defaultValue: DataTypes.UUIDV4,
+					validate: {
+						isUUID: 4,
+					},
+				},
+				token: {
+					type: DataTypes.TEXT,
+				},
+				scope: {
+					type: DataTypes.TEXT,
+				},
+				expires_at: {
+					type: DataTypes.DATE,
+				},
+				meta: {
+					type: DataTypes.JSONB,
 				},
 			},
-			token: {
-				type: DataTypes.TEXT,
-			},
-			scope: {
-				type: DataTypes.TEXT,
-			},
-			expires_at: {
-				type: DataTypes.DATE,
-			},
-			meta: {
-				type: DataTypes.JSONB,
-			}
+			timestamps(DataTypes),
+		),
+		{
+			sequelize,
+			paranoid: true,
+			tableName: 'oauth_refresh_tokens',
 		},
-		timestamps(DataTypes),
-	), {
-		sequelize,
-		paranoid: true,
-		tableName: 'oauth_refresh_tokens',
-	})
+	)
 
 	return RefreshToken
 }
diff --git a/src/database/models/User.js b/src/database/models/User.js
index ea4037e5a23b380a32d6e93d23371bcc6481ce05..3a23fb1193a0df99289205181b2c49f91e66df09 100644
--- a/src/database/models/User.js
+++ b/src/database/models/User.js
@@ -12,13 +12,13 @@ class User extends BaseModel {
 			as: 'likes',
 			through: 'user_file_likes',
 			foreignKey: 'user_id',
-			otherKey: 'file_id'
+			otherKey: 'file_id',
 		})
 		this.belongsToMany(models.BundleCode, {
 			through: 'user_bundle_codes',
 			foreignKey: 'user_id',
 			otherKey: 'bundle_code_id',
-			timestamps: false
+			timestamps: false,
 		})
 	}
 
@@ -38,11 +38,11 @@ class User extends BaseModel {
 		} else {
 			try {
 				id = await this.idFromOpaque(token)
-			} catch(e) {}
+			} catch (e) {}
 			if (id == null) {
 				try {
 					id = await this.idFromJwt(token)
-				} catch(e) {}
+				} catch (e) {}
 			}
 		}
 
@@ -139,15 +139,12 @@ class User extends BaseModel {
 		return b
 	}
 
-
 	async generateResetToken() {
 		const crypto = require('core/utils/crypto')
 		const moment = require('moment')
 
 		const id = this.id
-		const expires = moment.utc()
-			.add(1, 'hour')
-			.toISOString()
+		const expires = moment.utc().add(1, 'hour').toISOString()
 
 		const token = await crypto.encrypt(JSON.stringify({ id, expires }))
 		this.reset_token = token
@@ -156,7 +153,6 @@ class User extends BaseModel {
 		return token
 	}
 
-
 	toJSON() {
 		const meta = { ...this.meta }
 		delete meta.dob
@@ -173,35 +169,38 @@ class User extends BaseModel {
 }
 
 module.exports = (sequelize, DataTypes) => {
-	User.init(Object.assign(
-		{
-			id: {
-				type: DataTypes.UUID,
-				primaryKey: true,
-				defaultValue: DataTypes.UUIDV4,
-			},
-			name: {
-				type: DataTypes.TEXT,
-			},
-			email: {
-				type: DataTypes.TEXT,
+	User.init(
+		Object.assign(
+			{
+				id: {
+					type: DataTypes.UUID,
+					primaryKey: true,
+					defaultValue: DataTypes.UUIDV4,
+				},
+				name: {
+					type: DataTypes.TEXT,
+				},
+				email: {
+					type: DataTypes.TEXT,
+				},
+				password: {
+					type: DataTypes.TEXT,
+				},
+				reset_token: {
+					type: DataTypes.TEXT,
+				},
+				meta: {
+					type: DataTypes.JSONB,
+				},
 			},
-			password: {
-				type: DataTypes.TEXT,
-			},
-			reset_token: {
-				type: DataTypes.TEXT,
-			},
-			meta: {
-				type: DataTypes.JSONB,
-			}
+			timestamps(DataTypes),
+		),
+		{
+			sequelize,
+			paranoid: true,
+			tableName: 'users',
 		},
-		timestamps(DataTypes),
-	), {
-		sequelize,
-		paranoid: true,
-		tableName: 'users',
-	})
+	)
 
 	return User
 }
diff --git a/src/database/models/index.js b/src/database/models/index.js
index 3e66754f6c3f2c3da18d47c7b6d253b8fe6cdf29..470e9db10a5454f48acd146d99d47557d7e596f2 100644
--- a/src/database/models/index.js
+++ b/src/database/models/index.js
@@ -18,11 +18,17 @@ const sequelize = new Sequelize(
 	config('database.database'),
 	config('database.username'),
 	config('database.password'),
-	conf
+	conf,
 )
 
 fs.list(__dirname)
-	.filter(file => (file[0] !== '.') && file !== 'BaseModel.js' && (file !== basename) && (file.slice(-3) === '.js'))
+	.filter(
+		file =>
+			file[0] !== '.' &&
+			file !== 'BaseModel.js' &&
+			file !== basename &&
+			file.slice(-3) === '.js',
+	)
 	.forEach(file => {
 		const initialiser = require(path.join(__dirname, file))
 		const model = initialiser(sequelize, DataTypes)
@@ -49,14 +55,14 @@ if (config('database.log_queries')) {
 			const span = trace.startChild({
 				op: 'sql.query',
 			})
-			threadContext.set(`span_${ traceId }`, span)
+			threadContext.set(`span_${traceId}`, span)
 			query.options.trace = traceId
 		}
 	})
 
 	sequelize.addHook('afterQuery', (model, query) => {
 		if (query.options.trace) {
-			const trace = threadContext.get(`span_${ query.options.trace }`)
+			const trace = threadContext.get(`span_${query.options.trace}`)
 			if (trace) {
 				trace.description = query.sql
 				trace.setData('sql.params', query.options.bind)
diff --git a/src/domain/auth/AuthServer.js b/src/domain/auth/AuthServer.js
index 39573c83a0de51d69775d2c2c82222e3e1df4086..dfb7c1a6eaf8813abf870c7e86a3cc93e897b57f 100644
--- a/src/domain/auth/AuthServer.js
+++ b/src/domain/auth/AuthServer.js
@@ -1,5 +1,12 @@
 const OAuthServer = require('oauth2-server')
-const { Sequelize: { Op }, User, OAuthClient, AccessToken, RefreshToken, AuthorizationCode } = require('database/models')
+const {
+	Sequelize: { Op },
+	User,
+	OAuthClient,
+	AccessToken,
+	RefreshToken,
+	AuthorizationCode,
+} = require('database/models')
 const crypto = require('core/utils/crypto')
 const HttpError = require('core/errors/HttpError')
 
@@ -35,22 +42,31 @@ const model = {
 		}
 
 		const client = await OAuthClient.findOne({ where })
-		const asInterface =  client.toOAuthInterface()
+		const asInterface = client.toOAuthInterface()
 		return asInterface
 	},
 
 	getAccessToken(token) {
-		return AccessToken.findOne({ include: [ { model: User }, { model: OAuthClient } ], where: { token: { [Op.eq]: token } } })
+		return AccessToken.findOne({
+			include: [{ model: User }, { model: OAuthClient }],
+			where: { token: { [Op.eq]: token } },
+		})
 	},
 	async getRefreshToken(token) {
-		const t = await RefreshToken.findOne({ include: [ { model: User }, { model: OAuthClient } ], where: { token: { [Op.eq]: token } } })
+		const t = await RefreshToken.findOne({
+			include: [{ model: User }, { model: OAuthClient }],
+			where: { token: { [Op.eq]: token } },
+		})
 		return await t?.toOAuthInterface()
 	},
 
 	getUser: async function getAuthUser(email, password) {
-		const user = await User.findOne({
-			where: { email: { [Op.eq]: email } },
-		}, { })
+		const user = await User.findOne(
+			{
+				where: { email: { [Op.eq]: email } },
+			},
+			{},
+		)
 		if (user != null) {
 			const valid = await user.checkPassword(password)
 
@@ -93,7 +109,11 @@ const model = {
 		return createTokenPair(user, client, accessTokenModel, refreshTokenModel)
 	},
 
-	saveAuthorizationCode: async function ({ authorizationCode, expiresAt, redirectUri, scope }, client, user) {
+	saveAuthorizationCode: async function (
+		{ authorizationCode, expiresAt, redirectUri, scope },
+		client,
+		user,
+	) {
 		const authCode = {
 			auth_code: authorizationCode,
 			client_id: client.id,
@@ -108,7 +128,11 @@ const model = {
 		return code.toOAuthInterface()
 	},
 
-	revokeToken: async function revokeRefreshToken({ refreshToken, client, user }) {
+	revokeToken: async function revokeRefreshToken({
+		refreshToken,
+		client,
+		user,
+	}) {
 		const deletions = await RefreshToken.destroy({
 			where: {
 				token: { [Op.eq]: refreshToken },
@@ -120,7 +144,11 @@ const model = {
 		return deletions > 0
 	},
 
-	revokeAuthorizationCode: async function({ authorizationCode: code, client, user }) {
+	revokeAuthorizationCode: async function ({
+		authorizationCode: code,
+		client,
+		user,
+	}) {
 		const deletions = await AuthorizationCode.destroy({
 			where: {
 				auth_code: code,
@@ -132,7 +160,7 @@ const model = {
 		return deletions > 0
 	},
 
-	getAuthorizationCode: async function(auth_code) {
+	getAuthorizationCode: async function (auth_code) {
 		const code = await AuthorizationCode.findOne({ where: { auth_code } })
 		if (code) {
 			return code.toOAuthInterface()
@@ -172,13 +200,10 @@ class KoaOAuthServer {
 		this.authorize = async ctx => {
 			const OAuthFlow = require('./OAuthFlow')
 			const flow = await OAuthFlow.initialiseFlow(ctx)
-			const {
-				user,
-				redirect,
-			} = flow
+			const { user, redirect } = flow
 
 			if (!user) {
-				return ctx.redirect(`/login?auth_state=${ redirect }`)
+				return ctx.redirect(`/login?auth_state=${redirect}`)
 			} else if (ctx.method === 'GET') {
 				return await OAuthFlow.showOAuthConsent(ctx, flow)
 			} else {
@@ -199,7 +224,10 @@ class KoaOAuthServer {
 
 		this.token = async ctx => {
 			const { req, res } = this.transformContext(ctx)
-			await authServer.token(req, res, { allowExtendedTokenAttributes: true, accessTokenLifetime: 3600 * 24 * 7 })
+			await authServer.token(req, res, {
+				allowExtendedTokenAttributes: true,
+				accessTokenLifetime: 3600 * 24 * 7,
+			})
 			for (const [name, value] of Object.entries(res.headers)) {
 				ctx.response.set(name, value)
 			}
diff --git a/src/domain/auth/AuthenticationService.js b/src/domain/auth/AuthenticationService.js
index 07ae33497f7c74b5f6e35760b17e7a5dff76707d..7ea548c5defad19a3348c77093d6ff3c9be2158c 100644
--- a/src/domain/auth/AuthenticationService.js
+++ b/src/domain/auth/AuthenticationService.js
@@ -11,9 +11,7 @@ module.exports = class AuthenticationService extends ContextualModule {
 	}
 
 	static get profileMethods() {
-		return [
-			'getUser', 'attemptLogin', 'saveToSession'
-		]
+		return ['getUser', 'attemptLogin', 'saveToSession']
 	}
 
 	init() {
@@ -22,7 +20,10 @@ module.exports = class AuthenticationService extends ContextualModule {
 
 	async attemptLogin(email, password) {
 		if (this._user != null) {
-			if (this._user.email === email && await this._user.checkPassword(password)) {
+			if (
+				this._user.email === email &&
+				(await this._user.checkPassword(password))
+			) {
 				return this._user
 			} else {
 				throw new HttpError(401, 'Forbidden')
@@ -55,9 +56,10 @@ module.exports = class AuthenticationService extends ContextualModule {
 			}
 		} else if (this.ctx.get('Authorization')) {
 			const token = this.ctx.get('Authorization').substr(HEADER_PREFIX.length)
+
 			const accessToken = await AccessToken.findOne({
 				where: { token },
-				include: [{ model: User }]
+				include: [{ model: User }],
 			})
 
 			if (accessToken.User) {
@@ -66,13 +68,14 @@ module.exports = class AuthenticationService extends ContextualModule {
 			}
 		} else if (this.ctx.get('x-api-token')) {
 			const token = this.ctx.get('x-api-token')
+			console.log(token, this.ctx.get('x-token-type'))
 			try {
 				const user = await User.fromToken(token, this.ctx.get('x-token-type'))
 				if (user) {
 					this.authenticateAs(user)
 					return this._user
 				}
-			} catch(e) {
+			} catch (e) {
 				console.error(e)
 			}
 		}
@@ -87,7 +90,9 @@ module.exports = class AuthenticationService extends ContextualModule {
 	async saveToSession(logoutIfEmpty = true) {
 		const user = await this.getUser()
 		if (user) {
-			this.ctx.session.user = await crypto.encrypt(JSON.stringify({ id: user.id }))
+			this.ctx.session.user = await crypto.encrypt(
+				JSON.stringify({ id: user.id }),
+			)
 		} else if (logoutIfEmpty) {
 			this.ctx.session.user = null
 		}
diff --git a/src/domain/auth/OAuthFlow.js b/src/domain/auth/OAuthFlow.js
index b2d369efa8dbc145a2dc1660994699e0a4130e75..6ae94fa4ebc810dd1dcd1dbb51fa56bbcaf8de3b 100644
--- a/src/domain/auth/OAuthFlow.js
+++ b/src/domain/auth/OAuthFlow.js
@@ -24,7 +24,9 @@ exports.initialiseFlow = async ctx => {
 
 	const action = baseQuery.action
 
-	const redirectState = await crypto.encrypt(JSON.stringify({ redirect: 'authorize', query: baseQuery }))
+	const redirectState = await crypto.encrypt(
+		JSON.stringify({ redirect: 'authorize', query: baseQuery }),
+	)
 
 	return {
 		user,
@@ -36,13 +38,9 @@ exports.initialiseFlow = async ctx => {
 }
 
 exports.showOAuthConsent = async (ctx, queryState) => {
-	const {
-		user,
-		query,
-		redirect,
-	} = queryState
+	const { user, query, redirect } = queryState
 
-	const client = await OAuthClient.findOne({ where: { id: query.client_id }})
+	const client = await OAuthClient.findOne({ where: { id: query.client_id } })
 	if (client == null) {
 		throw new HttpError(400, 'Invalid client id specified')
 	}
@@ -63,7 +61,10 @@ exports.handleConsentRejection = async (ctx, flow) => {
 	const search = new URLSearchParams(redirect.searchParams)
 
 	search.set('error', 'access_denied')
-	search.set('error_description', 'The user has denied the requested permissions')
+	search.set(
+		'error_description',
+		'The user has denied the requested permissions',
+	)
 	redirect.search = search.toString()
 
 	ctx.set('Location', redirect.toString())
@@ -78,7 +79,13 @@ exports.handleConsentAcceptance = async (ctx, flow, server) => {
 	}
 
 	const { req, res } = server.transformContext(ctx, { query: queryState })
-	await server.getAuthServer().authorize(req, res, { authenticateHandler: { handle() { return flow.user } }})
+	await server.getAuthServer().authorize(req, res, {
+		authenticateHandler: {
+			handle() {
+				return flow.user
+			},
+		},
+	})
 	for (const [name, value] of Object.entries(res.headers)) {
 		ctx.response.set(name, value)
 	}
@@ -89,11 +96,13 @@ const scopeDescriptionMap = {
 	'*': {
 		icon: 'admin',
 		name: 'Full Access',
-		description: 'Full access to your account, including the ability to create, update and delete any user information, metrics and files.'
+		description:
+			'Full access to your account, including the ability to create, update and delete any user information, metrics and files.',
 	},
 	'metrics:create': {
 		name: 'Create Metrics',
-		description: 'The ability to add data metrics linked to your account. Remember that connected apps that create metrics will know your location!',
+		description:
+			'The ability to add data metrics linked to your account. Remember that connected apps that create metrics will know your location!',
 	},
 	'files:upload': {
 		name: 'Upload Files',
@@ -101,24 +110,26 @@ const scopeDescriptionMap = {
 	},
 	'files:read': {
 		name: 'Read Files',
-		description: 'The ability to see and download images that you\'ve uploaded to your account',
+		description:
+			"The ability to see and download images that you've uploaded to your account",
 	},
 	'profile:read': {
 		name: 'Read Profile',
-		description: 'The ability to see any information associated with your user profile. This includes your name and email address',
+		description:
+			'The ability to see any information associated with your user profile. This includes your name and email address',
 	},
 	'profile:write': {
 		name: 'Modify Profile',
-		description: 'The ability to edit any information associated with your user profile. This includes your name',
+		description:
+			'The ability to edit any information associated with your user profile. This includes your name',
 	},
 	'profile:stats': {
 		name: 'Profile Stats',
-		description: 'The ability to see information about your account stats, including your points and citizen scientist level',
+		description:
+			'The ability to see information about your account stats, including your points and citizen scientist level',
 	},
-
 }
 function describeScopeRequest(scope = '*') {
 	const scopes = scope.split(' ')
-	return scopes.map(s => scopeDescriptionMap[s])
-		.filter(Boolean)
+	return scopes.map(s => scopeDescriptionMap[s]).filter(Boolean)
 }
diff --git a/src/domain/auth/handlers/SendUserPasswordReset.js b/src/domain/auth/handlers/SendUserPasswordReset.js
index fd93380d9fbd33600525dc8b8d7795b0ffb0b6fa..d7dc21b9b34c31bee007b44c84029c719bd63c8e 100644
--- a/src/domain/auth/handlers/SendUserPasswordReset.js
+++ b/src/domain/auth/handlers/SendUserPasswordReset.js
@@ -2,38 +2,67 @@ const { config } = require('bootstrap')
 const HttpError = require('core/errors/HttpError')
 
 module.exports = async (body, ctx) => {
-	const { email } = body
+	const { email, device } = body
 
 	const user = await ctx.services['core.users'].findByEmail(email)
-	const token = await ctx.profile('user.generateResetToken', 'Create reset token', () => user.generateResetToken())
+	const token = await ctx.profile(
+		'user.generateResetToken',
+		'Create reset token',
+		() => user.generateResetToken(),
+	)
 
-	const name = user.name || 'Jetsam User (You haven\'t told us your name!)'
-	const reset_link = new URL(`/reset-password?token=${ token }`, config('app.host.web'))
+	const name = user.name || "Jetsam User (You haven't told us your name!)"
+	const reset_link = new URL(
+		`/reset-password?token=${token}`,
+		config('app.host.web'),
+	)
 
 	const { mail } = require('services')
 
+	const data = { name, reset_link, action_url: reset_link, path: reset_link }
+
+	if (device) {
+		data.security = {
+			operating_system: device.platform ?? '',
+			browser_name: device.info?.app_version
+				? `Jetsam ${device.info.app_version}`
+				: device.browser ?? 'Jetsam',
+		}
+	}
+
 	try {
 		await ctx.profile(
 			'services.mail.sendTemplate',
-			`template ${ config('mail.templates.reset-password')}`,
-			() => mail.sendTemplate(email, 'Reset Your Jetsam password', config('mail.templates.reset-password'), {
-				name,
-				reset_link,
-			})
+			`template ${config('mail.templates.reset-password')}`,
+			() =>
+				mail.sendTemplate(
+					email,
+					'Reset Your Jetsam password',
+					'22427615',
+					data,
+				),
 		)
 	} catch (e) {
 		// reporter.report(e)
-		console.log(e.response.body.errors)
+		console.error(e)
+		console.log(e?.response?.body?.errors)
 		throw new HttpError(
 			500,
 			'Failed To Send Reset Email',
-			{ status: 500, title: 'Failed to send reset email', description: 'Could not send the password reset email' },
 			{
-				sendgrid: (e.response?.body?.errors ?? []).reduce((acc, e, i) => ({
-					...acc,
-					[`err-${ i }`]: JSON.stringify(e, null, 2),
-				}), {}),
-			}
+				status: 500,
+				title: 'Failed to send reset email',
+				description: 'Could not send the password reset email',
+			},
+			{
+				sendgrid: (e?.response?.body?.errors ?? []).reduce(
+					(acc, e, i) => ({
+						...acc,
+						[`err-${i}`]: JSON.stringify(e, null, 2),
+					}),
+					{},
+				),
+			},
 		)
 	}
-}
\ No newline at end of file
+}
diff --git a/src/domain/data/MetricsService.js b/src/domain/data/MetricsService.js
index b9a737fd9c4ffd9fbcbde29df46e30f9226afbfb..996ccf0b5d7e9ad1fd76d6393582832c5d0d8805 100644
--- a/src/domain/data/MetricsService.js
+++ b/src/domain/data/MetricsService.js
@@ -12,17 +12,21 @@ module.exports = class MetricsService extends ContextualModule {
 	}
 
 	static get profileMethods() {
-		return [
-			'queryAggregate',
-			'queryAll',
-			'recordMetric',
-		]
+		return ['queryAggregate', 'queryAll', 'recordMetric']
 	}
 
 	async recordMetric(value, type, location) {
 		const user = await this.ctx.services['core.auth'].getUser()
-		const point = { type: 'Point', coordinates: [location.longitude, location.latitude] }
-		const payload = { value, type, location: point, author_id: user ? user.id : null }
+		const point = {
+			type: 'Point',
+			coordinates: [location.longitude, location.latitude],
+		}
+		const payload = {
+			value,
+			type,
+			location: point,
+			author_id: user ? user.id : null,
+		}
 		if (this.ctx.request.device) {
 			payload.meta = { device: this.ctx.request.device }
 		}
@@ -35,24 +39,23 @@ module.exports = class MetricsService extends ContextualModule {
 		return await Metric.findAll({
 			where: {
 				[Op.and]: [
-					Sequelize.literal(`ST_COVEREDBY(${ snapClause }, ST_POLYGONFROMTEXT('POLYGON((${ pointBuffer }))')::geography::geometry)`),
+					Sequelize.literal(
+						`ST_COVEREDBY(${snapClause}, ST_POLYGONFROMTEXT('POLYGON((${pointBuffer}))')::geography::geometry)`,
+					),
 				],
 				recorded_at: {
-					[Op.between]: [from, to]
+					[Op.between]: [from, to],
 				},
 				type: {
 					[Op.in]: types,
-				}
+				},
 			},
 			attributes: [
 				'type',
 				[Sequelize.fn('COUNT', Sequelize.col('value')), 'value'],
-				[Sequelize.literal(snapClause), 'location']
+				[Sequelize.literal(snapClause), 'location'],
 			],
-			group: [
-				Sequelize.literal(snapClause),
-				'type',
-			]
+			group: [Sequelize.literal(snapClause), 'type'],
 		})
 	}
 
@@ -60,14 +63,16 @@ module.exports = class MetricsService extends ContextualModule {
 		return await Metric.findAll({
 			where: {
 				[Op.and]: [
-					Sequelize.literal(`ST_COVEREDBY("Metric"."location", ST_POLYGONFROMTEXT('POLYGON((${ pointBuffer }))'))`),
+					Sequelize.literal(
+						`ST_COVEREDBY("Metric"."location", ST_POLYGONFROMTEXT('POLYGON((${pointBuffer}))'))`,
+					),
 				],
 				recorded_at: {
-					[Op.between]: [from, to]
+					[Op.between]: [from, to],
 				},
 				type: {
 					[Op.in]: types,
-				}
+				},
 			},
 		})
 	}
diff --git a/src/domain/users/UserService.js b/src/domain/users/UserService.js
index 2b3ce9e8519819e41cdda1ac4ae601dcc2da321b..167c18712a6dcf5edee7e5f8a208279be1f2f612 100644
--- a/src/domain/users/UserService.js
+++ b/src/domain/users/UserService.js
@@ -10,12 +10,7 @@ module.exports = class UserService extends ContextualModule {
 	}
 
 	static get profileMethods() {
-		return [
-			'register',
-			'findUser',
-			'findByEmail',
-			'findByAccessToken',
-		]
+		return ['register', 'findUser', 'findByEmail', 'findByAccessToken']
 	}
 
 	findUser(id) {
@@ -30,7 +25,10 @@ module.exports = class UserService extends ContextualModule {
 	}
 
 	async findByAccessToken(token) {
-		const accessToken = await AccessToken.findOne({ where: { token }, include: [{ model: User }] })
+		const accessToken = await AccessToken.findOne({
+			where: { token },
+			include: [{ model: User }],
+		})
 		if (accessToken) {
 			return accessToken.User
 		}
diff --git a/src/http/controllers/api/app.js b/src/http/controllers/api/app.js
index b4fcd9b9b8256691dcca1b93e48631ded22e99b9..cbfe3512f2fe7e5d893d5c95c2d852ed574b5a8e 100644
--- a/src/http/controllers/api/app.js
+++ b/src/http/controllers/api/app.js
@@ -8,10 +8,10 @@ exports.getBundles = async ctx => {
 		const bundles = await ctx.profile(
 			'user.getBundleCodes',
 			'Get codepush bundles for authed user',
-			() => user.getBundleCodes()
+			() => user.getBundleCodes(),
 		)
 		ctx.body = {
 			bundles,
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/src/http/controllers/api/auth.js b/src/http/controllers/api/auth.js
index 10290b3fd663c72e242de26f84a2899a6478b67c..126913a45670956e270b4259b7d2af05ea09c10c 100644
--- a/src/http/controllers/api/auth.js
+++ b/src/http/controllers/api/auth.js
@@ -14,11 +14,19 @@ exports.register = async ctx => {
 	const user = await User.findOne({ where: { email } })
 
 	if (user != null) {
-		throw new HttpError(409, 'Email Already Exists', { status: 409, title: 'Email Already Exists', description: 'That email address already exists. Please try another email address.' })
+		throw new HttpError(409, 'Email Already Exists', {
+			status: 409,
+			title: 'Email Already Exists',
+			description:
+				'That email address already exists. Please try another email address.',
+		})
 	}
 
-	const newUser = await ctx.services['core.users']
-		.register(name ?? null, email, password)
+	const newUser = await ctx.services['core.users'].register(
+		name ?? null,
+		email,
+		password,
+	)
 	await newUser.handleIncludes(ctx.includes)
 	const token = await newUser.asToken(ctx.get('x-token-type'))
 
@@ -27,8 +35,7 @@ exports.register = async ctx => {
 
 exports.login = async ctx => {
 	const { email, password } = ctx.request.body
-	const user = await ctx.services['core.auth']
-		.attemptLogin(email, password)
+	const user = await ctx.services['core.auth'].attemptLogin(email, password)
 
 	const token = await user.asToken(ctx.get('x-token-type'))
 
@@ -40,10 +47,20 @@ exports.triggerPasswordReset = async ctx => {
 	const { email } = ctx.request.body
 	const user = await ctx.services['core.users'].findByEmail(email)
 	if (!user) {
-		throw new HttpError(404, 'No Such Email', { status: 404, title: 'No Such Email', description: 'The provided email address is not associated with an account' })
+		throw new HttpError(404, 'No Such Email', {
+			status: 404,
+			title: 'No Such Email',
+			description:
+				'The provided email address is not associated with an account',
+		})
 	}
 
-	await queue.dispatch('send-user-password-reset', { email })
+	console.log(ctx.request.device)
+
+	await queue.dispatch('send-user-password-reset', {
+		email,
+		device: ctx.request.device,
+	})
 
 	ctx.body = {
 		reset_token: null,
@@ -116,7 +133,7 @@ exports.handlePasswordReset = async ctx => {
 	if (expires.isSameOrBefore(moment.utc())) {
 		ctx.body = {
 			error: {
-				message: 'The reset token was invalid or expired'
+				message: 'The reset token was invalid or expired',
 			},
 			params: {
 				new_password,
@@ -131,7 +148,7 @@ exports.handlePasswordReset = async ctx => {
 	if (!user) {
 		ctx.body = {
 			error: {
-				message: 'The reset token was invalid or expired'
+				message: 'The reset token was invalid or expired',
 			},
 			params: {
 				new_password,
@@ -148,4 +165,4 @@ exports.handlePasswordReset = async ctx => {
 
 	ctx.status = 200
 	ctx.body = { user }
-}
\ No newline at end of file
+}
diff --git a/src/http/controllers/api/content.js b/src/http/controllers/api/content.js
index 7b28757d47467eb33984317721fbe108e222d6b5..b65487206a8c072e967154514e7c3eefd2a71a33 100644
--- a/src/http/controllers/api/content.js
+++ b/src/http/controllers/api/content.js
@@ -7,7 +7,11 @@ exports.postMetric = async ctx => {
 	const allowedTypes = new Set(Metric.getSupportedMetricTypes())
 	const { value, type, location } = ctx.request.body
 
-	if (location == null || location.longitude == null || location.latitude == null) {
+	if (
+		location == null ||
+		location.longitude == null ||
+		location.latitude == null
+	) {
 		throw new HttpError({
 			status: 400,
 			code: 'MTR-001',
@@ -17,22 +21,31 @@ exports.postMetric = async ctx => {
 	}
 
 	if (allowedTypes.has(type)) {
-		const metric = await ctx.services['data.metrics'].recordMetric(value, type, location)
+		const metric = await ctx.services['data.metrics'].recordMetric(
+			value,
+			type,
+			location,
+		)
 		ctx.body = { metric: metric.toJSON() }
 	} else {
-		throw new HttpError({ status: 400, code: 'MTR-002', title: 'Invalid Metric', description: `${ type } is not a supported type`})
+		throw new HttpError({
+			status: 400,
+			code: 'MTR-002',
+			title: 'Invalid Metric',
+			description: `${type} is not a supported type`,
+		})
 	}
 }
 
 function splitString(str) {
-	return str.split(',')
+	return str
+		.split(',')
 		.map(s => s.trim())
 		.filter(Boolean)
 }
 
 function pointFromString(str) {
-	const parts = splitString(str)
-		.map(Number)
+	const parts = splitString(str).map(Number)
 
 	return {
 		latitude: parts[0],
@@ -47,7 +60,7 @@ exports.getWithin = async ctx => {
 		date_from,
 		date_to = moment.utc(),
 		types = '',
-		format = 'full'
+		format = 'full',
 	} = ctx.request.query
 
 	const fromPoint = pointFromString(point_from)
@@ -69,18 +82,21 @@ exports.getWithin = async ctx => {
 		[maxFromLong, maxFromLat],
 		[maxFromLong, minFromLat],
 		[minFromLong, minFromLat],
-	].map(pb => pb.map(Number).join(' ')).join(',')
+	]
+		.map(pb => pb.map(Number).join(' '))
+		.join(',')
 
-	const query = format === 'marker' ?
-		ctx.services['data.metrics'].queryAggregate
-		: ctx.services['data.metrics'].queryAll
+	const query =
+		format === 'marker'
+			? ctx.services['data.metrics'].queryAggregate
+			: ctx.services['data.metrics'].queryAll
 
 	const metrics = await query(
 		pointBuffer,
 		metricTypes,
 		// fromDate.toISOString(),
 		moment().subtract(12, 'months').toISOString(),
-		toDate.toISOString()
+		toDate.toISOString(),
 	)
 
 	ctx.body = { metrics }
diff --git a/src/http/controllers/api/feedback.js b/src/http/controllers/api/feedback.js
index 4a12d6998d075b6a6e789add8392b97f01466b50..a70e05ab2cbdf17b4e0569e6a34c4fec42e980b8 100644
--- a/src/http/controllers/api/feedback.js
+++ b/src/http/controllers/api/feedback.js
@@ -4,46 +4,46 @@ const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK
 
 function createSlackPayload({ name, email, message = '' }) {
 	return {
-		"blocks": [
+		blocks: [
 			{
-				"type": "divider"
+				type: 'divider',
 			},
 			{
-				"type": "section",
-				"text": {
-					"type": "mrkdwn",
-					"text": "Incoming message from an app user!"
-				}
+				type: 'section',
+				text: {
+					type: 'mrkdwn',
+					text: 'Incoming message from an app user!',
+				},
 			},
 			{
-				"type": "section",
-				"text": {
-					"type": "plain_text",
+				type: 'section',
+				text: {
+					type: 'plain_text',
 					text: message,
 					emoji: true,
-				}
+				},
 			},
 			{
-				"type": "divider"
+				type: 'divider',
 			},
 			{
-				"type": "context",
-				"elements": [
+				type: 'context',
+				elements: [
 					{
-						"type": "mrkdwn",
-						"text": (new Date()).toDateString(),
+						type: 'mrkdwn',
+						text: new Date().toDateString(),
 					},
 					name && {
-						"type": "plain_text",
-						"text": `From: ${ name }`
+						type: 'plain_text',
+						text: `From: ${name}`,
 					},
 					email && {
-						"type": "plain_text",
-						"text": `Email: ${ email }`
-					}
-				].filter(Boolean)
-			}
-		]
+						type: 'plain_text',
+						text: `Email: ${email}`,
+					},
+				].filter(Boolean),
+			},
+		],
 	}
 }
 
@@ -77,10 +77,10 @@ exports.send = async ctx => {
 		console.error(response)
 		ctx.status = 500
 		ctx.body = {
-			message: 'Something went wrong'
+			message: 'Something went wrong',
 		}
 	} else {
 		ctx.status = response.status
-		ctx.body = { message: 'Something Happened'}
+		ctx.body = { message: 'Something Happened' }
 	}
 }
diff --git a/src/http/controllers/api/oauth.js b/src/http/controllers/api/oauth.js
index a82997e69c096dae35aea9a1de61fe1524156c1f..643b2cdb93007e935918f21d4f42b00966f240f8 100644
--- a/src/http/controllers/api/oauth.js
+++ b/src/http/controllers/api/oauth.js
@@ -22,7 +22,7 @@ exports.listClients = async ctx => {
 	const clients = await user.getOAuthClients()
 
 	ctx.body = {
-		clients: clients.map(c => c.toOAuthInterface())
+		clients: clients.map(c => c.toOAuthInterface()),
 	}
 }
 
@@ -31,7 +31,10 @@ exports.addClientRedirect = async ctx => {
 	const user = await ctx.services['core.auth'].getUser()
 
 	if (client?.owner_id !== user.id) {
-		throw new HttpError(403, 'You do not have permission to modify this oauth client')
+		throw new HttpError(
+			403,
+			'You do not have permission to modify this oauth client',
+		)
 	}
 
 	const uri = ctx.request.body?.uri
@@ -42,10 +45,7 @@ exports.addClientRedirect = async ctx => {
 
 	const uris = new Set(client.redirect_uris)
 	if (!uris.has(uri)) {
-		client.redirect_uris = [
-			...client.redirect_uris,
-			uri,
-		]
+		client.redirect_uris = [...client.redirect_uris, uri]
 
 		await client.save()
 	}
@@ -58,7 +58,10 @@ exports.removeClientRedirect = async ctx => {
 	const user = await ctx.services['core.auth'].getUser()
 
 	if (client.owner_id !== user.id) {
-		throw new HttpError(403, 'You do not have permission to modify this oauth client')
+		throw new HttpError(
+			403,
+			'You do not have permission to modify this oauth client',
+		)
 	}
 
 	const uri = ctx.request.body?.uri
@@ -75,4 +78,4 @@ exports.removeClientRedirect = async ctx => {
 		await client.save()
 	}
 	ctx.body = { client: client.toOAuthInterface() }
-}
\ No newline at end of file
+}
diff --git a/src/http/controllers/api/storage.js b/src/http/controllers/api/storage.js
index e8886232ab4536d33751b2258442aad2d0a7b730..544e81ad653cb7e5bcedc695054e5c2434eabb75 100644
--- a/src/http/controllers/api/storage.js
+++ b/src/http/controllers/api/storage.js
@@ -5,7 +5,7 @@ const mimeType = require('mime-types')
 const { v4: uuid } = require('uuid')
 const { fs, env } = require('bootstrap')
 const moment = require('moment')
-const {unset} = require('../../../bootstrap')
+const { unset } = require('../../../bootstrap')
 // const reporter = require('services/Reporter')
 
 const { Op } = Sequelize
@@ -20,11 +20,15 @@ exports.saveFile = async ctx => {
 	const user = await ctx.profile(
 		'core.auth.getUser',
 		'Get logged in user',
-		async () => ctx.services['core.auth'].getUser()
+		async () => ctx.services['core.auth'].getUser(),
 	)
 
 	if (!user) {
-		throw new HttpError({ status: 403, title: 'Must be signed in to feature an image', description: 'You must be signed in to submit images to the content feed' })
+		throw new HttpError({
+			status: 403,
+			title: 'Must be signed in to feature an image',
+			description: 'You must be signed in to submit images to the content feed',
+		})
 	}
 
 	const { file } = ctx
@@ -37,13 +41,13 @@ exports.saveFile = async ctx => {
 			bucket: env('GCS_BUCKET'),
 			device: ctx.request.device,
 		},
-		file_name: `${ uuid() }.${ mimeType.extension(file.mimetype) }`,
-		file_root: `${ user.id }`,
+		file_name: `${uuid()}.${mimeType.extension(file.mimetype)}`,
+		file_root: `${user.id}`,
 		user_id: user.id,
 		stream: 'plastics-public',
 		provider: 'google',
 		comment: '',
-		tags : [],
+		tags: [],
 		approved: true,
 		featured: false,
 	}
@@ -56,9 +60,11 @@ exports.saveFile = async ctx => {
 		fileInfo.meta.location = location
 	}
 
-	const persistedFile = await File.create(fileInfo).catch(e => console.error(e) || null)
+	const persistedFile = await File.create(fileInfo).catch(
+		e => console.error(e) || null,
+	)
 
-	const storagePath = `${ fileInfo.file_root }/${ fileInfo.file_name }`
+	const storagePath = `${fileInfo.file_root}/${fileInfo.file_name}`
 	const bucket = storage.bucket(env('GCS_BUCKET'))
 
 	const uploadOpts = {
@@ -69,8 +75,8 @@ exports.saveFile = async ctx => {
 			metadata: {
 				mimetype: file.mimetype,
 				uploader: user.id,
-			}
-		}
+			},
+		},
 	}
 
 	await bucket.upload(file.path, uploadOpts)
@@ -88,11 +94,15 @@ exports.saveFile2 = async ctx => {
 	const user = await ctx.profile(
 		'core.auth.getUser',
 		'Get logged in user',
-		async () => ctx.services['core.auth'].getUser()
+		async () => ctx.services['core.auth'].getUser(),
 	)
 
 	if (!user) {
-		throw new HttpError({ status: 403, title: 'Must be signed in to feature an image', description: 'You must be signed in to submit images to the content feed' })
+		throw new HttpError({
+			status: 403,
+			title: 'Must be signed in to feature an image',
+			description: 'You must be signed in to submit images to the content feed',
+		})
 	}
 
 	const { file } = ctx
@@ -104,13 +114,13 @@ exports.saveFile2 = async ctx => {
 			size: file.size,
 			bucket: env('GCS_BUCKET'),
 		},
-		file_name: `${ uuid() }.${ mimeType.extension(file.mimetype) }`,
-		file_root: `${ user.id }`,
+		file_name: `${uuid()}.${mimeType.extension(file.mimetype)}`,
+		file_root: `${user.id}`,
 		user_id: user.id,
 		stream: 'plastics-public',
 		provider: 'google',
 		comment: title,
-		tags : Array.isArray(tags) ? tags : [tags],
+		tags: Array.isArray(tags) ? tags : [tags],
 		approved: true,
 		featured: false,
 	}
@@ -119,9 +129,11 @@ exports.saveFile2 = async ctx => {
 		fileinfo.meta.remote_id = remote_id
 	}
 
-	const persistedFile = await File.create(fileinfo).catch(e => console.error(e) || null)
+	const persistedFile = await File.create(fileinfo).catch(
+		e => console.error(e) || null,
+	)
 
-	const storagePath = `${ fileinfo.file_root }/${ fileinfo.file_name }`
+	const storagePath = `${fileinfo.file_root}/${fileinfo.file_name}`
 	const bucket = storage.bucket(env('GCS_BUCKET'))
 
 	const uploadOpts = {
@@ -133,8 +145,8 @@ exports.saveFile2 = async ctx => {
 				tags,
 				mimetype: file.mimetype,
 				uploader: user.id,
-			}
-		}
+			},
+		},
 	}
 
 	await bucket.upload(file.path, uploadOpts)
@@ -148,14 +160,18 @@ exports.getFiles = async ctx => {
 	const user = await ctx.profile(
 		'core.auth.getUser',
 		'Get logged in user',
-		async () => ctx.services['core.auth'].getUser()
+		async () => ctx.services['core.auth'].getUser(),
 	)
 
 	if (!user) {
-		throw new HttpError({ status: 403, title: 'Must be signed in to feature an image', description: 'You must be signed in to submit images to the content feed' })
+		throw new HttpError({
+			status: 403,
+			title: 'Must be signed in to feature an image',
+			description: 'You must be signed in to submit images to the content feed',
+		})
 	}
 
-	const where = { }
+	const where = {}
 	const { since } = ctx.query
 
 	if (since && moment.utc(since).isValid()) {
@@ -171,13 +187,21 @@ exports.featureImage = async ctx => {
 	const user = await ctx.services['core.auth'].getUser()
 
 	if (!user) {
-		throw new HttpError({ status: 403, title: 'Must be signed in to feature an image', description: 'You must be signed in to submit images to the content feed' })
+		throw new HttpError({
+			status: 403,
+			title: 'Must be signed in to feature an image',
+			description: 'You must be signed in to submit images to the content feed',
+		})
 	}
 
 	const { imageId } = ctx.params
 	const file = await File.findOne({ where: { id: imageId } })
 	if (file.user_id !== user.id) {
-		throw new HttpError({ status: 403, title: 'Cannot feature non-owned image', description: 'You can\'t feature an image that doesn\'t belong to you' })
+		throw new HttpError({
+			status: 403,
+			title: 'Cannot feature non-owned image',
+			description: "You can't feature an image that doesn't belong to you",
+		})
 	}
 
 	if (file.featured && file.featured_at) {
@@ -205,43 +229,42 @@ exports.feed = async ctx => {
 
 	if (user != null) {
 		includes.push([
-			Sequelize.literal(`exists (select * from user_file_likes where user_file_likes.user_id='${ user.id }' and user_file_likes.file_id="File".id)`),
+			Sequelize.literal(
+				`exists (select * from user_file_likes where user_file_likes.user_id='${user.id}' and user_file_likes.file_id="File".id)`,
+			),
 			'liked',
 		])
 	} else {
-		includes.push([
-			Sequelize.literal('false'),
-			'liked',
-		])
+		includes.push([Sequelize.literal('false'), 'liked'])
 	}
 
 	await ctx.profile('feed.list', 'Fetch Jetsam community feed', async () => {
 		ctx.body = {
-			feed: (await File.findAll({
-				where: {
-					approved: true,
-					featured: true,
-					created_at: {
-						[Sequelize.Op.gt]: moment.utc().subtract(1, 'month').toISOString(),
+			feed: (
+				await File.findAll({
+					where: {
+						approved: true,
+						featured: true,
+						created_at: {
+							[Sequelize.Op.gt]: moment
+								.utc()
+								.subtract(1, 'month')
+								.toISOString(),
+						},
 					},
-				},
-				order: [
-					['featured_at', 'desc'],
-					['created_at', 'desc'],
-				],
-				attributes: {
-					include: includes,
-					exclude: [
-						'labels',
-						'updated_at',
-						'requires_approval',
-						'labels',
-					]
-				}
-			})).map(inst => ({
+					order: [
+						['featured_at', 'desc'],
+						['created_at', 'desc'],
+					],
+					attributes: {
+						include: includes,
+						exclude: ['labels', 'updated_at', 'requires_approval', 'labels'],
+					},
+				})
+			).map(inst => ({
 				...inst.toJSON(),
 				liked: inst.get('liked'),
-			}))
+			})),
 		}
 	})
 }
@@ -251,28 +274,44 @@ exports.like = async ctx => {
 
 	const user = await ctx.services['core.auth'].getUser()
 	if (!user) {
-		throw new HttpError({ status: 403, title: 'Must be signed in to like an image', description: 'You must be signed in to like images' })
+		throw new HttpError({
+			status: 403,
+			title: 'Must be signed in to like an image',
+			description: 'You must be signed in to like images',
+		})
 	}
-	const file = await File.findOne({ where: { id: fileId }})
+	const file = await File.findOne({ where: { id: fileId } })
 	if (!file) {
-		throw new HttpError({ status: 404, title: 'Could not find image', description: 'Could not find image to like' })
+		throw new HttpError({
+			status: 404,
+			title: 'Could not find image',
+			description: 'Could not find image to like',
+		})
 	}
 
 	await user.addLike(file)
-	ctx.body = { file: {...file.toJSON(), liked: true }}
+	ctx.body = { file: { ...file.toJSON(), liked: true } }
 }
 exports.unlike = async ctx => {
 	const { fileId } = ctx.params
 
 	const user = await ctx.services['core.auth'].getUser()
 	if (!user) {
-		throw new HttpError({ status: 403, title: 'Must be signed in to like an image', description: 'You must be signed in to unlike images' })
+		throw new HttpError({
+			status: 403,
+			title: 'Must be signed in to like an image',
+			description: 'You must be signed in to unlike images',
+		})
 	}
-	const file = await File.findOne({ where: { id: fileId }})
+	const file = await File.findOne({ where: { id: fileId } })
 	if (!file) {
-		throw new HttpError({ status: 404, title: 'Could not find image', description: 'Could not find image to unlike' })
+		throw new HttpError({
+			status: 404,
+			title: 'Could not find image',
+			description: 'Could not find image to unlike',
+		})
 	}
 
 	await user.removeLike(file)
-	ctx.body = { file: {...file.toJSON(), liked: false }}
-}
\ No newline at end of file
+	ctx.body = { file: { ...file.toJSON(), liked: false } }
+}
diff --git a/src/http/controllers/api/user.js b/src/http/controllers/api/user.js
index 8e540b8a2780264d2f8f4869a121279e7386ec61..dbb3ee8fb1ff2064d4c3f5d40ecc1cef5b346804 100644
--- a/src/http/controllers/api/user.js
+++ b/src/http/controllers/api/user.js
@@ -28,8 +28,12 @@ exports.updateOne = async ctx => {
 		await user.save()
 		await user.handleIncludes(ctx.includes)
 	} else {
-		throw new HttpError({ status: 404, title: 'No such user', description: 'No user is currently logged in' })
+		throw new HttpError({
+			status: 404,
+			title: 'No such user',
+			description: 'No user is currently logged in',
+		})
 	}
 
 	ctx.body = { user }
-}
\ No newline at end of file
+}
diff --git a/src/http/controllers/auth.js b/src/http/controllers/auth.js
index 640453fd5d3cc267bd6056c555365c0e0f8ce7cc..d44678b5af34d3131e756f33d7f51b0e59e71b4e 100644
--- a/src/http/controllers/auth.js
+++ b/src/http/controllers/auth.js
@@ -15,7 +15,9 @@ exports.handleLoginRedirect = async ctx => {
 
 		console.log(values)
 		if (values.redirect === 'authorize') {
-			return ctx.redirect(`/auth/authorize?auth_state=${ values.query.auth_state }`)
+			return ctx.redirect(
+				`/auth/authorize?auth_state=${values.query.auth_state}`,
+			)
 		} else {
 			return ctx.redirect('/')
 		}
@@ -25,8 +27,10 @@ exports.handleLoginRedirect = async ctx => {
 }
 
 const resetErrorMessages = {
-	missing: 'No token was found in the URL. If you clicked a link to get here, please make sure that it contains a password reset token. You may need to request a new password reset in the Jetsam app.',
-	invalid: 'The link you clicked was invalid or has expired. Password reset links are valid for 1 hour from the time we send them to you; you may need to request a new password reset in the Jetsam app.',
+	missing:
+		'No token was found in the URL. If you clicked a link to get here, please make sure that it contains a password reset token. You may need to request a new password reset in the Jetsam app.',
+	invalid:
+		'The link you clicked was invalid or has expired. Password reset links are valid for 1 hour from the time we send them to you; you may need to request a new password reset in the Jetsam app.',
 }
 
 exports.resetPassword = async ctx => {
@@ -44,7 +48,12 @@ exports.resetPassword = async ctx => {
 	}
 
 	const { sequelize } = require('database/models')
-	const [ [ { exists } ] ] = await sequelize.query('SELECT exists(select reset_token from users where reset_token = :token AND deleted_at is null limit 1)', { replacements: { token } })
+	const [
+		[{ exists }],
+	] = await sequelize.query(
+		'SELECT exists(select reset_token from users where reset_token = :token AND deleted_at is null limit 1)',
+		{ replacements: { token } },
+	)
 
 	if (!exists) {
 		errorData.message = resetErrorMessages.invalid
@@ -65,7 +74,11 @@ exports.resetPassword = async ctx => {
 
 	const time = moment.utc(data.expires)
 
-	if (data.id == null || data.expires == null || time.isSameOrBefore(moment.utc())) {
+	if (
+		data.id == null ||
+		data.expires == null ||
+		time.isSameOrBefore(moment.utc())
+	) {
 		errorData.message = resetErrorMessages.invalid
 		await ctx.render('auth/reset-password-error', errorData)
 		ctx.status = 400
@@ -84,11 +97,11 @@ exports.handleResetPassword = async ctx => {
 	if (ctx.status >= 400) {
 		const { error } = ctx.body
 		await ctx.render('auth/reset-password-error', {
-			back_link: `/reset-password?token=${ ctx.request.body.reset_token }`,
+			back_link: `/reset-password?token=${ctx.request.body.reset_token}`,
 			message: error.message,
 		})
 		return
 	}
 
 	await ctx.render('auth/reset-password-success')
-}
\ No newline at end of file
+}
diff --git a/src/http/controllers/fs_local.js b/src/http/controllers/fs_local.js
index ae77418b2ba42a2df0dd4f27d5456270665bdee5..19a55f1cce1255dbaecdee5fc6971b05e30e956a 100644
--- a/src/http/controllers/fs_local.js
+++ b/src/http/controllers/fs_local.js
@@ -29,7 +29,7 @@ exports.uploadFile = async ctx => {
 	}
 
 	const extension = mime.extension(ctx.request.file.mimetype)
-	const fpath = p.normalize(`${ n(uid) }/${ n(fid) }.${ extension }`)
+	const fpath = p.normalize(`${n(uid)}/${n(fid)}.${extension}`)
 	await fs.write(fpath, ctx.request.file.buffer)
 
 	file.meta = { ...file.meta, path: fpath }
@@ -52,10 +52,9 @@ exports.serveFile = async ctx => {
 		throw new HttpError(404, 'File not found')
 	}
 
-
 	const { fs } = require('bootstrap')
 	const { fs: fileservice } = require('services')
 
 	ctx.set('Content-Type', file.meta.contentType)
 	ctx.body = fs.createReadStream(fs.path(fileservice.base, file.meta.path))
-}
\ No newline at end of file
+}
diff --git a/src/http/middleware/DeviceProperties.js b/src/http/middleware/DeviceProperties.js
index ec13422448a28ea1e881744052d596242b9e4133..ae8f9578b6bf2aa6dd2c3c7d250323538daf2dc5 100644
--- a/src/http/middleware/DeviceProperties.js
+++ b/src/http/middleware/DeviceProperties.js
@@ -1,11 +1,9 @@
 exports.extractDevice = async (ctx, next) => {
-	const deviceId = ctx.get('X-Request-Device')
-	const platform = ctx.get('X-Request-Platform')
-	const rawSlug = ctx.get('X-Request-Slug')
+	const deviceId = ctx.get('x-request-device')
+	const platform = ctx.get('x-request-platform')
+	const rawSlug = ctx.get('x-request-slug')
 
-	const slug = rawSlug ?
-		Buffer.from(rawSlug, 'base64').toString('utf-8')
-		: null
+	const slug = rawSlug ? Buffer.from(rawSlug, 'base64').toString('utf-8') : null
 
 	ctx.request.device = {
 		id: deviceId,
@@ -14,4 +12,4 @@ exports.extractDevice = async (ctx, next) => {
 	}
 
 	return await next()
-}
\ No newline at end of file
+}
diff --git a/src/http/middleware/ParseIncludes.js b/src/http/middleware/ParseIncludes.js
index 8869d3772452d76a5328336df91aad36aee91162..a928e3705d321f94d3f4d9fa81e4739fbaf8423c 100644
--- a/src/http/middleware/ParseIncludes.js
+++ b/src/http/middleware/ParseIncludes.js
@@ -1,7 +1,10 @@
 module.exports = async function (ctx, next) {
 	const { expand } = ctx.query
 	if (expand != null) {
-		const expanded = expand.split(',').map(s => s.trim()).filter(Boolean)
+		const expanded = expand
+			.split(',')
+			.map(s => s.trim())
+			.filter(Boolean)
 		ctx.includes = expanded
 	} else {
 		ctx.includes = []
diff --git a/src/http/middleware/Profiler.js b/src/http/middleware/Profiler.js
index d9b1e37c028b5b68789666d492ecbc86425ba6fd..9e3b5b086f66a0db44f0fbc309bb53cbf9df6909 100644
--- a/src/http/middleware/Profiler.js
+++ b/src/http/middleware/Profiler.js
@@ -1,16 +1,16 @@
-const Sentry = require("@sentry/node")
-const { extractTraceparentData }= require("@sentry/tracing")
+const Sentry = require('@sentry/node')
+const { extractTraceparentData } = require('@sentry/tracing')
 const threadContext = require('core/injection/ThreadContext')
 
 module.exports = async (ctx, next) => {
 	let traceparentData
-	if (ctx.request.get("sentry-trace")) {
-		traceparentData = extractTraceparentData(ctx.request.get("sentry-trace"));
+	if (ctx.request.get('sentry-trace')) {
+		traceparentData = extractTraceparentData(ctx.request.get('sentry-trace'))
 	}
 
 	const t = threadContext.getTransaction({
 		op: 'http.request',
-		name: `[${ ctx.method }] ${ ctx.path }`,
+		name: `[${ctx.method}] ${ctx.path}`,
 		traceparentData,
 		tags: {
 			'http.method': ctx.method,
@@ -41,7 +41,11 @@ module.exports = async (ctx, next) => {
 	try {
 		return await next()
 	} finally {
-		t.setName(`[${ ctx.method }] ${ ctx._matchedRouteName ?? ctx._matchedRoute ?? ctx.path }`)
+		t.setName(
+			`[${ctx.method}] ${
+				ctx._matchedRouteName ?? ctx._matchedRoute ?? ctx.path
+			}`,
+		)
 		t.setHttpStatus(ctx.status)
 		const user = ctx.services['core.auth']._user
 		Sentry.configureScope(scope => {
@@ -57,4 +61,4 @@ module.exports = async (ctx, next) => {
 			threadContext.stopTransaction()
 		})
 	}
-}
\ No newline at end of file
+}
diff --git a/src/http/middleware/RequiresAuth.js b/src/http/middleware/RequiresAuth.js
index 5f6cda3320b8a3077593bdb761d8c640a3363192..35c924ff221d080ba5e343ad168741fa9d8bbcd6 100644
--- a/src/http/middleware/RequiresAuth.js
+++ b/src/http/middleware/RequiresAuth.js
@@ -6,4 +6,4 @@ module.exports = async (ctx, next) => {
 		throw new UnauthorizedError()
 	}
 	return await next()
-}
\ No newline at end of file
+}
diff --git a/src/http/middleware/SentryReporter.js b/src/http/middleware/SentryReporter.js
index 694260b142d0f8fa4473b2fad32b202fff9a6ba4..dfa1ed1ab0acef621c37ba494cce5afb987a0d76 100644
--- a/src/http/middleware/SentryReporter.js
+++ b/src/http/middleware/SentryReporter.js
@@ -17,7 +17,7 @@ exports.report = async function reportErrorToSentry(error, ctx) {
 	}
 	let user = await getUserObject(ctx)
 
-	Sentry.withScope(function(scope) {
+	Sentry.withScope(function (scope) {
 		if (user) {
 			scope.setUser(user)
 		}
@@ -30,19 +30,23 @@ exports.report = async function reportErrorToSentry(error, ctx) {
 			scope.setContext('debugging', error._debugging)
 		}
 
-		scope.addEventProcessor(function(event) {
-			return Sentry.Handlers.parseRequest(event, ctx.request);
-		});
-		Sentry.captureException(error);
-	});
+		scope.addEventProcessor(function (event) {
+			return Sentry.Handlers.parseRequest(event, ctx.request)
+		})
+		Sentry.captureException(error)
+	})
 }
 
 exports.reportHttp = async function reportHttpToSentry(ctx) {
 	let user = await getUserObject(ctx)
 
-	const error = new HttpError(ctx.status, `[${ ctx.status }] A HTTP Error Occurred`, { body: ctx.body })
+	const error = new HttpError(
+		ctx.status,
+		`[${ctx.status}] A HTTP Error Occurred`,
+		{ body: ctx.body },
+	)
 
-	Sentry.withScope(function(scope) {
+	Sentry.withScope(function (scope) {
 		if (user) {
 			scope.setUser(user)
 		}
@@ -53,9 +57,9 @@ exports.reportHttp = async function reportHttpToSentry(ctx) {
 			scope.setContext('device', ctx.request.device)
 		}
 
-		scope.addEventProcessor(function(event) {
-			return Sentry.Handlers.parseRequest(event, ctx.request);
-		});
-		Sentry.captureException(error);
-	});
-}
\ No newline at end of file
+		scope.addEventProcessor(function (event) {
+			return Sentry.Handlers.parseRequest(event, ctx.request)
+		})
+		Sentry.captureException(error)
+	})
+}
diff --git a/src/http/params/event.js b/src/http/params/event.js
index 30d1f82abdb5bc74ec952f2cedda6cfbb10c6a67..41db4e4625a14e8ee805da891db25b1e2768590c 100644
--- a/src/http/params/event.js
+++ b/src/http/params/event.js
@@ -4,4 +4,4 @@ module.exports = async (id, ctx, next) => {
 	ctx.models = ctx.models ?? {}
 	ctx.models.event = await Event.findByPk(id)
 	return await next()
-}
\ No newline at end of file
+}
diff --git a/src/http/params/file.js b/src/http/params/file.js
index 23c7510705570b9427bb5cd3856416da9ca4bf44..17fcfd8ed0cc576beaeb3013a9aac1ec00e0660b 100644
--- a/src/http/params/file.js
+++ b/src/http/params/file.js
@@ -4,4 +4,4 @@ module.exports = async (id, ctx, next) => {
 	ctx.models = ctx.models ?? {}
 	ctx.models.file = await File.findByPk(id)
 	return await next()
-}
\ No newline at end of file
+}
diff --git a/src/http/params/oauth_client.js b/src/http/params/oauth_client.js
index ed386d809639ac9ce6a3df5d3db0e32029615ab3..91ee3d812c8a051727d959c57593720b425d3d9e 100644
--- a/src/http/params/oauth_client.js
+++ b/src/http/params/oauth_client.js
@@ -4,4 +4,4 @@ module.exports = async (id, ctx, next) => {
 	ctx.models = ctx.models ?? {}
 	ctx.models.oauthClient = await OAuthClient.findByPk(id)
 	return await next()
-}
\ No newline at end of file
+}
diff --git a/src/http/routes.js b/src/http/routes.js
index db1b7ceef2f3edf3cfeea732e6f57f87fb50b424..de30add0ff66bc058d002ca8f77b309658cd99f4 100644
--- a/src/http/routes.js
+++ b/src/http/routes.js
@@ -1,5 +1,5 @@
-const controller = (name, method) => require(`./controllers/${ name }`)[method]
-const param = (name) => require(`./params/${ name }`)
+const controller = (name, method) => require(`./controllers/${name}`)[method]
+const param = name => require(`./params/${name}`)
 
 const AuthServer = require('domain/auth/AuthServer')
 const { env, config } = require('bootstrap')
@@ -14,6 +14,7 @@ const includes = require('http/middleware/ParseIncludes')
 const profiling = require('http/middleware/Profiler')
 const loaders = require('http/middleware/MountLoaders')
 const userGate = require('http/middleware/RequiresAuth')
+const device = require('http/middleware/DeviceProperties').extractDevice
 
 const well_known = new Router({ prefix: '/.well-known' })
 well_known.get('wk.jwks', '/jwks.json', async ctx => {
@@ -26,16 +27,19 @@ well_known.get('wk.jwks', '/jwks.json', async ctx => {
 	ctx.set('Cache-Control', `public, max-age=30`)
 
 	ctx.body = {
-		keys: [{
-			use: 'sig',
-			...jwk,
-			alg: 'RS256',
-		}],
+		keys: [
+			{
+				use: 'sig',
+				...jwk,
+				alg: 'RS256',
+			},
+		],
 	}
 })
 
 const web = new Router()
 web.use(profiling)
+web.use(device)
 
 web.use(well_known.allowedMethods())
 web.use(well_known.routes())
@@ -55,19 +59,26 @@ web.post('/reset-password', controller('auth', 'handleResetPassword'))
 web.get('/auth/authorize', AuthServer.authorize)
 web.post('/auth/authorize', AuthServer.authorize)
 web.post('/auth/token', AuthServer.token)
-
-;(env('FS_DRIVER', 'local') === 'local') && (function() {
-	const debug = require('debug')('server:routes')
-	debug('Mounting local file upload routes for signed URLs')
-	const p = `${ config('fs.url') }/:uid/:fid`
-
-	web.put(p, errors, includes, loaders, userGate, upload.single('file'), controller('fs_local', 'uploadFile'))
-	web.get(p, errors, includes, loaders, controller('fs_local', 'serveFile'))
-
-	debug(`Mounted GET ${ p } to serve local files`)
-	debug(`Mounted PUT ${ p } to upload local files`)
-}())
-
+env('FS_DRIVER', 'local') === 'local' &&
+	(function () {
+		const debug = require('debug')('server:routes')
+		debug('Mounting local file upload routes for signed URLs')
+		const p = `${config('fs.url')}/:uid/:fid`
+
+		web.put(
+			p,
+			errors,
+			includes,
+			loaders,
+			userGate,
+			upload.single('file'),
+			controller('fs_local', 'uploadFile'),
+		)
+		web.get(p, errors, includes, loaders, controller('fs_local', 'serveFile'))
+
+		debug(`Mounted GET ${p} to serve local files`)
+		debug(`Mounted PUT ${p} to upload local files`)
+	})()
 
 const apiRouter = new Router({ prefix: '/api' })
 const apiLegacy = new Router({ prefix: '/api/api' })
@@ -78,6 +89,7 @@ function mount(api) {
 	api.use(errors)
 	api.use(includes)
 	api.use(loaders)
+	api.use(device)
 
 	api.get('/', ctx => {
 		const pkg = require('../../package.json')
@@ -87,42 +99,68 @@ function mount(api) {
 		}
 	})
 
-
 	api.post('/metrics', controller('api/content', 'postMetric'))
 	api.get('/metrics', controller('api/content', 'getWithin'))
 
 	api.get('/images', controller('api/storage', 'getFiles'))
-	api.post('/images', upload.single('featured_image'), controller('api/storage', 'saveFile'))
-	api.post('/images/:imageId/feature', controller('api/storage', 'featureImage'))
+	api.post(
+		'/images',
+		upload.single('featured_image'),
+		controller('api/storage', 'saveFile'),
+	)
+	api.post(
+		'/images/:imageId/feature',
+		controller('api/storage', 'featureImage'),
+	)
 
 	/** @deprecated */
-	api.post('/feature', upload.single('featured_image'), controller('api/storage', 'saveFile'))
+	api.post(
+		'/feature',
+		upload.single('featured_image'),
+		controller('api/storage', 'saveFile'),
+	)
 
 	api.get('/feed', controller('api/storage', 'feed'))
-	api.post('/feed/:fileId/like',controller('api/storage', 'like'))
+	api.post('/feed/:fileId/like', controller('api/storage', 'like'))
 	api.post('/feed/:fileId/unlike', controller('api/storage', 'unlike'))
 
 	api.post('/register', controller('api/auth', 'register'))
 	api.post('/login', controller('api/auth', 'login'))
 
 	api.post('/auth/reset-token', controller('api/auth', 'triggerPasswordReset'))
-	api.post('/auth/reset-password', controller('api/auth', 'handlePasswordReset'))
+	api.post(
+		'/auth/reset-password',
+		controller('api/auth', 'handlePasswordReset'),
+	)
 
 	api.param('oauthClientId', param('oauth_client'))
 
 	api.get('/oauth/clients', controller('api/oauth', 'listClients'))
 	api.post('/oauth/clients', controller('api/oauth', 'createClient'))
-	api.post('/oauth/clients/:oauthClientId/redirects', controller('api/oauth', 'addClientRedirect'))
-	api.delete('/oauth/clients/:oauthClientId/redirects', controller('api/oauth', 'removeClientRedirect'))
+	api.post(
+		'/oauth/clients/:oauthClientId/redirects',
+		controller('api/oauth', 'addClientRedirect'),
+	)
+	api.delete(
+		'/oauth/clients/:oauthClientId/redirects',
+		controller('api/oauth', 'removeClientRedirect'),
+	)
 
 	api.get('/self', controller('api/user', 'self'))
 	api.get('/self/bundles', controller('api/app', 'getBundles'))
 	api.put('/self/:property', controller('api/user', 'updateOne'))
 
+	api.post('/an/id', async ctx => {})
+	api.post('/an/ev', async ctx => {
+		console.log(ctx.request.body)
+		ctx.body = {
+			foo: true,
+		}
+	})
+
 	api.post('/feedback', controller('api/feedback', 'send'))
 }
 
-
 mount(apiRouter)
 mount(apiLegacy)
 
diff --git a/src/http/validators.js b/src/http/validators.js
index ea857a51e2069d7f400362810499332204a4d496..82ac43ba5efa41a0d307f1fe520808a166e97aaf 100644
--- a/src/http/validators.js
+++ b/src/http/validators.js
@@ -7,24 +7,20 @@ exports.validator = name => async (ctx, next) => {
 	const { value, error } = await schema.validate(ctx.request.body)
 
 	if (error instanceof ValidationError) {
-		throw new InputValidationError(error.details.map(detail => ({
-			field: detail.path.join('.'),
-			type: detail.type,
-			path: detail.path,
-		})))
+		throw new InputValidationError(
+			error.details.map(detail => ({
+				field: detail.path.join('.'),
+				type: detail.type,
+				path: detail.path,
+			})),
+		)
 	}
 
 	return await next()
 }
 
 exports.newEvent = Joi.object({
-	name: Joi.string()
-		.required(),
-	tags: Joi.array()
-		.items(Joi.string())
-		.unique()
-		.required(),
-	location: Joi.string()
-		.valid('online', 'physical')
-		.required(),
-})
\ No newline at end of file
+	name: Joi.string().required(),
+	tags: Joi.array().items(Joi.string()).unique().required(),
+	location: Joi.string().valid('online', 'physical').required(),
+})
diff --git a/src/services/cache/interface.js b/src/services/cache/interface.js
index 3ea3f6b8862c1863bf3657e7740b5d7768cceb75..b06ce3ff6d8d8099930dc731076aa550f8d57ecd 100644
--- a/src/services/cache/interface.js
+++ b/src/services/cache/interface.js
@@ -2,10 +2,18 @@ const { notImplemented } = require('services/utils')
 const { Model } = require('sequelize')
 
 module.exports = class Cache {
-	get() { notImplemented('cache', 'get') }
-	set() { notImplemented('cache', 'set') }
-	remember() { notImplemented('cache', 'remember') }
-	clear() { notImplemented('cache', 'clear') }
+	get() {
+		notImplemented('cache', 'get')
+	}
+	set() {
+		notImplemented('cache', 'set')
+	}
+	remember() {
+		notImplemented('cache', 'remember')
+	}
+	clear() {
+		notImplemented('cache', 'clear')
+	}
 
 	serialize(data) {
 		if (Array.isArray(data)) {
@@ -46,7 +54,11 @@ module.exports = class Cache {
 		}
 		// Enable for Sequelize model support
 		//
-		if (hydrated != null && typeof hydrated === 'object' && '$$_modelName' in hydrated) {
+		if (
+			hydrated != null &&
+			typeof hydrated === 'object' &&
+			'$$_modelName' in hydrated
+		) {
 			return this.deserializeModel(hydrated)
 		}
 		return this.deserializeObject(hydrated)
@@ -69,9 +81,10 @@ module.exports = class Cache {
 
 	deserializeObject(object) {
 		if (object != null && object.hasOwnProperty('expiresAt')) {
-			object = Object.assign({}, object, { data: this.deserialize(object.data) })
+			object = Object.assign({}, object, {
+				data: this.deserialize(object.data),
+			})
 		}
 		return object
 	}
-
 }
diff --git a/src/services/cache/memory.js b/src/services/cache/memory.js
index b76f5e151e3b8c051ebf88a319bbd37d484953b5..1d177b833f6b23ba965c033dc20476f916e6f26f 100644
--- a/src/services/cache/memory.js
+++ b/src/services/cache/memory.js
@@ -9,11 +9,15 @@ class MemoryCache extends Cache {
 	}
 
 	_generateKey(key) {
-		return `${ config('cache.prefix') }:${ key }`
+		return `${config('cache.prefix')}:${key}`
 	}
 
-	get(key) { return this.deserialize(this._cache.get(key)) }
-	set(key, value) { this._cache.set(key, this.serialize(value)) }
+	get(key) {
+		return this.deserialize(this._cache.get(key))
+	}
+	set(key, value) {
+		this._cache.set(key, this.serialize(value))
+	}
 	async remember(key, forS, fn) {
 		let value = null
 		if (this._cache.has(key)) {
@@ -28,14 +32,18 @@ class MemoryCache extends Cache {
 
 		if (value == null) {
 			value = await fn()
-			const payload = { expiresAt: moment.utc().add(forS, 'seconds').toISOString(), data: value }
+			const payload = {
+				expiresAt: moment.utc().add(forS, 'seconds').toISOString(),
+				data: value,
+			}
 			await this.set(key, payload)
 		}
 
 		return value
 	}
-	clear(key) { this._cache.delete(key) }
-
+	clear(key) {
+		this._cache.delete(key)
+	}
 }
 
 module.exports = new MemoryCache()
diff --git a/src/services/cache/null.js b/src/services/cache/null.js
index 1d0bac2ce385cb5e730495069191856f1cebbcaa..e79e7f96b3ee57d6806b85f2a1d522f6e5667e37 100644
--- a/src/services/cache/null.js
+++ b/src/services/cache/null.js
@@ -1,10 +1,18 @@
 const Cache = require('./interface')
 
 class NullCache extends Cache {
-	get() { /* noop */ }
-	set() { /* noop */ }
-	remember(_, __, fn) { return fn() }
-	clear() { /* noop */ }
+	get() {
+		/* noop */
+	}
+	set() {
+		/* noop */
+	}
+	remember(_, __, fn) {
+		return fn()
+	}
+	clear() {
+		/* noop */
+	}
 }
 
 module.exports = new NullCache()
diff --git a/src/services/cache/redis.js b/src/services/cache/redis.js
index d7bdc34403812f817f33ae75e1c8e85b12744379..c2f786379cec4be938692c40351abec0dbef2703 100644
--- a/src/services/cache/redis.js
+++ b/src/services/cache/redis.js
@@ -13,11 +13,10 @@ class RedisCache extends Cache {
 		debug('Connecting to redis server %s', this._dsn)
 		const ioredis = require('ioredis')
 		this._client = new ioredis(this._dsn)
-
 	}
 
 	_generateKey(key) {
-		return `${ this._prefix }:${ key }`
+		return `${this._prefix}:${key}`
 	}
 
 	async get(key) {
@@ -28,7 +27,12 @@ class RedisCache extends Cache {
 	async set(key, value, ttl = null) {
 		if (ttl) {
 			debug('Setting value %s for %s. Will expire in %d', value, key, ttl)
-			return this._client.set(this._generateKey(key), this.serialize(value), 'ex', ttl)
+			return this._client.set(
+				this._generateKey(key),
+				this.serialize(value),
+				'ex',
+				ttl,
+			)
 		} else {
 			debug('Setting value %s for %s', value, key)
 			return this._client.set(this._generateKey(key), this.serialize(value))
@@ -52,7 +56,6 @@ class RedisCache extends Cache {
 		debug('Clearing value for %s', key)
 		return this._client.del(this._generateKey(key))
 	}
-
 }
 
 module.exports = new RedisCache()
diff --git a/src/services/fs/gcs.js b/src/services/fs/gcs.js
index 8746df8f7d7421c39de48ffdf89da522b4e6a4f5..55ac411f66981e8334a7b6f61a5aed57b1abf814 100644
--- a/src/services/fs/gcs.js
+++ b/src/services/fs/gcs.js
@@ -7,7 +7,7 @@ const log = require('debug')('server:services:gcs')
 const FS = require('./interface')
 
 const { fs, config } = require('bootstrap')
-const {fromBase64} = require('core/utils/crypto')
+const { fromBase64 } = require('core/utils/crypto')
 
 /**
  * @type GCSFS
@@ -19,17 +19,19 @@ const {fromBase64} = require('core/utils/crypto')
  */
 class GCSFS extends FS {
 	constructor() {
-		super();
+		super()
 		this.bucketname = config('fs.bucket')
 		this.path = config('fs.path')
 		this.credentials = config('fs.credentials')
 
 		if (this.path != null) {
-			log(`Created GCS FS with credentials from ${ this.path }`)
+			log(`Created GCS FS with credentials from ${this.path}`)
 			this.storage = new Storage({ keyFilename: this.path })
 		} else if (this.credentials != null) {
 			log(`Created GCS FS with base 64 credentials from environment`)
-			this.storage = new Storage({ credentials: JSON.parse(fromBase64(this.credentials)) })
+			this.storage = new Storage({
+				credentials: JSON.parse(fromBase64(this.credentials)),
+			})
 		} else {
 			log(`Created GCS FS without specifying credentials`)
 			this.storage = new Storage()
@@ -66,7 +68,7 @@ class GCSFS extends FS {
 	async createUploadUrl(path, ttl, opts) {
 		const gopts = {
 			version: 'v4',
-			expires: Date.now() + (ttl * 1000),
+			expires: Date.now() + ttl * 1000,
 			action: 'write',
 			contentType: getContentType(opts.headers ?? {}) ?? opts.contentType,
 		}
@@ -77,13 +79,15 @@ class GCSFS extends FS {
 			url,
 			expires_at_ms: gopts.expires,
 			method: 'PUT',
-			headers: opts.headers ? { 'Content-Type': getContentType(opts.headers) ?? opts.contentType } : {},
+			headers: opts.headers
+				? { 'Content-Type': getContentType(opts.headers) ?? opts.contentType }
+				: {},
 		}
 	}
 	async createDownloadUrl(path, ttl, opts) {
 		const gopts = {
 			version: 'v4',
-			expires: Date.now() + (ttl * 1000),
+			expires: Date.now() + ttl * 1000,
 			action: 'read',
 		}
 
@@ -97,11 +101,15 @@ class GCSFS extends FS {
 		}
 	}
 	async getPublicUrl(path, opts) {
-		return `https://storage.googleapis.com/${ this.bucketname }/${ path }`
+		return `https://storage.googleapis.com/${this.bucketname}/${path}`
 	}
 
-	async makePublic(path, opts) { return this.bucket.file(path).makePublic() }
-	async makePrivate(path, opts) { return this.bucket.file(path).makePrivate() }
+	async makePublic(path, opts) {
+		return this.bucket.file(path).makePublic()
+	}
+	async makePrivate(path, opts) {
+		return this.bucket.file(path).makePrivate()
+	}
 
 	getTmpFilename() {
 		return fs.dir(fs.path(os.tmpdir(), 'hf')).path(uuid())
@@ -115,9 +123,11 @@ class GCSFS extends FS {
 }
 
 function getContentType(obj) {
-	return obj.headers?.['Content-Type'] ??
-	obj.headers?.['content-type'] ??
-	obj.headers?.contentType
+	return (
+		obj.headers?.['Content-Type'] ??
+		obj.headers?.['content-type'] ??
+		obj.headers?.contentType
+	)
 }
 
-module.exports = new GCSFS()
\ No newline at end of file
+module.exports = new GCSFS()
diff --git a/src/services/fs/interface.js b/src/services/fs/interface.js
index 8750a3571c0ad076a04e20170ffaae2b05f15b70..6eb25003ad523a109311fce35bb652cc315f5d73 100644
--- a/src/services/fs/interface.js
+++ b/src/services/fs/interface.js
@@ -1,17 +1,33 @@
 const { notImplemented } = require('services/utils')
 
 module.exports = class FS {
-	async write(path, contents, opts) { notImplemented('fs', 'write') }
+	async write(path, contents, opts) {
+		notImplemented('fs', 'write')
+	}
 	async writeAll(fds, opts) {
 		for (const fd of fds) {
 			await this.write(fd.path, fd.contents, opts)
 		}
 	}
-	async read(path, opts) { notImplemented('fs', 'read') }
-	async list(path, opts) { notImplemented('fs', 'list') }
-	async createUploadUrl(path, ttl, opts) { notImplemented('fs', 'createUploadUrl') }
-	async createDownloadUrl(path, ttl, opts) { notImplemented('fs', 'createDownloadUrl') }
-	async getPublicUrl(path, opts) { notImplemented('fs', 'getPublicUrl') }
-	async makePublic(path, opts) { notImplemented('fs', 'makePublic') }
-	async makePrivate(path, opts) { notImplemented('fs', 'makePrivate') }
+	async read(path, opts) {
+		notImplemented('fs', 'read')
+	}
+	async list(path, opts) {
+		notImplemented('fs', 'list')
+	}
+	async createUploadUrl(path, ttl, opts) {
+		notImplemented('fs', 'createUploadUrl')
+	}
+	async createDownloadUrl(path, ttl, opts) {
+		notImplemented('fs', 'createDownloadUrl')
+	}
+	async getPublicUrl(path, opts) {
+		notImplemented('fs', 'getPublicUrl')
+	}
+	async makePublic(path, opts) {
+		notImplemented('fs', 'makePublic')
+	}
+	async makePrivate(path, opts) {
+		notImplemented('fs', 'makePrivate')
+	}
 }
diff --git a/src/services/fs/local.js b/src/services/fs/local.js
index 4a8ea7164ebdd8a59531e01eb7ef8903cb6ccc71..33e2be03f91d9db9a7b822b2f9183a8b1b0c3c7b 100644
--- a/src/services/fs/local.js
+++ b/src/services/fs/local.js
@@ -5,7 +5,7 @@ const pathUtil = require('path')
 
 class LocalFS extends FS {
 	constructor() {
-		super();
+		super()
 		this.base = fs.path(config('fs.root'))
 		this.urlRoot = fs.path(config('fs.url'))
 	}
@@ -28,29 +28,41 @@ class LocalFS extends FS {
 		return await fs.inspectTreeAsync(filePath, opts)
 	}
 	async createUploadUrl(path, ttl, opts) {
-		const url = new URL(pathUtil.normalize(this.urlRoot + '/' + path), config('app.host.web'))
+		const url = new URL(
+			pathUtil.normalize(this.urlRoot + '/' + path),
+			config('app.host.web'),
+		)
 		return {
 			url,
-			expires_at_ms: Date.now() + (ttl * 1000),
+			expires_at_ms: Date.now() + ttl * 1000,
 			method: 'PUT',
 			headers: opts.headers ?? {},
 		}
 	}
 	async createDownloadUrl(path, ttl, opts) {
-		const url = new URL(pathUtil.normalize(this.urlRoot + '/' + path), config('app.host.web'))
+		const url = new URL(
+			pathUtil.normalize(this.urlRoot + '/' + path),
+			config('app.host.web'),
+		)
 		return {
 			url,
-			expires_at_ms: Date.now() + (ttl * 1000),
+			expires_at_ms: Date.now() + ttl * 1000,
 			method: 'GET',
 			headers: opts.headers ?? {},
 		}
 	}
 	async getPublicUrl(path, opts) {
-		return new URL(pathUtil.normalize(this.urlRoot + '/' + path), config('app.host.web')).toString()
-
+		return new URL(
+			pathUtil.normalize(this.urlRoot + '/' + path),
+			config('app.host.web'),
+		).toString()
+	}
+	async makePublic(path, opts) {
+		return true
+	}
+	async makePrivate(path, opts) {
+		return true
 	}
-	async makePublic(path, opts) { return true }
-	async makePrivate(path, opts) { return true }
 }
 
-module.exports = new LocalFS()
\ No newline at end of file
+module.exports = new LocalFS()
diff --git a/src/services/index.js b/src/services/index.js
index 8592613c6a50ff0ce4fbe4eb3b7184dd84634a64..bd3a963ed6aaeda939cc317a85c665c9ba85deb2 100644
--- a/src/services/index.js
+++ b/src/services/index.js
@@ -10,10 +10,12 @@ const SERVICES = [
 
 const services = {}
 
-SERVICES.forEach(([name, env, fallback]) => Object.defineProperty(services, name, {
-	get() {
-		return loadEnvService(name, env, fallback)
-	},
-}))
+SERVICES.forEach(([name, env, fallback]) =>
+	Object.defineProperty(services, name, {
+		get() {
+			return loadEnvService(name, env, fallback)
+		},
+	}),
+)
 
 module.exports = services
diff --git a/src/services/mail/interface.js b/src/services/mail/interface.js
index 3bce2d96aeb46215553127005b188fdab4255eab..dccb63645cf4d4f0b87ff84614e4466758434419 100644
--- a/src/services/mail/interface.js
+++ b/src/services/mail/interface.js
@@ -10,14 +10,21 @@ module.exports = class Mail {
 		return this._renderer
 	}
 
-	get fromAddress() { return config('mail.from') }
-	get replyAddress() { return config('mail.replyto') }
+	get fromAddress() {
+		return config('mail.from')
+	}
+	get replyAddress() {
+		return config('mail.replyto')
+	}
 
-	sendTo() { notImplemented('Mail', 'sendTo') }
-	send(view, to, subject, data = {}, cc = [], bcc = []) { notImplemented('Mail', 'send') }
+	sendTo() {
+		notImplemented('Mail', 'sendTo')
+	}
+	send(view, to, subject, data = {}, cc = [], bcc = []) {
+		notImplemented('Mail', 'send')
+	}
 	sendTemplate(to, subject, templateId, data, opts) {
-		const templatePath = `templates/sg/${ templateId }`
+		const templatePath = `templates/sg/${templateId}`
 		return this.send(templatePath, to, subject, data)
 	}
 }
-
diff --git a/src/services/mail/log.js b/src/services/mail/log.js
index 2ba3205f067642cc9d6a93e1bb46367692ee9a21..12d564e6b09d6c76ff852c6240e222a46b526051 100644
--- a/src/services/mail/log.js
+++ b/src/services/mail/log.js
@@ -1,5 +1,7 @@
 const log = {
-	info(...args) { console.log('[Info]', ...args)}
+	info(...args) {
+		console.log('[Info]', ...args)
+	},
 }
 
 const Mail = require('./interface')
@@ -12,23 +14,18 @@ class LogMailer extends Mail {
 	async send(view, to, subject, data = {}, cc = [], bcc = []) {
 		// const paths = await getMailerPaths(view)
 
+		log.info('[Mailer]', `To: [${to}]`, subject)
+		log.info('[Mailer]', `CC: [${cc.join(', ')}]`, `BCC: [${bcc.join(', ')}]`)
 		log.info(
 			'[Mailer]',
-			`To: [${ to }]`,
-			subject
-		)
-		log.info(
-			'[Mailer]',
-			`CC: [${ cc.join(', ') }]`,
-			`BCC: [${ bcc.join(', ') }]`
+			`From: [${this.fromAddress}]`,
+			`Reply To: [${this.replyAddress}]`,
 		)
+		// log.info('[Mailer] Text:', paths.text ? await this.renderer.render(paths.text, data) : 'null')
 		log.info(
-			'[Mailer]',
-			`From: [${ this.fromAddress }]`,
-			`Reply To: [${ this.replyAddress }]`
+			'[Mailer] Html:',
+			view ? await this.renderer.render(view, data) : 'null',
 		)
-		// log.info('[Mailer] Text:', paths.text ? await this.renderer.render(paths.text, data) : 'null')
-		log.info('[Mailer] Html:', view ? await this.renderer.render(view, data) : 'null')
 	}
 }
 
diff --git a/src/services/mail/postmark.js b/src/services/mail/postmark.js
new file mode 100644
index 0000000000000000000000000000000000000000..7171d32321b336afb00d81e734e2705674f1fdf6
--- /dev/null
+++ b/src/services/mail/postmark.js
@@ -0,0 +1,82 @@
+const postmark = require('postmark')
+const Mail = require('./interface')
+const { config } = require('bootstrap')
+
+class PostmarkMailer extends Mail {
+	constructor() {
+		super()
+		const key = config('mail.key')
+		this.mailer = new postmark.ServerClient(key)
+	}
+
+	async sendTo(options) {
+		const renderer = this.renderer
+
+		const data = Object.assign(
+			{
+				meta: {
+					subject: options.subject,
+				},
+			},
+			options.data,
+		)
+
+		const value = await renderer.render(options.htmlView, data)
+
+		const message = {
+			To: options.to, // 'test@blackhole.postmarkapp.com', // options.to,
+			From: options.from,
+			// fromName: options.name,
+			Subject: options.subject,
+			// replyTo: options.replyto,
+			TextBody: value,
+			HtmlBody: value,
+			// cc: options.cc,
+			// bcc: options.bcc,
+			MessageStream: 'outbound',
+		}
+
+		return this.mailer.sendEmail(message)
+	}
+
+	async send(view, to, subject, data = {}, cc = [], bcc = []) {
+		// const paths = await getMailerPaths(view)
+		// if (paths.html == null || paths.text == null) {
+		// 	throw new InvalidMailerPathError(view)
+		// }
+
+		return this.sendTo({
+			to,
+			from: this.fromAddress,
+			subject,
+			name: config('mail.name'),
+			replyto: this.replyAddress,
+			textView: view,
+			htmlView: view,
+			cc,
+			bcc,
+			data,
+		})
+	}
+
+	async sendTemplate(to, subject, templateId, data = {}, opts = {}) {
+		// const baseOpts = config('mail.opts')
+
+		await this.mailer.sendEmailWithTemplate({
+			// ...baseOpts,
+			...opts,
+			To: to,
+			From: opts.from ?? this.fromAddress,
+			TemplateId: templateId,
+			TemplateModel: {
+				product_name: 'Jetsam',
+				product_url: 'https://jetsam.tech',
+				company_name: 'Jetsam Tech Ltd',
+				subject,
+				...data,
+			},
+		})
+	}
+}
+
+module.exports = new PostmarkMailer()
diff --git a/src/services/mail/sendgrid.js b/src/services/mail/sendgrid.js
index 80b4ac00add710a02be448c22ee8596fc8f2b109..9156087925a6877cc9676709fdb13ae368a01304 100644
--- a/src/services/mail/sendgrid.js
+++ b/src/services/mail/sendgrid.js
@@ -12,11 +12,14 @@ class SendgridMailer extends Mail {
 	async sendTo(options) {
 		const renderer = this.renderer
 
-		const data = Object.assign({
-			meta: {
-				subject: options.subject,
+		const data = Object.assign(
+			{
+				meta: {
+					subject: options.subject,
+				},
 			},
-		}, options.data)
+			options.data,
+		)
 
 		const value = await renderer.render(options.htmlView, data)
 
diff --git a/src/services/mail/smtp.js b/src/services/mail/smtp.js
index c05b1120e2bda295dcc14f2b490ed50193b7aedc..f320fa36d1055ea6458e0bbd54b9a88b9a278a5b 100644
--- a/src/services/mail/smtp.js
+++ b/src/services/mail/smtp.js
@@ -21,25 +21,34 @@ class SmtpMailer extends Mail {
 	async sendTo(options) {
 		const renderer = this.renderer
 
-		const data = Object.assign({
-			meta: {
-				subject: options.subject,
+		const data = Object.assign(
+			{
+				meta: {
+					subject: options.subject,
+				},
 			},
-		}, options.data)
+			options.data,
+		)
 
 		const value = await renderer.render(options.htmlView, data)
 
 		if (Array.isArray(options.to)) {
-			return Promise.all(options.to.map(to => this.transport.sendMail({
-				to,
-				from: options.from,
-				subject: options.subject,
-				replyTo: options.replyto,
-				text: value,
-				html: value,
-				cc: options.cc,
-				bcc: options.bcc,
-			}).catch(e => e)))
+			return Promise.all(
+				options.to.map(to =>
+					this.transport
+						.sendMail({
+							to,
+							from: options.from,
+							subject: options.subject,
+							replyTo: options.replyto,
+							text: value,
+							html: value,
+							cc: options.cc,
+							bcc: options.bcc,
+						})
+						.catch(e => e),
+				),
+			)
 		}
 
 		const message = {
diff --git a/src/services/queue/amqp.js b/src/services/queue/amqp.js
index f543de9946916b743584277ad2e473fdc4c47329..5df4b10c9da6e71a784e1cad69d3e62acebfa83a 100644
--- a/src/services/queue/amqp.js
+++ b/src/services/queue/amqp.js
@@ -26,31 +26,41 @@ class AmqpQueue extends Queue {
 		this.dlq = 'jetsam.jobs.dlq'
 		this._handlers = {}
 
-		this._init.then(q => {
-			debug('Established connection to AMQP queue')
-			this._amqp = q
-		}).catch(e => {
-			debug('Failed to connect to AMQP queue')
-			console.error(e)
-		})
+		this._init
+			.then(q => {
+				debug('Established connection to AMQP queue')
+				this._amqp = q
+			})
+			.catch(e => {
+				debug('Failed to connect to AMQP queue')
+				console.error(e)
+			})
 	}
 
 	bind(jobname, handler) {
 		this._handlers[jobname] = handler
 	}
 	dispatch(jobname, payload, attempt = 0) {
-		return threadContext.profile('queue.dispatch', jobname, () => this.dispatchAfter(jobname, payload, 0, attempt))
+		return threadContext.profile('queue.dispatch', jobname, () =>
+			this.dispatchAfter(jobname, payload, 0, attempt),
+		)
 	}
 	dispatchAfter(jobname, payload, delay, attempt = 0) {
 		return this._initialise().then(() => {
-			debug(`Processing job ${ jobname } via AMQP connection`)
-
-			this._channel.publish(this._exchange, 'jobs', Buffer.from(JSON.stringify({
-				type: jobname,
-				payload,
-				delay,
-				attempt,
-			})))
+			debug(`Processing job ${jobname} via AMQP connection`)
+
+			this._channel.publish(
+				this._exchange,
+				'jobs',
+				Buffer.from(
+					JSON.stringify({
+						type: jobname,
+						payload,
+						delay,
+						attempt,
+					}),
+				),
+			)
 		})
 	}
 
@@ -67,7 +77,7 @@ class AmqpQueue extends Queue {
 						debug('Starting Trace')
 						threadContext.getTransaction({
 							op: 'queue.job',
-							name: 'AMQP Queue Handler'
+							name: 'AMQP Queue Handler',
 						})
 						await this._handleMessage(object)
 					} finally {
@@ -78,9 +88,10 @@ class AmqpQueue extends Queue {
 			})
 		})
 
-		return () => this._init.then(q => {
-			q.close()
-		})
+		return () =>
+			this._init.then(q => {
+				q.close()
+			})
 	}
 
 	async _handleMessage(object) {
@@ -95,40 +106,46 @@ class AmqpQueue extends Queue {
 		t.setData('job.attempt', attempt)
 		t.setData('job.body', body)
 
-
 		if (!this._handlers.hasOwnProperty(type)) {
 			t.setName(`[Q] Unknown`)
-			debug(`No handler for type ${ type }, discarding message`)
+			debug(`No handler for type ${type}, discarding message`)
 			await this._channel.ack(object)
 			return
 		}
 
-		t.setName(`[Q] ${ type }`)
+		t.setName(`[Q] ${type}`)
 
-		debug(`Processing message ${ type }`)
+		debug(`Processing message ${type}`)
 		try {
 			const ctx = await ServiceProvider.detached()
 			const handler = this._handlers[type]
 			t.description = handler.name
-			await threadContext.profile('job.handler', undefined, () => handler(body, ctx))
+			await threadContext.profile('job.handler', undefined, () =>
+				handler(body, ctx),
+			)
 			await this._channel.ack(object)
-			debug(`Processed message ${ type }`)
+			debug(`Processed message ${type}`)
 		} catch (e) {
 			Sentry.withScope(scope => {
 				scope.setContext('job', { type, body, delay, attempt })
 				Sentry.captureException(e)
 			})
-			debug(`Failed message ${ type }`)
+			debug(`Failed message ${type}`)
 			debug(e)
-			if (attempt < 5) {
+			if (attempt < this.conf.retries ?? 5) {
 				const next = attempt + 1
-				debug(`Re-queue message ${ type } #${ next }`)
-				await this._channel.sendToQueue(this.dlq, Buffer.from(JSON.stringify({
-					type,
-					payload: body,
-					delay,
-					attempt: next,
-				})))
+				debug(`Re-queue message ${type} #${next}`)
+				await this._channel.sendToQueue(
+					this.dlq,
+					Buffer.from(
+						JSON.stringify({
+							type,
+							payload: body,
+							delay,
+							attempt: next,
+						}),
+					),
+				)
 				// await this.dispatchAfter(type, body, next * 500, next)
 			} else {
 				debug('Discarding message')
@@ -152,7 +169,7 @@ class AmqpQueue extends Queue {
 			await this._channel.assertQueue(this.dlq, {
 				deadLetterExchange: this._exchange,
 				deadLetterRoutingKey: 'jobs',
-				messageTtl: 500
+				messageTtl: 500,
 			})
 		}
 	}
diff --git a/src/services/queue/async.js b/src/services/queue/async.js
index 98186d394710e80547f1635ca5e0ff0b96a6ef4c..8aac842ed9724d0e7e3f8c6d805fe084eba5c2ca 100644
--- a/src/services/queue/async.js
+++ b/src/services/queue/async.js
@@ -4,7 +4,7 @@ const debug = require('debug')('server:services:queue')
 
 class AsyncQueue extends Queue {
 	constructor() {
-		super();
+		super()
 		this._emitter = new EventEmitter()
 		this._buffer = []
 		this._active = false
@@ -18,19 +18,19 @@ class AsyncQueue extends Queue {
 		if (this._active) {
 			this.dispatchAfter(jobname, payload, 0)
 		} else {
-			debug(`Adding job ${ jobname } to buffer, no queue listener present`)
+			debug(`Adding job ${jobname} to buffer, no queue listener present`)
 			this._buffer.push({ name: jobname, payload })
 		}
 	}
 
 	dispatchAfter(jobname, payload, delay) {
 		if (this._active) {
-			debug(`Processing job ${ jobname } asynchronously`)
+			debug(`Processing job ${jobname} asynchronously`)
 			setTimeout(() => {
 				this._emitter.emit(jobname, payload)
 			}, delay)
 		} else {
-			debug(`Adding job ${ jobname } to buffer, no queue listener present`)
+			debug(`Adding job ${jobname} to buffer, no queue listener present`)
 			this._buffer.push({ name: jobname, payload, delay })
 		}
 	}
diff --git a/src/services/queue/interface.js b/src/services/queue/interface.js
index 909485a877109960873a66e67d381acf8d94d9b1..a8e3c7ab531d514138ac53dfa5ce4b06179cba61 100644
--- a/src/services/queue/interface.js
+++ b/src/services/queue/interface.js
@@ -1,8 +1,16 @@
 const { notImplemented } = require('services/utils')
 
 module.exports = class Queue {
-	bind(jobname, handler) { notImplemented('Queue', 'bind') }
-	dispatch(jobname, payload) { notImplemented('Queue', 'dispatch') }
-	dispatchAfter(jobname, payload, delay) { notImplemented('Queue', 'dispatchAfter') }
-	listen() { notImplemented('Queue', 'listen') }
+	bind(jobname, handler) {
+		notImplemented('Queue', 'bind')
+	}
+	dispatch(jobname, payload) {
+		notImplemented('Queue', 'dispatch')
+	}
+	dispatchAfter(jobname, payload, delay) {
+		notImplemented('Queue', 'dispatchAfter')
+	}
+	listen() {
+		notImplemented('Queue', 'listen')
+	}
 }
diff --git a/src/services/totp/vault.js b/src/services/totp/vault.js
index eb2fac397f3d1391df100dd2f0b9ad8ac6869fcd..3baaaf1bfeba5bb653f73665627f33adc358c39c 100644
--- a/src/services/totp/vault.js
+++ b/src/services/totp/vault.js
@@ -2,7 +2,12 @@ const TotpProvider = require('./interface')
 const { config } = require('bootstrap')
 const { URL } = require('url')
 const threadContext = require('core/injection/ThreadContext')
-const { VaultClient, VaultSimpleAuth, VaultKVStore, VaultTOTPStore } = require('@commander-lol/vault-client')
+const {
+	VaultClient,
+	VaultSimpleAuth,
+	VaultKVStore,
+	VaultTOTPStore,
+} = require('@commander-lol/vault-client')
 
 class VaultTotpProvider extends TotpProvider {
 	constructor() {
@@ -17,7 +22,7 @@ class VaultTotpProvider extends TotpProvider {
 			options: {
 				auth: {
 					path: config('vault.auth_path'),
-					credentials: config('vault.credentials')
+					credentials: config('vault.credentials'),
 				},
 				kv: {
 					path: config('vault.kv_path'),
@@ -38,7 +43,11 @@ class VaultTotpProvider extends TotpProvider {
 			const email = user.email
 
 			try {
-				const { barcode, url } = await this.client.stores.totp.createProvider(id, 'Jetsam', email)
+				const { barcode, url } = await this.client.stores.totp.createProvider(
+					id,
+					'Jetsam',
+					email,
+				)
 				const parsed = new URL(url)
 				const secret = parsed.searchParams.get('secret')
 
@@ -47,7 +56,7 @@ class VaultTotpProvider extends TotpProvider {
 					url,
 					secret,
 				}
-			} catch(e) {
+			} catch (e) {
 				console.error(e)
 				return null
 			}
@@ -77,7 +86,9 @@ class VaultTotpProvider extends TotpProvider {
 		const hashes = await Promise.all(codes.map(c => crypto.hash(c)))
 
 		try {
-			await this.client.stores.kv.write(`totp_recovery/${ userid }`, { codes: hashes })
+			await this.client.stores.kv.write(`totp_recovery/${userid}`, {
+				codes: hashes,
+			})
 		} catch (e) {
 			console.log(e)
 			return null
@@ -90,7 +101,7 @@ class VaultTotpProvider extends TotpProvider {
 		let data = null
 
 		try {
-			;({ data } = await this.client.stores.kv.read(`totp_recovery/${ userid }`))
+			;({ data } = await this.client.stores.kv.read(`totp_recovery/${userid}`))
 		} catch (e) {
 			console.log(e)
 		}
@@ -108,7 +119,9 @@ class VaultTotpProvider extends TotpProvider {
 
 			if (found != null) {
 				const newCodes = codes.filter(c => c !== found)
-				await this.client.stores.kv.write(`totp_recovery/${ userid }`, { codes: newCodes })
+				await this.client.stores.kv.write(`totp_recovery/${userid}`, {
+					codes: newCodes,
+				})
 				return true
 			}
 		}
@@ -117,4 +130,4 @@ class VaultTotpProvider extends TotpProvider {
 	}
 }
 
-module.exports = new VaultTotpProvider()
\ No newline at end of file
+module.exports = new VaultTotpProvider()
diff --git a/src/services/utils.js b/src/services/utils.js
index 095c78dec410d754f7577d75df19d67a95bbb53f..6fec15093d262b9e423c2cb373c9c029e16e99f0 100644
--- a/src/services/utils.js
+++ b/src/services/utils.js
@@ -1,23 +1,33 @@
 const { env } = require('bootstrap')
 const debug = require('debug')('server:services')
 
-exports.loadEnvService = function loadEnvService(serviceName, envName, fallbackName = null) {
+exports.loadEnvService = function loadEnvService(
+	serviceName,
+	envName,
+	fallbackName = null,
+) {
 	const driverName = env(envName, fallbackName)
 	if (driverName != null) {
-		const service = require(`./${ serviceName }/${ driverName }`)
-		const interfaze = require(`./${ serviceName }/interface`)
+		const service = require(`./${serviceName}/${driverName}`)
+		const interfaze = require(`./${serviceName}/interface`)
 
-		debug('Loading service interface %s for service type %s', driverName, serviceName)
+		debug(
+			'Loading service interface %s for service type %s',
+			driverName,
+			serviceName,
+		)
 
 		if (service instanceof interfaze) {
 			return service
 		}
-		throw new Error(`Bad service implementation; expected ${ serviceName }/${ driverName } to implement interface, but did not`)
+		throw new Error(
+			`Bad service implementation; expected ${serviceName}/${driverName} to implement interface, but did not`,
+		)
 	} else {
 		return null
 	}
 }
 
 exports.notImplemented = function throwNotImplemented(clazz, methodName) {
-	throw new Error(`${ clazz } method not implemented: ${ methodName }`)
+	throw new Error(`${clazz} method not implemented: ${methodName}`)
 }
diff --git a/src/vendor/koa-handlebars.js b/src/vendor/koa-handlebars.js
index 89020322c1632288039f1cfa89480ae1c799c1b4..363b9293e19b13afa5c58e348759198cb7c971f1 100644
--- a/src/vendor/koa-handlebars.js
+++ b/src/vendor/koa-handlebars.js
@@ -4,23 +4,23 @@ const Handlebars = require('handlebars')
 
 function createPathsFromContext(context = '', filename) {
 	const { name } = path.parse(filename)
-	return [
-		path.join(context, filename),
-		path.join(context, name),
-		name,
-	]
+	return [path.join(context, filename), path.join(context, name), name]
 }
 
 function loadPartials(fsinst, root, ext, debug) {
 	const tree = fsinst.inspectTree(root)
 
 	if (!tree || (tree.type !== 'dir' && tree.type !== 'file')) {
-		debug('[koa-handlebars] Partials directory does not exist, skipping partials')
+		debug(
+			'[koa-handlebars] Partials directory does not exist, skipping partials',
+		)
 		return []
 	}
 
 	if (tree.type !== 'dir') {
-		throw new TypeError(`[koa-handlebars] Partials directory path must point to a directory, found ${ tree.type }`)
+		throw new TypeError(
+			`[koa-handlebars] Partials directory path must point to a directory, found ${tree.type}`,
+		)
 	}
 
 	const processing = tree.children
@@ -31,15 +31,20 @@ function loadPartials(fsinst, root, ext, debug) {
 
 		if (current.type === 'dir') {
 			for (const child of current.children) {
-				child.context = current.context ? path.join(current.context, current.name) : current.name
+				child.context = current.context
+					? path.join(current.context, current.name)
+					: current.name
 				processing.push(child)
 			}
 		} else if (current.type === 'file') {
 			if (path.extname(current.name) === ext) {
-				const [pathWithExt, pathWithoutExt] = createPathsFromContext(current.context, current.name)
+				const [pathWithExt, pathWithoutExt] = createPathsFromContext(
+					current.context,
+					current.name,
+				)
 				const content = fsinst.read(fsinst.path(root, pathWithExt), 'utf8')
 
-				debug(`[koa-mustache] Loading partial ${ pathWithExt }`)
+				debug(`[koa-mustache] Loading partial ${pathWithExt}`)
 
 				partials.push({ name: pathWithoutExt, content })
 			}
@@ -50,7 +55,6 @@ function loadPartials(fsinst, root, ext, debug) {
 }
 
 function loadHelpers(fsinst, root, debug) {
-
 	const tree = fsinst.inspectTree(root)
 
 	if (!tree || (tree.type !== 'dir' && tree.type !== 'file')) {
@@ -59,29 +63,38 @@ function loadHelpers(fsinst, root, debug) {
 	}
 
 	if (tree.type !== 'dir') {
-		throw new TypeError(`[koa-handlebars] Helpers directory path must point to a directory, found ${ tree.type }`)
+		throw new TypeError(
+			`[koa-handlebars] Helpers directory path must point to a directory, found ${tree.type}`,
+		)
 	}
 
 	const processing = tree.children
 	const helpers = []
 
-	processing.forEach(p => p.context = fsinst.path(root))
+	processing.forEach(p => (p.context = fsinst.path(root)))
 
 	while (processing.length > 0) {
 		const current = processing.shift()
 
 		if (current.type === 'dir') {
 			for (const child of current.children) {
-				child.context = current.context ? path.join(current.context, current.name) : current.name
+				child.context = current.context
+					? path.join(current.context, current.name)
+					: current.name
 				processing.push(child)
 			}
 		} else if (current.type === 'file') {
-			const [pathWithExt, _, name ] = createPathsFromContext(current.context, current.name)
+			const [pathWithExt, _, name] = createPathsFromContext(
+				current.context,
+				current.name,
+			)
 			try {
 				const helperMap = require(pathWithExt)
 				helpers.push({ name, content: helperMap })
-				debug(`[koa-mustache] Loading helper ${ pathWithExt }`)
-			} catch(e) { console.warn(e.message) }
+				debug(`[koa-mustache] Loading helper ${pathWithExt}`)
+			} catch (e) {
+				console.warn(e.message)
+			}
 		}
 	}
 
@@ -91,7 +104,11 @@ function loadHelpers(fsinst, root, debug) {
 module.exports = function createRenderMiddleware(root, opts = {}) {
 	const instance = module.exports.createRenderer(root, opts)
 	return async function handlebarsMiddleware(ctx, next) {
-		ctx.render = async function renderTemplateData(template, data = {}, opts = {}) {
+		ctx.render = async function renderTemplateData(
+			template,
+			data = {},
+			opts = {},
+		) {
 			const content = await instance.render(template, data, opts)
 			if (content == null) {
 				this.status = 404
@@ -123,8 +140,8 @@ class Renderer {
 		if (!template) {
 			let fileData = await viewDir.inspectAsync(path)
 			if (fileData == null) {
-				path = `${ path }${ this._ext }`
-				fileData = await viewDir.inspectAsync(`${ view }${ this._ext }`)
+				path = `${path}${this._ext}`
+				fileData = await viewDir.inspectAsync(`${view}${this._ext}`)
 				if (fileData == null) {
 					return null
 				}
@@ -151,12 +168,14 @@ class Renderer {
 	}
 }
 
-module.exports.createRenderer = function(root, opts = {}) {
-	const useCache = opts.hasOwnProperty('cache') ? opts.cache : process.env.NODE_ENV === 'production'
+module.exports.createRenderer = function (root, opts = {}) {
+	const useCache = opts.hasOwnProperty('cache')
+		? opts.cache
+		: process.env.NODE_ENV === 'production'
 	const extension = opts.hasOwnProperty('extension') ? opts.extension : '.hbs'
 	const partials = opts.hasOwnProperty('partials') ? opts.partials : 'partials'
 	const helpers = opts.hasOwnProperty('helpers') ? opts.helpers : 'helpers'
-	const extend = opts.hasOwnProperty('extend') ? opts.extend : (i) => i
+	const extend = opts.hasOwnProperty('extend') ? opts.extend : i => i
 	const debug = opts.debug || (() => {})
 	const fsinst = opts.hasOwnProperty('jetpack') ? opts.jetpack : fs
 
@@ -165,13 +184,17 @@ module.exports.createRenderer = function(root, opts = {}) {
 
 	const helperContent = loadHelpers(viewDir, helpers, debug)
 	helperContent.forEach(({ name, content }) =>
-		typeof content === 'function' ?
-			instance.registerHelper(name, content) :
-			Object.entries(content).forEach(([helper, fn]) => instance.registerHelper(helper, fn))
+		typeof content === 'function'
+			? instance.registerHelper(name, content)
+			: Object.entries(content).forEach(([helper, fn]) =>
+					instance.registerHelper(helper, fn),
+			  ),
 	)
 
 	const partialContent = loadPartials(viewDir, partials, extension, debug)
-	partialContent.forEach(({ name, content }) => instance.registerPartial(name, content))
+	partialContent.forEach(({ name, content }) =>
+		instance.registerPartial(name, content),
+	)
 
 	extend(instance)
 
diff --git a/src/vendor/sentry.js b/src/vendor/sentry.js
index 50a6cacea898596ca4698ddabc2a3a082678b1b6..84f12e23fa758283590eeef046edc3ffeb3107fe 100644
--- a/src/vendor/sentry.js
+++ b/src/vendor/sentry.js
@@ -1,24 +1,22 @@
 const Sentry = require('@sentry/node')
 const Tracing = require('@sentry/tracing')
 
-const blockedPaths = new Set([
-	'/api/.secure/jwks',
-	'/api',
-])
+const blockedPaths = new Set(['/api/.secure/jwks', '/api'])
 
-exports.configure = function() {
+exports.configure = function () {
 	const pkg = require('../../package.json')
 	const { config } = require('bootstrap')
 
 	Sentry.init({
 		dsn: config('sentry.dsn'),
-		integrations: integrations => integrations.filter(itg => itg.name !== 'Console'),
+		integrations: integrations =>
+			integrations.filter(itg => itg.name !== 'Console'),
 		environment: config('app.env'),
-		release: `${ pkg.name }@${ pkg.version }`,
-		tracesSampler: (ctx) => {
+		release: `${pkg.name}@${pkg.version}`,
+		tracesSampler: ctx => {
 			if (
-				ctx.transactionContext?.op === 'http.request'
-				&& blockedPaths.has(ctx.transactionContext?.tags?.['http.path'])
+				ctx.transactionContext?.op === 'http.request' &&
+				blockedPaths.has(ctx.transactionContext?.tags?.['http.path'])
 			) {
 				return 0
 			}
@@ -26,5 +24,4 @@ exports.configure = function() {
 			return config('sentry.samples')
 		},
 	})
-
-}
\ No newline at end of file
+}
diff --git a/worker.js b/worker.js
index 98c2c564614cfc2379675de5f5535837b944bcb3..35c30f863b8876e6295386b88aa29f51765f9063 100644
--- a/worker.js
+++ b/worker.js
@@ -23,7 +23,9 @@ async function main() {
 	close = await queue.listen()
 	await new Promise(async r => {
 		debug('Starting worker spin loop')
-		while (!done) { await new Promise(rr => setTimeout(rr, 10)) }
+		while (!done) {
+			await new Promise(rr => setTimeout(rr, 10))
+		}
 		debug('Ending worker spin loop')
 		r(true)
 	})
@@ -35,24 +37,18 @@ function bindSentry() {
 
 	debug('Binding sentry to process level errors')
 
-	process.on("error", (err) => {
-		Sentry.captureException(err);
-	});
-}
-
-main()
-	.catch(e => {
-		console.error(e)
-		Sentry.captureException(e);
-		process.exit(1)
+	process.on('error', err => {
+		Sentry.captureException(err)
 	})
+}
 
+main().catch(e => {
+	console.error(e)
+	Sentry.captureException(e)
+	process.exit(1)
+})
 
-const cleanupsigs = [
-	'SIGINT',
-	'SIGTERM',
-	'SIGUSR2',
-]
+const cleanupsigs = ['SIGINT', 'SIGTERM', 'SIGUSR2']
 
 cleanupsigs.forEach(signal => {
 	process.on(signal, () => {