diff --git a/misuzu.php b/misuzu.php
index 73b4532b..d6e42fa6 100644
--- a/misuzu.php
+++ b/misuzu.php
@@ -1,8 +1,6 @@
 <?php
 namespace Misuzu;
 
-use Index\Db\DbBackends;
-use Index\Config\Db\DbConfig;
 use Index\Config\Fs\FsConfig;
 
 define('MSZ_STARTUP', microtime(true));
@@ -37,14 +35,4 @@ if($env->hasValues('sentry:dsn'))
         });
     })($env->scopeTo('sentry'));
 
-$db = DbBackends::create($env->getString('database:dsn', 'null:'));
-$db->execute(<<<SQL
-    SET SESSION time_zone = '+00:00',
-        sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
-SQL);
-
-$cfg = new DbConfig($db, 'msz_config');
-
-Mailer::init($cfg->scopeTo('mail'));
-
-$msz = new MisuzuContext($db, $cfg, $env);
+$msz = new MisuzuContext($env);
diff --git a/public-legacy/auth/password.php b/public-legacy/auth/password.php
index 2c3936ff..a59b8f7e 100644
--- a/public-legacy/auth/password.php
+++ b/public-legacy/auth/password.php
@@ -114,6 +114,8 @@ while($canResetPassword) {
         } catch(RuntimeException $ex) {
             $tokenInfo = $msz->authCtx->recoveryTokens->createToken($forgotUser, $ipAddress);
 
+            $msz->initMailer();
+
             $recoveryMessage = Mailer::template('password-recovery', [
                 'username' => $forgotUser->name,
                 'token' => $tokenInfo->code,
diff --git a/public-legacy/forum/posting.php b/public-legacy/forum/posting.php
index 58f24a73..531bb8cc 100644
--- a/public-legacy/forum/posting.php
+++ b/public-legacy/forum/posting.php
@@ -153,7 +153,7 @@ if(!empty($_POST)) {
         $isEditingTopic = empty($topicInfo) || ($mode === 'edit' && $originalPostInfo->id == $postInfo->id);
 
         if($mode === 'create') {
-            $postTimeout = $cfg->getInteger('forum.posting.timeout', 5);
+            $postTimeout = $msz->config->getInteger('forum.posting.timeout', 5);
             if($postTimeout > 0) {
                 $postTimeoutThreshold = new CarbonImmutable(sprintf('-%d seconds', $postTimeout));
                 $lastPostCreatedAt = $msz->forumCtx->posts->getUserLastPostCreatedAt($currentUser);
@@ -173,7 +173,7 @@ if(!empty($_POST)) {
             $originalTopicType = $topicInfo?->typeString ?? 'discussion'; // @phpstan-ignore-line: this also
             $topicTypeChanged = $topicType !== null && $topicType !== $originalTopicType;
 
-            $topicTitleLengths = $cfg->getValues([
+            $topicTitleLengths = $msz->config->getValues([
                 ['forum.topic.minLength:i', 3],
                 ['forum.topic.maxLength:i', 100],
             ]);
@@ -191,7 +191,7 @@ if(!empty($_POST)) {
             }
         }
 
-        $postTextLengths = $cfg->getValues([
+        $postTextLengths = $msz->config->getValues([
             ['forum.post.minLength:i', 1],
             ['forum.post.maxLength:i', 60000],
         ]);
diff --git a/public-legacy/manage/users/user.php b/public-legacy/manage/users/user.php
index 3e09b65d..992d9de2 100644
--- a/public-legacy/manage/users/user.php
+++ b/public-legacy/manage/users/user.php
@@ -81,6 +81,7 @@ if(CSRF::validateRequest() && $canEdit) {
         } elseif(!is_string($_POST['send_test_email']) || $_POST['send_test_email'] !== 'yes_send_it') {
             $notices[] = 'Invalid request thing shut the fuck up.';
         } else {
+            $msz->initMailer();
             $testMail = Mailer::sendMessage(
                 [$userInfo->emailAddress => $userInfo->name],
                 'Flashii Test E-mail',
diff --git a/public-legacy/settings/data.php b/public-legacy/settings/data.php
index 50f7efc0..f338ed23 100644
--- a/public-legacy/settings/data.php
+++ b/public-legacy/settings/data.php
@@ -11,7 +11,7 @@ if(!isset($msz) || !($msz instanceof \Misuzu\MisuzuContext))
 if(!$msz->authInfo->loggedIn)
     Template::throwError(401);
 
-$dbConn = $msz->dbConn;
+$dbConn = $msz->dbCtx->conn;
 
 /** @param string[] $fieldInfos */
 function db_to_zip(
diff --git a/src/DatabaseContext.php b/src/DatabaseContext.php
new file mode 100644
index 00000000..805343bb
--- /dev/null
+++ b/src/DatabaseContext.php
@@ -0,0 +1,46 @@
+<?php
+namespace Misuzu;
+
+use Index\Config\Config;
+use Index\Db\{DbBackends,DbConnection};
+use Index\Db\Migration\{DbMigrationManager,DbMigrationRepo,FsDbMigrationRepo};
+use Index\Http\Routing\{HttpMiddleware,RouteHandler,RouteHandlerCommon};
+
+class DatabaseContext implements RouteHandler {
+    use RouteHandlerCommon;
+
+    public private(set) DbConnection $conn;
+
+    public function __construct(Config $config) {
+        $this->conn = DbBackends::create($config->getString('dsn', 'null:'));
+        $this->conn->execute(<<<SQL
+            SET SESSION
+                time_zone = '+00:00',
+                sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
+        SQL);
+    }
+
+    public function getQueryCount(): int {
+        $result = $this->conn->query("SHOW SESSION STATUS LIKE 'Questions'");
+        return $result->next() ? $result->getInteger(1) : 0;
+    }
+
+    public function createMigrationManager(): DbMigrationManager {
+        return new DbMigrationManager($this->conn, sprintf('msz_%s', DbMigrationManager::DEFAULT_TABLE));
+    }
+
+    public function createMigrationRepo(): DbMigrationRepo {
+        return new FsDbMigrationRepo(MSZ_MIGRATIONS);
+    }
+
+    public function getMigrateLockPath(): string {
+        return sys_get_temp_dir() . '/misuzu-migration-' . hash('sha256', MSZ_ROOT) . '.lock';
+    }
+
+    /** @return void|int */
+    #[HttpMiddleware('/')]
+    public function middleware() {
+        if(is_file($this->getMigrateLockPath()))
+            return 503;
+    }
+}
diff --git a/src/MisuzuContext.php b/src/MisuzuContext.php
index 3389f773..b568d9ff 100644
--- a/src/MisuzuContext.php
+++ b/src/MisuzuContext.php
@@ -3,6 +3,7 @@ namespace Misuzu;
 
 use Index\Dependencies;
 use Index\Config\Config;
+use Index\Config\Db\DbConfig;
 use Index\Db\DbConnection;
 use Index\Db\Migration\{DbMigrationManager,DbMigrationRepo,FsDbMigrationRepo};
 use Index\Http\HttpRequest;
@@ -16,6 +17,7 @@ use RPCii\Server\{HttpRpcServer,RpcServer};
 class MisuzuContext {
     public private(set) Dependencies $deps;
 
+    public private(set) Config $config;
     public private(set) TplEnvironment $templating;
 
     public private(set) AuditLog\AuditLogData $auditLog;
@@ -26,6 +28,7 @@ class MisuzuContext {
     public private(set) News\NewsData $news;
     public private(set) Comments\CommentsData $comments;
 
+    public private(set) DatabaseContext $dbCtx;
     public private(set) Apps\AppsContext $appsCtx;
     public private(set) Auth\AuthContext $authCtx;
     public private(set) Forum\ForumContext $forumCtx;
@@ -43,28 +46,29 @@ class MisuzuContext {
     public private(set) UrlRegistry $urls;
 
     public function __construct(
-        public private(set) DbConnection $dbConn,
-        public private(set) Config $config,
         public private(set) Config $env
     ) {
         $this->deps = new Dependencies;
         $this->deps->register($this->deps);
-        $this->deps->register($this->dbConn);
-        $this->deps->register($this->config);
         $this->deps->register($this->env);
 
-        $this->deps->register($this->perms = new Perms\PermissionsData($dbConn));
+        $this->deps->register($this->dbCtx = new DatabaseContext($this->env->scopeTo('database')));
+        $this->deps->register($this->dbCtx->conn);
+
+        $this->deps->register($this->config = $this->deps->constructLazy(DbConfig::class, tableName: 'msz_config'));
+
+        $this->deps->register($this->perms = $this->deps->constructLazy(Perms\PermissionsData::class));
         $this->deps->register($this->authInfo = $this->deps->constructLazy(Auth\AuthInfo::class));
-        $this->deps->register($this->siteInfo = $this->deps->constructLazy(SiteInfo::class, config: $config->scopeTo('site')));
+        $this->deps->register($this->siteInfo = $this->deps->constructLazy(SiteInfo::class, config: $this->config->scopeTo('site')));
 
         $this->deps->register($this->appsCtx = $this->deps->constructLazy(Apps\AppsContext::class));
-        $this->deps->register($this->authCtx = $this->deps->constructLazy(Auth\AuthContext::class, config: $config->scopeTo('auth')));
+        $this->deps->register($this->authCtx = $this->deps->constructLazy(Auth\AuthContext::class, config: $this->config->scopeTo('auth')));
         $this->deps->register($this->forumCtx = $this->deps->constructLazy(Forum\ForumContext::class));
         $this->deps->register($this->messagesCtx = $this->deps->constructLazy(Messages\MessagesContext::class));
-        $this->deps->register($this->oauth2Ctx = $this->deps->constructLazy(OAuth2\OAuth2Context::class, config: $config->scopeTo('oauth2')));
+        $this->deps->register($this->oauth2Ctx = $this->deps->constructLazy(OAuth2\OAuth2Context::class, config: $this->config->scopeTo('oauth2')));
         $this->deps->register($this->profileCtx = $this->deps->constructLazy(Profile\ProfileContext::class));
         $this->deps->register($this->usersCtx = $this->deps->constructLazy(Users\UsersContext::class));
-        $this->deps->register($this->redirectsCtx = $this->deps->constructLazy(Redirects\RedirectsContext::class, config: $config->scopeTo('redirects')));
+        $this->deps->register($this->redirectsCtx = $this->deps->constructLazy(Redirects\RedirectsContext::class, config: $this->config->scopeTo('redirects')));
 
         $this->deps->register($this->auditLog = $this->deps->constructLazy(AuditLog\AuditLogData::class));
         $this->deps->register($this->changelog = $this->deps->constructLazy(Changelog\ChangelogData::class));
@@ -74,17 +78,8 @@ class MisuzuContext {
         $this->deps->register($this->news = $this->deps->constructLazy(News\NewsData::class));
     }
 
-    public function getDbQueryCount(): int {
-        $result = $this->dbConn->query('SHOW SESSION STATUS LIKE "Questions"');
-        return $result->next() ? $result->getInteger(1) : 0;
-    }
-
-    public function createMigrationManager(): DbMigrationManager {
-        return new DbMigrationManager($this->dbConn, 'msz_' . DbMigrationManager::DEFAULT_TABLE);
-    }
-
-    public function createMigrationRepo(): DbMigrationRepo {
-        return new FsDbMigrationRepo(MSZ_MIGRATIONS);
+    public function initMailer(): void {
+        Mailer::init($this->config->scopeTo('mail'));
     }
 
     /** @param mixed[] $params */
diff --git a/src/TemplatingExtension.php b/src/TemplatingExtension.php
index f4c91de5..d136773b 100644
--- a/src/TemplatingExtension.php
+++ b/src/TemplatingExtension.php
@@ -38,7 +38,7 @@ final class TemplatingExtension extends AbstractExtension {
             new TwigFunction('git_tag', GitInfo::tag(...)),
             new TwigFunction('git_branch', GitInfo::branch(...)),
             new TwigFunction('startup_time', fn(float $time = MSZ_STARTUP) => microtime(true) - $time),
-            new TwigFunction('sql_query_count', $this->ctx->getDbQueryCount(...)),
+            new TwigFunction('sql_query_count', $this->ctx->dbCtx->getQueryCount(...)),
             new TwigFunction('msz_header_menu', $this->getHeaderMenu(...)),
             new TwigFunction('msz_user_menu', $this->getUserMenu(...)),
             new TwigFunction('msz_manage_menu', $this->getManageMenu(...)),
diff --git a/src/Users/Assets/UserAvatarAsset.php b/src/Users/Assets/UserAvatarAsset.php
index d47cd816..993b57ae 100644
--- a/src/Users/Assets/UserAvatarAsset.php
+++ b/src/Users/Assets/UserAvatarAsset.php
@@ -17,15 +17,15 @@ class UserAvatarAsset extends UserImageAsset implements UserAssetScalableInterfa
     private const MAX_BYTES  = 1000000;
 
     public function getMaxWidth(): int {
-        global $cfg;
-        return $cfg->getInteger('avatar.max_res', self::MAX_RES);
+        global $msz;
+        return $msz->config->getInteger('avatar.max_res', self::MAX_RES);
     }
     public function getMaxHeight(): int {
         return $this->getMaxWidth();
     }
     public function getMaxBytes(): int {
-        global $cfg;
-        return $cfg->getInteger('avatar.max_size', self::MAX_BYTES);
+        global $msz;
+        return $msz->config->getInteger('avatar.max_size', self::MAX_BYTES);
     }
 
     public static function clampDimensions(int $dimensions): int {
diff --git a/src/Users/Assets/UserBackgroundAsset.php b/src/Users/Assets/UserBackgroundAsset.php
index 055e960a..432d3d39 100644
--- a/src/Users/Assets/UserBackgroundAsset.php
+++ b/src/Users/Assets/UserBackgroundAsset.php
@@ -12,16 +12,16 @@ class UserBackgroundAsset extends UserImageAsset {
     private const MAX_BYTES  = 1500000;
 
     public function getMaxWidth(): int {
-        global $cfg;
-        return $cfg->getInteger('background.max_width', self::MAX_WIDTH);
+        global $msz;
+        return $msz->config->getInteger('background.max_width', self::MAX_WIDTH);
     }
     public function getMaxHeight(): int {
-        global $cfg;
-        return $cfg->getInteger('background.max_height', self::MAX_HEIGHT);
+        global $msz;
+        return $msz->config->getInteger('background.max_height', self::MAX_HEIGHT);
     }
     public function getMaxBytes(): int {
-        global $cfg;
-        return $cfg->getInteger('background.max_size', self::MAX_BYTES);
+        global $msz;
+        return $msz->config->getInteger('background.max_size', self::MAX_BYTES);
     }
 
     public function getFileName(): string {
diff --git a/src/Users/UserNameHistoryInfo.php b/src/Users/UserNameHistoryInfo.php
index 4fea1c75..93f6fe58 100644
--- a/src/Users/UserNameHistoryInfo.php
+++ b/src/Users/UserNameHistoryInfo.php
@@ -14,8 +14,8 @@ class UserNameHistoryInfo {
         public private(set) int $createdTime
     ) {}
 
-    public static function fromResult(DbResult $result): UserPasswordInfo {
-        return new UserPasswordInfo(
+    public static function fromResult(DbResult $result): UserNameHistoryInfo {
+        return new UserNameHistoryInfo(
             id: $result->getString(0),
             userId: $result->getString(1),
             nameBefore: $result->getString(2),
diff --git a/tools/cron b/tools/cron
index 0d28187f..1b542358 100755
--- a/tools/cron
+++ b/tools/cron
@@ -147,7 +147,7 @@ msz_sched_task_func('Resync statistics counters.', true, function() use ($msz) {
     ];
 
     foreach($stats as $name => $query) {
-        $result = $msz->dbConn->query($query);
+        $result = $msz->dbCtx->conn->query($query);
         $msz->counters->set($name, $result->next() ? $result->getInteger(0) : 0);
     }
 });
@@ -166,7 +166,7 @@ msz_sched_task_func('Omega disliking comments...', true, function() use ($msz) {
     if(!is_array($commentIds))
         return;
 
-    $stmt = $msz->dbConn->prepare('REPLACE INTO msz_comments_votes (comment_id, user_id, comment_vote) SELECT ?, user_id, -1 FROM msz_users');
+    $stmt = $msz->dbCtx->conn->prepare('REPLACE INTO msz_comments_votes (comment_id, user_id, comment_vote) SELECT ?, user_id, -1 FROM msz_users');
     foreach($commentIds as $commentId) {
         $stmt->addParameter(1, $commentId);
         $stmt->execute();
@@ -178,9 +178,9 @@ msz_sched_task_func('Announcing random topics...', true, function() use ($msz) {
     if(!is_array($categoryIds))
         return;
 
-    $stmtRevert = $msz->dbConn->prepare('UPDATE msz_forum_topics SET topic_type = 0 WHERE forum_id = ? AND topic_type = 2');
-    $stmtRandom = $msz->dbConn->prepare('SELECT topic_id FROM msz_forum_topics WHERE forum_id = ? AND topic_deleted IS NULL ORDER BY RAND() LIMIT 1');
-    $stmtAnnounce = $msz->dbConn->prepare('UPDATE msz_forum_topics SET topic_type = 2 WHERE topic_id = ?');
+    $stmtRevert = $msz->dbCtx->conn->prepare('UPDATE msz_forum_topics SET topic_type = 0 WHERE forum_id = ? AND topic_type = 2');
+    $stmtRandom = $msz->dbCtx->conn->prepare('SELECT topic_id FROM msz_forum_topics WHERE forum_id = ? AND topic_deleted IS NULL ORDER BY RAND() LIMIT 1');
+    $stmtAnnounce = $msz->dbCtx->conn->prepare('UPDATE msz_forum_topics SET topic_type = 2 WHERE topic_id = ?');
     foreach($categoryIds as $categoryId) {
         $stmtRevert->addParameter(1, $categoryId);
         $stmtRevert->execute();
@@ -201,8 +201,8 @@ msz_sched_task_func('Changing category icons...', false, function() use ($msz) {
     if(!is_array($categoryIds))
         return;
 
-    $stmtIcon = $msz->dbConn->prepare('UPDATE msz_forum_categories SET forum_icon = COALESCE(?, forum_icon), forum_name = COALESCE(?, forum_name), forum_colour = ((ROUND(RAND() * 255) << 16) | (ROUND(RAND() * 255) << 8) | ROUND(RAND() * 255)) WHERE forum_id = ?');
-    $stmtUnlock = $msz->dbConn->prepare('UPDATE msz_forum_topics SET topic_locked = IF(?, NULL, COALESCE(topic_locked, NOW())) WHERE topic_id = ?');
+    $stmtIcon = $msz->dbCtx->conn->prepare('UPDATE msz_forum_categories SET forum_icon = COALESCE(?, forum_icon), forum_name = COALESCE(?, forum_name), forum_colour = ((ROUND(RAND() * 255) << 16) | (ROUND(RAND() * 255) << 8) | ROUND(RAND() * 255)) WHERE forum_id = ?');
+    $stmtUnlock = $msz->dbCtx->conn->prepare('UPDATE msz_forum_topics SET topic_locked = IF(?, NULL, COALESCE(topic_locked, NOW())) WHERE topic_id = ?');
 
     foreach($categoryIds as $categoryId) {
         $scoped = $msz->config->scopeTo(sprintf('forum.rngicon.fc%d', $categoryId));
@@ -247,7 +247,7 @@ foreach($schedTasks as $task) {
     try {
         switch($task->type) {
             case 'sql':
-                $affected = $msz->dbConn->execute($task->command);
+                $affected = $msz->dbCtx->conn->execute($task->command);
                 echo $affected . ' rows affected. ' . PHP_EOL;
                 break;
 
diff --git a/tools/migrate b/tools/migrate
index 29e1d2b9..73be4e08 100755
--- a/tools/migrate
+++ b/tools/migrate
@@ -9,13 +9,13 @@ try {
     chmod(MSZ_ROOT . '/.migrating', 0777);
 
     echo 'Creating migration manager...' . PHP_EOL;
-    $manager = $msz->createMigrationManager();
+    $manager = $msz->dbCtx->createMigrationManager();
 
     echo 'Preparing to run migrations...' . PHP_EOL;
     $manager->init();
 
     echo 'Creating migration repository...' . PHP_EOL;
-    $repo = $msz->createMigrationRepo();
+    $repo = $msz->dbCtx->createMigrationRepo();
 
     echo 'Running migrations...' . PHP_EOL;
     $completed = $manager->processMigrations($repo);
diff --git a/tools/new-migration b/tools/new-migration
index 8b0c47fb..b242e052 100755
--- a/tools/new-migration
+++ b/tools/new-migration
@@ -4,14 +4,14 @@ use Index\Db\Migration\FsDbMigrationRepo;
 
 require_once __DIR__ . '/../misuzu.php';
 
-$repo = $msz->createMigrationRepo();
+$repo = $msz->dbCtx->createMigrationRepo();
 if(!($repo instanceof FsDbMigrationRepo)) {
     echo 'Migration repository type does not support creation of templates.' . PHP_EOL;
     return;
 }
 
 $baseName = implode(' ', array_slice($argv, 1));
-$manager = $msz->createMigrationManager();
+$manager = $msz->dbCtx->createMigrationManager();
 
 try {
     $names = $manager->createNames($baseName);