diff --git a/assets/misuzu.css/news/feeds.css b/assets/misuzu.css/news/feeds.css
index bf0cda23..0ee8584f 100644
--- a/assets/misuzu.css/news/feeds.css
+++ b/assets/misuzu.css/news/feeds.css
@@ -1,6 +1,6 @@
 .news__feeds {
     display: grid;
-    grid-template-columns: 1fr 1fr;
+    grid-template-columns: 1fr;
     grid-gap: 2px;
     padding: 2px;
 }
diff --git a/composer.json b/composer.json
index fe0c0d04..ac5956ed 100644
--- a/composer.json
+++ b/composer.json
@@ -1,15 +1,13 @@
 {
     "require": {
-        "flashwave/index": "^0.2408.40014",
-        "flashwave/sasae": "^1.1",
-        "flashwave/syokuhou": "^1.2",
+        "flashwave/index": "^0.2410",
+        "flashwave/aiwass": "^1.1",
         "erusev/parsedown": "~1.6",
         "chillerlan/php-qrcode": "^4.3",
         "symfony/mailer": "^6.0",
         "matomo/device-detector": "^6.1",
         "sentry/sdk": "^4.0",
-        "nesbot/carbon": "^3.7",
-        "flashwave/aiwass": "^1.0"
+        "nesbot/carbon": "^3.7"
     },
     "autoload": {
         "classmap": [
diff --git a/composer.lock b/composer.lock
index a95687c0..7cb8ba62 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "c8edfa21e13dedc126a6351b7d367559",
+    "content-hash": "526f12235a073ac908e932627e1a16fa",
     "packages": [
         {
             "name": "carbonphp/carbon-doctrine-types",
@@ -418,15 +418,15 @@
         },
         {
             "name": "flashwave/aiwass",
-            "version": "v1.0.0",
+            "version": "v1.1.0",
             "source": {
                 "type": "git",
                 "url": "https://patchii.net/flashii/aiwass.git",
-                "reference": "de27da54b603f0fdc06dab89341908e73ea7a880"
+                "reference": "cf6653ed4676d549b543eeba2b9db517c1feb356"
             },
             "require": {
                 "ext-msgpack": ">=2.2",
-                "flashwave/index": "^0.2408.40014",
+                "flashwave/index": "^0.2410",
                 "php": ">=8.3"
             },
             "require-dev": {
@@ -453,27 +453,32 @@
             ],
             "description": "Shared HTTP RPC client/server library.",
             "homepage": "https://railgun.sh/aiwass",
-            "time": "2024-08-16T15:59:19+00:00"
+            "time": "2024-10-05T00:04:26+00:00"
         },
         {
             "name": "flashwave/index",
-            "version": "v0.2408.611934",
+            "version": "v0.2410.42339",
             "source": {
                 "type": "git",
                 "url": "https://patchii.net/flash/index.git",
-                "reference": "2217c8c3de8e4ec8d7f9ec0f37078dbc2183d14e"
+                "reference": "d3e4d0985a1189d15fb8ed9eb105830c9dc38c4d"
             },
             "require": {
                 "ext-mbstring": "*",
-                "php": ">=8.3"
+                "php": ">=8.3",
+                "twig/html-extra": "^3.13",
+                "twig/twig": "^3.14"
             },
             "require-dev": {
                 "phpstan/phpstan": "^1.11",
                 "phpunit/phpunit": "^11.2"
             },
             "suggest": {
-                "ext-mysqli": "Support for the Index\\Data\\MariaDB namespace (both mysqlnd and libmysql are supported).",
-                "ext-sqlite3": "Support for the Index\\Data\\SQLite namespace."
+                "ext-memcache": "Support for the Index\\Cache\\Memcached namespace (only if you can't use ext-memcached for some reason).",
+                "ext-memcached": "Support for the Index\\Cache\\Memcached namespace (you should use this instead of ext-memcache).",
+                "ext-mysqli": "Support for the Index\\Db\\MariaDb namespace (both mysqlnd and libmysql are supported).",
+                "ext-redis": "Support for the Index\\Cache\\Valkey namespace.",
+                "ext-sqlite3": "Support for the Index\\Db\\Sqlite namespace."
             },
             "type": "library",
             "autoload": {
@@ -495,85 +500,7 @@
             ],
             "description": "Composer package for the common library for my projects.",
             "homepage": "https://railgun.sh/index",
-            "time": "2024-09-30T17:34:51+00:00"
-        },
-        {
-            "name": "flashwave/sasae",
-            "version": "v1.1.1",
-            "source": {
-                "type": "git",
-                "url": "https://patchii.net/flash/sasae.git",
-                "reference": "897a28e56926ad465bf0daf587caf3d92a311383"
-            },
-            "require": {
-                "flashwave/index": "^0.2408.40014",
-                "php": ">=8.3",
-                "twig/html-extra": "^3.12",
-                "twig/twig": "^3.12"
-            },
-            "require-dev": {
-                "phpstan/phpstan": "^1.11",
-                "phpunit/phpunit": "^11.2"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Sasae\\": "src"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "bsd-3-clause-clear"
-            ],
-            "authors": [
-                {
-                    "name": "flashwave",
-                    "email": "packagist@flash.moe",
-                    "homepage": "https://flash.moe",
-                    "role": "mom"
-                }
-            ],
-            "description": "A wrapper for Twig with added common functionality.",
-            "homepage": "https://railgun.sh/sasae",
-            "time": "2024-09-01T20:38:47+00:00"
-        },
-        {
-            "name": "flashwave/syokuhou",
-            "version": "v1.2.0",
-            "source": {
-                "type": "git",
-                "url": "https://patchii.net/flash/syokuhou.git",
-                "reference": "129a46c0d917382f9bc195cce278be51984eb87d"
-            },
-            "require": {
-                "flashwave/index": "^0.2408.40014",
-                "php": ">=8.3"
-            },
-            "require-dev": {
-                "phpstan/phpstan": "^1.11",
-                "phpunit/phpunit": "^11.2"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Syokuhou\\": "src"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "bsd-3-clause-clear"
-            ],
-            "authors": [
-                {
-                    "name": "flashwave",
-                    "email": "packagist@flash.moe",
-                    "homepage": "https://flash.moe",
-                    "role": "mom"
-                }
-            ],
-            "description": "Configuration library for PHP.",
-            "homepage": "https://railgun.sh/syokuhou",
-            "time": "2024-08-04T01:07:23+00:00"
+            "time": "2024-10-04T23:39:32+00:00"
         },
         {
             "name": "guzzlehttp/psr7",
diff --git a/database/2023_01_07_023235_initial_structure_ndx.php b/database/2023_01_07_023235_initial_structure_ndx.php
index 72106797..38260614 100644
--- a/database/2023_01_07_023235_initial_structure_ndx.php
+++ b/database/2023_01_07_023235_initial_structure_ndx.php
@@ -1,11 +1,11 @@
 <?php
-use Index\Data\IDbConnection;
-use Index\Data\Migration\IDbMigration;
+use Index\Db\DbConnection;
+use Index\Db\Migration\DbMigration;
 
 // Switching to the Index migration system!!!!!!
 
-final class InitialStructureNdx_20230107_023235 implements IDbMigration {
-    public function migrate(IDbConnection $conn): void {
+final class InitialStructureNdx_20230107_023235 implements DbMigration {
+    public function migrate(DbConnection $conn): void {
         $hasMszTrack = false;
 
         // check if the old migrations table exists
diff --git a/database/2023_04_30_001226_create_topic_redirs_table.php b/database/2023_04_30_001226_create_topic_redirs_table.php
index adea1b95..a9ee5cba 100644
--- a/database/2023_04_30_001226_create_topic_redirs_table.php
+++ b/database/2023_04_30_001226_create_topic_redirs_table.php
@@ -1,9 +1,9 @@
 <?php
-use Index\Data\IDbConnection;
-use Index\Data\Migration\IDbMigration;
+use Index\Db\DbConnection;
+use Index\Db\Migration\DbMigration;
 
-final class CreateTopicRedirsTable_20230430_001226 implements IDbMigration {
-    public function migrate(IDbConnection $conn): void {
+final class CreateTopicRedirsTable_20230430_001226 implements DbMigration {
+    public function migrate(DbConnection $conn): void {
         $conn->execute('
             CREATE TABLE msz_forum_topics_redirects (
                 topic_id            INT(10) UNSIGNED NOT NULL,
diff --git a/database/2023_07_21_121854_update_user_agent_storage.php b/database/2023_07_21_121854_update_user_agent_storage.php
index 2f8e299f..de993a4f 100644
--- a/database/2023_07_21_121854_update_user_agent_storage.php
+++ b/database/2023_07_21_121854_update_user_agent_storage.php
@@ -1,10 +1,10 @@
 <?php
-use Index\Data\IDbConnection;
-use Index\Data\Migration\IDbMigration;
+use Index\Db\DbConnection;
+use Index\Db\Migration\DbMigration;
 use Misuzu\ClientInfo;
 
-final class UpdateUserAgentStorage_20230721_121854 implements IDbMigration {
-    public function migrate(IDbConnection $conn): void {
+final class UpdateUserAgentStorage_20230721_121854 implements DbMigration {
+    public function migrate(DbConnection $conn): void {
         // convert user agent fields to BLOB and add field for client info storage
         $conn->execute('
             ALTER TABLE msz_login_attempts
diff --git a/database/2023_07_24_201010_add_moderator_notes_table.php b/database/2023_07_24_201010_add_moderator_notes_table.php
index 140aaea9..5d80532a 100644
--- a/database/2023_07_24_201010_add_moderator_notes_table.php
+++ b/database/2023_07_24_201010_add_moderator_notes_table.php
@@ -1,9 +1,9 @@
 <?php
-use Index\Data\IDbConnection;
-use Index\Data\Migration\IDbMigration;
+use Index\Db\DbConnection;
+use Index\Db\Migration\DbMigration;
 
-final class AddModeratorNotesTable_20230724_201010 implements IDbMigration {
-    public function migrate(IDbConnection $conn): void {
+final class AddModeratorNotesTable_20230724_201010 implements DbMigration {
+    public function migrate(DbConnection $conn): void {
         $conn->execute('
             CREATE TABLE msz_users_modnotes (
                 note_id      INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
diff --git a/database/2023_07_26_175936_add_new_bans_table.php b/database/2023_07_26_175936_add_new_bans_table.php
index 39175a6c..4b802bd0 100644
--- a/database/2023_07_26_175936_add_new_bans_table.php
+++ b/database/2023_07_26_175936_add_new_bans_table.php
@@ -1,9 +1,9 @@
 <?php
-use Index\Data\IDbConnection;
-use Index\Data\Migration\IDbMigration;
+use Index\Db\DbConnection;
+use Index\Db\Migration\DbMigration;
 
-final class AddNewBansTable_20230726_175936 implements IDbMigration {
-    public function migrate(IDbConnection $conn): void {
+final class AddNewBansTable_20230726_175936 implements DbMigration {
+    public function migrate(DbConnection $conn): void {
         $conn->execute('
             CREATE TABLE msz_users_bans (
                 ban_id             INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
diff --git a/database/2023_07_26_210150_redo_warnings_table.php b/database/2023_07_26_210150_redo_warnings_table.php
index 7ad629fa..f3017e9e 100644
--- a/database/2023_07_26_210150_redo_warnings_table.php
+++ b/database/2023_07_26_210150_redo_warnings_table.php
@@ -1,9 +1,9 @@
 <?php
-use Index\Data\IDbConnection;
-use Index\Data\Migration\IDbMigration;
+use Index\Db\DbConnection;
+use Index\Db\Migration\DbMigration;
 
-final class RedoWarningsTable_20230726_210150 implements IDbMigration {
-    public function migrate(IDbConnection $conn): void {
+final class RedoWarningsTable_20230726_210150 implements DbMigration {
+    public function migrate(DbConnection $conn): void {
         $conn->execute('
             CREATE TABLE msz_users_warnings (
                 warn_id      INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
diff --git a/database/2023_07_27_130516_pluralise_users_for_role_relations.php b/database/2023_07_27_130516_pluralise_users_for_role_relations.php
index 8b50af4b..cdd0c10d 100644
--- a/database/2023_07_27_130516_pluralise_users_for_role_relations.php
+++ b/database/2023_07_27_130516_pluralise_users_for_role_relations.php
@@ -1,9 +1,9 @@
 <?php
-use Index\Data\IDbConnection;
-use Index\Data\Migration\IDbMigration;
+use Index\Db\DbConnection;
+use Index\Db\Migration\DbMigration;
 
-final class PluraliseUsersForRoleRelations_20230727_130516 implements IDbMigration {
-    public function migrate(IDbConnection $conn): void {
+final class PluraliseUsersForRoleRelations_20230727_130516 implements DbMigration {
+    public function migrate(DbConnection $conn): void {
         $conn->execute('RENAME TABLE msz_user_roles TO msz_users_roles');
     }
 }
diff --git a/database/2023_07_28_212101_create_counters_table.php b/database/2023_07_28_212101_create_counters_table.php
index 2b436f16..668e5948 100644
--- a/database/2023_07_28_212101_create_counters_table.php
+++ b/database/2023_07_28_212101_create_counters_table.php
@@ -1,9 +1,9 @@
 <?php
-use Index\Data\IDbConnection;
-use Index\Data\Migration\IDbMigration;
+use Index\Db\DbConnection;
+use Index\Db\Migration\DbMigration;
 
-final class CreateCountersTable_20230728_212101 implements IDbMigration {
-    public function migrate(IDbConnection $conn): void {
+final class CreateCountersTable_20230728_212101 implements DbMigration {
+    public function migrate(DbConnection $conn): void {
         $conn->execute('
             CREATE TABLE msz_counters (
                 counter_name    VARBINARY(64) NOT NULL,
diff --git a/database/2023_08_03_114403_update_collations_in_various_tables.php b/database/2023_08_03_114403_update_collations_in_various_tables.php
index 30a7f2eb..6e0fff19 100644
--- a/database/2023_08_03_114403_update_collations_in_various_tables.php
+++ b/database/2023_08_03_114403_update_collations_in_various_tables.php
@@ -1,9 +1,9 @@
 <?php
-use Index\Data\IDbConnection;
-use Index\Data\Migration\IDbMigration;
+use Index\Db\DbConnection;
+use Index\Db\Migration\DbMigration;
 
-final class UpdateCollationsInVariousTables_20230803_114403 implements IDbMigration {
-    public function migrate(IDbConnection $conn): void {
+final class UpdateCollationsInVariousTables_20230803_114403 implements DbMigration {
+    public function migrate(DbConnection $conn): void {
         $conn->execute('
             ALTER TABLE msz_audit_log
                 CHANGE COLUMN log_action  log_action  VARCHAR(50) NOT NULL              COLLATE "ascii_general_ci" AFTER user_id,
diff --git a/database/2023_08_30_213930_new_permissions_system.php b/database/2023_08_30_213930_new_permissions_system.php
index 3cff692e..9d1b22b3 100644
--- a/database/2023_08_30_213930_new_permissions_system.php
+++ b/database/2023_08_30_213930_new_permissions_system.php
@@ -1,9 +1,9 @@
 <?php
-use Index\Data\IDbConnection;
-use Index\Data\Migration\IDbMigration;
+use Index\Db\DbConnection;
+use Index\Db\Migration\DbMigration;
 
-final class NewPermissionsSystem_20230830_213930 implements IDbMigration {
-    public function migrate(IDbConnection $conn): void {
+final class NewPermissionsSystem_20230830_213930 implements DbMigration {
+    public function migrate(DbConnection $conn): void {
         // make sure cron doesn't fuck us over
         $conn->execute('DELETE FROM msz_config WHERE config_name = "perms.needsRecalc"');
 
diff --git a/database/2024_01_30_233734_create_messages_table.php b/database/2024_01_30_233734_create_messages_table.php
index d46d4d96..b127243c 100644
--- a/database/2024_01_30_233734_create_messages_table.php
+++ b/database/2024_01_30_233734_create_messages_table.php
@@ -1,9 +1,9 @@
 <?php
-use Index\Data\IDbConnection;
-use Index\Data\Migration\IDbMigration;
+use Index\Db\DbConnection;
+use Index\Db\Migration\DbMigration;
 
-final class CreateMessagesTable_20240130_233734 implements IDbMigration {
-    public function migrate(IDbConnection $conn): void {
+final class CreateMessagesTable_20240130_233734 implements DbMigration {
+    public function migrate(DbConnection $conn): void {
         $conn->execute('
             CREATE TABLE msz_messages (
                 msg_id BINARY(8) NOT NULL,
diff --git a/database/2024_06_02_194809_base_sixty_four_encode_pms_in_db.php b/database/2024_06_02_194809_base_sixty_four_encode_pms_in_db.php
index 7d130c9b..fb74b355 100644
--- a/database/2024_06_02_194809_base_sixty_four_encode_pms_in_db.php
+++ b/database/2024_06_02_194809_base_sixty_four_encode_pms_in_db.php
@@ -1,9 +1,9 @@
 <?php
-use Index\Data\IDbConnection;
-use Index\Data\Migration\IDbMigration;
+use Index\Db\DbConnection;
+use Index\Db\Migration\DbMigration;
 
-final class BaseSixtyFourEncodePmsInDb_20240602_194809 implements IDbMigration {
-    public function migrate(IDbConnection $conn): void {
+final class BaseSixtyFourEncodePmsInDb_20240602_194809 implements DbMigration {
+    public function migrate(DbConnection $conn): void {
         $conn->execute('UPDATE msz_messages SET msg_title = TO_BASE64(msg_title), msg_body = TO_BASE64(msg_body)');
         $conn->execute('
             ALTER TABLE `msz_messages`
diff --git a/database/2024_09_16_205613_add_role_id_string.php b/database/2024_09_16_205613_add_role_id_string.php
index 9353f708..143fdbc1 100644
--- a/database/2024_09_16_205613_add_role_id_string.php
+++ b/database/2024_09_16_205613_add_role_id_string.php
@@ -1,9 +1,9 @@
 <?php
-use Index\Data\IDbConnection;
-use Index\Data\Migration\IDbMigration;
+use Index\Db\DbConnection;
+use Index\Db\Migration\DbMigration;
 
-final class AddRoleIdString_20240916_205613 implements IDbMigration {
-    public function migrate(IDbConnection $conn): void {
+final class AddRoleIdString_20240916_205613 implements DbMigration {
+    public function migrate(DbConnection $conn): void {
         $conn->execute(<<<SQL
             ALTER TABLE msz_roles
                 ADD COLUMN role_string VARCHAR(20) NULL DEFAULT NULL COLLATE 'ascii_general_ci' AFTER role_id,
diff --git a/docs/source.md b/docs/source.md
index 2a01468f..12628353 100644
--- a/docs/source.md
+++ b/docs/source.md
@@ -13,6 +13,7 @@ Below are a number of links to source code repositories related to Flashii.net a
   - [Seria](https://patchii.net/flashii/seria): Software used by the downloads tracker.
   - [Mince](https://patchii.net/flashii/mince): Source code for the Minecraft servers subwebsite.
   - [Awaki](https://patchii.net/flashii/awaki): Redirect service hosted on fii.moe.
+  - [Aleister](https://patchii.net/flashii/aleister): Public API gateway.
 
 ## Tools & Software
   - [SoFii](https://patchii.net/flashii/sofii): Launcher for Soldier of Fortune 2
@@ -21,8 +22,7 @@ Below are a number of links to source code repositories related to Flashii.net a
 
 ## First-Party Libraries
   - [Index](https://patchii.net/flash/index): Base library used in almost any component of the website that uses PHP.
-  - [Sasae](https://patchii.net/flash/sasae): Extension to the Twig templating library.
-  - [Syokuhou](https://patchii.net/flash/syokuhou): Configuration library.
+  - [Aiwass](https://patchii.net/flashii/aiwass): Internal RPC extension, mainly used to supply data to the API gateway.
 
 ## Historical
   - [AJAX Chat (fork)](https://patchii.net/flashii/ajax-chat): Old chat software (2013-2015). Still kept on life support for the nostalgia.
diff --git a/misuzu.php b/misuzu.php
index dd57a25f..c328dd5a 100644
--- a/misuzu.php
+++ b/misuzu.php
@@ -1,9 +1,9 @@
 <?php
 namespace Misuzu;
 
-use Index\Data\DbTools;
-use Syokuhou\DbConfig;
-use Syokuhou\FileConfig;
+use Index\Db\DbTools;
+use Index\Config\Db\DbConfig;
+use Index\Config\Fs\FsConfig;
 
 define('MSZ_STARTUP', microtime(true));
 define('MSZ_ROOT', __DIR__);
@@ -22,7 +22,7 @@ error_reporting(MSZ_DEBUG ? -1 : 0);
 mb_internal_encoding('UTF-8');
 date_default_timezone_set('GMT');
 
-$cfg = FileConfig::fromFile(MSZ_CONFIG . '/config.cfg');
+$cfg = FsConfig::fromFile(MSZ_CONFIG . '/config.cfg');
 
 if($cfg->hasValues('sentry:dsn'))
     (function($cfg) {
diff --git a/package-lock.json b/package-lock.json
index 2223cd1e..60909eb1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -417,9 +417,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001664",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz",
-      "integrity": "sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==",
+      "version": "1.0.30001667",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz",
+      "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==",
       "funding": [
         {
           "type": "opencollective",
@@ -703,9 +703,9 @@
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.5.29",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.29.tgz",
-      "integrity": "sha512-PF8n2AlIhCKXQ+gTpiJi0VhcHDb69kYX4MtCiivctc2QD3XuNZ/XIOlbGzt7WAjjEev0TtaH6Cu3arZExm5DOw==",
+      "version": "1.5.32",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.32.tgz",
+      "integrity": "sha512-M+7ph0VGBQqqpTT2YrabjNKSQ2fEl9PVx6AK3N558gDH9NO8O6XN9SXXFWRo9u9PbEg/bWq+tjXQr+eXmxubCw==",
       "license": "ISC"
     },
     "node_modules/entities": {
diff --git a/public-legacy/auth/login.php b/public-legacy/auth/login.php
index 48d8a223..33083707 100644
--- a/public-legacy/auth/login.php
+++ b/public-legacy/auth/login.php
@@ -4,7 +4,7 @@ namespace Misuzu;
 use Exception;
 use Misuzu\Auth\AuthTokenCookie;
 
-$urls = $msz->getURLs();
+$urls = $msz->getUrls();
 $authInfo = $msz->getAuthInfo();
 if($authInfo->isLoggedIn()) {
     Tools::redirect($urls->format('index'));
diff --git a/public-legacy/auth/logout.php b/public-legacy/auth/logout.php
index 195f714e..1f9eb732 100644
--- a/public-legacy/auth/logout.php
+++ b/public-legacy/auth/logout.php
@@ -24,4 +24,4 @@ if($authInfo->isLoggedIn()) {
     AuthTokenCookie::apply($tokenPacker->pack($tokenInfo));
 }
 
-Tools::redirect($msz->getURLs()->format('index'));;
+Tools::redirect($msz->getUrls()->format('index'));;
diff --git a/public-legacy/auth/password.php b/public-legacy/auth/password.php
index 444b2dbe..5f699eca 100644
--- a/public-legacy/auth/password.php
+++ b/public-legacy/auth/password.php
@@ -4,7 +4,7 @@ namespace Misuzu;
 use RuntimeException;
 use Misuzu\Users\User;
 
-$urls = $msz->getURLs();
+$urls = $msz->getUrls();
 $authInfo = $msz->getAuthInfo();
 if($authInfo->isLoggedIn()) {
     Tools::redirect($urls->format('settings-account'));
diff --git a/public-legacy/auth/register.php b/public-legacy/auth/register.php
index 85b95ca8..59e6b7a4 100644
--- a/public-legacy/auth/register.php
+++ b/public-legacy/auth/register.php
@@ -4,7 +4,7 @@ namespace Misuzu;
 use RuntimeException;
 use Misuzu\Users\User;
 
-$urls = $msz->getURLs();
+$urls = $msz->getUrls();
 $authInfo = $msz->getAuthInfo();
 if($authInfo->isLoggedIn()) {
     Tools::redirect($urls->format('index'));
diff --git a/public-legacy/auth/revert.php b/public-legacy/auth/revert.php
index 5d97a0db..e215be71 100644
--- a/public-legacy/auth/revert.php
+++ b/public-legacy/auth/revert.php
@@ -3,7 +3,7 @@ namespace Misuzu;
 
 use Misuzu\Auth\AuthTokenCookie;
 
-$urls = $msz->getURLs();
+$urls = $msz->getUrls();
 if(CSRF::validateRequest()) {
     $tokenInfo = $msz->getAuthInfo()->getTokenInfo();
 
diff --git a/public-legacy/auth/twofactor.php b/public-legacy/auth/twofactor.php
index 56bcc0a8..d3e08e37 100644
--- a/public-legacy/auth/twofactor.php
+++ b/public-legacy/auth/twofactor.php
@@ -5,7 +5,7 @@ use RuntimeException;
 use Misuzu\TOTPGenerator;
 use Misuzu\Auth\AuthTokenCookie;
 
-$urls = $msz->getURLs();
+$urls = $msz->getUrls();
 $authInfo = $msz->getAuthInfo();
 if($authInfo->isLoggedIn()) {
     Tools::redirect($urls->format('index'));
diff --git a/public-legacy/comments.php b/public-legacy/comments.php
index 7781f60e..d254a617 100644
--- a/public-legacy/comments.php
+++ b/public-legacy/comments.php
@@ -4,7 +4,7 @@ namespace Misuzu;
 use RuntimeException;
 
 $usersCtx = $msz->getUsersContext();
-$redirect = filter_input(INPUT_GET, 'return') ?? $_SERVER['HTTP_REFERER'] ?? $msz->getURLs()->format('index');
+$redirect = filter_input(INPUT_GET, 'return') ?? $_SERVER['HTTP_REFERER'] ?? $msz->getUrls()->format('index');
 
 if(!Tools::isLocalURL($redirect))
     Template::displayInfo('Possible request forgery detected.', 403);
diff --git a/public-legacy/forum/index.php b/public-legacy/forum/index.php
index 5ae28c2f..fec71b69 100644
--- a/public-legacy/forum/index.php
+++ b/public-legacy/forum/index.php
@@ -32,14 +32,14 @@ if($mode === 'mark') {
                 $forumCategories->updateUserReadCategory($userInfo, $categoryInfo);
         }
 
-        Tools::redirect($msz->getURLs()->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]));
+        Tools::redirect($msz->getUrls()->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]));
         return;
     }
 
     Template::render('confirm', [
         'title' => 'Mark forum as read',
         'message' => 'Are you sure you want to mark ' . ($categoryId < 1 ? 'the entire' : 'this') . ' forum as read?',
-        'return' => $msz->getURLs()->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]),
+        'return' => $msz->getUrls()->format($categoryId ? 'forum-category' : 'forum-index', ['forum' => $categoryId]),
         'params' => [
             'forum' => $categoryId,
         ]
diff --git a/public-legacy/forum/leaderboard.php b/public-legacy/forum/leaderboard.php
index b4a03379..19ee0616 100644
--- a/public-legacy/forum/leaderboard.php
+++ b/public-legacy/forum/leaderboard.php
@@ -94,7 +94,7 @@ MD;
         $markdown .= sprintf("| %s | [%s](%s%s) | %s |\r\n", $ranking->position,
             $ranking->user?->getName() ?? 'Deleted User',
             $msz->getSiteInfo()->getURL(),
-            $msz->getURLs()->format('user-profile', ['user' => $ranking->userId]),
+            $msz->getUrls()->format('user-profile', ['user' => $ranking->userId]),
             number_format($ranking->postsCount));
     }
 
diff --git a/public-legacy/forum/post.php b/public-legacy/forum/post.php
index 7ef0b344..31f45010 100644
--- a/public-legacy/forum/post.php
+++ b/public-legacy/forum/post.php
@@ -3,7 +3,7 @@ namespace Misuzu;
 
 use RuntimeException;
 
-$urls = $msz->getURLs();
+$urls = $msz->getUrls();
 $forumCtx = $msz->getForumContext();
 $forumPosts = $forumCtx->getPosts();
 $usersCtx = $msz->getUsersContext();
diff --git a/public-legacy/forum/posting.php b/public-legacy/forum/posting.php
index 2cf7b3f9..173ac719 100644
--- a/public-legacy/forum/posting.php
+++ b/public-legacy/forum/posting.php
@@ -258,7 +258,7 @@ if(!empty($_POST)) {
 
             if(empty($notices)) {
                 // does this ternary ever return forum-topic?
-                $redirect = $msz->getURLs()->format(empty($topicInfo) ? 'forum-topic' : 'forum-post', [
+                $redirect = $msz->getUrls()->format(empty($topicInfo) ? 'forum-topic' : 'forum-post', [
                     'topic' => $topicId ?? 0,
                     'post' => $postId ?? 0,
                 ]);
diff --git a/public-legacy/forum/topic.php b/public-legacy/forum/topic.php
index ad204fd3..81fb52ae 100644
--- a/public-legacy/forum/topic.php
+++ b/public-legacy/forum/topic.php
@@ -4,7 +4,7 @@ namespace Misuzu;
 use stdClass;
 use RuntimeException;
 
-$urls = $msz->getURLs();
+$urls = $msz->getUrls();
 $forumCtx = $msz->getForumContext();
 $forumCategories = $forumCtx->getCategories();
 $forumTopics = $forumCtx->getTopics();
diff --git a/public-legacy/manage/changelog/change.php b/public-legacy/manage/changelog/change.php
index 981b2957..b3ac9b4d 100644
--- a/public-legacy/manage/changelog/change.php
+++ b/public-legacy/manage/changelog/change.php
@@ -15,7 +15,7 @@ $changeActions = [];
 foreach(Changelog::ACTIONS as $action)
     $changeActions[$action] = Changelog::actionText($action);
 
-$urls = $msz->getURLs();
+$urls = $msz->getUrls();
 $changelog = $msz->getChangelog();
 $changeId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
 $changeInfo = null;
diff --git a/public-legacy/manage/changelog/tag.php b/public-legacy/manage/changelog/tag.php
index 1e4ae07d..9983604c 100644
--- a/public-legacy/manage/changelog/tag.php
+++ b/public-legacy/manage/changelog/tag.php
@@ -6,7 +6,7 @@ use RuntimeException;
 if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CL_TAGS_MANAGE))
     Template::throwError(403);
 
-$urls = $msz->getURLs();
+$urls = $msz->getUrls();
 $changelog = $msz->getChangelog();
 $tagId = (string)filter_input(INPUT_GET, 't', FILTER_SANITIZE_NUMBER_INT);
 $loadTagInfo = fn() => $changelog->getTag($tagId);
diff --git a/public-legacy/manage/forum/redirs.php b/public-legacy/manage/forum/redirs.php
index 06c8a38d..91735e42 100644
--- a/public-legacy/manage/forum/redirs.php
+++ b/public-legacy/manage/forum/redirs.php
@@ -5,7 +5,7 @@ $authInfo = $msz->getAuthInfo();
 if(!$authInfo->getPerms('global')->check(Perm::G_FORUM_TOPIC_REDIRS_MANAGE))
     Template::throwError(403);
 
-$urls = $msz->getURLs();
+$urls = $msz->getUrls();
 $forumCtx = $msz->getForumContext();
 $forumTopicRedirects = $forumCtx->getTopicRedirects();
 
diff --git a/public-legacy/manage/general/emoticon.php b/public-legacy/manage/general/emoticon.php
index 823a7e39..fb8faa4e 100644
--- a/public-legacy/manage/general/emoticon.php
+++ b/public-legacy/manage/general/emoticon.php
@@ -97,7 +97,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
         [$emoteInfo->getId()]
     );
 
-    Tools::redirect($msz->getURLs()->format('manage-general-emoticon', ['emote' => $emoteInfo->getId()]));
+    Tools::redirect($msz->getUrls()->format('manage-general-emoticon', ['emote' => $emoteInfo->getId()]));
     return;
 }
 
diff --git a/public-legacy/manage/general/emoticons.php b/public-legacy/manage/general/emoticons.php
index 66ad09e3..a17488d2 100644
--- a/public-legacy/manage/general/emoticons.php
+++ b/public-legacy/manage/general/emoticons.php
@@ -37,7 +37,7 @@ if(CSRF::validateRequest() && !empty($_GET['emote'])) {
         }
     }
 
-    Tools::redirect($msz->getURLs()->format('manage-general-emoticons'));
+    Tools::redirect($msz->getUrls()->format('manage-general-emoticons'));
     return;
 }
 
diff --git a/public-legacy/manage/general/setting-delete.php b/public-legacy/manage/general/setting-delete.php
index 872c92f0..4f5c8183 100644
--- a/public-legacy/manage/general/setting-delete.php
+++ b/public-legacy/manage/general/setting-delete.php
@@ -13,7 +13,7 @@ if($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
     $valueName = $valueInfo->getName();
     $msz->createAuditLog('CONFIG_DELETE', [$valueName]);
     $cfg->removeValues($valueName);
-    Tools::redirect($msz->getURLs()->format('manage-general-settings'));
+    Tools::redirect($msz->getUrls()->format('manage-general-settings'));
     return;
 }
 
diff --git a/public-legacy/manage/general/setting.php b/public-legacy/manage/general/setting.php
index e03e08ea..617a0580 100644
--- a/public-legacy/manage/general/setting.php
+++ b/public-legacy/manage/general/setting.php
@@ -1,7 +1,7 @@
 <?php
 namespace Misuzu;
 
-use Syokuhou\DbConfig;
+use Index\Config\Db\DbConfig;
 
 if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_CONFIG_MANAGE))
     Template::throwError(403);
@@ -73,7 +73,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
 
     $msz->createAuditLog($isNew ? 'CONFIG_CREATE' : 'CONFIG_UPDATE', [$sName]);
     $applyFunc($sName, $sValue);
-    Tools::redirect($msz->getURLs()->format('manage-general-settings'));
+    Tools::redirect($msz->getUrls()->format('manage-general-settings'));
     return;
 }
 
diff --git a/public-legacy/manage/news/category.php b/public-legacy/manage/news/category.php
index 6cb7077e..978a2e5a 100644
--- a/public-legacy/manage/news/category.php
+++ b/public-legacy/manage/news/category.php
@@ -6,7 +6,7 @@ use RuntimeException;
 if(!$msz->getAuthInfo()->getPerms('global')->check(Perm::G_NEWS_CATEGORIES_MANAGE))
     Template::throwError(403);
 
-$urls = $msz->getURLs();
+$urls = $msz->getUrls();
 $news = $msz->getNews();
 $categoryId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
 $loadCategoryInfo = fn() => $news->getCategory(categoryId: $categoryId);
diff --git a/public-legacy/manage/news/post.php b/public-legacy/manage/news/post.php
index faeef965..66bc809b 100644
--- a/public-legacy/manage/news/post.php
+++ b/public-legacy/manage/news/post.php
@@ -7,7 +7,7 @@ $authInfo = $msz->getAuthInfo();
 if(!$authInfo->getPerms('global')->check(Perm::G_NEWS_POSTS_MANAGE))
     Template::throwError(403);
 
-$urls = $msz->getURLs();
+$urls = $msz->getUrls();
 $news = $msz->getNews();
 $postId = (string)filter_input(INPUT_GET, 'p', FILTER_SANITIZE_NUMBER_INT);
 $loadPostInfo = fn() => $news->getPost($postId);
diff --git a/public-legacy/manage/users/ban.php b/public-legacy/manage/users/ban.php
index b537e0e4..0680f712 100644
--- a/public-legacy/manage/users/ban.php
+++ b/public-legacy/manage/users/ban.php
@@ -9,7 +9,7 @@ $authInfo = $msz->getAuthInfo();
 if(!$authInfo->getPerms('user')->check(Perm::U_BANS_MANAGE))
     Template::throwError(403);
 
-$urls = $msz->getURLs();
+$urls = $msz->getUrls();
 $usersCtx = $msz->getUsersContext();
 $bans = $usersCtx->getBans();
 
diff --git a/public-legacy/manage/users/note.php b/public-legacy/manage/users/note.php
index a69e1dc7..e49d2c69 100644
--- a/public-legacy/manage/users/note.php
+++ b/public-legacy/manage/users/note.php
@@ -13,7 +13,7 @@ $hasUserId = filter_has_var(INPUT_GET, 'u');
 if((!$hasNoteId && !$hasUserId) || ($hasNoteId && $hasUserId))
     Template::throwError(400);
 
-$urls = $msz->getURLs();
+$urls = $msz->getUrls();
 $usersCtx = $msz->getUsersContext();
 $modNotes = $usersCtx->getModNotes();
 
diff --git a/public-legacy/manage/users/role.php b/public-legacy/manage/users/role.php
index 721ff9bf..d1ce6d96 100644
--- a/public-legacy/manage/users/role.php
+++ b/public-legacy/manage/users/role.php
@@ -168,7 +168,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
         $msz->getConfig()->setBoolean('perms.needsRecalc', true);
     }
 
-    Tools::redirect($msz->getURLs()->format('manage-role', ['role' => $roleInfo->getId()]));
+    Tools::redirect($msz->getUrls()->format('manage-role', ['role' => $roleInfo->getId()]));
     return;
 }
 
diff --git a/public-legacy/manage/users/user.php b/public-legacy/manage/users/user.php
index 7574ee0a..40f18d96 100644
--- a/public-legacy/manage/users/user.php
+++ b/public-legacy/manage/users/user.php
@@ -12,7 +12,7 @@ $viewerPerms = $authInfo->getPerms('user');
 if(!$authInfo->isLoggedIn())
     Template::throwError(403);
 
-$urls = $msz->getURLs();
+$urls = $msz->getUrls();
 $usersCtx = $msz->getUsersContext();
 $users = $usersCtx->getUsers();
 $roles = $usersCtx->getRoles();
diff --git a/public-legacy/manage/users/warning.php b/public-legacy/manage/users/warning.php
index 7fc19e7b..68f35067 100644
--- a/public-legacy/manage/users/warning.php
+++ b/public-legacy/manage/users/warning.php
@@ -7,7 +7,7 @@ $authInfo = $msz->getAuthInfo();
 if(!$authInfo->getPerms('user')->check(Perm::U_WARNINGS_MANAGE))
     Template::throwError(403);
 
-$urls = $msz->getURLs();
+$urls = $msz->getUrls();
 $usersCtx = $msz->getUsersContext();
 $users = $usersCtx->getUsers();
 $warns = $usersCtx->getWarnings();
diff --git a/public-legacy/profile.php b/public-legacy/profile.php
index ae1f4446..8d9a2b99 100644
--- a/public-legacy/profile.php
+++ b/public-legacy/profile.php
@@ -14,7 +14,7 @@ $userId = !empty($_GET['u']) && is_string($_GET['u']) ? trim($_GET['u']) : 0;
 $profileMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
 $isEditing = !empty($_GET['edit']) && is_string($_GET['edit']) ? (bool)$_GET['edit'] : !empty($_POST) && is_array($_POST);
 
-$urls = $msz->getURLs();
+$urls = $msz->getUrls();
 $usersCtx = $msz->getUsersContext();
 $users = $usersCtx->getUsers();
 $forumCtx = $msz->getForumContext();
diff --git a/public-legacy/settings/data.php b/public-legacy/settings/data.php
index 9d115fb6..e4002fa1 100644
--- a/public-legacy/settings/data.php
+++ b/public-legacy/settings/data.php
@@ -3,7 +3,6 @@ namespace Misuzu;
 
 use ZipArchive;
 use Index\XString;
-use Index\IO\FileStream;
 use Misuzu\Users\UserInfo;
 
 $authInfo = $msz->getAuthInfo();
@@ -42,7 +41,7 @@ function db_to_zip(ZipArchive $archive, UserInfo $userInfo, string $baseName, ar
     }
 
     $tmpName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . sprintf('msz-user-data-%s-%s-%s.tmp', $userId, $baseName, XString::random(8));
-    $tmpStream = FileStream::newWrite($tmpName);
+    $tmpHandle = fopen($tmpName, 'wb');
 
     try {
         $stmt = $dbConn->prepare(sprintf('SELECT %s FROM msz_%s WHERE %s = ?', implode(', ', $fields), $baseName, $userIdField));
@@ -85,12 +84,12 @@ function db_to_zip(ZipArchive $archive, UserInfo $userInfo, string $baseName, ar
                 $row[$fieldInfo['name']] = $fieldValue;
             }
 
-            $tmpStream->write(json_encode($row, JSON_INVALID_UTF8_SUBSTITUTE));
-            $tmpStream->write("\n");
+            fwrite($tmpHandle, json_encode($row, JSON_INVALID_UTF8_SUBSTITUTE));
+            fwrite($tmpHandle, "\n");
         }
     } finally {
-        $tmpStream->flush();
-        $tmpStream->close();
+        fflush($tmpHandle);
+        fclose($tmpHandle);
     }
 
     $archive->addFile($tmpName, $baseName . '.jsonl');
diff --git a/public-legacy/settings/sessions.php b/public-legacy/settings/sessions.php
index cc5be10f..ab366786 100644
--- a/public-legacy/settings/sessions.php
+++ b/public-legacy/settings/sessions.php
@@ -37,7 +37,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
     }
 
     if($activeSessionKilled) {
-        Tools::redirect($msz->getURLs()->format('index'));
+        Tools::redirect($msz->getUrls()->format('index'));
         return;
     } else break;
 }
diff --git a/public/index.php b/public/index.php
index fb2c82b7..29a91f08 100644
--- a/public/index.php
+++ b/public/index.php
@@ -2,10 +2,7 @@
 namespace Misuzu;
 
 use RuntimeException;
-use Misuzu\Auth\AuthTokenBuilder;
-use Misuzu\Auth\AuthTokenCookie;
-use Misuzu\Auth\AuthTokenInfo;
-use Sasae\SasaeEnvironment;
+use Misuzu\Auth\{AuthTokenBuilder,AuthTokenCookie,AuthTokenInfo};
 
 require_once __DIR__ . '/../misuzu.php';
 
diff --git a/src/AuditLog/AuditLog.php b/src/AuditLog/AuditLog.php
index 3eefb9ad..0b82baf0 100644
--- a/src/AuditLog/AuditLog.php
+++ b/src/AuditLog/AuditLog.php
@@ -2,9 +2,7 @@
 namespace Misuzu\AuditLog;
 
 use InvalidArgumentException;
-use Index\Data\DbStatementCache;
-use Index\Data\IDbConnection;
-use Index\Data\IDbResult;
+use Index\Db\{DbConnection,DbStatementCache};
 use Index\Net\IPAddress;
 use Misuzu\Pagination;
 use Misuzu\Users\UserInfo;
@@ -12,7 +10,7 @@ use Misuzu\Users\UserInfo;
 class AuditLog {
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->cache = new DbStatementCache($dbConn);
     }
 
diff --git a/src/AuditLog/AuditLogInfo.php b/src/AuditLog/AuditLogInfo.php
index 41f0e745..1689d72c 100644
--- a/src/AuditLog/AuditLogInfo.php
+++ b/src/AuditLog/AuditLogInfo.php
@@ -3,7 +3,7 @@ namespace Misuzu\AuditLog;
 
 use ValueError;
 use Carbon\CarbonImmutable;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 use Index\Net\IPAddress;
 
 class AuditLogInfo {
@@ -16,7 +16,7 @@ class AuditLogInfo {
         private string $country,
     ) {}
 
-    public static function fromResult(IDbResult $result): AuditLogInfo {
+    public static function fromResult(DbResult $result): AuditLogInfo {
         return new AuditLogInfo(
             userId: $result->getStringOrNull(0),
             action: $result->getString(1),
diff --git a/src/Auth/AuthContext.php b/src/Auth/AuthContext.php
index c245eed6..450f00d9 100644
--- a/src/Auth/AuthContext.php
+++ b/src/Auth/AuthContext.php
@@ -1,8 +1,8 @@
 <?php
 namespace Misuzu\Auth;
 
-use Index\Data\IDbConnection;
-use Syokuhou\IConfig;
+use Index\Config\Config;
+use Index\Db\DbConnection;
 
 class AuthContext {
     private Sessions $sessions;
@@ -10,9 +10,9 @@ class AuthContext {
     private RecoveryTokens $recoveryTokens;
     private TwoFactorAuthSessions $tfaSessions;
 
-    private IConfig $config;
+    private Config $config;
 
-    public function __construct(IDbConnection $dbConn, IConfig $config) {
+    public function __construct(DbConnection $dbConn, Config $config) {
         $this->config = $config;
         $this->sessions = new Sessions($dbConn);
         $this->loginAttempts = new LoginAttempts($dbConn);
diff --git a/src/Auth/AuthRpcActions.php b/src/Auth/AuthRpcActions.php
index 84c1110d..8296c0ce 100644
--- a/src/Auth/AuthRpcActions.php
+++ b/src/Auth/AuthRpcActions.php
@@ -4,11 +4,11 @@ namespace Misuzu\Auth;
 use RuntimeException;
 use Misuzu\Users\{UsersContext,UserInfo};
 use Aiwass\Server\{RpcActionHandler,RpcProcedure};
-use Syokuhou\IConfig;
+use Index\Config\Config;
 
 final class AuthRpcActions extends RpcActionHandler {
     public function __construct(
-        private IConfig $impersonateConfig,
+        private Config $impersonateConfig,
         private UsersContext $usersCtx,
         private AuthContext $authCtx
     ) {}
diff --git a/src/Auth/AuthTokenPacker.php b/src/Auth/AuthTokenPacker.php
index e3562887..fd9a346c 100644
--- a/src/Auth/AuthTokenPacker.php
+++ b/src/Auth/AuthTokenPacker.php
@@ -3,7 +3,6 @@ namespace Misuzu\Auth;
 
 use RuntimeException;
 use Index\UriBase64;
-use Index\IO\MemoryStream;
 
 class AuthTokenPacker {
     private const EPOCH_V2 = 1682985600;
@@ -64,32 +63,40 @@ class AuthTokenPacker {
 
             $unpackTime = unpack('Nts', $timestamp);
             if($unpackTime === false)
-                throw new RuntimeException('$token does not contain a valid timestamp.');
+                return AuthTokenInfo::empty();
 
             $timestamp = $unpackTime['ts'] + self::EPOCH_V2;
 
-            $stream = MemoryStream::fromString($data);
-            $stream->seek(0);
+            $handle = fopen('php://memory', 'rb+');
+            if($handle === false)
+                return AuthTokenInfo::empty();
 
-            for(;;) {
-                $length = $stream->readChar();
-                if($length === null)
-                    break;
+            try {
+                fwrite($handle, $data);
+                fseek($handle, 0);
 
-                $length = ord($length);
-                if($length < 1)
-                    break;
+                for(;;) {
+                    $length = fgetc($handle);
+                    if($length === false)
+                        break;
 
-                $name = $stream->read($length);
-                $value = null;
-                $length = $stream->readChar();
-                if($length !== null) {
                     $length = ord($length);
-                    if($length > 0)
-                        $value = $stream->read($length);
-                }
+                    if($length < 1)
+                        break;
 
-                $builder->setProperty($name, $value);
+                    $name = fread($handle, $length);
+                    $value = null;
+                    $length = fgetc($handle);
+                    if($length !== false) {
+                        $length = ord($length);
+                        if($length > 0)
+                            $value = fread($handle, $length);
+                    }
+
+                    $builder->setProperty($name, $value);
+                }
+            } finally {
+                fclose($handle);
             }
         } else
             return AuthTokenInfo::empty();
diff --git a/src/Auth/LoginAttemptInfo.php b/src/Auth/LoginAttemptInfo.php
index 21888e7e..302ab31b 100644
--- a/src/Auth/LoginAttemptInfo.php
+++ b/src/Auth/LoginAttemptInfo.php
@@ -3,7 +3,7 @@ namespace Misuzu\Auth;
 
 use Misuzu\ClientInfo;
 use Carbon\CarbonImmutable;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 use Index\Net\IPAddress;
 
 class LoginAttemptInfo {
@@ -17,7 +17,7 @@ class LoginAttemptInfo {
         private string $clientInfo,
     ) {}
 
-    public static function fromResult(IDbResult $result): LoginAttemptInfo {
+    public static function fromResult(DbResult $result): LoginAttemptInfo {
         return new LoginAttemptInfo(
             userId: $result->getStringOrNull(0),
             success: $result->getBoolean(1),
diff --git a/src/Auth/LoginAttempts.php b/src/Auth/LoginAttempts.php
index 6ebda9ce..2eb7d9ba 100644
--- a/src/Auth/LoginAttempts.php
+++ b/src/Auth/LoginAttempts.php
@@ -1,8 +1,7 @@
 <?php
 namespace Misuzu\Auth;
 
-use Index\Data\DbStatementCache;
-use Index\Data\IDbConnection;
+use Index\Db\{DbConnection,DbStatementCache};
 use Index\Net\IPAddress;
 use Misuzu\ClientInfo;
 use Misuzu\Pagination;
@@ -14,7 +13,7 @@ class LoginAttempts {
 
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->cache = new DbStatementCache($dbConn);
     }
 
diff --git a/src/Auth/RecoveryTokenInfo.php b/src/Auth/RecoveryTokenInfo.php
index 70b0e928..ce48a470 100644
--- a/src/Auth/RecoveryTokenInfo.php
+++ b/src/Auth/RecoveryTokenInfo.php
@@ -2,7 +2,7 @@
 namespace Misuzu\Auth;
 
 use Carbon\CarbonImmutable;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 use Index\Net\IPAddress;
 
 class RecoveryTokenInfo {
@@ -13,7 +13,7 @@ class RecoveryTokenInfo {
     private int $created;
     private ?string $code;
 
-    public function __construct(IDbResult $result) {
+    public function __construct(DbResult $result) {
         $this->userId = (string)$result->getInteger(0);
         $this->remoteAddr = $result->getString(1);
         $this->created = $result->getInteger(2);
diff --git a/src/Auth/RecoveryTokens.php b/src/Auth/RecoveryTokens.php
index 72a255d8..c2970b51 100644
--- a/src/Auth/RecoveryTokens.php
+++ b/src/Auth/RecoveryTokens.php
@@ -4,8 +4,7 @@ namespace Misuzu\Auth;
 use InvalidArgumentException;
 use RuntimeException;
 use Index\Base32;
-use Index\Data\DbStatementCache;
-use Index\Data\IDbConnection;
+use Index\Db\{DbConnection,DbStatementCache};
 use Index\Net\IPAddress;
 use Misuzu\ClientInfo;
 use Misuzu\Pagination;
@@ -14,7 +13,7 @@ use Misuzu\Users\UserInfo;
 class RecoveryTokens {
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->cache = new DbStatementCache($dbConn);
     }
 
diff --git a/src/Auth/SessionInfo.php b/src/Auth/SessionInfo.php
index 13e1ff1f..e049977e 100644
--- a/src/Auth/SessionInfo.php
+++ b/src/Auth/SessionInfo.php
@@ -3,7 +3,7 @@ namespace Misuzu\Auth;
 
 use Misuzu\ClientInfo;
 use Carbon\CarbonImmutable;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 use Index\Net\IPAddress;
 
 class SessionInfo {
@@ -22,7 +22,7 @@ class SessionInfo {
         private ?int $lastActive,
     ) {}
 
-    public static function fromResult(IDbResult $result): SessionInfo {
+    public static function fromResult(DbResult $result): SessionInfo {
         return new SessionInfo(
             id: $result->getString(0),
             userId: $result->getString(1),
diff --git a/src/Auth/Sessions.php b/src/Auth/Sessions.php
index 42c35e3c..a65d4342 100644
--- a/src/Auth/Sessions.php
+++ b/src/Auth/Sessions.php
@@ -4,19 +4,17 @@ namespace Misuzu\Auth;
 use InvalidArgumentException;
 use RuntimeException;
 use Index\XString;
-use Index\Data\DbStatementCache;
-use Index\Data\DbTools;
-use Index\Data\IDbConnection;
+use Index\Db\{DbConnection,DbStatementCache,DbTools};
 use Index\Net\IPAddress;
 use Misuzu\ClientInfo;
 use Misuzu\Pagination;
 use Misuzu\Users\UserInfo;
 
 class Sessions {
-    private IDbConnection $dbConn;
+    private DbConnection $dbConn;
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->dbConn = $dbConn;
         $this->cache = new DbStatementCache($dbConn);
     }
diff --git a/src/Auth/TwoFactorAuthSessions.php b/src/Auth/TwoFactorAuthSessions.php
index afafd7ce..ca0d33d4 100644
--- a/src/Auth/TwoFactorAuthSessions.php
+++ b/src/Auth/TwoFactorAuthSessions.php
@@ -2,14 +2,13 @@
 namespace Misuzu\Auth;
 
 use Index\XString;
-use Index\Data\DbStatementCache;
-use Index\Data\IDbConnection;
+use Index\Db\{DbConnection,DbStatementCache};
 use Misuzu\Users\UserInfo;
 
 class TwoFactorAuthSessions {
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->cache = new DbStatementCache($dbConn);
     }
 
diff --git a/src/CSRF.php b/src/CSRF.php
index d4e065f4..ffaab08c 100644
--- a/src/CSRF.php
+++ b/src/CSRF.php
@@ -1,19 +1,19 @@
 <?php
 namespace Misuzu;
 
-use Index\CSRFP;
+use Index\CsrfToken;
 
 final class CSRF {
-    private static CSRFP $instance;
+    private static CsrfToken $instance;
     private static string $secretKey = '';
 
-    public static function create(string $identity, ?string $secretKey = null): CSRFP {
+    public static function create(string $identity, ?string $secretKey = null): CsrfToken {
         if($secretKey === null)
             $secretKey = self::$secretKey;
         else
             self::$secretKey = $secretKey;
 
-        return new CSRFP($secretKey, $identity);
+        return new CsrfToken($secretKey, $identity);
     }
 
     public static function init(string $secretKey, string $identity): void {
diff --git a/src/Changelog/ChangeInfo.php b/src/Changelog/ChangeInfo.php
index a7656686..f32d05ba 100644
--- a/src/Changelog/ChangeInfo.php
+++ b/src/Changelog/ChangeInfo.php
@@ -2,7 +2,7 @@
 namespace Misuzu\Changelog;
 
 use Carbon\CarbonImmutable;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class ChangeInfo {
     public function __construct(
@@ -14,7 +14,7 @@ class ChangeInfo {
         private string $body,
     ) {}
 
-    public static function fromResult(IDbResult $result): ChangeInfo {
+    public static function fromResult(DbResult $result): ChangeInfo {
         return new ChangeInfo(
             id: $result->getString(0),
             userId: $result->getStringOrNull(1),
diff --git a/src/Changelog/ChangeTagInfo.php b/src/Changelog/ChangeTagInfo.php
index 3ad14a0f..0ad2c7ac 100644
--- a/src/Changelog/ChangeTagInfo.php
+++ b/src/Changelog/ChangeTagInfo.php
@@ -3,7 +3,7 @@ namespace Misuzu\Changelog;
 
 use Stringable;
 use Carbon\CarbonImmutable;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class ChangeTagInfo implements Stringable {
     private string $id;
@@ -13,7 +13,7 @@ class ChangeTagInfo implements Stringable {
     private int $archived;
     private int $changes;
 
-    public function __construct(IDbResult $result) {
+    public function __construct(DbResult $result) {
         $this->id = (string)$result->getInteger(0);
         $this->name = $result->getString(1);
         $this->description = $result->getString(2);
diff --git a/src/Changelog/Changelog.php b/src/Changelog/Changelog.php
index f6634150..f9edfadb 100644
--- a/src/Changelog/Changelog.php
+++ b/src/Changelog/Changelog.php
@@ -4,10 +4,7 @@ namespace Misuzu\Changelog;
 use InvalidArgumentException;
 use RuntimeException;
 use DateTimeInterface;
-use Index\Data\DbStatementCache;
-use Index\Data\DbTools;
-use Index\Data\IDbConnection;
-use Index\Data\IDbResult;
+use Index\Db\{DbConnection,DbStatementCache,DbTools};
 use Misuzu\Pagination;
 use Misuzu\Users\UserInfo;
 
@@ -15,12 +12,12 @@ class Changelog {
     // not a strict list but useful to have
     public const ACTIONS = ['add', 'remove', 'update', 'fix', 'import', 'revert'];
 
-    private IDbConnection $dbConn;
+    private DbConnection $dbConn;
     private DbStatementCache $cache;
 
     private array $tags = [];
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->dbConn = $dbConn;
         $this->cache = new DbStatementCache($dbConn);
     }
diff --git a/src/Changelog/ChangelogRoutes.php b/src/Changelog/ChangelogRoutes.php
index 0cdd9f08..18c23d60 100644
--- a/src/Changelog/ChangelogRoutes.php
+++ b/src/Changelog/ChangelogRoutes.php
@@ -3,18 +3,20 @@ namespace Misuzu\Changelog;
 
 use ErrorException;
 use RuntimeException;
-use Index\Http\Routing\{HttpGet,RouteHandler};
+use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
+use Index\Syndication\FeedBuilder;
+use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait};
 use Misuzu\{Pagination,SiteInfo,Template};
 use Misuzu\Auth\AuthInfo;
 use Misuzu\Comments\{Comments,CommentsEx};
-use Misuzu\Feeds\{Feed,FeedItem,AtomFeedSerializer,RssFeedSerializer};
-use Misuzu\URLs\{URLInfo,URLRegistry};
 use Misuzu\Users\UsersContext;
 
-final class ChangelogRoutes extends RouteHandler {
+final class ChangelogRoutes implements RouteHandler, UrlSource {
+    use RouteHandlerTrait, UrlSourceTrait;
+
     public function __construct(
         private SiteInfo $siteInfo,
-        private URLRegistry $urls,
+        private UrlRegistry $urls,
         private Changelog $changelog,
         private UsersContext $usersCtx,
         private AuthInfo $authInfo,
@@ -27,7 +29,7 @@ final class ChangelogRoutes extends RouteHandler {
     }
 
     #[HttpGet('/changelog')]
-    #[URLInfo('changelog-index', '/changelog', ['date' => '<date>', 'user' => '<user>', 'tags' => '<tags>', 'p' => '<page>'])]
+    #[UrlFormat('changelog-index', '/changelog', ['date' => '<date>', 'user' => '<user>', 'tags' => '<tags>', 'p' => '<page>'])]
     public function getIndex($response, $request) {
         $filterDate = (string)$request->getParam('date');
         $filterUser = (string)$request->getParam('user', FILTER_SANITIZE_NUMBER_INT);
@@ -96,8 +98,8 @@ final class ChangelogRoutes extends RouteHandler {
     }
 
     #[HttpGet('/changelog/change/([0-9]+)')]
-    #[URLInfo('changelog-change', '/changelog/change/<change>')]
-    #[URLInfo('changelog-change-comments', '/changelog/change/<change>', fragment: 'comments')]
+    #[UrlFormat('changelog-change', '/changelog/change/<change>')]
+    #[UrlFormat('changelog-change-comments', '/changelog/change/<change>', fragment: 'comments')]
     public function getChange($response, $request, string $changeId) {
         try {
             $changeInfo = $this->changelog->getChange($changeId);
@@ -117,44 +119,29 @@ final class ChangelogRoutes extends RouteHandler {
         ]);
     }
 
-    private function createFeed(string $feedMode): Feed {
+    #[HttpGet('/changelog.(xml|rss|atom)')]
+    #[UrlFormat('changelog-feed', '/changelog.xml')]
+    public function getFeed($response) {
+        $response->setContentType('application/rss+xml; charset=utf-8');
+
         $siteName = $this->siteInfo->getName();
+        $siteUrl = $this->siteInfo->getURL();
         $changes = $this->changelog->getChanges(pagination: new Pagination(10));
 
-        $feed = (new Feed)
-            ->setTitle($siteName . ' » Changelog')
-            ->setDescription('Live feed of changes to ' . $siteName . '.')
-            ->setContentUrl($this->siteInfo->getURL() . $this->urls->format('changelog-index'))
-            ->setFeedUrl($this->siteInfo->getURL() . $this->urls->format("changelog-feed-{$feedMode}"));
+        $feed = new FeedBuilder;
+        $feed->setTitle(sprintf('%s » Changelog', $siteName));
+        $feed->setDescription(sprintf('Live feed of changes to %s.', $siteName));
+        $feed->setContentUrl($siteUrl . $this->urls->format('changelog-index'));
+        $feed->setFeedUrl($siteUrl . $this->urls->format('changelog-feed'));
 
-        foreach($changes as $change) {
-            $changeUrl = $this->siteInfo->getURL() . $this->urls->format('changelog-change', ['change' => $change->getId()]);
-            $commentsUrl = $this->siteInfo->getURL() . $this->urls->format('changelog-change-comments', ['change' => $change->getId()]);
+        foreach($changes as $change)
+            $feed->createEntry(function($item) use ($change, $siteUrl) {
+                $item->setTitle(sprintf('%s: %s', $change->getActionText(), $change->getSummary()));
+                $item->setCreatedAt($change->getCreatedTime());
+                $item->setContentUrl($siteUrl . $this->urls->format('changelog-change', ['change' => $change->getId()]));
+                $item->setCommentsUrl($siteUrl . $this->urls->format('changelog-change-comments', ['change' => $change->getId()]));
+            });
 
-            $feedItem = (new FeedItem)
-                ->setTitle($change->getActionText() . ': ' . $change->getSummary())
-                ->setCreationDate($change->getCreatedTime())
-                ->setUniqueId($changeUrl)
-                ->setContentUrl($changeUrl)
-                ->setCommentsUrl($commentsUrl);
-
-            $feed->addItem($feedItem);
-        }
-
-        return $feed;
-    }
-
-    #[HttpGet('/changelog.rss')]
-    #[URLInfo('changelog-feed-rss', '/changelog.rss')]
-    public function getFeedRSS($response) {
-        $response->setContentType('application/rss+xml; charset=utf-8');
-        return (new RssFeedSerializer)->serializeFeed($this->createFeed('rss'));
-    }
-
-    #[HttpGet('/changelog.atom')]
-    #[URLInfo('changelog-feed-atom', '/changelog.atom')]
-    public function getFeedAtom($response) {
-        $response->setContentType('application/atom+xml; charset=utf-8');
-        return (new AtomFeedSerializer)->serializeFeed($this->createFeed('atom'));
+        return $feed->toXmlString();
     }
 }
diff --git a/src/Comments/Comments.php b/src/Comments/Comments.php
index 6e091e7d..df0e14e1 100644
--- a/src/Comments/Comments.php
+++ b/src/Comments/Comments.php
@@ -3,17 +3,15 @@ namespace Misuzu\Comments;
 
 use InvalidArgumentException;
 use RuntimeException;
-use Index\Data\DbStatementCache;
-use Index\Data\IDbConnection;
-use Index\Data\IDbResult;
+use Index\Db\{DbConnection,DbStatementCache};
 use Misuzu\Pagination;
 use Misuzu\Users\UserInfo;
 
 class Comments {
-    private IDbConnection $dbConn;
+    private DbConnection $dbConn;
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->dbConn = $dbConn;
         $this->cache = new DbStatementCache($dbConn);
     }
diff --git a/src/Comments/CommentsCategoryInfo.php b/src/Comments/CommentsCategoryInfo.php
index afd2df71..1c29ba0d 100644
--- a/src/Comments/CommentsCategoryInfo.php
+++ b/src/Comments/CommentsCategoryInfo.php
@@ -3,7 +3,7 @@ namespace Misuzu\Comments;
 
 use Misuzu\Users\UserInfo;
 use Carbon\CarbonImmutable;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class CommentsCategoryInfo {
     public function __construct(
@@ -15,7 +15,7 @@ class CommentsCategoryInfo {
         private int $comments,
     ) {}
 
-    public static function fromResult(IDbResult $result): CommentsCategoryInfo {
+    public static function fromResult(DbResult $result): CommentsCategoryInfo {
         return new CommentsCategoryInfo(
             id: $result->getString(0),
             name: $result->getString(1),
diff --git a/src/Comments/CommentsPostInfo.php b/src/Comments/CommentsPostInfo.php
index c3e811a7..997a1a10 100644
--- a/src/Comments/CommentsPostInfo.php
+++ b/src/Comments/CommentsPostInfo.php
@@ -2,7 +2,7 @@
 namespace Misuzu\Comments;
 
 use Carbon\CarbonImmutable;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class CommentsPostInfo {
     public function __construct(
diff --git a/src/Comments/CommentsPostVoteInfo.php b/src/Comments/CommentsPostVoteInfo.php
index 4a48a458..250d2c51 100644
--- a/src/Comments/CommentsPostVoteInfo.php
+++ b/src/Comments/CommentsPostVoteInfo.php
@@ -1,14 +1,14 @@
 <?php
 namespace Misuzu\Comments;
 
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class CommentsPostVoteInfo {
     private string $commentId;
     private string $userId;
     private int $weight;
 
-    public function __construct(IDbResult $result) {
+    public function __construct(DbResult $result) {
         $this->commentId = (string)$result->getInteger(0);
         $this->userId = (string)$result->getInteger(1);
         $this->weight = $result->getInteger(2);
diff --git a/src/Counters/CounterInfo.php b/src/Counters/CounterInfo.php
index 66967b61..980c7823 100644
--- a/src/Counters/CounterInfo.php
+++ b/src/Counters/CounterInfo.php
@@ -2,7 +2,7 @@
 namespace Misuzu\Counters;
 
 use Carbon\CarbonImmutable;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class CounterInfo {
     public function __construct(
@@ -11,7 +11,7 @@ class CounterInfo {
         private int $updated,
     ) {}
 
-    public static function fromResult(IDbResult $result): CounterInfo {
+    public static function fromResult(DbResult $result): CounterInfo {
         return new CounterInfo(
             name: $result->getString(0),
             value: $result->getInteger(1),
diff --git a/src/Counters/Counters.php b/src/Counters/Counters.php
index 858551ba..fb3d65a9 100644
--- a/src/Counters/Counters.php
+++ b/src/Counters/Counters.php
@@ -2,9 +2,7 @@
 namespace Misuzu\Counters;
 
 use InvalidArgumentException;
-use Index\Data\DbStatementCache;
-use Index\Data\DbTools;
-use Index\Data\IDbConnection;
+use Index\Db\{DbConnection,DbStatementCache,DbTools};
 use Misuzu\Pagination;
 
 // insert increment and decrement calls in places someday
@@ -12,7 +10,7 @@ use Misuzu\Pagination;
 class Counters {
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->cache = new DbStatementCache($dbConn);
     }
 
diff --git a/src/Emoticons/EmoteInfo.php b/src/Emoticons/EmoteInfo.php
index 576aa2be..0e8a0037 100644
--- a/src/Emoticons/EmoteInfo.php
+++ b/src/Emoticons/EmoteInfo.php
@@ -2,7 +2,7 @@
 namespace Misuzu\Emoticons;
 
 use Stringable;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class EmoteInfo implements Stringable {
     public function __construct(
@@ -12,7 +12,7 @@ class EmoteInfo implements Stringable {
         private string $url,
     ) {}
 
-    public static function fromResult(IDbResult $result): EmoteInfo {
+    public static function fromResult(DbResult $result): EmoteInfo {
         return new EmoteInfo(
             id: $result->getString(0),
             order: $result->getInteger(1),
diff --git a/src/Emoticons/EmoteStringInfo.php b/src/Emoticons/EmoteStringInfo.php
index 42b002a0..f1f8b356 100644
--- a/src/Emoticons/EmoteStringInfo.php
+++ b/src/Emoticons/EmoteStringInfo.php
@@ -2,7 +2,7 @@
 namespace Misuzu\Emoticons;
 
 use Stringable;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class EmoteStringInfo implements Stringable {
     public function __construct(
@@ -11,7 +11,7 @@ class EmoteStringInfo implements Stringable {
         private string $string,
     ) {}
 
-    public static function fromResult(IDbResult $result): EmoteStringInfo {
+    public static function fromResult(DbResult $result): EmoteStringInfo {
         return new EmoteStringInfo(
             emoteId: $result->getString(0),
             order: $result->getInteger(1),
diff --git a/src/Emoticons/Emotes.php b/src/Emoticons/Emotes.php
index 766fb173..26f5df9f 100644
--- a/src/Emoticons/Emotes.php
+++ b/src/Emoticons/Emotes.php
@@ -3,8 +3,7 @@ namespace Misuzu\Emoticons;
 
 use InvalidArgumentException;
 use RuntimeException;
-use Index\Data\DbStatementCache;
-use Index\Data\IDbConnection;
+use Index\Db\{DbConnection,DbStatementCache};
 
 class Emotes {
     private const EMOTE_ORDER = [
@@ -13,10 +12,10 @@ class Emotes {
         'rank' => 'emote_hierarchy',
     ];
 
-    private IDbConnection $dbConn;
+    private DbConnection $dbConn;
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->dbConn = $dbConn;
         $this->cache = new DbStatementCache($dbConn);
     }
diff --git a/src/Feeds/AtomFeedSerializer.php b/src/Feeds/AtomFeedSerializer.php
deleted file mode 100644
index 29052934..00000000
--- a/src/Feeds/AtomFeedSerializer.php
+++ /dev/null
@@ -1,117 +0,0 @@
-<?php
-namespace Misuzu\Feeds;
-
-use DOMDocument;
-use DOMElement;
-use DOMNode;
-
-class AtomFeedSerializer extends XmlFeedSerializer {
-    protected function formatTime(int $time): string {
-        return date('c', $time);
-    }
-
-    protected function createRoot(DOMDocument $document, Feed $feed): DOMNode {
-        $atom = $document->appendChild($document->createElement('feed'));
-        if($atom instanceof DOMElement)
-            $atom->setAttribute('xmlns', 'http://www.w3.org/2005/Atom');
-
-        $atom->appendChild(
-            $document->createElement(
-                'id',
-                $feed->hasContentUrl()
-                    ? $this->cleanString($feed->getContentUrl())
-                    : time()
-            )
-        );
-
-        return $atom;
-    }
-
-    protected function createTitle(DOMDocument $document, string $title): DOMNode {
-        return $document->createElement('title', $this->cleanString($title));
-    }
-
-    protected function createDescription(DOMDocument $document, string $description): ?DOMNode {
-        return $document->createElement('subtitle', $this->cleanString($description));
-    }
-
-    protected function createLastUpdate(DOMDocument $document, int $lastUpdate): ?DOMNode {
-        return $document->createElement('updated', $this->formatTime($lastUpdate));
-    }
-
-    protected function createContentUrl(DOMDocument $document, string $contentUrl): ?DOMNode {
-        $link = $document->createElement('link');
-        $link->setAttribute('href', $this->cleanString($contentUrl));
-        return $link;
-    }
-
-    protected function createFeedUrl(DOMDocument $document, string $feedUrl): ?DOMNode {
-        $link = $document->createElement('link');
-        $link->setAttribute('href', $this->cleanString($feedUrl));
-        $link->setAttribute('ref', 'self');
-        return $link;
-    }
-
-    protected function createItem(DOMDocument $document, FeedItem $feedItem): DOMNode {
-        $elem = $document->createElement('entry');
-
-        $elem->appendChild(
-            $document->createElement(
-                'id',
-                $feedItem->hasContentUrl()
-                    ? $this->cleanString($feedItem->getContentUrl())
-                    : time()
-            )
-        );
-
-        return $elem;
-    }
-
-    protected function createItemTitle(DOMDocument $document, string $title): DOMNode {
-        return $document->createElement('title', $this->cleanString($title));
-    }
-
-    protected function createItemSummary(DOMDocument $document, string $summary): ?DOMNode {
-        return $document->createElement('summary', $this->cleanString($summary));
-    }
-
-    protected function createItemContent(DOMDocument $document, string $content): ?DOMNode {
-        $elem = $document->createElement('content', $this->cleanString($content));
-        $elem->setAttribute('type', 'html');
-        return $elem;
-    }
-
-    protected function createItemCreationDate(DOMDocument $document, int $creationDate): ?DOMNode {
-        return $document->createElement('updated', $this->formatTime($creationDate));
-    }
-
-    protected function createItemUniqueId(DOMDocument $document, string $uniqueId): ?DOMNode {
-        return null;
-    }
-
-    protected function createItemContentUrl(DOMDocument $document, string $contentUrl): ?DOMNode {
-        $elem = $document->createElement('link');
-        $elem->setAttribute('href', $this->cleanString($contentUrl));
-        $elem->setAttribute('type', 'text/html');
-        return $elem;
-    }
-
-    protected function createItemCommentsUrl(DOMDocument $document, string $commentsUrl): ?DOMNode {
-        return null;
-    }
-
-    protected function createItemAuthor(DOMDocument $document, ?string $authorName, ?string $authorUrl): ?DOMNode {
-        if(empty($authorName) && empty($authorUrl))
-            return null;
-
-        $elem = $document->createElement('author');
-
-        if(!empty($authorName))
-            $elem->appendChild($document->createElement('name', $this->cleanString($authorName)));
-
-        if(!empty($authorUrl))
-            $elem->appendChild($document->createElement('uri', $this->cleanString($authorUrl)));
-
-        return $elem;
-    }
-}
diff --git a/src/Feeds/Feed.php b/src/Feeds/Feed.php
deleted file mode 100644
index ad59392b..00000000
--- a/src/Feeds/Feed.php
+++ /dev/null
@@ -1,74 +0,0 @@
-<?php
-namespace Misuzu\Feeds;
-
-class Feed {
-    private string $title = '';
-    private ?string $description = null;
-    private ?int $lastUpdate = null;
-    private ?string $contentUrl = null;
-    private ?string $feedUrl = null;
-    private array $feedItems = [];
-
-    public function getTitle(): string {
-        return $this->title;
-    }
-    public function setTitle(string $title): self {
-        $this->title = $title;
-        return $this;
-    }
-
-    public function getDescription(): string {
-        return $this->description ?? '';
-    }
-    public function hasDescription(): bool {
-        return isset($this->description);
-    }
-    public function setDescription(?string $description): self {
-        $this->description = $description;
-        return $this;
-    }
-
-    public function getLastUpdate(): int {
-        return $this->lastUpdate ?? 0;
-    }
-    public function hasLastUpdate(): bool {
-        return isset($this->lastUpdate);
-    }
-    public function setLastUpdate(?int $lastUpdate): self {
-        $this->lastUpdate = $lastUpdate;
-        return $this;
-    }
-
-    public function getContentUrl(): string {
-        return $this->contentUrl ?? '';
-    }
-    public function hasContentUrl(): bool {
-        return isset($this->contentUrl);
-    }
-    public function setContentUrl(?string $contentUrl): self {
-        $this->contentUrl = $contentUrl;
-        return $this;
-    }
-
-    public function getFeedUrl(): string {
-        return $this->feedUrl ?? '';
-    }
-    public function hasFeedUrl(): bool {
-        return isset($this->feedUrl);
-    }
-    public function setFeedUrl(?string $feedUrl): self {
-        $this->feedUrl = $feedUrl;
-        return $this;
-    }
-
-    public function getItems(): array {
-        return $this->feedItems;
-    }
-    public function hasItems(): bool {
-        return count($this->feedItems) > 0;
-    }
-    public function addItem(FeedItem $item): self {
-        $this->feedItems[] = $item;
-        return $this;
-    }
-}
diff --git a/src/Feeds/FeedItem.php b/src/Feeds/FeedItem.php
deleted file mode 100644
index 34d8e6d5..00000000
--- a/src/Feeds/FeedItem.php
+++ /dev/null
@@ -1,110 +0,0 @@
-<?php
-namespace Misuzu\Feeds;
-
-class FeedItem {
-    private string $title = '';
-    private ?string $summary = null;
-    private ?string $content = null;
-    private ?int $creationDate = null;
-    private ?string $uniqueId = null;
-    private ?string $contentUrl = null;
-    private ?string $commentsUrl = null;
-    private ?string $authorName = null;
-    private ?string $authorUrl = null;
-
-    public function getTitle(): string {
-        return $this->title;
-    }
-    public function setTitle(string $title): self {
-        $this->title = $title;
-        return $this;
-    }
-
-    public function getSummary(): string {
-        return $this->summary ?? '';
-    }
-    public function hasSummary(): bool {
-        return isset($this->summary);
-    }
-    public function setSummary(?string $summary): self {
-        $this->summary = $summary;
-        return $this;
-    }
-
-    public function getContent(): string {
-        return $this->content ?? '';
-    }
-    public function hasContent(): bool {
-        return isset($this->content);
-    }
-    public function setContent(?string $content): self {
-        $this->content = $content;
-        return $this;
-    }
-
-    public function getCreationDate(): int {
-        return $this->creationDate;
-    }
-    public function hasCreationDate(): bool {
-        return isset($this->creationDate);
-    }
-    public function setCreationDate(?int $creationDate): self {
-        $this->creationDate = $creationDate;
-        return $this;
-    }
-
-    public function getUniqueId(): string {
-        return $this->uniqueId ?? '';
-    }
-    public function hasUniqueId(): bool {
-        return isset($this->uniqueId);
-    }
-    public function setUniqueId(?string $uniqueId): self {
-        $this->uniqueId = $uniqueId;
-        return $this;
-    }
-
-    public function getContentUrl(): string {
-        return $this->contentUrl ?? '';
-    }
-    public function hasContentUrl(): bool {
-        return isset($this->contentUrl);
-    }
-    public function setContentUrl(?string $contentUrl): self {
-        $this->contentUrl = $contentUrl;
-        return $this;
-    }
-
-    public function getCommentsUrl(): string {
-        return $this->commentsUrl ?? '';
-    }
-    public function hasCommentsUrl(): bool {
-        return isset($this->commentsUrl);
-    }
-    public function setCommentsUrl(?string $commentsUrl): self {
-        $this->commentsUrl = $commentsUrl;
-        return $this;
-    }
-
-    public function getAuthorName(): string {
-        return $this->authorName ?? '';
-    }
-    public function hasAuthorName(): bool {
-        return isset($this->authorName);
-    }
-    public function setAuthorName(?string $authorName): self {
-        $this->authorName = $authorName;
-        return $this;
-    }
-
-    public function getAuthorUrl(): string {
-        return $this->authorUrl ?? '';
-    }
-    public function hasAuthorUrl(): bool {
-        return isset($this->authorUrl);
-    }
-    public function setAuthorUrl(?string $authorUrl): self {
-        $this->authorUrl = $authorUrl;
-        return $this;
-    }
-}
diff --git a/src/Feeds/FeedSerializer.php b/src/Feeds/FeedSerializer.php
deleted file mode 100644
index 6595b762..00000000
--- a/src/Feeds/FeedSerializer.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-namespace Misuzu\Feeds;
-
-abstract class FeedSerializer {
-    abstract public function serializeFeed(Feed $feed): string;
-}
diff --git a/src/Feeds/RssFeedSerializer.php b/src/Feeds/RssFeedSerializer.php
deleted file mode 100644
index f70758bf..00000000
--- a/src/Feeds/RssFeedSerializer.php
+++ /dev/null
@@ -1,85 +0,0 @@
-<?php
-namespace Misuzu\Feeds;
-
-use DOMDocument;
-use DOMElement;
-use DOMNode;
-
-class RssFeedSerializer extends XmlFeedSerializer {
-    protected function formatTime(int $time): string {
-        return date('r', $time);
-    }
-
-    protected function createRoot(DOMDocument $document, Feed $feed): DOMNode {
-        $rss = $document->appendChild($document->createElement('rss'));
-        if($rss instanceof DOMElement) {
-            $rss->setAttribute('version', '2.0');
-            $rss->setAttribute('xmlns:atom', 'http://www.w3.org/2005/Atom');
-        }
-
-        $channel = $rss->appendChild($document->createElement('channel'));
-        $channel->appendChild($document->createElement('ttl', '900'));
-        return $channel;
-    }
-
-    protected function createTitle(DOMDocument $document, string $title): DOMNode {
-        return $document->createElement('title', $this->cleanString($title));
-    }
-
-    protected function createDescription(DOMDocument $document, string $description): ?DOMNode {
-        return $document->createElement('description', $this->cleanString($description));
-    }
-
-    protected function createLastUpdate(DOMDocument $document, int $lastUpdate): ?DOMNode {
-        return $document->createElement('pubDate', $this->formatTime($lastUpdate));
-    }
-
-    protected function createContentUrl(DOMDocument $document, string $contentUrl): ?DOMNode {
-        return $document->createElement('link', $this->cleanString($contentUrl));
-    }
-
-    protected function createFeedUrl(DOMDocument $document, string $feedUrl): ?DOMNode {
-        $link = $document->createElement('atom:link');
-        $link->setAttribute('href', $this->cleanString($feedUrl));
-        $link->setAttribute('ref', 'self');
-        return $link;
-    }
-
-    protected function createItem(DOMDocument $document, FeedItem $feedItem): DOMNode {
-        return $document->createElement('item');
-    }
-
-    protected function createItemTitle(DOMDocument $document, string $title): DOMNode {
-        return $document->createElement('title', $this->cleanString($title));
-    }
-
-    protected function createItemSummary(DOMDocument $document, string $summary): ?DOMNode {
-        return $document->createElement('description', $this->cleanString($summary));
-    }
-
-    protected function createItemContent(DOMDocument $document, string $content): ?DOMNode {
-        return null;
-    }
-
-    protected function createItemCreationDate(DOMDocument $document, int $creationDate): ?DOMNode {
-        return $document->createElement('pubDate', $this->formatTime($creationDate));
-    }
-
-    protected function createItemUniqueId(DOMDocument $document, string $uniqueId): ?DOMNode {
-        $elem = $document->createElement('guid', $uniqueId);
-        $elem->setAttribute('isPermaLink', 'true');
-        return $elem;
-    }
-
-    protected function createItemContentUrl(DOMDocument $document, string $contentUrl): ?DOMNode {
-        return $document->createElement('link', $contentUrl);
-    }
-
-    protected function createItemCommentsUrl(DOMDocument $document, string $commentsUrl): ?DOMNode {
-        return $document->createElement('comments', $commentsUrl);
-    }
-
-    protected function createItemAuthor(DOMDocument $document, ?string $authorName, ?string $authorUrl): ?DOMNode {
-        return null;
-    }
-}
diff --git a/src/Feeds/XmlFeedSerializer.php b/src/Feeds/XmlFeedSerializer.php
deleted file mode 100644
index c7236a67..00000000
--- a/src/Feeds/XmlFeedSerializer.php
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-namespace Misuzu\Feeds;
-
-use DOMDocument;
-use DOMNode;
-
-abstract class XmlFeedSerializer extends FeedSerializer {
-    public function serializeFeed(Feed $feed): string {
-        $document = new DOMDocument('1.0', 'utf-8');
-        $root = $this->createRoot($document, $feed);
-        $root->appendChild($this->createTitle($document, $feed->getTitle()));
-
-        if($feed->hasDescription())
-            self::appendChild($root, $this->createDescription($document, $feed->getDescription()));
-        if($feed->hasLastUpdate())
-            self::appendChild($root, $this->createLastUpdate($document, $feed->getLastUpdate()));
-        if($feed->hasContentUrl())
-            self::appendChild($root, $this->createContentUrl($document, $feed->getContentUrl()));
-        if($feed->hasFeedUrl())
-            self::appendChild($root, $this->createFeedUrl($document, $feed->getFeedUrl()));
-
-        if($feed->hasItems()) {
-            foreach($feed->getItems() as $item) {
-                $root->appendChild($this->serializeFeedItem($document, $item));
-            }
-        }
-
-        return $document->saveXML();
-    }
-
-    private function serializeFeedItem(DOMDocument $document, FeedItem $feedItem): DOMNode {
-        $elem = $this->createItem($document, $feedItem);
-        $elem->appendChild($this->createItemTitle($document, $feedItem->getTitle()));
-
-        if($feedItem->hasSummary())
-            self::appendChild($elem, $this->createItemSummary($document, $feedItem->getSummary()));
-        if($feedItem->hasContent())
-            self::appendChild($elem, $this->createItemContent($document, $feedItem->getContent()));
-        if($feedItem->hasCreationDate())
-            self::appendChild($elem, $this->createItemCreationDate($document, $feedItem->getCreationDate()));
-        if($feedItem->hasUniqueId())
-            self::appendChild($elem, $this->createItemUniqueId($document, $feedItem->getUniqueId()));
-        if($feedItem->hasContentUrl())
-            self::appendChild($elem, $this->createItemContentUrl($document, $feedItem->getContentUrl()));
-        if($feedItem->hasCommentsUrl())
-            self::appendChild($elem, $this->createItemCommentsUrl($document, $feedItem->getCommentsUrl()));
-        if($feedItem->hasAuthorName() || $feedItem->hasAuthorUrl())
-            self::appendChild($elem, $this->createItemAuthor($document, $feedItem->getAuthorName(), $feedItem->getAuthorUrl()));
-
-        return $elem;
-    }
-
-    protected function cleanString(string $string): string {
-        return htmlspecialchars($string, ENT_XML1 | ENT_COMPAT | ENT_SUBSTITUTE);
-    }
-
-    protected static function appendChild(DOMNode $parent, ?DOMNode $elem): ?DOMNode {
-        if($elem !== null)
-            return $parent->appendChild($elem);
-        return $elem;
-    }
-
-    abstract protected function formatTime(int $time): string;
-    abstract protected function createRoot(DOMDocument $document, Feed $feed): DOMNode;
-    abstract protected function createTitle(DOMDocument $document, string $title): DOMNode;
-    abstract protected function createDescription(DOMDocument $document, string $description): ?DOMNode;
-    abstract protected function createLastUpdate(DOMDocument $document, int $lastUpdate): ?DOMNode;
-    abstract protected function createContentUrl(DOMDocument $document, string $contentUrl): ?DOMNode;
-    abstract protected function createFeedUrl(DOMDocument $document, string $feedUrl): ?DOMNode;
-    abstract protected function createItem(DOMDocument $document, FeedItem $feedItem): DOMNode;
-    abstract protected function createItemTitle(DOMDocument $document, string $title): DOMNode;
-    abstract protected function createItemSummary(DOMDocument $document, string $summary): ?DOMNode;
-    abstract protected function createItemContent(DOMDocument $document, string $content): ?DOMNode;
-    abstract protected function createItemCreationDate(DOMDocument $document, int $creationDate): ?DOMNode;
-    abstract protected function createItemUniqueId(DOMDocument $document, string $uniqueId): ?DOMNode;
-    abstract protected function createItemContentUrl(DOMDocument $document, string $contentUrl): ?DOMNode;
-    abstract protected function createItemCommentsUrl(DOMDocument $document, string $commentsUrl): ?DOMNode;
-    abstract protected function createItemAuthor(DOMDocument $document, ?string $authorName, ?string $authorUrl): ?DOMNode;
-}
diff --git a/src/Forum/ForumCategories.php b/src/Forum/ForumCategories.php
index d8d4def7..95d304f0 100644
--- a/src/Forum/ForumCategories.php
+++ b/src/Forum/ForumCategories.php
@@ -5,16 +5,14 @@ use InvalidArgumentException;
 use RuntimeException;
 use stdClass;
 use Index\Colour\Colour;
-use Index\Data\DbStatementCache;
-use Index\Data\DbTools;
-use Index\Data\IDbConnection;
+use Index\Db\{DbConnection,DbStatementCache,DbTools};
 use Misuzu\Pagination;
 use Misuzu\Users\UserInfo;
 
 class ForumCategories {
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->cache = new DbStatementCache($dbConn);
     }
 
diff --git a/src/Forum/ForumCategoryInfo.php b/src/Forum/ForumCategoryInfo.php
index 89259217..822d6840 100644
--- a/src/Forum/ForumCategoryInfo.php
+++ b/src/Forum/ForumCategoryInfo.php
@@ -3,7 +3,7 @@ namespace Misuzu\Forum;
 
 use Carbon\CarbonImmutable;
 use Index\Colour\Colour;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class ForumCategoryInfo {
     // should the types just be replaced with flags indicating what's allowed?
@@ -44,7 +44,7 @@ class ForumCategoryInfo {
         private int $postsCount,
     ) {}
 
-    public static function fromResult(IDbResult $result): ForumCategoryInfo {
+    public static function fromResult(DbResult $result): ForumCategoryInfo {
         return new ForumCategoryInfo(
             id: $result->getString(0),
             order: $result->getInteger(1),
diff --git a/src/Forum/ForumContext.php b/src/Forum/ForumContext.php
index 14272485..e361e629 100644
--- a/src/Forum/ForumContext.php
+++ b/src/Forum/ForumContext.php
@@ -2,7 +2,7 @@
 namespace Misuzu\Forum;
 
 use stdClass;
-use Index\Data\IDbConnection;
+use Index\Db\DbConnection;
 use Misuzu\Users\UserInfo;
 
 class ForumContext {
@@ -14,7 +14,7 @@ class ForumContext {
     private array $totalUserTopics = [];
     private array $totalUserPosts = [];
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->categories = new ForumCategories($dbConn);
         $this->topics = new ForumTopics($dbConn);
         $this->topicRedirects = new ForumTopicRedirects($dbConn);
diff --git a/src/Forum/ForumPostInfo.php b/src/Forum/ForumPostInfo.php
index 85d40141..a41fff60 100644
--- a/src/Forum/ForumPostInfo.php
+++ b/src/Forum/ForumPostInfo.php
@@ -4,7 +4,7 @@ namespace Misuzu\Forum;
 use Misuzu\Parsers\Parser;
 use Carbon\CarbonImmutable;
 use Index\XDateTime;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 use Index\Net\IPAddress;
 
 class ForumPostInfo {
@@ -22,7 +22,7 @@ class ForumPostInfo {
         private ?int $deleted,
     ) {}
 
-    public static function fromResult(IDbResult $result): ForumPostInfo {
+    public static function fromResult(DbResult $result): ForumPostInfo {
         return new ForumPostInfo(
             id: $result->getString(0),
             topicId: $result->getString(1),
diff --git a/src/Forum/ForumPosts.php b/src/Forum/ForumPosts.php
index 7fe4e8a8..e9c05276 100644
--- a/src/Forum/ForumPosts.php
+++ b/src/Forum/ForumPosts.php
@@ -5,18 +5,16 @@ use InvalidArgumentException;
 use RuntimeException;
 use stdClass;
 use Carbon\CarbonImmutable;
-use Index\Data\DbStatementCache;
-use Index\Data\DbTools;
-use Index\Data\IDbConnection;
+use Index\Db\{DbConnection,DbStatementCache,DbTools};
 use Index\Net\IPAddress;
 use Misuzu\Pagination;
 use Misuzu\Users\UserInfo;
 
 class ForumPosts {
-    private IDbConnection $dbConn;
+    private DbConnection $dbConn;
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->dbConn = $dbConn;
         $this->cache = new DbStatementCache($dbConn);
     }
diff --git a/src/Forum/ForumTopicInfo.php b/src/Forum/ForumTopicInfo.php
index 2376af60..2278dcbe 100644
--- a/src/Forum/ForumTopicInfo.php
+++ b/src/Forum/ForumTopicInfo.php
@@ -3,7 +3,7 @@ namespace Misuzu\Forum;
 
 use Carbon\CarbonImmutable;
 use Index\XDateTime;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class ForumTopicInfo {
     public const TYPE_DISCUSSION = 0;
@@ -33,7 +33,7 @@ class ForumTopicInfo {
         private ?int $locked,
     ) {}
 
-    public static function fromResult(IDbResult $result): ForumTopicInfo {
+    public static function fromResult(DbResult $result): ForumTopicInfo {
         return new ForumTopicInfo(
             id: $result->getString(0),
             categoryId: $result->getString(1),
diff --git a/src/Forum/ForumTopicRedirectInfo.php b/src/Forum/ForumTopicRedirectInfo.php
index 74aeeae5..4e42f1a7 100644
--- a/src/Forum/ForumTopicRedirectInfo.php
+++ b/src/Forum/ForumTopicRedirectInfo.php
@@ -2,7 +2,7 @@
 namespace Misuzu\Forum;
 
 use Carbon\CarbonImmutable;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class ForumTopicRedirectInfo {
     public function __construct(
@@ -12,7 +12,7 @@ class ForumTopicRedirectInfo {
         private int $created,
     ) {}
 
-    public static function fromResult(IDbResult $result): ForumTopicRedirectInfo {
+    public static function fromResult(DbResult $result): ForumTopicRedirectInfo {
         return new ForumTopicRedirectInfo(
             topicId: $result->getString(0),
             userId: $result->getStringOrNull(1),
diff --git a/src/Forum/ForumTopicRedirects.php b/src/Forum/ForumTopicRedirects.php
index 71049522..e0e8fa34 100644
--- a/src/Forum/ForumTopicRedirects.php
+++ b/src/Forum/ForumTopicRedirects.php
@@ -2,15 +2,14 @@
 namespace Misuzu\Forum;
 
 use RuntimeException;
-use Index\Data\DbStatementCache;
-use Index\Data\IDbConnection;
+use Index\Db\{DbConnection,DbStatementCache};
 use Misuzu\Pagination;
 use Misuzu\Users\UserInfo;
 
 class ForumTopicRedirects {
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->cache = new DbStatementCache($dbConn);
     }
 
diff --git a/src/Forum/ForumTopics.php b/src/Forum/ForumTopics.php
index a2747891..bbf869ce 100644
--- a/src/Forum/ForumTopics.php
+++ b/src/Forum/ForumTopics.php
@@ -4,17 +4,15 @@ namespace Misuzu\Forum;
 use InvalidArgumentException;
 use RuntimeException;
 use stdClass;
-use Index\Data\DbStatementCache;
-use Index\Data\DbTools;
-use Index\Data\IDbConnection;
+use Index\Db\{DbConnection,DbStatementCache,DbTools};
 use Misuzu\Pagination;
 use Misuzu\Users\UserInfo;
 
 class ForumTopics {
-    private IDbConnection $dbConn;
+    private DbConnection $dbConn;
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->dbConn = $dbConn;
         $this->cache = new DbStatementCache($dbConn);
     }
diff --git a/src/Hanyuu/HanyuuRpcActions.php b/src/Hanyuu/HanyuuRpcActions.php
index 7f4cb2c2..3e4fac66 100644
--- a/src/Hanyuu/HanyuuRpcActions.php
+++ b/src/Hanyuu/HanyuuRpcActions.php
@@ -4,17 +4,17 @@ namespace Misuzu\Hanyuu;
 use RuntimeException;
 use Misuzu\CSRF;
 use Misuzu\Auth\AuthContext;
-use Misuzu\URLs\URLRegistry;
 use Misuzu\Users\{UsersContext,UserInfo};
 use Aiwass\Server\{RpcActionHandler,RpcProcedure};
 use Index\Colour\Colour;
-use Syokuhou\IConfig;
+use Index\Config\Config;
+use Index\Urls\UrlRegistry;
 
 final class HanyuuRpcActions extends RpcActionHandler {
     public function __construct(
         private $getBaseUrl,
-        private IConfig $impersonateConfig,
-        private URLRegistry $urls,
+        private Config $impersonateConfig,
+        private UrlRegistry $urls,
         private UsersContext $usersCtx,
         private AuthContext $authCtx
     ) {}
diff --git a/src/Home/HomeRoutes.php b/src/Home/HomeRoutes.php
index 6fa98077..0e2c857c 100644
--- a/src/Home/HomeRoutes.php
+++ b/src/Home/HomeRoutes.php
@@ -3,22 +3,24 @@ namespace Misuzu\Home;
 
 use RuntimeException;
 use Index\XDateTime;
-use Index\Data\{DbTools,IDbConnection};
-use Index\Http\Routing\{HttpGet,RouteHandler};
-use Syokuhou\IConfig;
+use Index\Config\Config;
+use Index\Db\{DbConnection,DbTools};
+use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
+use Index\Urls\{UrlFormat,UrlSource,UrlSourceTrait};
 use Misuzu\{Pagination,SiteInfo,Template};
 use Misuzu\Auth\AuthInfo;
 use Misuzu\Changelog\Changelog;
 use Misuzu\Comments\Comments;
 use Misuzu\Counters\Counters;
 use Misuzu\News\News;
-use Misuzu\URLs\URLInfo;
 use Misuzu\Users\UsersContext;
 
-class HomeRoutes extends RouteHandler {
+class HomeRoutes implements RouteHandler, UrlSource {
+    use RouteHandlerTrait, UrlSourceTrait;
+
     public function __construct(
-        private IConfig $config,
-        private IDbConnection $dbConn,
+        private Config $config,
+        private DbConnection $dbConn,
         private SiteInfo $siteInfo,
         private AuthInfo $authInfo,
         private Changelog $changelog,
@@ -151,7 +153,7 @@ class HomeRoutes extends RouteHandler {
     }
 
     #[HttpGet('/')]
-    #[URLInfo('index', '/')]
+    #[UrlFormat('index', '/')]
     public function getIndex(...$args) {
         return $this->authInfo->isLoggedIn()
             ? $this->getHome(...$args)
diff --git a/src/Info/InfoRoutes.php b/src/Info/InfoRoutes.php
index 3cd57e86..c9574878 100644
--- a/src/Info/InfoRoutes.php
+++ b/src/Info/InfoRoutes.php
@@ -1,12 +1,14 @@
 <?php
 namespace Misuzu\Info;
 
-use Index\Http\Routing\{HttpGet,RouteHandler};
+use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
+use Index\Urls\{UrlFormat,UrlSource,UrlSourceTrait};
 use Misuzu\Template;
 use Misuzu\Parsers\Parser;
-use Misuzu\URLs\URLInfo;
 
-class InfoRoutes extends RouteHandler {
+class InfoRoutes implements RouteHandler, UrlSource {
+    use RouteHandlerTrait, UrlSourceTrait;
+
     private const DOCS_PATH = MSZ_ROOT . '/docs';
     private const PROJECT_PATHS = [
         'misuzu' => MSZ_ROOT,
@@ -18,14 +20,14 @@ class InfoRoutes extends RouteHandler {
     ];
 
     #[HttpGet('/info')]
-    #[URLInfo('info-index', '/info')]
+    #[UrlFormat('info-index', '/info')]
     public function getIndex() {
         return Template::renderRaw('info.index');
     }
 
     #[HttpGet('/info/([A-Za-z0-9_]+)')]
-    #[URLInfo('info', '/info/<title>')]
-    #[URLInfo('info-doc', '/info/<title>')]
+    #[UrlFormat('info', '/info/<title>')]
+    #[UrlFormat('info-doc', '/info/<title>')]
     public function getDocsPage($response, $request, string $name) {
         return $this->serveMarkdownDocument(
             sprintf('%s/%s.md', self::DOCS_PATH, $name)
@@ -71,7 +73,7 @@ class InfoRoutes extends RouteHandler {
     }
 
     #[HttpGet('/info/([A-Za-z0-9_]+)/([A-Za-z0-9_]+)')]
-    #[URLInfo('info-project-doc', '/info/<project>/<title>')]
+    #[UrlFormat('info-project-doc', '/info/<project>/<title>')]
     public function getProjectPage($response, $request, string $project, string $name) {
         if(!array_key_exists($project, self::PROJECT_PATHS))
             return 404;
diff --git a/src/LegacyRoutes.php b/src/LegacyRoutes.php
index a9ac8ebc..2e5cf1a6 100644
--- a/src/LegacyRoutes.php
+++ b/src/LegacyRoutes.php
@@ -1,15 +1,17 @@
 <?php
 namespace Misuzu;
 
-use Index\Http\Routing\{HttpGet,RouteHandler};
-use Misuzu\URLs\{IURLSource,URLInfo,URLRegistry};
+use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
+use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait};
+
+class LegacyRoutes implements RouteHandler, UrlSource {
+    use RouteHandlerTrait, UrlSourceTrait;
 
-class LegacyRoutes extends RouteHandler implements IURLSource {
     public function __construct(
-        private URLRegistry $urls
+        private UrlRegistry $urls
     ) {}
 
-    public function registerURLs(URLRegistry $urls): void {
+    public function registerUrls(UrlRegistry $urls): void {
         // eventually this should be handled by context classes
         $urls->register('search-index', '/search.php');
         $urls->register('search-query', '/search.php', ['q' => '<query>'], '<section>');
@@ -156,20 +158,6 @@ class LegacyRoutes extends RouteHandler implements IURLSource {
         $response->redirect($location, true);
     }
 
-    #[HttpGet('/news.php/rss')]
-    public function getNewsRssPHP($response, $request): void {
-        $catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
-        $location = $this->urls->format($catId > 0 ? 'news-category-feed-rss' : 'news-feed-rss', ['category' => $catId]);
-        $response->redirect($location, true);
-    }
-
-    #[HttpGet('/news.php/atom')]
-    public function getNewsAtomPHP($response, $request): void {
-        $catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
-        $location = $this->urls->format($catId > 0 ? 'news-category-feed-atom' : 'news-feed-atom', ['category' => $catId]);
-        $response->redirect($location, true);
-    }
-
     #[HttpGet('/news/index.php')]
     public function getNewsIndexPHP($response, $request): void {
         $response->redirect($this->urls->format('news-index', [
@@ -192,25 +180,15 @@ class LegacyRoutes extends RouteHandler implements IURLSource {
         ]), true);
     }
 
+    #[HttpGet('/news.php/rss')]
+    #[HttpGet('/news.php/atom')]
     #[HttpGet('/news/feed.php')]
-    public function getNewsFeedPHP($response, $request): int {
-        return 400;
-    }
-
     #[HttpGet('/news/feed.php/rss')]
-    public function getNewsFeedRssPHP($response, $request): void {
-        $catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
-        $response->redirect($this->urls->format(
-            $catId > 0 ? 'news-category-feed-rss' : 'news-feed-rss',
-            ['category' => $catId]
-        ), true);
-    }
-
     #[HttpGet('/news/feed.php/atom')]
-    public function getNewsFeedAtomPHP($response, $request): void {
+    public function getNewsFeedPHP($response, $request): void {
         $catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
         $response->redirect($this->urls->format(
-            $catId > 0 ? 'news-category-feed-atom' : 'news-feed-atom',
+            $catId > 0 ? 'news-category-feed' : 'news-feed',
             ['category' => $catId]
         ), true);
     }
@@ -262,7 +240,7 @@ class LegacyRoutes extends RouteHandler implements IURLSource {
     }
 
     #[HttpGet('/manage')]
-    #[URLInfo('manage-index', '/manage')]
+    #[UrlFormat('manage-index', '/manage')]
     public function getManageIndex($response): void {
         $response->redirect($this->urls->format('manage-general-overview'));
     }
diff --git a/src/Mailer.php b/src/Mailer.php
index 27105fa4..2c11a182 100644
--- a/src/Mailer.php
+++ b/src/Mailer.php
@@ -2,7 +2,7 @@
 namespace Misuzu;
 
 use InvalidArgumentException;
-use Syokuhou\IConfig;
+use Index\Config\Config;
 use Symfony\Component\Mime\Email as SymfonyMessage;
 use Symfony\Component\Mime\Address as SymfonyAddress;
 use Symfony\Component\Mailer\Transport as SymfonyTransport;
@@ -10,10 +10,10 @@ use Symfony\Component\Mailer\Transport as SymfonyTransport;
 final class Mailer {
     private const TEMPLATE_PATH = MSZ_ROOT . '/config/emails/%s.txt';
 
-    private static IConfig $config;
+    private static Config $config;
     private static $transport = null;
 
-    public static function init(IConfig $config): void {
+    public static function init(Config $config): void {
         self::$config = $config;
     }
 
diff --git a/src/Messages/MessageInfo.php b/src/Messages/MessageInfo.php
index 60c96a19..caa804ee 100644
--- a/src/Messages/MessageInfo.php
+++ b/src/Messages/MessageInfo.php
@@ -3,7 +3,7 @@ namespace Misuzu\Messages;
 
 use Misuzu\Parsers\Parser;
 use Carbon\CarbonImmutable;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class MessageInfo {
     public function __construct(
@@ -21,7 +21,7 @@ class MessageInfo {
         private ?int $deleted,
     ) {}
 
-    public static function fromResult(IDbResult $result): MessageInfo {
+    public static function fromResult(DbResult $result): MessageInfo {
         return new MessageInfo(
             messageId: $result->getString(0),
             ownerId: $result->getString(1),
diff --git a/src/Messages/MessagesContext.php b/src/Messages/MessagesContext.php
index 10d45003..f1144cb5 100644
--- a/src/Messages/MessagesContext.php
+++ b/src/Messages/MessagesContext.php
@@ -1,11 +1,12 @@
 <?php
 namespace Misuzu\Messages;
 
-use Index\Data\IDbConnection;
+use Index\Db\DbConnection;
+
 class MessagesContext {
     private MessagesDatabase $database;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->database = new MessagesDatabase($dbConn);
     }
 
diff --git a/src/Messages/MessagesDatabase.php b/src/Messages/MessagesDatabase.php
index a3b0ce4c..c634fb74 100644
--- a/src/Messages/MessagesDatabase.php
+++ b/src/Messages/MessagesDatabase.php
@@ -4,14 +4,14 @@ namespace Misuzu\Messages;
 use InvalidArgumentException;
 use RuntimeException;
 use DateTimeInterface;
-use Index\Data\{DbStatementCache,DbTools,IDbConnection};
+use Index\Db\{DbConnection,DbStatementCache,DbTools};
 use Misuzu\Pagination;
 use Misuzu\Users\UserInfo;
 
 class MessagesDatabase {
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->cache = new DbStatementCache($dbConn);
     }
 
diff --git a/src/Messages/MessagesRoutes.php b/src/Messages/MessagesRoutes.php
index b6d40853..0a984647 100644
--- a/src/Messages/MessagesRoutes.php
+++ b/src/Messages/MessagesRoutes.php
@@ -5,16 +5,18 @@ use stdClass;
 use InvalidArgumentException;
 use RuntimeException;
 use Index\XString;
-use Index\Http\Routing\{HttpGet,HttpMiddleware,HttpPost,RouteHandler};
-use Syokuhou\IConfig;
+use Index\Config\Config;
+use Index\Http\Routing\{HttpGet,HttpMiddleware,HttpPost,RouteHandler,RouteHandlerTrait};
+use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait};
 use Misuzu\{CSRF,Pagination,Perm,Template};
 use Misuzu\Auth\AuthInfo;
 use Misuzu\Parsers\Parser;
 use Misuzu\Perms\Permissions;
-use Misuzu\URLs\{URLInfo,URLRegistry};
 use Misuzu\Users\{UsersContext,UserInfo};
 
-class MessagesRoutes extends RouteHandler {
+class MessagesRoutes implements RouteHandler, UrlSource {
+    use RouteHandlerTrait, UrlSourceTrait;
+
     public const FOLDER_META = [
         'inbox' => [ 'title' => 'Inbox', 'icon' => 'fas fa-inbox fa-fw' ],
         'drafts' => [ 'title' => 'Drafts', 'icon' => 'fas fa-pencil-alt fa-fw' ],
@@ -23,8 +25,8 @@ class MessagesRoutes extends RouteHandler {
     ];
 
     public function __construct(
-        private IConfig $config,
-        private URLRegistry $urls,
+        private Config $config,
+        private UrlRegistry $urls,
         private AuthInfo $authInfo,
         private MessagesContext $msgsCtx,
         private UsersContext $usersCtx,
@@ -77,7 +79,7 @@ class MessagesRoutes extends RouteHandler {
     }
 
     #[HttpGet('/messages')]
-    #[URLInfo('messages-index', '/messages', ['folder' => '<folder>', 'page' => '<page>'])]
+    #[UrlFormat('messages-index', '/messages', ['folder' => '<folder>', 'page' => '<page>'])]
     public function getIndex($response, $request, string $folderName = '') {
         $folderName = (string)$request->getParam('folder');
         if($folderName === '')
@@ -130,7 +132,7 @@ class MessagesRoutes extends RouteHandler {
     }
 
     #[HttpGet('/messages/stats')]
-    #[URLInfo('messages-stats', '/messages/stats')]
+    #[UrlFormat('messages-stats', '/messages/stats')]
     public function getStats() {
         $selfInfo = $this->authInfo->getUserInfo();
         $msgsDb = $this->msgsCtx->getDatabase();
@@ -147,7 +149,7 @@ class MessagesRoutes extends RouteHandler {
     }
 
     #[HttpPost('/messages/recipient')]
-    #[URLInfo('messages-recipient', '/messages/recipient')]
+    #[UrlFormat('messages-recipient', '/messages/recipient')]
     public function postRecipient($response, $request) {
         if(!$request->isFormContent())
             return 400;
@@ -187,7 +189,7 @@ class MessagesRoutes extends RouteHandler {
     }
 
     #[HttpGet('/messages/compose')]
-    #[URLInfo('messages-compose', '/messages/compose', ['recipient' => '<recipient>'])]
+    #[UrlFormat('messages-compose', '/messages/compose', ['recipient' => '<recipient>'])]
     public function getEditor($response, $request) {
         if(!$this->canSendMessages)
             return 403;
@@ -198,7 +200,7 @@ class MessagesRoutes extends RouteHandler {
     }
 
     #[HttpGet('/messages/([A-Za-z0-9]+)')]
-    #[URLInfo('messages-view', '/messages/<message>')]
+    #[UrlFormat('messages-view', '/messages/<message>')]
     public function getView($response, $request, string $messageId) {
         if(strlen($messageId) !== 8)
             return 404;
@@ -328,7 +330,7 @@ class MessagesRoutes extends RouteHandler {
     }
 
     #[HttpPost('/messages/create')]
-    #[URLInfo('messages-create', '/messages/create')]
+    #[UrlFormat('messages-create', '/messages/create')]
     public function postCreate($response, $request) {
         if(!$request->isFormContent())
             return 400;
@@ -431,7 +433,7 @@ class MessagesRoutes extends RouteHandler {
     }
 
     #[HttpPost('/messages/([A-Za-z0-9]+)')]
-    #[URLInfo('messages-update', '/messages/<message>')]
+    #[UrlFormat('messages-update', '/messages/<message>')]
     public function postUpdate($response, $request, string $messageId) {
         if(!$request->isFormContent())
             return 400;
@@ -522,7 +524,7 @@ class MessagesRoutes extends RouteHandler {
     }
 
     #[HttpPost('/messages/mark')]
-    #[URLInfo('messages-mark', '/messages/mark')]
+    #[UrlFormat('messages-mark', '/messages/mark')]
     public function postMark($response, $request) {
         if(!$request->isFormContent())
             return 400;
@@ -553,7 +555,7 @@ class MessagesRoutes extends RouteHandler {
     }
 
     #[HttpPost('/messages/delete')]
-    #[URLInfo('messages-delete', '/messages/delete')]
+    #[UrlFormat('messages-delete', '/messages/delete')]
     public function postDelete($response, $request) {
         if(!$request->isFormContent())
             return 400;
@@ -580,7 +582,7 @@ class MessagesRoutes extends RouteHandler {
     }
 
     #[HttpPost('/messages/restore')]
-    #[URLInfo('messages-restore', '/messages/restore')]
+    #[UrlFormat('messages-restore', '/messages/restore')]
     public function postRestore($response, $request) {
         if(!$request->isFormContent())
             return 400;
@@ -607,7 +609,7 @@ class MessagesRoutes extends RouteHandler {
     }
 
     #[HttpPost('/messages/nuke')]
-    #[URLInfo('messages-nuke', '/messages/nuke')]
+    #[UrlFormat('messages-nuke', '/messages/nuke')]
     public function postNuke($response, $request) {
         if(!$request->isFormContent())
             return 400;
diff --git a/src/MisuzuContext.php b/src/MisuzuContext.php
index f559f3af..20fb654f 100644
--- a/src/MisuzuContext.php
+++ b/src/MisuzuContext.php
@@ -13,14 +13,14 @@ use Misuzu\Messages\MessagesContext;
 use Misuzu\News\News;
 use Misuzu\Perms\Permissions;
 use Misuzu\Profile\ProfileFields;
-use Misuzu\URLs\URLRegistry;
 use Misuzu\Users\{UsersContext,UserInfo};
 use Aiwass\HmacVerificationProvider;
 use Aiwass\Server\RpcServer;
-use Index\Data\IDbConnection;
-use Index\Data\Migration\{IDbMigrationRepo,DbMigrationManager,FsDbMigrationRepo};
-use Sasae\SasaeEnvironment;
-use Syokuhou\IConfig;
+use Index\Config\Config;
+use Index\Db\DbConnection;
+use Index\Db\Migration\{DbMigrationManager,DbMigrationRepo,FsDbMigrationRepo};
+use Index\Templating\TplEnvironment;
+use Index\Urls\UrlRegistry;
 
 // this class should function as the root for everything going forward
 // no more magical static classes that are just kind of assumed to exist
@@ -29,9 +29,9 @@ use Syokuhou\IConfig;
 //  dunno if i want null checks some maybe some kind of init func should be called first like is the case
 //  with the http shit
 class MisuzuContext {
-    private IDbConnection $dbConn;
-    private IConfig $config;
-    private SasaeEnvironment $templating;
+    private DbConnection $dbConn;
+    private Config $config;
+    private TplEnvironment $templating;
 
     private AuditLog $auditLog;
     private Counters $counters;
@@ -53,9 +53,9 @@ class MisuzuContext {
     private SiteInfo $siteInfo;
 
     // this probably shouldn't be available
-    private URLRegistry $urls;
+    private UrlRegistry $urls;
 
-    public function __construct(IDbConnection $dbConn, IConfig $config) {
+    public function __construct(DbConnection $dbConn, Config $config) {
         $this->dbConn = $dbConn;
         $this->config = $config;
 
@@ -77,7 +77,7 @@ class MisuzuContext {
         $this->profileFields = new ProfileFields($dbConn);
     }
 
-    public function getDbConn(): IDbConnection {
+    public function getDbConn(): DbConnection {
         return $this->dbConn;
     }
 
@@ -90,15 +90,15 @@ class MisuzuContext {
         return new DbMigrationManager($this->dbConn, 'msz_' . DbMigrationManager::DEFAULT_TABLE);
     }
 
-    public function createMigrationRepo(): IDbMigrationRepo {
+    public function createMigrationRepo(): DbMigrationRepo {
         return new FsDbMigrationRepo(MSZ_MIGRATIONS);
     }
 
-    public function getURLs(): URLRegistry {
+    public function getUrls(): UrlRegistry {
         return $this->urls;
     }
 
-    public function getConfig(): IConfig {
+    public function getConfig(): Config {
         return $this->config;
     }
 
@@ -198,12 +198,12 @@ class MisuzuContext {
         $globals['active_ban_info'] = $this->usersCtx->tryGetActiveBan($this->authInfo->getUserInfo());
         $globals['display_timings_info'] = $isDebug || $this->authInfo->getPerms('global')->check(Perm::G_TIMINGS_VIEW);
 
-        $this->templating = new SasaeEnvironment(
+        $this->templating = new TplEnvironment(
             MSZ_TEMPLATES,
             cache: $isDebug ? null : ['Misuzu', GitInfo::hash(true)],
             debug: $isDebug
         );
-        $this->templating->addExtension(new MisuzuSasaeExtension($this));
+        $this->templating->addExtension(new TemplatingExtension($this));
         $this->templating->addGlobal('globals', $globals);
 
         Template::init($this->templating);
@@ -212,7 +212,7 @@ class MisuzuContext {
     public function createRouting(): RoutingContext {
         $routingCtx = new RoutingContext();
 
-        $this->urls = $routingCtx->getURLs();
+        $this->urls = $routingCtx->getUrls();
 
         $routingCtx->register(new \Misuzu\Home\HomeRoutes(
             $this->config,
diff --git a/src/News/News.php b/src/News/News.php
index 877ac8b8..586a6427 100644
--- a/src/News/News.php
+++ b/src/News/News.php
@@ -4,18 +4,16 @@ namespace Misuzu\News;
 use InvalidArgumentException;
 use RuntimeException;
 use DateTimeInterface;
-use Index\Data\DbStatementCache;
-use Index\Data\IDbConnection;
-use Index\Data\IDbResult;
+use Index\Db\{DbConnection,DbStatementCache};
 use Misuzu\Pagination;
 use Misuzu\Comments\CommentsCategoryInfo;
 use Misuzu\Users\UserInfo;
 
 class News {
-    private IDbConnection $dbConn;
+    private DbConnection $dbConn;
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->dbConn = $dbConn;
         $this->cache = new DbStatementCache($dbConn);
     }
diff --git a/src/News/NewsCategoryInfo.php b/src/News/NewsCategoryInfo.php
index eb499896..5558bd0b 100644
--- a/src/News/NewsCategoryInfo.php
+++ b/src/News/NewsCategoryInfo.php
@@ -2,7 +2,7 @@
 namespace Misuzu\News;
 
 use Carbon\CarbonImmutable;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class NewsCategoryInfo {
     public function __construct(
@@ -14,7 +14,7 @@ class NewsCategoryInfo {
         private int $posts,
     ) {}
 
-    public static function fromResult(IDbResult $result): NewsCategoryInfo {
+    public static function fromResult(DbResult $result): NewsCategoryInfo {
         return new NewsCategoryInfo(
             id: $result->getString(0),
             name: $result->getString(1),
diff --git a/src/News/NewsPostInfo.php b/src/News/NewsPostInfo.php
index c2da950f..c841e6e7 100644
--- a/src/News/NewsPostInfo.php
+++ b/src/News/NewsPostInfo.php
@@ -2,7 +2,7 @@
 namespace Misuzu\News;
 
 use Carbon\CarbonImmutable;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class NewsPostInfo {
     public function __construct(
@@ -19,7 +19,7 @@ class NewsPostInfo {
         private ?int $deleted,
     ) {}
 
-    public static function fromResult(IDbResult $result): NewsPostInfo {
+    public static function fromResult(DbResult $result): NewsPostInfo {
         return new NewsPostInfo(
             id: $result->getString(0),
             categoryId: $result->getString(1),
diff --git a/src/News/NewsRoutes.php b/src/News/NewsRoutes.php
index fa1bd490..6f790dce 100644
--- a/src/News/NewsRoutes.php
+++ b/src/News/NewsRoutes.php
@@ -2,21 +2,22 @@
 namespace Misuzu\News;
 
 use RuntimeException;
-use Index\Data\{DbTools,IDbConnection};
-use Index\Http\Routing\{HttpGet,RouteHandler};
+use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
+use Index\Syndication\FeedBuilder;
+use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait};
 use Misuzu\{Pagination,SiteInfo,Template};
 use Misuzu\Auth\AuthInfo;
 use Misuzu\Comments\{Comments,CommentsCategory,CommentsEx};
-use Misuzu\Feeds\{Feed,FeedItem,AtomFeedSerializer,RssFeedSerializer};
 use Misuzu\Parsers\Parser;
-use Misuzu\URLs\{URLInfo,URLRegistry};
 use Misuzu\Users\UsersContext;
 
-class NewsRoutes extends RouteHandler {
+class NewsRoutes implements RouteHandler, UrlSource {
+    use RouteHandlerTrait, UrlSourceTrait;
+
     public function __construct(
         private SiteInfo $siteInfo,
         private AuthInfo $authInfo,
-        private URLRegistry $urls,
+        private UrlRegistry $urls,
         private News $news,
         private UsersContext $usersCtx,
         private Comments $comments
@@ -81,7 +82,7 @@ class NewsRoutes extends RouteHandler {
     }
 
     #[HttpGet('/news')]
-    #[URLInfo('news-index', '/news', ['p' => '<page>'])]
+    #[UrlFormat('news-index', '/news', ['p' => '<page>'])]
     public function getIndex() {
         $categories = $this->news->getCategories(hidden: false);
 
@@ -98,20 +99,8 @@ class NewsRoutes extends RouteHandler {
         ]);
     }
 
-    #[HttpGet('/news.rss')]
-    #[URLInfo('news-feed-rss', '/news.rss')]
-    public function getFeedRss($response) {
-        return $this->getFeed($response, 'rss');
-    }
-
-    #[HttpGet('/news.atom')]
-    #[URLInfo('news-feed-atom', '/news.atom')]
-    public function getFeedAtom($response) {
-        return $this->getFeed($response, 'atom');
-    }
-
-    #[HttpGet('/news/([0-9]+)(?:\.(rss|atom))?')]
-    #[URLInfo('news-category', '/news/<category>', ['p' => '<page>'])]
+    #[HttpGet('/news/([0-9]+)(?:\.(xml|rss|atom))?')]
+    #[UrlFormat('news-category', '/news/<category>', ['p' => '<page>'])]
     public function getCategory($response, $request, string $categoryId, string $type = '') {
         try {
             $categoryInfo = $this->news->getCategory(categoryId: $categoryId);
@@ -119,12 +108,8 @@ class NewsRoutes extends RouteHandler {
             return 404;
         }
 
-        if($type === 'rss')
-            return $this->getCategoryFeedRss($response, $request, $categoryInfo);
-        elseif($type === 'atom')
-            return $this->getCategoryFeedAtom($response, $request, $categoryInfo);
-        elseif($type !== '')
-            return 404;
+        if($type !== '')
+            return $this->getFeed($response, $request, $categoryInfo);
 
         $pagination = new Pagination($this->news->countPosts(categoryInfo: $categoryInfo), 5);
         if(!$pagination->hasValidOffset())
@@ -139,19 +124,9 @@ class NewsRoutes extends RouteHandler {
         ]);
     }
 
-    #[URLInfo('news-category-feed-rss', '/news/<category>.rss')]
-    private function getCategoryFeedRss($response, $request, NewsCategoryInfo $categoryInfo) {
-        return $this->getFeed($response, 'rss', $categoryInfo);
-    }
-
-    #[URLInfo('news-category-feed-atom', '/news/<category>.atom')]
-    private function getCategoryFeedAtom($response, $request, NewsCategoryInfo $categoryInfo) {
-        return $this->getFeed($response, 'atom', $categoryInfo);
-    }
-
     #[HttpGet('/news/post/([0-9]+)')]
-    #[URLInfo('news-post', '/news/post/<post>')]
-    #[URLInfo('news-post-comments', '/news/post/<post>', fragment: 'comments')]
+    #[UrlFormat('news-post', '/news/post/<post>')]
+    #[UrlFormat('news-post-comments', '/news/post/<post>', fragment: 'comments')]
     public function getPost($response, $request, string $postId) {
         try {
             $postInfo = $this->news->getPost($postId);
@@ -186,56 +161,56 @@ class NewsRoutes extends RouteHandler {
         ]);
     }
 
-    private function getFeed($response, string $feedType, ?NewsCategoryInfo $categoryInfo = null) {
+    #[HttpGet('/news.(?:xml|rss|atom)')]
+    #[UrlFormat('news-feed', '/news.xml')]
+    #[UrlFormat('news-category-feed', '/news/<category>.xml')]
+    public function getFeed($response, $request, ?NewsCategoryInfo $categoryInfo = null) {
+        $response->setContentType('application/rss+xml; charset=utf-8');
+
         $hasCategory = $categoryInfo !== null;
         $siteName = $this->siteInfo->getName();
+        $siteUrl = $this->siteInfo->getURL();
         $posts = $this->getNewsPostsForFeed($categoryInfo);
 
-        $serialiser = match($feedType) {
-            'rss' => new RssFeedSerializer,
-            'atom' => new AtomFeedSerializer,
-            default => throw new RuntimeException('Invalid $feedType specified.'),
-        };
-
-        $response->setContentType(sprintf('application/%s+xml; charset=utf-8', $feedType));
-        $feed = (new Feed)
-            ->setTitle($siteName . ' » ' . ($hasCategory ? $categoryInfo->getName() : 'Featured News'))
-            ->setDescription($hasCategory ? $categoryInfo->getDescription() : 'A live featured news feed.')
-            ->setContentUrl($this->siteInfo->getURL() . ($hasCategory ? $this->urls->format('news-category', ['category' => $categoryInfo->getId()]) : $this->urls->format('news-index')))
-            ->setFeedUrl($this->siteInfo->getURL() . ($hasCategory ? $this->urls->format("news-category-feed-{$feedType}", ['category' => $categoryInfo->getId()]) : $this->urls->format("news-feed-{$feedType}")));
-
-        foreach($posts as $post) {
-            $postInfo = $post['post'];
-            $userInfo = $post['user'];
-
-            $userId = 0;
-            $userName = 'Author';
-            if($userInfo !== null) {
-                $userId = $userInfo->getId();
-                $userName = $userInfo->getName();
-            }
-
-            $postUrl = $this->siteInfo->getURL() . $this->urls->format('news-post', ['post' => $postInfo->getId()]);
-            $commentsUrl = $this->siteInfo->getURL() . $this->urls->format('news-post-comments', ['post' => $postInfo->getId()]);
-            $authorUrl = $this->siteInfo->getURL() . $this->urls->format('user-profile', ['user' => $userId]);
-
-            $feedItem = (new FeedItem)
-                ->setTitle($postInfo->getTitle())
-                ->setSummary($postInfo->getFirstParagraph())
-                ->setContent(Parser::instance(Parser::MARKDOWN)->parseText($postInfo->getBody()))
-                ->setCreationDate($postInfo->getCreatedTime())
-                ->setUniqueId($postUrl)
-                ->setContentUrl($postUrl)
-                ->setCommentsUrl($commentsUrl)
-                ->setAuthorName($userName)
-                ->setAuthorUrl($authorUrl);
-
-            if(!$feed->hasLastUpdate() || $feed->getLastUpdate() < $feedItem->getCreationDate())
-                $feed->setLastUpdate($feedItem->getCreationDate());
-
-            $feed->addItem($feedItem);
+        $feed = new FeedBuilder;
+        if($hasCategory) {
+            $feed->setTitle(sprintf('%s » %s', $siteName, $categoryInfo->getName()));
+            $feed->setDescription($categoryInfo->getDescription());
+            $feed->setContentUrl($siteUrl . $this->urls->format('news-category', ['category' => $categoryInfo->getId()]));
+            $feed->setFeedUrl($siteUrl . $this->urls->format('news-category-feed', ['category' => $categoryInfo->getId()]));
+        } else {
+            $feed->setTitle(sprintf('%s » Featured News', $siteName));
+            $feed->setDescription('A live featured news feed.');
+            $feed->setContentUrl($siteUrl . $this->urls->format('news-index'));
+            $feed->setFeedUrl($siteUrl . $this->urls->format('news-feed'));
         }
 
-        return $serialiser->serializeFeed($feed);
+        $feedUpdatedAt = 0;
+        foreach($posts as $post)
+            $feed->createEntry(function($item) use ($feed, $post, $siteUrl, &$feedUpdatedAt) {
+                $postInfo = $post['post'];
+                $userInfo = $post['user'];
+
+                $item->setTitle($postInfo->getTitle());
+                $item->setDescription(Parser::instance(Parser::MARKDOWN)->parseText($postInfo->getBody()));
+                $item->setCreatedAt($postInfo->getCreatedTime());
+                $item->setContentUrl($siteUrl . $this->urls->format('news-post', ['post' => $postInfo->getId()]));
+                $item->setCommentsUrl($siteUrl . $this->urls->format('news-post-comments', ['post' => $postInfo->getId()]));
+
+                if($userInfo !== null) {
+                    $item->setAuthorName($userInfo->getName());
+                    $item->setAuthorUrl($siteUrl . $this->urls->format('user-profile', ['user' => $userInfo->getId()]));
+                }
+
+                $itemUpdatedAt = $postInfo->getUpdatedTime();
+                if($feedUpdatedAt < $itemUpdatedAt) {
+                    $feed->setUpdatedAt($feedUpdatedAt = $itemUpdatedAt);
+
+                    if($postInfo->getCreatedTime() < $itemUpdatedAt)
+                        $item->setUpdatedAt($itemUpdatedAt);
+                }
+            });
+
+        return $feed->toXmlString();
     }
 }
diff --git a/src/Perms/PermissionInfo.php b/src/Perms/PermissionInfo.php
index 141d3087..8d8902a4 100644
--- a/src/Perms/PermissionInfo.php
+++ b/src/Perms/PermissionInfo.php
@@ -1,7 +1,7 @@
 <?php
 namespace Misuzu\Perms;
 
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class PermissionInfo implements IPermissionResult {
     use PermissionResultShared;
@@ -19,7 +19,7 @@ class PermissionInfo implements IPermissionResult {
         $this->calculated = $this->allow & ~$this->deny;
     }
 
-    public static function fromResult(IDbResult $result): PermissionInfo {
+    public static function fromResult(DbResult $result): PermissionInfo {
         return new PermissionInfo(
             userId: $result->getStringOrNull(0),
             roleId: $result->getStringOrNull(1),
diff --git a/src/Perms/Permissions.php b/src/Perms/Permissions.php
index e5677ba7..b095791e 100644
--- a/src/Perms/Permissions.php
+++ b/src/Perms/Permissions.php
@@ -5,14 +5,9 @@ use stdClass;
 use InvalidArgumentException;
 use RuntimeException;
 use Index\XDateTime;
-use Index\Data\DbStatementCache;
-use Index\Data\DbTools;
-use Index\Data\IDbConnection;
-use Index\Data\IDbStatement;
-use Misuzu\Forum\ForumCategories;
-use Misuzu\Forum\ForumCategoryInfo;
-use Misuzu\Users\RoleInfo;
-use Misuzu\Users\UserInfo;
+use Index\Db\{DbConnection,DbStatement,DbStatementCache,DbTools};
+use Misuzu\Forum\{ForumCategories,ForumCategoryInfo};
+use Misuzu\Users\{RoleInfo,UserInfo};
 
 class Permissions {
     // limiting this to 53-bit in case it ever has to be sent to javascript or any other implicit float language
@@ -21,10 +16,10 @@ class Permissions {
     public const PERMS_MIN = 0;
     public const PERMS_MAX = 9007199254740991;
 
-    private IDbConnection $dbConn;
+    private DbConnection $dbConn;
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->dbConn = $dbConn;
         $this->cache = new DbStatementCache($dbConn);
     }
@@ -302,7 +297,7 @@ class Permissions {
         self::precalculatePermissionsLog('Finished permission precalculations!');
     }
 
-    private function precalculatePermissionsForForumCategory(IDbStatement $insert, array $userIds, object $forumCat, bool $doGuest, array $catIds = []): void {
+    private function precalculatePermissionsForForumCategory(DbStatement $insert, array $userIds, object $forumCat, bool $doGuest, array $catIds = []): void {
         $catIds[] = $currentCatId = $forumCat->info->getId();
         self::precalculatePermissionsLog('Precalcuting permissions for forum category #%s (%s)...', $currentCatId, implode(' <- ', $catIds));
 
diff --git a/src/Profile/ProfileFieldFormatInfo.php b/src/Profile/ProfileFieldFormatInfo.php
index 7e5b8530..78ae04d2 100644
--- a/src/Profile/ProfileFieldFormatInfo.php
+++ b/src/Profile/ProfileFieldFormatInfo.php
@@ -1,7 +1,7 @@
 <?php
 namespace Misuzu\Profile;
 
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class ProfileFieldFormatInfo {
     public function __construct(
@@ -12,7 +12,7 @@ class ProfileFieldFormatInfo {
         private string $displayFormat,
     ) {}
 
-    public static function fromResult(IDbResult $result): ProfileFieldFormatInfo {
+    public static function fromResult(DbResult $result): ProfileFieldFormatInfo {
         return new ProfileFieldFormatInfo(
             id: $result->getString(0),
             fieldId: $result->getString(1),
diff --git a/src/Profile/ProfileFieldInfo.php b/src/Profile/ProfileFieldInfo.php
index aa5a62f4..476abf72 100644
--- a/src/Profile/ProfileFieldInfo.php
+++ b/src/Profile/ProfileFieldInfo.php
@@ -1,7 +1,7 @@
 <?php
 namespace Misuzu\Profile;
 
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class ProfileFieldInfo {
     public function __construct(
@@ -12,7 +12,7 @@ class ProfileFieldInfo {
         private string $regex,
     ) {}
 
-    public static function fromResult(IDbResult $result): ProfileFieldInfo {
+    public static function fromResult(DbResult $result): ProfileFieldInfo {
         return new ProfileFieldInfo(
             id: $result->getString(0),
             order: $result->getInteger(1),
diff --git a/src/Profile/ProfileFieldValueInfo.php b/src/Profile/ProfileFieldValueInfo.php
index 889e2470..2bfd418e 100644
--- a/src/Profile/ProfileFieldValueInfo.php
+++ b/src/Profile/ProfileFieldValueInfo.php
@@ -1,7 +1,7 @@
 <?php
 namespace Misuzu\Profile;
 
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class ProfileFieldValueInfo {
     public function __construct(
@@ -11,7 +11,7 @@ class ProfileFieldValueInfo {
         private string $value,
     ) {}
 
-    public static function fromResult(IDbResult $result): ProfileFieldValueInfo {
+    public static function fromResult(DbResult $result): ProfileFieldValueInfo {
         return new ProfileFieldValueInfo(
             fieldId: $result->getString(0),
             userId: $result->getString(1),
diff --git a/src/Profile/ProfileFields.php b/src/Profile/ProfileFields.php
index 3a046cd2..e948b741 100644
--- a/src/Profile/ProfileFields.php
+++ b/src/Profile/ProfileFields.php
@@ -3,16 +3,13 @@ namespace Misuzu\Profile;
 
 use InvalidArgumentException;
 use RuntimeException;
-use Index\Data\DbStatementCache;
-use Index\Data\DbTools;
-use Index\Data\IDbConnection;
-use Index\Data\IDbResult;
+use Index\Db\{DbConnection,DbStatementCache,DbTools};
 use Misuzu\Users\UserInfo;
 
 class ProfileFields {
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->cache = new DbStatementCache($dbConn);
     }
 
diff --git a/src/RoutingContext.php b/src/RoutingContext.php
index 3316070f..606f1f58 100644
--- a/src/RoutingContext.php
+++ b/src/RoutingContext.php
@@ -1,33 +1,32 @@
 <?php
 namespace Misuzu;
 
-use Index\Http\Routing\{HttpRouter,IRouter,IRouteHandler};
-use Misuzu\URLs\{IURLSource,URLInfo,URLRegistry};
+use Index\Http\Routing\{HttpRouter,Router,RouteHandler};
+use Index\Urls\{ArrayUrlRegistry,UrlFormat,UrlRegistry,UrlSource};
 
 class RoutingContext {
-    private URLRegistry $urls;
+    private UrlRegistry $urls;
     private HttpRouter $router;
 
     public function __construct() {
-        $this->urls = new URLRegistry;
+        $this->urls = new ArrayUrlRegistry;
         $this->router = new HttpRouter(errorHandler: new RoutingErrorHandler);
         $this->router->use('/', fn($resp) => $resp->setPoweredBy('Misuzu'));
     }
 
-    public function getURLs(): URLRegistry {
+    public function getUrls(): UrlRegistry {
         return $this->urls;
     }
 
-    public function getRouter(): IRouter {
+    public function getRouter(): Router {
         return $this->router;
     }
 
-    public function register(IRouteHandler|IURLSource $handler): void {
-        if($handler instanceof IRouteHandler)
+    public function register(RouteHandler|UrlSource $handler): void {
+        if($handler instanceof RouteHandler)
             $this->router->register($handler);
-        if($handler instanceof IURLSource)
-            $handler->registerURLs($this->urls);
-        URLInfo::handleAttributes($this->urls, $handler);
+        if($handler instanceof UrlSource)
+            $this->urls->register($handler);
     }
 
     public function dispatch(...$args): void {
diff --git a/src/RoutingErrorHandler.php b/src/RoutingErrorHandler.php
index 24c6f6f3..ac09bc90 100644
--- a/src/RoutingErrorHandler.php
+++ b/src/RoutingErrorHandler.php
@@ -1,10 +1,9 @@
 <?php
 namespace Misuzu;
 
-use Index\Http\{HttpResponseBuilder,HttpRequest};
-use Index\Http\ErrorHandling\HtmlErrorHandler;
+use Index\Http\{HtmlHttpErrorHandler,HttpResponseBuilder,HttpRequest};
 
-class RoutingErrorHandler extends HtmlErrorHandler {
+class RoutingErrorHandler extends HtmlHttpErrorHandler {
     public function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void {
         if(str_starts_with($request->getPath(), '/_')) {
             $response->setTypePlain();
diff --git a/src/Satori/SatoriRoutes.php b/src/Satori/SatoriRoutes.php
index 4423fb7c..ffad5786 100644
--- a/src/Satori/SatoriRoutes.php
+++ b/src/Satori/SatoriRoutes.php
@@ -3,17 +3,19 @@ namespace Misuzu\Satori;
 
 use RuntimeException;
 use Index\Colour\Colour;
-use Index\Http\Routing\{HttpGet,HttpMiddleware,RouteHandler};
-use Syokuhou\IConfig;
+use Index\Config\Config;
+use Index\Http\Routing\{HttpGet,HttpMiddleware,RouteHandler,RouteHandlerTrait};
 use Misuzu\Pagination;
 use Misuzu\RoutingContext;
 use Misuzu\Forum\ForumContext;
 use Misuzu\Profile\ProfileFields;
 use Misuzu\Users\UsersContext;
 
-final class SatoriRoutes extends RouteHandler {
+final class SatoriRoutes implements RouteHandler {
+    use RouteHandlerTrait;
+
     public function __construct(
-        private IConfig $config,
+        private Config $config,
         private UsersContext $usersCtx,
         private ForumContext $forumCtx,
         private ProfileFields $profileFields
diff --git a/src/SharpChat/SharpChatRoutes.php b/src/SharpChat/SharpChatRoutes.php
index 65ce102e..cdf69e91 100644
--- a/src/SharpChat/SharpChatRoutes.php
+++ b/src/SharpChat/SharpChatRoutes.php
@@ -6,19 +6,21 @@ use Misuzu\RoutingContext;
 use Misuzu\Auth\{AuthContext,AuthInfo,Sessions};
 use Misuzu\Emoticons\Emotes;
 use Misuzu\Perms\Permissions;
-use Misuzu\URLs\URLRegistry;
 use Misuzu\Users\{Bans,UsersContext,UserInfo};
 use Index\Colour\Colour;
-use Index\Http\Routing\{HandlerAttribute,HttpDelete,HttpGet,HttpOptions,HttpPost,RouteHandler};
-use Syokuhou\IConfig;
+use Index\Config\Config;
+use Index\Http\Routing\{HandlerAttribute,HttpDelete,HttpGet,HttpOptions,HttpPost,RouteHandler,RouteHandlerTrait};
+use Index\Urls\UrlRegistry;
+
+final class SharpChatRoutes implements RouteHandler {
+    use RouteHandlerTrait;
 
-final class SharpChatRoutes extends RouteHandler {
     private string $hashKey;
 
     public function __construct(
-        private IConfig $config,
-        private IConfig $impersonateConfig, // this sucks lol
-        private URLRegistry $urls,
+        private Config $config,
+        private Config $impersonateConfig, // this sucks lol
+        private UrlRegistry $urls,
         private UsersContext $usersCtx,
         private AuthContext $authCtx,
         private Emotes $emotes,
diff --git a/src/SiteInfo.php b/src/SiteInfo.php
index 54eabe52..f8c167a8 100644
--- a/src/SiteInfo.php
+++ b/src/SiteInfo.php
@@ -1,14 +1,14 @@
 <?php
 namespace Misuzu;
 
-use Syokuhou\IConfig;
+use Index\Config\Config;
 
 class SiteInfo {
     private bool $loaded = false;
     private array $props;
 
     public function __construct(
-        private IConfig $config
+        private Config $config
     ) {}
 
     public function load(): void {
diff --git a/src/Template.php b/src/Template.php
index 95709bec..0b26d3bf 100644
--- a/src/Template.php
+++ b/src/Template.php
@@ -2,17 +2,15 @@
 namespace Misuzu;
 
 use InvalidArgumentException;
-use Sasae\SasaeContext;
-use Sasae\SasaeEnvironment;
-use Misuzu\MisuzuContext;
+use Index\Templating\TplEnvironment;
 
 final class Template {
     private const FILE_EXT = '.twig';
 
-    private static SasaeEnvironment $env;
+    private static TplEnvironment $env;
     private static array $vars = [];
 
-    public static function init(SasaeEnvironment $env): void {
+    public static function init(TplEnvironment $env): void {
         self::$env = $env;
     }
 
diff --git a/src/MisuzuSasaeExtension.php b/src/TemplatingExtension.php
similarity index 97%
rename from src/MisuzuSasaeExtension.php
rename to src/TemplatingExtension.php
index c92c4660..3146ab12 100644
--- a/src/MisuzuSasaeExtension.php
+++ b/src/TemplatingExtension.php
@@ -10,7 +10,7 @@ use Twig\Extension\AbstractExtension;
 use Twig\TwigFilter;
 use Twig\TwigFunction;
 
-final class MisuzuSasaeExtension extends AbstractExtension {
+final class TemplatingExtension extends AbstractExtension {
     private MisuzuContext $ctx;
     private ?object $assets;
 
@@ -30,7 +30,7 @@ final class MisuzuSasaeExtension extends AbstractExtension {
     public function getFunctions() {
         return [
             new TwigFunction('asset', $this->getAssetPath(...)),
-            new TwigFunction('url', $this->ctx->getURLs()->format(...)),
+            new TwigFunction('url', $this->ctx->getUrls()->format(...)),
             new TwigFunction('csrf_token', CSRF::token(...)),
             new TwigFunction('git_commit_hash', GitInfo::hash(...)),
             new TwigFunction('git_tag', GitInfo::tag(...)),
@@ -63,7 +63,7 @@ final class MisuzuSasaeExtension extends AbstractExtension {
 
     public function getHeaderMenu(): array {
         $menu = [];
-        $urls = $this->ctx->getURLs();
+        $urls = $this->ctx->getUrls();
         $authInfo = $this->ctx->getAuthInfo();
 
         $home = [
@@ -124,7 +124,7 @@ final class MisuzuSasaeExtension extends AbstractExtension {
 
     public function getUserMenu(bool $inBroomCloset, string $manageUrl = ''): array {
         $menu = [];
-        $urls = $this->ctx->getURLs();
+        $urls = $this->ctx->getUrls();
         $authInfo = $this->ctx->getAuthInfo();
         $usersCtx = $this->ctx->getUsersContext();
 
@@ -194,7 +194,7 @@ final class MisuzuSasaeExtension extends AbstractExtension {
     }
 
     public function getManageMenu(): array {
-        $urls = $this->ctx->getURLs();
+        $urls = $this->ctx->getUrls();
         $authInfo = $this->ctx->getAuthInfo();
         $globalPerms = $authInfo->getPerms('global');
         if(!$authInfo->isLoggedIn() || !$globalPerms->check(Perm::G_IS_JANITOR))
diff --git a/src/URLs/IURLSource.php b/src/URLs/IURLSource.php
deleted file mode 100644
index c01884b7..00000000
--- a/src/URLs/IURLSource.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-namespace Misuzu\URLs;
-
-interface IURLSource {
-    public function registerURLs(URLRegistry $urls): void;
-}
diff --git a/src/URLs/URLInfo.php b/src/URLs/URLInfo.php
deleted file mode 100644
index 176d7cc7..00000000
--- a/src/URLs/URLInfo.php
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-namespace Misuzu\URLs;
-
-use Attribute;
-use InvalidArgumentException;
-use ReflectionObject;
-
-#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
-class URLInfo {
-    public function __construct(
-        private string $name,
-        private string $path,
-        private array $query = [],
-        private string $fragment = ''
-    ) {
-        if($path === '')
-            throw new InvalidArgumentException('$path may not be empty.');
-        if($path[0] !== '/')
-            throw new InvalidArgumentException('$path must begin with /.');
-    }
-
-    public function getName(): string {
-        return $this->name;
-    }
-
-    public function getPath(): string {
-        return $this->path;
-    }
-
-    public function hasQuery(): bool {
-        return !empty($this->query);
-    }
-
-    public function getQuery(): array {
-        return $this->query;
-    }
-
-    public function hasFragment(): bool {
-        return $this->fragment !== '';
-    }
-
-    public function getFragment(): string {
-        return $this->fragment;
-    }
-
-    public static function handleAttributes(URLRegistry $urls, object $source): void {
-        $objectInfo = new ReflectionObject($source);
-        $methodInfos = $objectInfo->getMethods();
-
-        foreach($methodInfos as $methodInfo) {
-            $attrInfos = $methodInfo->getAttributes(URLInfo::class);
-            foreach($attrInfos as $attrInfo)
-                $urls->registerInfo($attrInfo->newInstance());
-        }
-    }
-}
diff --git a/src/URLs/URLRegistry.php b/src/URLs/URLRegistry.php
deleted file mode 100644
index c0585cdb..00000000
--- a/src/URLs/URLRegistry.php
+++ /dev/null
@@ -1,100 +0,0 @@
-<?php
-namespace Misuzu\URLs;
-
-use InvalidArgumentException;
-
-class URLRegistry {
-    private array $infos = [];
-
-    public function register(string $name, string $path, array $query = [], string $fragment = ''): void {
-        if(array_key_exists($name, $this->infos))
-            throw new InvalidArgumentException('A URL with $name has already been registered.');
-
-        $this->infos[$name] = new URLInfo($name, $path, $query, $fragment);
-    }
-
-    public function registerInfo(URLInfo $info): void {
-        if(array_key_exists($info->getName(), $this->infos))
-            throw new InvalidArgumentException('A URL with $name has already been registered.');
-
-        $this->infos[$info->getName()] = $info;
-    }
-
-    public function format(string $name, array $vars = [], bool $spacesAsPlus = false): string {
-        if(!array_key_exists($name, $this->infos))
-            return '';
-
-        $urlInfo = $this->infos[$name];
-        $urlStr = self::replaceVariables($urlInfo->getPath(), $vars);
-
-        if($urlInfo->hasQuery()) {
-            $query = [];
-            $queryParts = $urlInfo->getQuery();
-            foreach($queryParts as $name => $value) {
-                $value = self::replaceVariables($value, $vars);
-                if($value !== '' && !($name === 'page' && (int)$value <= 1))
-                    $query[$name] = $value;
-            }
-
-            if(!empty($query))
-                $urlStr .= '?' . http_build_query($query, '', '&', $spacesAsPlus ? PHP_QUERY_RFC1738 : PHP_QUERY_RFC3986);
-        }
-
-        if($urlInfo->hasFragment()) {
-            $fragment = self::replaceVariables($urlInfo->getFragment(), $vars);
-            if($fragment !== '')
-                $urlStr .= '#' . $fragment;
-        }
-
-        return $urlStr;
-    }
-
-    public static function replaceVariables(string $str, array $vars): string {
-        // i: have no idea what i'm doing
-        $out = '';
-        $varName = '';
-        $inVarName = false;
-
-        $length = strlen($str);
-        for($i = 0; $i < $length; ++$i) {
-            $char = $str[$i];
-
-            if($inVarName) {
-                if($char === '>') {
-                    $inVarName = false;
-                    if(array_key_exists($varName, $vars)) {
-                        $varValue = $vars[$varName];
-                        if(is_array($varValue))
-                            $varValue = empty($varValue) ? '' : implode(',', $varValue);
-                        elseif(is_int($varValue))
-                            $varValue = ($varName === 'page' ? $varValue < 2 : $varValue === 0) ? '' : (string)$varValue;
-                        else
-                            $varValue = (string)$varValue;
-                    } else
-                        $varValue = '';
-
-                    $out .= $varValue;
-                    $varName = '';
-                    continue;
-                }
-
-                if($char === '<') {
-                    $out .= '<' . $varName;
-                    $varName = '';
-                    continue;
-                }
-
-                $varName .= $char;
-            } else
-                $inVarName = $char === '<';
-
-            if(!$inVarName)
-                $out .= $char;
-        }
-
-        if($varName !== '')
-            $out .= $varName;
-
-        return $out;
-    }
-}
diff --git a/src/Users/Assets/AssetsRoutes.php b/src/Users/Assets/AssetsRoutes.php
index db1aece4..a2047e61 100644
--- a/src/Users/Assets/AssetsRoutes.php
+++ b/src/Users/Assets/AssetsRoutes.php
@@ -3,16 +3,18 @@ namespace Misuzu\Users\Assets;
 
 use InvalidArgumentException;
 use RuntimeException;
-use Index\Http\Routing\{HttpGet,RouteHandler};
+use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
+use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceTrait};
 use Misuzu\Perm;
 use Misuzu\Auth\AuthInfo;
-use Misuzu\URLs\{URLInfo,URLRegistry};
 use Misuzu\Users\{UsersContext,UserInfo};
 
-class AssetsRoutes extends RouteHandler {
+class AssetsRoutes implements RouteHandler, UrlSource {
+    use RouteHandlerTrait, UrlSourceTrait;
+
     public function __construct(
         private AuthInfo $authInfo,
-        private URLRegistry $urls,
+        private UrlRegistry $urls,
         private UsersContext $usersCtx
     ) {}
 
@@ -28,7 +30,7 @@ class AssetsRoutes extends RouteHandler {
 
     #[HttpGet('/assets/avatar')]
     #[HttpGet('/assets/avatar/([0-9]+)(?:\.[a-z]+)?')]
-    #[URLInfo('user-avatar', '/assets/avatar/<user>', ['res' => '<res>'])]
+    #[UrlFormat('user-avatar', '/assets/avatar/<user>', ['res' => '<res>'])]
     public function getAvatar($response, $request, string $userId = '') {
         $assetInfo = new StaticUserImageAsset(MSZ_PUBLIC . '/images/no-avatar.png', MSZ_PUBLIC);
 
@@ -50,7 +52,7 @@ class AssetsRoutes extends RouteHandler {
 
     #[HttpGet('/assets/profile-background')]
     #[HttpGet('/assets/profile-background/([0-9]+)(?:\.[a-z]+)?')]
-    #[URLInfo('user-background', '/assets/profile-background/<user>')]
+    #[UrlFormat('user-background', '/assets/profile-background/<user>')]
     public function getProfileBackground($response, $request, string $userId = '') {
         try {
             $userInfo = $this->usersCtx->getUserInfo($userId);
diff --git a/src/Users/BanInfo.php b/src/Users/BanInfo.php
index 6db545cf..7d64e5c2 100644
--- a/src/Users/BanInfo.php
+++ b/src/Users/BanInfo.php
@@ -2,7 +2,7 @@
 namespace Misuzu\Users;
 
 use Carbon\CarbonImmutable;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class BanInfo {
     public function __construct(
@@ -16,7 +16,7 @@ class BanInfo {
         private ?int $expires,
     ) {}
 
-    public static function fromResult(IDbResult $result): BanInfo {
+    public static function fromResult(DbResult $result): BanInfo {
         return new BanInfo(
             id: $result->getString(0),
             userId: $result->getString(1),
diff --git a/src/Users/Bans.php b/src/Users/Bans.php
index f251b812..de5b9818 100644
--- a/src/Users/Bans.php
+++ b/src/Users/Bans.php
@@ -5,19 +5,17 @@ use InvalidArgumentException;
 use RuntimeException;
 use DateTimeInterface;
 use Misuzu\Pagination;
-use Index\Data\DbStatementCache;
-use Index\Data\DbTools;
-use Index\Data\IDbConnection;
+use Index\Db\{DbConnection,DbStatementCache,DbTools};
 
 class Bans {
     public const SEVERITY_MAX = 10;
     public const SEVERITY_MIN = -10;
     public const SEVERITY_DEFAULT = 0;
 
-    private IDbConnection $dbConn;
+    private DbConnection $dbConn;
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->dbConn = $dbConn;
         $this->cache = new DbStatementCache($dbConn);
     }
diff --git a/src/Users/ModNoteInfo.php b/src/Users/ModNoteInfo.php
index 4a4c2f9d..5b1c28a9 100644
--- a/src/Users/ModNoteInfo.php
+++ b/src/Users/ModNoteInfo.php
@@ -2,7 +2,7 @@
 namespace Misuzu\Users;
 
 use Carbon\CarbonImmutable;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class ModNoteInfo {
     public function __construct(
@@ -14,7 +14,7 @@ class ModNoteInfo {
         private string $body,
     ) {}
 
-    public static function fromResult(IDbResult $result): ModNoteInfo {
+    public static function fromResult(DbResult $result): ModNoteInfo {
         return new ModNoteInfo(
             noteId: $result->getString(0),
             userId: $result->getString(1),
diff --git a/src/Users/ModNotes.php b/src/Users/ModNotes.php
index a40851ca..7479483d 100644
--- a/src/Users/ModNotes.php
+++ b/src/Users/ModNotes.php
@@ -3,16 +3,14 @@ namespace Misuzu\Users;
 
 use InvalidArgumentException;
 use RuntimeException;
-use Index\Data\DbStatementCache;
-use Index\Data\DbTools;
-use Index\Data\IDbConnection;
+use Index\Db\{DbConnection,DbStatementCache,DbTools};
 use Misuzu\Pagination;
 
 class ModNotes {
-    private IDbConnection $dbConn;
+    private DbConnection $dbConn;
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->dbConn = $dbConn;
         $this->cache = new DbStatementCache($dbConn);
     }
diff --git a/src/Users/RoleInfo.php b/src/Users/RoleInfo.php
index 906295d9..b4700045 100644
--- a/src/Users/RoleInfo.php
+++ b/src/Users/RoleInfo.php
@@ -4,7 +4,7 @@ namespace Misuzu\Users;
 use Stringable;
 use Carbon\CarbonImmutable;
 use Index\Colour\Colour;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class RoleInfo implements Stringable {
     public function __construct(
@@ -20,7 +20,7 @@ class RoleInfo implements Stringable {
         private int $created,
     ) {}
 
-    public static function fromResult(IDbResult $result): RoleInfo {
+    public static function fromResult(DbResult $result): RoleInfo {
         return new RoleInfo(
             id: $result->getString(0),
             string: $result->getStringOrNull(1),
diff --git a/src/Users/Roles.php b/src/Users/Roles.php
index 1ec61b68..dcba4669 100644
--- a/src/Users/Roles.php
+++ b/src/Users/Roles.php
@@ -4,18 +4,16 @@ namespace Misuzu\Users;
 use InvalidArgumentException;
 use RuntimeException;
 use Index\Colour\Colour;
-use Index\Data\DbStatementCache;
-use Index\Data\DbTools;
-use Index\Data\IDbConnection;
+use Index\Db\{DbConnection,DbStatementCache,DbTools};
 use Misuzu\Pagination;
 
 class Roles {
     public const DEFAULT_ROLE = '1';
 
-    private IDbConnection $dbConn;
+    private DbConnection $dbConn;
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->dbConn = $dbConn;
         $this->cache = new DbStatementCache($dbConn);
     }
diff --git a/src/Users/UserInfo.php b/src/Users/UserInfo.php
index 0604d97f..ead325c6 100644
--- a/src/Users/UserInfo.php
+++ b/src/Users/UserInfo.php
@@ -4,7 +4,7 @@ namespace Misuzu\Users;
 use Misuzu\Parsers\Parser;
 use Carbon\CarbonImmutable;
 use Index\Colour\Colour;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 use Index\Net\IPAddress;
 
 class UserInfo {
@@ -32,7 +32,7 @@ class UserInfo {
         private ?string $title,
     ) {}
 
-    public static function fromResult(IDbResult $result): self {
+    public static function fromResult(DbResult $result): self {
         return new UserInfo(
             id: $result->getString(0),
             name: $result->getString(1),
diff --git a/src/Users/Users.php b/src/Users/Users.php
index c110f3f6..8808b81a 100644
--- a/src/Users/Users.php
+++ b/src/Users/Users.php
@@ -6,19 +6,17 @@ use RuntimeException;
 use DateTimeInterface;
 use Index\XString;
 use Index\Colour\Colour;
-use Index\Data\DbStatementCache;
-use Index\Data\DbTools;
-use Index\Data\IDbConnection;
+use Index\Db\{DbConnection,DbStatementCache,DbTools};
 use Index\Net\IPAddress;
 use Misuzu\Pagination;
 use Misuzu\Tools;
 use Misuzu\Parsers\Parser;
 
 class Users {
-    private IDbConnection $dbConn;
+    private DbConnection $dbConn;
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->dbConn = $dbConn;
         $this->cache = new DbStatementCache($dbConn);
     }
diff --git a/src/Users/UsersContext.php b/src/Users/UsersContext.php
index 55a8454b..b051ffb4 100644
--- a/src/Users/UsersContext.php
+++ b/src/Users/UsersContext.php
@@ -2,7 +2,7 @@
 namespace Misuzu\Users;
 
 use Index\Colour\Colour;
-use Index\Data\IDbConnection;
+use Index\Db\DbConnection;
 
 class UsersContext {
     private Users $users;
@@ -17,7 +17,7 @@ class UsersContext {
 
     private array $activeBans = [];
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->users = new Users($dbConn);
         $this->roles = new Roles($dbConn);
         $this->bans = new Bans($dbConn);
diff --git a/src/Users/UsersRpcActions.php b/src/Users/UsersRpcActions.php
index 5abe05c0..79426dcf 100644
--- a/src/Users/UsersRpcActions.php
+++ b/src/Users/UsersRpcActions.php
@@ -3,16 +3,16 @@ namespace Misuzu\Users;
 
 use RuntimeException;
 use Misuzu\SiteInfo;
-use Misuzu\URLs\URLRegistry;
 use Misuzu\Users\Assets\UserAvatarAsset;
 use Aiwass\Server\{RpcActionHandler,RpcQuery};
 use Index\XArray;
 use Index\Colour\{Colour,ColourRGB};
+use Index\Urls\UrlRegistry;
 
 final class UsersRpcActions extends RpcActionHandler {
     public function __construct(
         private SiteInfo $siteInfo,
-        private URLRegistry $urls,
+        private UrlRegistry $urls,
         private UsersContext $usersCtx
     ) {}
 
diff --git a/src/Users/WarningInfo.php b/src/Users/WarningInfo.php
index 61eaa73d..e249d5f5 100644
--- a/src/Users/WarningInfo.php
+++ b/src/Users/WarningInfo.php
@@ -2,7 +2,7 @@
 namespace Misuzu\Users;
 
 use Carbon\CarbonImmutable;
-use Index\Data\IDbResult;
+use Index\Db\DbResult;
 
 class WarningInfo {
     public function __construct(
@@ -13,7 +13,7 @@ class WarningInfo {
         private int $created,
     ) {}
 
-    public static function fromResult(IDbResult $result): WarningInfo {
+    public static function fromResult(DbResult $result): WarningInfo {
         return new WarningInfo(
             id: $result->getString(0),
             userId: $result->getString(1),
diff --git a/src/Users/Warnings.php b/src/Users/Warnings.php
index 268f29dd..3ae2e5f0 100644
--- a/src/Users/Warnings.php
+++ b/src/Users/Warnings.php
@@ -3,9 +3,7 @@ namespace Misuzu\Users;
 
 use InvalidArgumentException;
 use RuntimeException;
-use Index\Data\DbStatementCache;
-use Index\Data\DbTools;
-use Index\Data\IDbConnection;
+use Index\Db\{DbConnection,DbStatementCache,DbTools};
 use Misuzu\Pagination;
 
 // this system is currently kinda useless because it only silently shows up on profiles
@@ -14,10 +12,10 @@ use Misuzu\Pagination;
 class Warnings {
     public const VISIBLE_BACKLOG = 90 * 24 * 60 * 60;
 
-    private IDbConnection $dbConn;
+    private DbConnection $dbConn;
     private DbStatementCache $cache;
 
-    public function __construct(IDbConnection $dbConn) {
+    public function __construct(DbConnection $dbConn) {
         $this->dbConn = $dbConn;
         $this->cache = new DbStatementCache($dbConn);
     }
diff --git a/templates/changelog/index.twig b/templates/changelog/index.twig
index 6c44932a..f1b327d8 100644
--- a/templates/changelog/index.twig
+++ b/templates/changelog/index.twig
@@ -31,14 +31,9 @@
     {% set feeds = [
         {
             'type': 'rss',
-            'title': '',
-            'url': url('changelog-feed-rss'),
-        },
-        {
-            'type': 'atom',
-            'title': '',
-            'url': url('changelog-feed-atom'),
-        },
+            'title': 'Changelog feed',
+            'url': url('changelog-feed'),
+        }
     ] %}
 {% endif %}
 
diff --git a/templates/news/category.twig b/templates/news/category.twig
index 56549097..f4967d4a 100644
--- a/templates/news/category.twig
+++ b/templates/news/category.twig
@@ -12,14 +12,9 @@
 {% set feeds = [
     {
         'type': 'rss',
-        'title': '',
-        'url': url('news-category-feed-rss', {'category': news_category.id}),
-    },
-    {
-        'type': 'atom',
-        'title': '',
-        'url': url('news-category-feed-atom', {'category': news_category.id}),
-    },
+        'title': news_category.name ~ ' feed',
+        'url': url('news-category-feed', {'category': news_category.id}),
+    }
 ] %}
 
 {% block content %}
@@ -47,15 +42,7 @@
                 {{ container_title('Feeds') }}
 
                 <div class="news__feeds">
-                    <a href="{{ url('news-category-feed-atom', {'category': news_category.id}) }}" class="news__feed">
-                        <div class="news__feed__icon">
-                            <i class="fas fa-rss"></i>
-                        </div>
-                        <div class="news__feed__type">
-                            Atom
-                        </div>
-                    </a>
-                    <a href="{{ url('news-category-feed-rss', {'category': news_category.id}) }}" class="news__feed">
+                    <a href="{{ url('news-category-feed', {'category': news_category.id}) }}" class="news__feed">
                         <div class="news__feed__icon">
                             <i class="fas fa-rss"></i>
                         </div>
diff --git a/templates/news/index.twig b/templates/news/index.twig
index 7904d681..27dbc272 100644
--- a/templates/news/index.twig
+++ b/templates/news/index.twig
@@ -11,14 +11,9 @@
 {% set feeds = [
     {
         'type': 'rss',
-        'title': '',
-        'url': url('news-feed-rss'),
-    },
-    {
-        'type': 'atom',
-        'title': '',
-        'url': url('news-feed-atom'),
-    },
+        'title': 'Featured news feed',
+        'url': url('news-feed'),
+    }
 ] %}
 
 {% block content %}
@@ -55,15 +50,7 @@
                 {{ container_title('Feeds') }}
 
                 <div class="news__feeds">
-                    <a href="{{ url('news-feed-atom') }}" class="news__feed">
-                        <div class="news__feed__icon">
-                            <i class="fas fa-rss"></i>
-                        </div>
-                        <div class="news__feed__type">
-                            Atom
-                        </div>
-                    </a>
-                    <a href="{{ url('news-feed-rss') }}" class="news__feed">
+                    <a href="{{ url('news-feed') }}" class="news__feed">
                         <div class="news__feed__icon">
                             <i class="fas fa-rss"></i>
                         </div>
diff --git a/tools/migrate b/tools/migrate
index 576e12d9..29e1d2b9 100755
--- a/tools/migrate
+++ b/tools/migrate
@@ -1,6 +1,6 @@
 #!/usr/bin/env php
 <?php
-use Index\Data\Migration\FsDbMigrationRepo;
+use Index\Db\Migration\FsDbMigrationRepo;
 
 require_once __DIR__ . '/../misuzu.php';
 
diff --git a/tools/new-migration b/tools/new-migration
index d69479eb..8b0c47fb 100755
--- a/tools/new-migration
+++ b/tools/new-migration
@@ -1,6 +1,6 @@
 #!/usr/bin/env php
 <?php
-use Index\Data\Migration\FsDbMigrationRepo;
+use Index\Db\Migration\FsDbMigrationRepo;
 
 require_once __DIR__ . '/../misuzu.php';