diff --git a/.config/docker_example.env b/.config/docker_example.env
new file mode 100644
index 0000000000000000000000000000000000000000..411d93659bf35981e21a86aa370a5eabe0ab1639
--- /dev/null
+++ b/.config/docker_example.env
@@ -0,0 +1,5 @@
+# db settings
+POSTGRES_PASSWORD="example-misskey-pass"
+POSTGRES_USER="example-misskey-user"
+POSTGRES_DB="misskey"
+
diff --git a/.config/example.yml b/.config/example.yml
index 70c096baa13706ef52bd938dfa74c1a7e2153128..10239f1a760bc538146449ab27ca0d9f01beafb3 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -1,8 +1,16 @@
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+# Misskey configuration
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+#   ┌─────┐
+#───┘ URL └─────────────────────────────────────────────────────
+
 # Final accessible URL seen by a user.
 url: https://example.tld/
 
+#   ┌───────────────────────┐
+#───┘ Port and TLS settings └───────────────────────────────────
 
-### Port and TLS settings ######################################
 #
 # Misskey supports two deployment options for public.
 #
@@ -30,28 +38,51 @@ url: https://example.tld/
 #   You need to set Certificate in 'https' section.
 
 # To use option 1, uncomment below line.
-# port: 3000    # A port that your Misskey server should listen.
+#port: 3000    # A port that your Misskey server should listen.
 
 # To use option 2, uncomment below lines.
-# port: 443
-#
-# https:
-#   # path for certification
-#   key: /etc/letsencrypt/live/example.tld/privkey.pem
-#   cert: /etc/letsencrypt/live/example.tld/fullchain.pem
+#port: 443
 
-################################################################
+#https:
+#  # path for certification
+#  key: /etc/letsencrypt/live/example.tld/privkey.pem
+#  cert: /etc/letsencrypt/live/example.tld/fullchain.pem
 
+#   ┌──────────────────────────┐
+#───┘ PostgreSQL configuration └────────────────────────────────
 
-mongodb:
+db:
   host: localhost
-  port: 27017
+  port: 5432
+
+  # Database name
   db: misskey
+
+  # Auth
   user: example-misskey-user
   pass: example-misskey-pass
 
+#   ┌─────────────────────┐
+#───┘ Redis configuration └─────────────────────────────────────
+
+#redis:
+#  host: localhost
+#  port: 6379
+#  pass: example-pass
+
+#   ┌─────────────────────────────┐
+#───┘ Elasticsearch configuration └─────────────────────────────
+
+#elasticsearch:
+#  host: localhost
+#  port: 9200
+#  pass: null
+
+#   ┌────────────────────────────────────┐
+#───┘ File storage (Drive) configuration └──────────────────────
+
 drive:
-  storage: 'db'
+  storage: 'fs'
 
   # OR
 
@@ -88,25 +119,43 @@ drive:
   #   accessKey: XXX
   #   secretKey: YYY
 
-# If enabled:
-#  The first account created is automatically marked as Admin.
-autoAdmin: true
+#   ┌───────────────┐
+#───┘ ID generation └───────────────────────────────────────────
 
-#
-# Below settings are optional
-#
+# You can select the ID generation method.
+# You don't usually need to change this setting, but you can
+# change it according to your preferences.
 
-# Redis
-#redis:
-#  host: localhost
-#  port: 6379
-#  pass: example-pass
+# Available methods:
+# aid1 ... Use AID for ID generation (with random 1 char)
+# aid2 ... Use AID for ID generation (with random 2 chars)
+# aid3 ... Use AID for ID generation (with random 3 chars)
+# aid4 ... Use AID for ID generation (with random 4 chars)
+# ulid ... Use ulid for ID generation
+# objectid ... This is left for backward compatibility.
 
-# Elasticsearch
-#elasticsearch:
-#  host: localhost
-#  port: 9200
-#  pass: null
+# AID(n) is the original ID generation method.
+# The trailing n represents the number of random characters that
+# will be suffixed.
+# The larger n is the safer. If n is small, the possibility of
+# collision at the same time increases, but there are also
+# advantages such as shortening of the URL.
+
+# ULID: Universally Unique Lexicographically Sortable Identifier.
+# for more details: https://github.com/ulid/spec
+# * Normally, AID should be sufficient.
+
+# ObjectID is the method used in previous versions of Misskey.
+# * Choose this if you are migrating from a previous Misskey.
+
+id: 'aid2'
+
+#   ┌─────────────────────┐
+#───┘ Other configuration └─────────────────────────────────────
+
+# If enabled:
+#  The first account created is automatically marked as Admin.
+autoAdmin: true
 
 # Whether disable HSTS
 #disableHsts: true
diff --git a/.config/mongo_initdb_example.js b/.config/mongo_initdb_example.js
deleted file mode 100644
index b7e7321f3531cd085fb36b5d126012912683cbbe..0000000000000000000000000000000000000000
--- a/.config/mongo_initdb_example.js
+++ /dev/null
@@ -1,13 +0,0 @@
-var user = {
-	user: 'example-misskey-user',
-	pwd: 'example-misskey-pass',
-	roles: [
-	    {
-		    role: 'readWrite',
-		    db: 'misskey'
-	    }
-	]
-};
-
-db.createUser(user);
-
diff --git a/.dockerignore b/.dockerignore
old mode 100755
new mode 100644
index a25d4e571827fba23c37c8aa7a3fb250665da632..324c4bce58c3289c6add88e87151b9c7c9bf1b55
--- a/.dockerignore
+++ b/.dockerignore
@@ -5,8 +5,8 @@
 .vscode
 Dockerfile
 build/
+db/
 docker-compose.yml
+elasticsearch/
 node_modules/
-mongo/
 redis/
-elasticsearch/
diff --git a/.gitignore b/.gitignore
index 98ee82cd7e0e1d05fe31118c217285b130310345..650d4f6128fddda70ef35a4ca122fd80412bb892 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,14 +8,15 @@
 built
 /data
 /.cache-loader
+/db
+/elasticsearch
 npm-debug.log
 *.pem
 run.bat
 api-docs.json
 *.log
 /redis
-/mongo
-/elasticsearch
 *.code-workspace
 yarn.lock
 .DS_Store
+/files
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c1ad1f8041d8efa4db10b65ce4ee1d760147b1cb..a45ed5cb5f3662e685d56b9db00909493c91a86d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -75,3 +75,61 @@ src ... Source code
 test ... Test code
 
 ```
+
+## Notes
+### placeholder
+SQLをクエリビルダで組み立てる際、使用するプレースホルダは重複してはならない
+例えば
+``` ts
+query.andWhere(new Brackets(qb => {
+	for (const type of ps.fileType) {
+		qb.orWhere(`:type = ANY(note.attachedFileTypes)`, { type: type });
+	}
+}));
+```
+と書くと、ループ中で`type`というプレースホルダが複数回使われてしまいおかしくなる
+だから次のようにする必要がある
+```ts
+query.andWhere(new Brackets(qb => {
+	for (const type of ps.fileType) {
+		const i = ps.fileType.indexOf(type);
+		qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
+	}
+}));
+```
+
+### `null` in SQL
+SQLを発行する際、パラメータが`null`になる可能性のある場合はSQL文を出し分けなければならない
+例えば
+``` ts
+query.where('file.folderId = :folderId', { folderId: ps.folderId });
+```
+という処理で、`ps.folderId`が`null`だと結果的に`file.folderId = null`のようなクエリが発行されてしまい、これは正しいSQLではないので期待した結果が得られない
+だから次のようにする必要がある
+``` ts
+if (ps.folderId) {
+	query.where('file.folderId = :folderId', { folderId: ps.folderId });
+} else {
+	query.where('file.folderId IS NULL');
+}
+```
+
+### `[]` in SQL
+SQLを発行する際、`IN`のパラメータが`[]`(空の配列)になる可能性のある場合はSQL文を出し分けなければならない
+例えば
+``` ts
+const users = await Users.find({
+	id: In(userIds)
+});
+```
+という処理で、`userIds`が`[]`だと結果的に`user.id IN ()`のようなクエリが発行されてしまい、これは正しいSQLではないので期待した結果が得られない
+だから次のようにする必要がある
+``` ts
+const users = userIds.length > 0 ? await Users.find({
+	id: In(userIds)
+}) : [];
+```
+
+### `undefined`にご用心
+MongoDBの時とは違い、findOneでレコードを取得する時に対象レコードが存在しない場合 **`undefined`** が返ってくるので注意。
+MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`とか書くとバグる。代わりに`if (x == null)`と書いてください
diff --git a/Dockerfile b/Dockerfile
index ad04fb33dc57b1c2b7ad6d5818b49fffddb065c7..ec7d8a6a2795b609ed38f956674c886e2d068222 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -23,8 +23,9 @@ RUN apk add --no-cache \
     zlib-dev
 RUN npm i -g yarn
 
-COPY . ./
+COPY package.json ./
 RUN yarn install
+COPY . ./
 RUN yarn build
 
 FROM base AS runner
diff --git a/binding.gyp b/binding.gyp
deleted file mode 100644
index 0349526d52368e10ea34df96a8fc8eb4ef362903..0000000000000000000000000000000000000000
--- a/binding.gyp
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-	'targets': [
-		{
-			'target_name': 'crypto_key',
-			'sources': ['src/crypto_key.cc'],
-			'include_dirs': ['<!(node -e "require(\'nan\')")']
-		}
-	]
-}
diff --git a/cli/migration/2.0.0.js b/cli/migration/2.0.0.js
deleted file mode 100644
index f7298972e5a0d385655a47361e7698b4b73f6ba1..0000000000000000000000000000000000000000
--- a/cli/migration/2.0.0.js
+++ /dev/null
@@ -1,57 +0,0 @@
-// for Node.js interpret
-
-const chalk = require('chalk');
-const sequential = require('promise-sequential');
-
-const { default: User } = require('../../built/models/user');
-const { default: DriveFile } = require('../../built/models/drive-file');
-
-async function main() {
-	const promiseGens = [];
-
-	const count = await DriveFile.count({});
-
-	let prev;
-
-	for (let i = 0; i < count; i++) {
-		promiseGens.push(() => {
-			const promise = new Promise(async (res, rej) => {
-				const file = await DriveFile.findOne(prev ? {
-					_id: { $gt: prev._id }
-				} : {}, {
-					sort: {
-						_id: 1
-					}
-				});
-
-				prev = file;
-
-				const user = await User.findOne({ _id: file.metadata.userId });
-
-				DriveFile.update({
-					_id: file._id
-				}, {
-					$set: {
-						'metadata._user': {
-							host: user.host
-						}
-					}
-				}).then(() => {
-					res([i, file]);
-				}).catch(rej);
-			});
-
-			promise.then(([i, file]) => {
-				console.log(chalk`{gray ${i}} {green done: {bold ${file._id}} ${file.filename}}`);
-			});
-
-			return promise;
-		});
-	}
-
-	return await sequential(promiseGens);
-}
-
-main().then(() => {
-	console.log('ALL DONE');
-}).catch(console.error);
diff --git a/cli/migration/2.4.0.js b/cli/migration/2.4.0.js
deleted file mode 100644
index aa37849aa1c48018f9b0cc0806b51136b896d060..0000000000000000000000000000000000000000
--- a/cli/migration/2.4.0.js
+++ /dev/null
@@ -1,71 +0,0 @@
-// for Node.js interpret
-
-const chalk = require('chalk');
-const sequential = require('promise-sequential');
-
-const { default: User } = require('../../built/models/user');
-const { default: DriveFile } = require('../../built/models/drive-file');
-
-async function main() {
-	const promiseGens = [];
-
-	const count = await User.count({});
-
-	let prev;
-
-	for (let i = 0; i < count; i++) {
-		promiseGens.push(() => {
-			const promise = new Promise(async (res, rej) => {
-				const user = await User.findOne(prev ? {
-					_id: { $gt: prev._id }
-				} : {}, {
-					sort: {
-						_id: 1
-					}
-				});
-
-				prev = user;
-
-				const set = {};
-
-				if (user.avatarId != null) {
-					const file = await DriveFile.findOne({ _id: user.avatarId });
-
-					if (file && file.metadata.properties.avgColor) {
-						set.avatarColor = file.metadata.properties.avgColor;
-					}
-				}
-
-				if (user.bannerId != null) {
-					const file = await DriveFile.findOne({ _id: user.bannerId });
-
-					if (file && file.metadata.properties.avgColor) {
-						set.bannerColor = file.metadata.properties.avgColor;
-					}
-				}
-
-				if (Object.keys(set).length === 0) return res([i, user]);
-
-				User.update({
-					_id: user._id
-				}, {
-					$set: set
-				}).then(() => {
-					res([i, user]);
-				}).catch(rej);
-			});
-
-			promise.then(([i, user]) => {
-				console.log(chalk`{gray ${i}} {green done: {bold ${user._id}} @${user.username}}`);
-			});
-
-			return promise;
-		});
-	}
-
-	return await sequential(promiseGens);
-}
-
-main().then(() => {
-	console.log('ALL DONE');
-}).catch(console.error);
diff --git a/cli/migration/5.0.0.js b/cli/migration/5.0.0.js
deleted file mode 100644
index bef103fe4a84fbb6ea42074b63294bbbe70bc290..0000000000000000000000000000000000000000
--- a/cli/migration/5.0.0.js
+++ /dev/null
@@ -1,9 +0,0 @@
-const { default: DriveFile } = require('../../built/models/drive-file');
-
-DriveFile.update({}, {
-	$rename: {
-		'metadata.isMetaOnly': 'metadata.withoutChunks'
-	}
-}, {
-	multi: true
-});
diff --git a/cli/migration/7.0.0.js b/cli/migration/7.0.0.js
deleted file mode 100644
index fa5e363db87d8f1a2e2b06f14b4aa0667ebfa67d..0000000000000000000000000000000000000000
--- a/cli/migration/7.0.0.js
+++ /dev/null
@@ -1,134 +0,0 @@
-const { default: Stats } = require('../../built/models/stats');
-const { default: User } = require('../../built/models/user');
-const { default: Note } = require('../../built/models/note');
-const { default: DriveFile } = require('../../built/models/drive-file');
-
-const now = new Date();
-const y = now.getFullYear();
-const m = now.getMonth();
-const d = now.getDate();
-const today = new Date(y, m, d);
-
-async function main() {
-	const localUsersCount = await User.count({
-		host: null
-	});
-
-	const remoteUsersCount = await User.count({
-		host: { $ne: null }
-	});
-
-	const localNotesCount = await Note.count({
-		'_user.host': null
-	});
-
-	const remoteNotesCount = await Note.count({
-		'_user.host': { $ne: null }
-	});
-
-	const localDriveFilesCount = await DriveFile.count({
-		'metadata._user.host': null
-	});
-
-	const remoteDriveFilesCount = await DriveFile.count({
-		'metadata._user.host': { $ne: null }
-	});
-
-	const localDriveFilesSize = await DriveFile
-		.aggregate([{
-			$match: {
-				'metadata._user.host': null,
-				'metadata.deletedAt': { $exists: false }
-			}
-		}, {
-			$project: {
-				length: true
-			}
-		}, {
-			$group: {
-				_id: null,
-				usage: { $sum: '$length' }
-			}
-		}])
-		.then(aggregates => {
-			if (aggregates.length > 0) {
-				return aggregates[0].usage;
-			}
-			return 0;
-		});
-
-	const remoteDriveFilesSize = await DriveFile
-		.aggregate([{
-			$match: {
-				'metadata._user.host': { $ne: null },
-				'metadata.deletedAt': { $exists: false }
-			}
-		}, {
-			$project: {
-				length: true
-			}
-		}, {
-			$group: {
-				_id: null,
-				usage: { $sum: '$length' }
-			}
-		}])
-		.then(aggregates => {
-			if (aggregates.length > 0) {
-				return aggregates[0].usage;
-			}
-			return 0;
-		});
-
-	await Stats.insert({
-		date: today,
-		users: {
-			local: {
-				total: localUsersCount,
-				diff: 0
-			},
-			remote: {
-				total: remoteUsersCount,
-				diff: 0
-			}
-		},
-		notes: {
-			local: {
-				total: localNotesCount,
-				diff: 0,
-				diffs: {
-					normal: 0,
-					reply: 0,
-					renote: 0
-				}
-			},
-			remote: {
-				total: remoteNotesCount,
-				diff: 0,
-				diffs: {
-					normal: 0,
-					reply: 0,
-					renote: 0
-				}
-			}
-		},
-		drive: {
-			local: {
-				totalCount: localDriveFilesCount,
-				totalSize: localDriveFilesSize,
-				diffCount: 0,
-				diffSize: 0
-			},
-			remote: {
-				totalCount: remoteDriveFilesCount,
-				totalSize: remoteDriveFilesSize,
-				diffCount: 0,
-				diffSize: 0
-			}
-		}
-	});
-
-	console.log('done');
-}
-
-main();
diff --git a/cli/migration/8.0.0.js b/cli/migration/8.0.0.js
deleted file mode 100644
index fd6cb245250268f7198051394d531ddf515c7410..0000000000000000000000000000000000000000
--- a/cli/migration/8.0.0.js
+++ /dev/null
@@ -1,144 +0,0 @@
-const { default: Stats } = require('../../built/models/stats');
-const { default: User } = require('../../built/models/user');
-const { default: Note } = require('../../built/models/note');
-const { default: DriveFile } = require('../../built/models/drive-file');
-
-const now = new Date();
-const y = now.getFullYear();
-const m = now.getMonth();
-const d = now.getDate();
-const h = now.getHours();
-const date = new Date(y, m, d, h);
-
-async function main() {
-	await Stats.update({}, {
-		$set: {
-			span: 'day'
-		}
-	}, {
-		multi: true
-	});
-
-	const localUsersCount = await User.count({
-		host: null
-	});
-
-	const remoteUsersCount = await User.count({
-		host: { $ne: null }
-	});
-
-	const localNotesCount = await Note.count({
-		'_user.host': null
-	});
-
-	const remoteNotesCount = await Note.count({
-		'_user.host': { $ne: null }
-	});
-
-	const localDriveFilesCount = await DriveFile.count({
-		'metadata._user.host': null
-	});
-
-	const remoteDriveFilesCount = await DriveFile.count({
-		'metadata._user.host': { $ne: null }
-	});
-
-	const localDriveFilesSize = await DriveFile
-		.aggregate([{
-			$match: {
-				'metadata._user.host': null,
-				'metadata.deletedAt': { $exists: false }
-			}
-		}, {
-			$project: {
-				length: true
-			}
-		}, {
-			$group: {
-				_id: null,
-				usage: { $sum: '$length' }
-			}
-		}])
-		.then(aggregates => {
-			if (aggregates.length > 0) {
-				return aggregates[0].usage;
-			}
-			return 0;
-		});
-
-	const remoteDriveFilesSize = await DriveFile
-		.aggregate([{
-			$match: {
-				'metadata._user.host': { $ne: null },
-				'metadata.deletedAt': { $exists: false }
-			}
-		}, {
-			$project: {
-				length: true
-			}
-		}, {
-			$group: {
-				_id: null,
-				usage: { $sum: '$length' }
-			}
-		}])
-		.then(aggregates => {
-			if (aggregates.length > 0) {
-				return aggregates[0].usage;
-			}
-			return 0;
-		});
-
-	await Stats.insert({
-		date: date,
-		span: 'hour',
-		users: {
-			local: {
-				total: localUsersCount,
-				diff: 0
-			},
-			remote: {
-				total: remoteUsersCount,
-				diff: 0
-			}
-		},
-		notes: {
-			local: {
-				total: localNotesCount,
-				diff: 0,
-				diffs: {
-					normal: 0,
-					reply: 0,
-					renote: 0
-				}
-			},
-			remote: {
-				total: remoteNotesCount,
-				diff: 0,
-				diffs: {
-					normal: 0,
-					reply: 0,
-					renote: 0
-				}
-			}
-		},
-		drive: {
-			local: {
-				totalCount: localDriveFilesCount,
-				totalSize: localDriveFilesSize,
-				diffCount: 0,
-				diffSize: 0
-			},
-			remote: {
-				totalCount: remoteDriveFilesCount,
-				totalSize: remoteDriveFilesSize,
-				diffCount: 0,
-				diffSize: 0
-			}
-		}
-	});
-
-	console.log('done');
-}
-
-main();
diff --git a/docker-compose.yml b/docker-compose.yml
index 7ff8f6a268bc75e13ffc2a717403b6b24637accc..184738aa8caf0dabff46e3c8c11197daaef61687 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -5,7 +5,7 @@ services:
     build: .
     restart: always
     links:
-      - mongo
+      - db
 #      - redis
 #      - es
     ports:
@@ -19,21 +19,18 @@ services:
 #    image: redis:4.0-alpine
 #    networks:
 #      - internal_network
-### Uncomment to enable Redis persistance
-##    volumes:
-##      - ./redis:/data
+#    volumes:
+#      - ./redis:/data
 
-  mongo:
+  db:
     restart: always
-    image: mongo:4.1
+    image: postgres:11.2-alpine
     networks:
       - internal_network
-    environment:
-      MONGO_INITDB_DATABASE: "misskey"
+    env_file:
+      - .config/docker.env
     volumes:
-      - ./.config/mongo_initdb.js:/docker-entrypoint-initdb.d/mongo_initdb.js:ro
-### Uncomment to enable MongoDB persistance
-#      - ./mongo:/data
+      - ./db:/var/lib/postgresql/data
 
 #  es:
 #    restart: always
@@ -42,9 +39,8 @@ services:
 #      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
 #    networks:
 #      - internal_network
-#### Uncomment to enable ES persistence
-##    volumes:
-##      - ./elasticsearch:/usr/share/elasticsearch/data
+#    volumes:
+#      - ./elasticsearch:/usr/share/elasticsearch/data
 
 networks:
   internal_network:
diff --git a/docs/backup.fr.md b/docs/backup.fr.md
deleted file mode 100644
index 19e99068cec24a2616d34326cdac5253a45b21ee..0000000000000000000000000000000000000000
--- a/docs/backup.fr.md
+++ /dev/null
@@ -1,22 +0,0 @@
-Comment faire une sauvegarde de votre Misskey ?
-==========================
-
-Assurez-vous d'avoir installé **mongodb-tools**.
-
----
-
-Dans votre terminal :
-``` shell
-$ mongodump --archive=db-backup -u <VotreNomdUtilisateur> -p <VotreMotDePasse>
-```
-
-Pour plus de détails, merci de consulter [la documentation de mongodump](https://docs.mongodb.com/manual/reference/program/mongodump/).
-
-Restauration
--------
-
-``` shell
-$ mongorestore --archive=db-backup
-```
-
-Pour plus de détails, merci de consulter [la documentation de mongorestore](https://docs.mongodb.com/manual/reference/program/mongorestore/).
diff --git a/docs/backup.md b/docs/backup.md
deleted file mode 100644
index a69af0255b21d2c977c70552a73c78b45fe645a9..0000000000000000000000000000000000000000
--- a/docs/backup.md
+++ /dev/null
@@ -1,22 +0,0 @@
-How to backup your Misskey
-==========================
-
-Make sure **mongodb-tools** installed.
-
----
-
-In your shell:
-``` shell
-$ mongodump --archive=db-backup -u <YourUserName> -p <YourPassword>
-```
-
-For details, please see [mongodump docs](https://docs.mongodb.com/manual/reference/program/mongodump/).
-
-Restore
--------
-
-``` shell
-$ mongorestore --archive=db-backup
-```
-
-For details, please see [mongorestore docs](https://docs.mongodb.com/manual/reference/program/mongorestore/).
diff --git a/docs/docker.en.md b/docs/docker.en.md
index f0fcdb66d570a65d0cffa59604626e3d66bcdbc9..ee69b6d7ae59d0a695c6ceaf4389b3b01a5ff806 100644
--- a/docs/docker.en.md
+++ b/docs/docker.en.md
@@ -15,9 +15,37 @@ This guide describes how to install and setup Misskey with Docker.
 
 *2.* Configure Misskey
 ----------------------------------------------------------------
-1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`.
-2. `cp .config/mongo_initdb_example.js .config/mongo_initdb.js` Copy the `.config/mongo_initdb_example.js` and rename it to `mongo_initdb.js`.
-3. Edit `default.yml` and `mongo_initdb.js`.
+
+Create configuration files with following:
+
+```bash
+cd .config
+cp example.yml default.yml
+cp docker_example.env docker.env
+```
+
+### `default.yml`
+
+Edit this file the same as non-Docker environment.  
+However hostname of Postgresql, Redis and Elasticsearch are not `localhost`, they are set in `docker-compose.yml`.  
+The following is default hostname:
+
+| Service       | Hostname |
+|---------------|----------|
+| Postgresql    | `db`     |
+| Redis         | `redis`  |
+| Elasticsearch | `es`     |
+
+### `docker.env`
+
+Configure Postgresql in this file.  
+The minimum required settings are:
+
+| name                | Description   |
+|---------------------|---------------|
+| `POSTGRES_PASSWORD` | Password      |
+| `POSTGRES_USER`     | Username      |
+| `POSTGRES_DB`       | Database name |
 
 *3.* Configure Docker
 ----------------------------------------------------------------
diff --git a/docs/docker.ja.md b/docs/docker.ja.md
index 0baf2857284725c3631d959636da825f12d114fc..060d4e7bda3615e2df40ea2a7cc48f96d02805f3 100644
--- a/docs/docker.ja.md
+++ b/docs/docker.ja.md
@@ -13,11 +13,39 @@ Dockerを使ったMisskey構築方法
 2. `cd misskey` misskeyディレクトリに移動
 3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認
 
-*2.* 設定ファイルを作成する
+*2.* 設定ファイルの作成と編集
 ----------------------------------------------------------------
-1. `cp .config/example.yml .config/default.yml` `.config/example.yml`をコピーし名前を`default.yml`にする
-2. `cp .config/mongo_initdb_example.js .config/mongo_initdb.js` `.config/mongo_initdb_example.js`をコピーし名前を`mongo_initdb.js`にする
-3. `default.yml`と`mongo_initdb.js`を編集する
+
+下記コマンドで設定ファイルを作成してください。
+
+```bash
+cd .config
+cp example.yml default.yml
+cp docker_example.env docker.env
+```
+
+### `default.yml`の編集
+
+非Docker環境と同じ様に編集してください。  
+ただし、Postgresql、RedisとElasticsearchのホストは`localhost`ではなく、`docker-compose.yml`で設定されたサービス名になっています。  
+標準設定では次の通りです。
+
+| サービス       | ホスト名 |
+|---------------|---------|
+| Postgresql    |`db`     |
+| Redis         |`redis`  |
+| Elasticsearch |`es`     |
+
+### `docker.env`の編集
+
+このファイルはPostgresqlの設定を記述します。  
+最低限記述する必要がある設定は次の通りです。
+
+| 設定                 | 内容         |
+|---------------------|--------------|
+| `POSTGRES_PASSWORD` | パスワード    |
+| `POSTGRES_USER`     | ユーザー名    |
+| `POSTGRES_DB`       | データベース名 |
 
 *3.* Dockerの設定
 ----------------------------------------------------------------
diff --git a/docs/setup.en.md b/docs/setup.en.md
index 1125081445dfd7f0ecb30f1c2cceca83f3fa788b..28de1f32f319dcad1e67f5d0cf5f77cf2de70a09 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -22,8 +22,8 @@ adduser --disabled-password --disabled-login misskey
 Please install and setup these softwares:
 
 #### Dependencies :package:
-* **[Node.js](https://nodejs.org/en/)** >= 10.0.0
-* **[MongoDB](https://www.mongodb.com/)** >= 3.6
+* **[Node.js](https://nodejs.org/en/)** >= 11.7.0
+* **[PostgreSQL](https://www.postgresql.org/)** >= 10
 
 ##### Optional
 * [Redis](https://redis.io/)
@@ -31,13 +31,9 @@ Please install and setup these softwares:
 * [Elasticsearch](https://www.elastic.co/) - required to enable the search feature
 * [FFmpeg](https://www.ffmpeg.org/)
 
-*3.* Setup MongoDB
+*3.* Setup PostgreSQL
 ----------------------------------------------------------------
-As root:
-1. `mongo` Go to the mongo shell
-2. `use misskey` Use the misskey database
-3. `db.createUser( { user: "misskey", pwd: "<password>", roles: [ { role: "readWrite", db: "misskey" } ] } )` Create the misskey user.
-4. `exit` You're done!
+:)
 
 *4.* Install Misskey
 ----------------------------------------------------------------
@@ -68,7 +64,13 @@ If you're still encountering errors about some modules, use node-gyp:
 3. `node-gyp build`
 4. `NODE_ENV=production npm run build`
 
-*7.* That is it.
+*7.* Init DB
+----------------------------------------------------------------
+``` shell
+npm run init
+```
+
+*8.* That is it.
 ----------------------------------------------------------------
 Well done! Now, you have an environment that run to Misskey.
 
diff --git a/docs/setup.fr.md b/docs/setup.fr.md
index 959ec3392ffb4ccc51476d156ef855557bbfefef..217a4c6a5b941bec77a5d2c8e89e3d89608155f5 100644
--- a/docs/setup.fr.md
+++ b/docs/setup.fr.md
@@ -22,8 +22,8 @@ adduser --disabled-password --disabled-login misskey
 Installez les paquets suivants :
 
 #### Dépendences :package:
-* **[Node.js](https://nodejs.org/en/)** >= 10.0.0
-* **[MongoDB](https://www.mongodb.com/)** >= 3.6
+* **[Node.js](https://nodejs.org/en/)** >= 11.7.0
+* **[PostgreSQL](https://www.postgresql.org/)** >= 10
 
 ##### Optionnels
 * [Redis](https://redis.io/)
@@ -31,13 +31,9 @@ Installez les paquets suivants :
 * [Elasticsearch](https://www.elastic.co/) - requis pour pouvoir activer la fonctionnalité de recherche
 * [FFmpeg](https://www.ffmpeg.org/)
 
-*3.* Paramètrage de MongoDB
+*3.* Paramètrage de PostgreSQL
 ----------------------------------------------------------------
-En root :
-1. `mongo` Ouvrez le shell mongo
-2. `use misskey` Utilisez la base de données misskey
-3. `db.createUser( { user: "misskey", pwd: "<password>", roles: [ { role: "readWrite", db: "misskey" } ] } )` Créez l'utilisateur misskey.
-4. `exit` Vous avez terminé !
+:)
 
 *4.* Installation de Misskey
 ----------------------------------------------------------------
diff --git a/docs/setup.ja.md b/docs/setup.ja.md
index 8a21e104d6349a4446a9269daa7425153ff913c3..1543541eee8188e70a46702406271fdc0ce50ecf 100644
--- a/docs/setup.ja.md
+++ b/docs/setup.ja.md
@@ -22,8 +22,8 @@ adduser --disabled-password --disabled-login misskey
 これらのソフトウェアをインストール・設定してください:
 
 #### 依存関係 :package:
-* **[Node.js](https://nodejs.org/en/)** (10.0.0以上)
-* **[MongoDB](https://www.mongodb.com/)** (3.6以上)
+* **[Node.js](https://nodejs.org/en/)** (11.7.0以上)
+* **[PostgreSQL](https://www.postgresql.org/)** (10以上)
 
 ##### オプション
 * [Redis](https://redis.io/)
@@ -38,13 +38,9 @@ adduser --disabled-password --disabled-login misskey
 	* 検索機能を有効にするためにはインストールが必要です。
 * [FFmpeg](https://www.ffmpeg.org/)
 
-*3.* MongoDBの設定
+*3.* PostgreSQLの設定
 ----------------------------------------------------------------
-ルートで:
-1. `mongo` mongoシェルを起動
-2. `use misskey` misskeyデータベースを使用
-3. `db.createUser( { user: "misskey", pwd: "<password>", roles: [ { role: "readWrite", db: "misskey" } ] } )` misskeyユーザーを作成
-4. `exit` mongoシェルを終了
+:)
 
 *4.* Misskeyのインストール
 ----------------------------------------------------------------
@@ -74,7 +70,13 @@ Debianをお使いであれば、`build-essential`パッケージをインスト
 3. `node-gyp build`
 4. `NODE_ENV=production npm run build`
 
-*7.* 以上です!
+*7.* データベースを初期化
+----------------------------------------------------------------
+``` shell
+npm run init
+```
+
+*8.* 以上です!
 ----------------------------------------------------------------
 お疲れ様でした。これでMisskeyを動かす準備は整いました。
 
diff --git a/gulpfile.ts b/gulpfile.ts
index b2956c2403b7886c20fdacd4d5da2eae9599fb54..bf0b87ef20ed4c9ce337d70c3b0577cc9e6f15f3 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -49,7 +49,6 @@ gulp.task('build:copy:views', () =>
 
 gulp.task('build:copy', gulp.parallel('build:copy:views', () =>
 	gulp.src([
-		'./build/Release/crypto_key.node',
 		'./src/const.json',
 		'./src/server/web/views/**/*',
 		'./src/**/assets/**/*',
diff --git a/index.js b/index.js
index 5b7d1347aa67fdec4cfbbc850b25e0d82ca169ab..bc7e8b2f3a31888a1dcb2bb4210e3c4eea35a8c7 100644
--- a/index.js
+++ b/index.js
@@ -1 +1 @@
-require('./built');
+require('./built').default();
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index d4457b6594a7307e19f7686181c412bb5fab6d95..43d8cb309a2b7a38ddb28e4c90baea0b017226e2 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1238,11 +1238,6 @@ admin/views/instance.vue:
   save: "保存"
   saved: "保存しました"
   user-recommendation-config: "おすすめユーザー"
-  enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
-  external-user-recommendation-engine: "エンジン"
-  external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
-  external-user-recommendation-timeout: "タイムアウト"
-  external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
   email-config: "メールサーバーの設定"
   email-config-info: "メールアドレス確認やパスワードリセットの際に使われます。"
   enable-email: "メール配信を有効にする"
diff --git a/package.json b/package.json
index 39e6aa7deb26c38fe6e3a1cffb822df0266253cc..a7c12862ffffd626c5aa31048ddaf53bfd6d90e7 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "10.99.0",
-	"codename": "nighthike",
+	"version": "11.0.0",
+	"codename": "daybreak",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/syuilo/misskey.git"
@@ -11,6 +11,7 @@
 	"private": true,
 	"scripts": {
 		"start": "node ./index.js",
+		"init": "node ./built/init.js",
 		"debug": "DEBUG=misskey:* node ./index.js",
 		"build": "webpack && gulp build",
 		"webpack": "webpack",
@@ -62,10 +63,9 @@
 		"@types/koa-send": "4.1.1",
 		"@types/koa-views": "2.0.3",
 		"@types/koa__cors": "2.2.3",
+		"@types/lolex": "3.1.1",
 		"@types/minio": "7.0.1",
-		"@types/mkdirp": "0.5.2",
-		"@types/mocha": "5.2.5",
-		"@types/mongodb": "3.1.20",
+		"@types/mocha": "5.2.6",
 		"@types/node": "11.10.4",
 		"@types/nodemailer": "4.6.6",
 		"@types/nprogress": "0.0.29",
@@ -107,6 +107,7 @@
 		"chai": "4.2.0",
 		"chai-http": "4.2.1",
 		"chalk": "2.4.2",
+		"cli-highlight": "2.1.0",
 		"commander": "2.20.0",
 		"content-disposition": "0.5.3",
 		"crc-32": "1.2.0",
@@ -114,12 +115,10 @@
 		"cssnano": "4.1.10",
 		"dateformat": "3.0.3",
 		"deep-equal": "1.0.1",
-		"deepcopy": "0.6.3",
 		"diskusage": "1.0.0",
 		"double-ended-queue": "2.1.0-0",
 		"elasticsearch": "15.4.1",
 		"emojilib": "2.4.0",
-		"escape-regexp": "0.0.1",
 		"eslint": "5.15.1",
 		"eslint-plugin-vue": "5.2.2",
 		"eventemitter3": "3.1.0",
@@ -163,23 +162,22 @@
 		"koa-views": "6.2.0",
 		"langmap": "0.0.16",
 		"loader-utils": "1.2.3",
+		"lolex": "3.1.0",
 		"lookup-dns-cache": "2.1.0",
 		"minio": "7.0.5",
-		"mkdirp": "0.5.1",
-		"mocha": "5.2.0",
+		"mocha": "6.0.2",
 		"moji": "0.5.1",
 		"moment": "2.24.0",
-		"mongodb": "3.2.2",
-		"monk": "6.0.6",
 		"ms": "2.1.1",
-		"nan": "2.12.1",
 		"nested-property": "0.0.7",
+		"node-fetch": "2.3.0",
 		"nodemailer": "5.1.1",
 		"nprogress": "0.2.0",
 		"object-assign-deep": "0.4.0",
 		"os-utils": "0.0.14",
 		"parse5": "5.1.0",
 		"parsimmon": "1.12.0",
+		"pg": "7.9.0",
 		"portscanner": "2.2.0",
 		"postcss-loader": "3.0.0",
 		"prismjs": "1.16.0",
@@ -195,10 +193,12 @@
 		"recaptcha-promise": "0.1.3",
 		"reconnecting-websocket": "4.1.10",
 		"redis": "2.8.0",
+		"reflect-metadata": "0.1.13",
 		"rename": "1.0.4",
 		"request": "2.88.0",
 		"request-promise-native": "1.0.7",
 		"request-stats": "3.0.0",
+		"require-all": "3.0.0",
 		"rimraf": "2.6.3",
 		"rndstr": "1.0.0",
 		"s-age": "1.1.2",
@@ -219,12 +219,14 @@
 		"tinycolor2": "1.4.1",
 		"tmp": "0.0.33",
 		"ts-loader": "5.3.3",
-		"ts-node": "8.0.3",
+		"ts-node": "7.0.1",
 		"tslint": "5.13.1",
 		"tslint-sonarts": "1.9.0",
+		"typeorm": "0.2.16-rc.1",
 		"typescript": "3.3.3333",
 		"typescript-eslint-parser": "22.0.0",
 		"uglify-es": "3.3.9",
+		"ulid": "2.3.0",
 		"url-loader": "1.1.2",
 		"uuid": "3.3.2",
 		"v-animate-css": "0.0.3",
diff --git a/src/@types/deepcopy.d.ts b/src/@types/deepcopy.d.ts
deleted file mode 100644
index f276b7e678b934f7d2e667c00921777e05209b41..0000000000000000000000000000000000000000
--- a/src/@types/deepcopy.d.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-declare module 'deepcopy' {
-	type DeepcopyCustomizerValueType = 'Object';
-
-	type DeepcopyCustomizer<T> = (
-		value: T,
-		valueType: DeepcopyCustomizerValueType) => T;
-
-	interface IDeepcopyOptions<T> {
-		customizer: DeepcopyCustomizer<T>;
-	}
-
-	function deepcopy<T>(
-		value: T,
-		options?: IDeepcopyOptions<T> | DeepcopyCustomizer<T>): T;
-
-	namespace deepcopy {} // Hack
-
-	export = deepcopy;
-}
diff --git a/src/@types/escape-regexp.d.ts b/src/@types/escape-regexp.d.ts
deleted file mode 100644
index d68e6048a14595021278f5f64d49728bddecf3f2..0000000000000000000000000000000000000000
--- a/src/@types/escape-regexp.d.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-declare module 'escape-regexp' {
-	function escapeRegExp(str: string): string;
-
-	namespace escapeRegExp {} // Hack
-
-	export = escapeRegExp;
-}
diff --git a/src/argv.ts b/src/argv.ts
index b5540441cca5a5f14bde2a3cf5de1ea4258cee7d..562852d17b421d8d8f53ca21a1aed62b5eeea28e 100644
--- a/src/argv.ts
+++ b/src/argv.ts
@@ -15,5 +15,8 @@ program
 	.parse(process.argv);
 
 if (process.env.MK_ONLY_QUEUE) program.onlyQueue = true;
+if (process.env.NODE_ENV === 'test') program.disableClustering = true;
+if (process.env.NODE_ENV === 'test') program.quiet = true;
+if (process.env.NODE_ENV === 'test') program.noDaemons = true;
 
 export { program };
diff --git a/src/boot/index.ts b/src/boot/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2c86d8ed8cc7b3ef6a090bf8fc2e4ab35ffe1375
--- /dev/null
+++ b/src/boot/index.ts
@@ -0,0 +1,77 @@
+import * as cluster from 'cluster';
+import chalk from 'chalk';
+import Xev from 'xev';
+
+import Logger from '../services/logger';
+import { program } from '../argv';
+
+// for typeorm
+import 'reflect-metadata';
+import { masterMain } from './master';
+import { workerMain } from './worker';
+
+const logger = new Logger('core', 'cyan');
+const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
+const ev = new Xev();
+
+/**
+ * Init process
+ */
+export default async function() {
+	process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`;
+
+	if (cluster.isMaster || program.disableClustering) {
+		await masterMain();
+
+		if (cluster.isMaster) {
+			ev.mount();
+		}
+	}
+
+	if (cluster.isWorker || program.disableClustering) {
+		await workerMain();
+	}
+
+	// ユニットテスト時にMisskeyが子プロセスで起動された時のため
+	// それ以外のときは process.send は使えないので弾く
+	if (process.send) {
+		process.send('ok');
+	}
+}
+
+//#region Events
+
+// Listen new workers
+cluster.on('fork', worker => {
+	clusterLogger.debug(`Process forked: [${worker.id}]`);
+});
+
+// Listen online workers
+cluster.on('online', worker => {
+	clusterLogger.debug(`Process is now online: [${worker.id}]`);
+});
+
+// Listen for dying workers
+cluster.on('exit', worker => {
+	// Replace the dead worker,
+	// we're not sentimental
+	clusterLogger.error(chalk.red(`[${worker.id}] died :(`));
+	cluster.fork();
+});
+
+// Display detail of unhandled promise rejection
+if (!program.quiet) {
+	process.on('unhandledRejection', console.dir);
+}
+
+// Display detail of uncaught exception
+process.on('uncaughtException', err => {
+	logger.error(err);
+});
+
+// Dying away...
+process.on('exit', code => {
+	logger.info(`The process is going to exit with code ${code}`);
+});
+
+//#endregion
diff --git a/src/boot/master.ts b/src/boot/master.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2d4080fdb026db242bafb958b73e73cda6242e8e
--- /dev/null
+++ b/src/boot/master.ts
@@ -0,0 +1,176 @@
+import * as os from 'os';
+import * as cluster from 'cluster';
+import chalk from 'chalk';
+import * as portscanner from 'portscanner';
+import * as isRoot from 'is-root';
+
+import Logger from '../services/logger';
+import loadConfig from '../config/load';
+import { Config } from '../config/types';
+import { lessThan } from '../prelude/array';
+import * as pkg from '../../package.json';
+import { program } from '../argv';
+import { showMachineInfo } from '../misc/show-machine-info';
+import { initDb } from '../db/postgre';
+
+const logger = new Logger('core', 'cyan');
+const bootLogger = logger.createSubLogger('boot', 'magenta', false);
+
+function greet() {
+	if (!program.quiet) {
+		//#region Misskey logo
+		const v = `v${pkg.version}`;
+		console.log('  _____ _         _           ');
+		console.log(' |     |_|___ ___| |_ ___ _ _ ');
+		console.log(' | | | | |_ -|_ -| \'_| -_| | |');
+		console.log(' |_|_|_|_|___|___|_,_|___|_  |');
+		console.log(' ' + chalk.gray(v) + ('                        |___|\n'.substr(v.length)));
+		//#endregion
+
+		console.log(' Misskey is maintained by @syuilo, @AyaMorisawa, @mei23, and @acid-chicken.');
+		console.log(chalk.keyword('orange')(' If you like Misskey, please donate to support development. https://www.patreon.com/syuilo'));
+
+		console.log('');
+		console.log(chalk`< ${os.hostname()} {gray (PID: ${process.pid.toString()})} >`);
+	}
+
+	bootLogger.info('Welcome to Misskey!');
+	bootLogger.info(`Misskey v${pkg.version}`, null, true);
+}
+
+/**
+ * Init master process
+ */
+export async function masterMain() {
+	greet();
+
+	let config: Config;
+
+	try {
+		// initialize app
+		config = await init();
+
+		if (config.port == null) {
+			bootLogger.error('The port is not configured. Please configure port.', null, true);
+			process.exit(1);
+		}
+
+		if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) {
+			bootLogger.error('You need root privileges to listen on well-known port on Linux', null, true);
+			process.exit(1);
+		}
+
+		if (!await isPortAvailable(config.port)) {
+			bootLogger.error(`Port ${config.port} is already in use`, null, true);
+			process.exit(1);
+		}
+	} catch (e) {
+		bootLogger.error('Fatal error occurred during initialization', null, true);
+		process.exit(1);
+	}
+
+	bootLogger.succ('Misskey initialized');
+
+	if (!program.disableClustering) {
+		await spawnWorkers(config.clusterLimit);
+	}
+
+	if (!program.noDaemons) {
+		require('../daemons/server-stats').default();
+		require('../daemons/notes-stats').default();
+		require('../daemons/queue-stats').default();
+	}
+
+	bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
+}
+
+const runningNodejsVersion = process.version.slice(1).split('.').map(x => parseInt(x, 10));
+const requiredNodejsVersion = [11, 7, 0];
+const satisfyNodejsVersion = !lessThan(runningNodejsVersion, requiredNodejsVersion);
+
+function isWellKnownPort(port: number): boolean {
+	return port < 1024;
+}
+
+async function isPortAvailable(port: number): Promise<boolean> {
+	return await portscanner.checkPortStatus(port, '127.0.0.1') === 'closed';
+}
+
+function showEnvironment(): void {
+	const env = process.env.NODE_ENV;
+	const logger = bootLogger.createSubLogger('env');
+	logger.info(typeof env == 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`);
+
+	if (env !== 'production') {
+		logger.warn('The environment is not in production mode.');
+		logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true);
+	}
+
+	logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`);
+}
+
+/**
+ * Init app
+ */
+async function init(): Promise<Config> {
+	showEnvironment();
+
+	const nodejsLogger = bootLogger.createSubLogger('nodejs');
+
+	nodejsLogger.info(`Version ${runningNodejsVersion.join('.')}`);
+
+	if (!satisfyNodejsVersion) {
+		nodejsLogger.error(`Node.js version is less than ${requiredNodejsVersion.join('.')}. Please upgrade it.`, null, true);
+		process.exit(1);
+	}
+
+	await showMachineInfo(bootLogger);
+
+	const configLogger = bootLogger.createSubLogger('config');
+	let config;
+
+	try {
+		config = loadConfig();
+	} catch (exception) {
+		if (typeof exception === 'string') {
+			configLogger.error(exception);
+			process.exit(1);
+		}
+		if (exception.code === 'ENOENT') {
+			configLogger.error('Configuration file not found', null, true);
+			process.exit(1);
+		}
+		throw exception;
+	}
+
+	configLogger.succ('Loaded');
+
+	// Try to connect to DB
+	try {
+		bootLogger.info('Connecting database...');
+		await initDb();
+	} catch (e) {
+		bootLogger.error('Cannot connect to database', null, true);
+		bootLogger.error(e);
+		process.exit(1);
+	}
+
+	return config;
+}
+
+async function spawnWorkers(limit: number = Infinity) {
+	const workers = Math.min(limit, os.cpus().length);
+	bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
+	await Promise.all([...Array(workers)].map(spawnWorker));
+	bootLogger.succ('All workers started');
+}
+
+function spawnWorker(): Promise<void> {
+	return new Promise(res => {
+		const worker = cluster.fork();
+		worker.on('message', message => {
+			if (message !== 'ready') return;
+			res();
+		});
+	});
+}
diff --git a/src/boot/worker.ts b/src/boot/worker.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ca3716972a169b293e8c931f273cafad6fa98447
--- /dev/null
+++ b/src/boot/worker.ts
@@ -0,0 +1,20 @@
+import * as cluster from 'cluster';
+import { initDb } from '../db/postgre';
+
+/**
+ * Init worker process
+ */
+export async function workerMain() {
+	await initDb();
+
+	// start server
+	await require('../server').default();
+
+	// start job queue
+	require('../queue').default();
+
+	if (cluster.isWorker) {
+		// Send a 'ready' message to parent process
+		process.send('ready');
+	}
+}
diff --git a/src/client/app/admin/views/drive.vue b/src/client/app/admin/views/drive.vue
index 7812aadaaf152ca199a270ceaff866e486155592..491050b1f7aaa0dd9a3a69439ac980a9831bc069 100644
--- a/src/client/app/admin/views/drive.vue
+++ b/src/client/app/admin/views/drive.vue
@@ -48,7 +48,7 @@
 							<div>
 								<div>
 									<span style="margin-right:16px;">{{ file.type }}</span>
-									<span>{{ file.datasize | bytes }}</span>
+									<span>{{ file.size | bytes }}</span>
 								</div>
 								<div><mk-time :time="file.createdAt" mode="detail"/></div>
 							</div>
diff --git a/src/client/app/admin/views/hashtags.vue b/src/client/app/admin/views/hashtags.vue
index b3190c29c415b70ef01b9ce06a5db0f9219c3aca..e1cc4b494d685e4f7c38d5a0871a08f93ffaf32c 100644
--- a/src/client/app/admin/views/hashtags.vue
+++ b/src/client/app/admin/views/hashtags.vue
@@ -3,7 +3,7 @@
 	<ui-card>
 		<template #title>{{ $t('hided-tags') }}</template>
 		<section>
-			<textarea class="jdnqwkzlnxcfftthoybjxrebyolvoucw" v-model="hidedTags"></textarea>
+			<textarea class="jdnqwkzlnxcfftthoybjxrebyolvoucw" v-model="hiddenTags"></textarea>
 			<ui-button @click="save">{{ $t('save') }}</ui-button>
 		</section>
 	</ui-card>
@@ -18,18 +18,18 @@ export default Vue.extend({
 	i18n: i18n('admin/views/hashtags.vue'),
 	data() {
 		return {
-			hidedTags: '',
+			hiddenTags: '',
 		};
 	},
 	created() {
 		this.$root.getMeta().then(meta => {
-			this.hidedTags = meta.hidedTags.join('\n');
+			this.hiddenTags = meta.hiddenTags.join('\n');
 		});
 	},
 	methods: {
 		save() {
 			this.$root.api('admin/update-meta', {
-				hidedTags: this.hidedTags.split('\n')
+				hiddenTags: this.hiddenTags.split('\n')
 			}).then(() => {
 				//this.$root.os.apis.dialog({ text: `Saved` });
 			}).catch(e => {
diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue
index 2d2a07784b9912908a33e3a8219b58594be83067..bc2a5fba85d253ac935482799993bebda0532226 100644
--- a/src/client/app/admin/views/instance.vue
+++ b/src/client/app/admin/views/instance.vue
@@ -77,12 +77,6 @@
 			<header>summaly Proxy</header>
 			<ui-input v-model="summalyProxy">URL</ui-input>
 		</section>
-		<section>
-			<header><fa :icon="faUserPlus"/> {{ $t('user-recommendation-config') }}</header>
-			<ui-switch v-model="enableExternalUserRecommendation">{{ $t('enable-external-user-recommendation') }}</ui-switch>
-			<ui-input v-model="externalUserRecommendationEngine" :disabled="!enableExternalUserRecommendation">{{ $t('external-user-recommendation-engine') }}<template #desc>{{ $t('external-user-recommendation-engine-desc') }}</template></ui-input>
-			<ui-input v-model="externalUserRecommendationTimeout" type="number" :disabled="!enableExternalUserRecommendation">{{ $t('external-user-recommendation-timeout') }}<template #suffix>ms</template><template #desc>{{ $t('external-user-recommendation-timeout-desc') }}</template></ui-input>
-		</section>
 		<section>
 			<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
 		</section>
@@ -184,9 +178,6 @@ export default Vue.extend({
 			discordClientSecret: null,
 			proxyAccount: null,
 			inviteCode: null,
-			enableExternalUserRecommendation: false,
-			externalUserRecommendationEngine: null,
-			externalUserRecommendationTimeout: null,
 			summalyProxy: null,
 			enableEmail: false,
 			email: null,
@@ -205,8 +196,8 @@ export default Vue.extend({
 
 	created() {
 		this.$root.getMeta().then(meta => {
-			this.maintainerName = meta.maintainer.name;
-			this.maintainerEmail = meta.maintainer.email;
+			this.maintainerName = meta.maintainerName;
+			this.maintainerEmail = meta.maintainerEmail;
 			this.disableRegistration = meta.disableRegistration;
 			this.disableLocalTimeline = meta.disableLocalTimeline;
 			this.disableGlobalTimeline = meta.disableGlobalTimeline;
@@ -236,9 +227,6 @@ export default Vue.extend({
 			this.enableDiscordIntegration = meta.enableDiscordIntegration;
 			this.discordClientId = meta.discordClientId;
 			this.discordClientSecret = meta.discordClientSecret;
-			this.enableExternalUserRecommendation = meta.enableExternalUserRecommendation;
-			this.externalUserRecommendationEngine = meta.externalUserRecommendationEngine;
-			this.externalUserRecommendationTimeout = meta.externalUserRecommendationTimeout;
 			this.summalyProxy = meta.summalyProxy;
 			this.enableEmail = meta.enableEmail;
 			this.email = meta.email;
@@ -299,9 +287,6 @@ export default Vue.extend({
 				enableDiscordIntegration: this.enableDiscordIntegration,
 				discordClientId: this.discordClientId,
 				discordClientSecret: this.discordClientSecret,
-				enableExternalUserRecommendation: this.enableExternalUserRecommendation,
-				externalUserRecommendationEngine: this.externalUserRecommendationEngine,
-				externalUserRecommendationTimeout: parseInt(this.externalUserRecommendationTimeout, 10),
 				summalyProxy: this.summalyProxy,
 				enableEmail: this.enableEmail,
 				email: this.email,
diff --git a/src/client/app/admin/views/logs.vue b/src/client/app/admin/views/logs.vue
index 4a2d957ed7e7c5e8ed45b27294ff0dd9e6ebdbbd..5c2cfdb3964730814f0eaa4859248c5783f62089 100644
--- a/src/client/app/admin/views/logs.vue
+++ b/src/client/app/admin/views/logs.vue
@@ -19,7 +19,7 @@
 			</ui-horizon-group>
 
 			<div class="nqjzuvev">
-				<code v-for="log in logs" :key="log._id" :class="log.level">
+				<code v-for="log in logs" :key="log.id" :class="log.level">
 					<details>
 						<summary><mk-time :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary>
 						<vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty>
diff --git a/src/client/app/admin/views/users.vue b/src/client/app/admin/views/users.vue
index ff485cec868181bfb2a5e844dbda891b5a8538b9..0f46b564a93cb0ac7ef4a3a059603383388f2e58 100644
--- a/src/client/app/admin/views/users.vue
+++ b/src/client/app/admin/views/users.vue
@@ -165,7 +165,7 @@ export default Vue.extend({
 
 		/** 処理対象ユーザーの情報を更新する */
 		async refreshUser() {
-			this.$root.api('admin/show-user', { userId: this.user._id }).then(info => {
+			this.$root.api('admin/show-user', { userId: this.user.id }).then(info => {
 				this.user = info;
 			});
 		},
@@ -173,7 +173,7 @@ export default Vue.extend({
 		async resetPassword() {
 			if (!await this.getConfirmed(this.$t('reset-password-confirm'))) return;
 
-			this.$root.api('admin/reset-password', { userId: this.user._id }).then(res => {
+			this.$root.api('admin/reset-password', { userId: this.user.id }).then(res => {
 				this.$root.dialog({
 					type: 'success',
 					text: this.$t('password-updated', { password: res.password })
@@ -187,7 +187,7 @@ export default Vue.extend({
 			this.verifying = true;
 
 			const process = async () => {
-				await this.$root.api('admin/verify-user', { userId: this.user._id });
+				await this.$root.api('admin/verify-user', { userId: this.user.id });
 				this.$root.dialog({
 					type: 'success',
 					text: this.$t('verified')
@@ -212,7 +212,7 @@ export default Vue.extend({
 			this.unverifying = true;
 
 			const process = async () => {
-				await this.$root.api('admin/unverify-user', { userId: this.user._id });
+				await this.$root.api('admin/unverify-user', { userId: this.user.id });
 				this.$root.dialog({
 					type: 'success',
 					text: this.$t('unverified')
@@ -233,7 +233,7 @@ export default Vue.extend({
 
 		async silenceUser() {
 			const process = async () => {
-				await this.$root.api('admin/silence-user', { userId: this.user._id });
+				await this.$root.api('admin/silence-user', { userId: this.user.id });
 				this.$root.dialog({
 					type: 'success',
 					splash: true
@@ -252,7 +252,7 @@ export default Vue.extend({
 
 		async unsilenceUser() {
 			const process = async () => {
-				await this.$root.api('admin/unsilence-user', { userId: this.user._id });
+				await this.$root.api('admin/unsilence-user', { userId: this.user.id });
 				this.$root.dialog({
 					type: 'success',
 					splash: true
@@ -275,7 +275,7 @@ export default Vue.extend({
 			this.suspending = true;
 
 			const process = async () => {
-				await this.$root.api('admin/suspend-user', { userId: this.user._id });
+				await this.$root.api('admin/suspend-user', { userId: this.user.id });
 				this.$root.dialog({
 					type: 'success',
 					text: this.$t('suspended')
@@ -300,7 +300,7 @@ export default Vue.extend({
 			this.unsuspending = true;
 
 			const process = async () => {
-				await this.$root.api('admin/unsuspend-user', { userId: this.user._id });
+				await this.$root.api('admin/unsuspend-user', { userId: this.user.id });
 				this.$root.dialog({
 					type: 'success',
 					text: this.$t('unsuspended')
@@ -320,7 +320,7 @@ export default Vue.extend({
 		},
 
 		async updateRemoteUser() {
-			this.$root.api('admin/update-remote-user', { userId: this.user._id }).then(res => {
+			this.$root.api('admin/update-remote-user', { userId: this.user.id }).then(res => {
 				this.$root.dialog({
 					type: 'success',
 					text: this.$t('remote-user-updated')
diff --git a/src/client/app/auth/views/form.vue b/src/client/app/auth/views/form.vue
index 105af375b6d0c34b4c2c1275c90d69e5484419b1..d5d6b25f009bc365572320f29e3ded88a4f29b77 100644
--- a/src/client/app/auth/views/form.vue
+++ b/src/client/app/auth/views/form.vue
@@ -14,15 +14,15 @@
 			<h2>{{ $t('permission-ask') }}</h2>
 			<ul>
 				<template v-for="p in app.permission">
-					<li v-if="p == 'account-read'">{{ $t('account-read') }}</li>
-					<li v-if="p == 'account-write'">{{ $t('account-write') }}</li>
-					<li v-if="p == 'note-write'">{{ $t('note-write') }}</li>
+					<li v-if="p == 'read:account'">{{ $t('read:account') }}</li>
+					<li v-if="p == 'write:account'">{{ $t('write:account') }}</li>
+					<li v-if="p == 'write:notes'">{{ $t('write:notes') }}</li>
 					<li v-if="p == 'like-write'">{{ $t('like-write') }}</li>
-					<li v-if="p == 'following-write'">{{ $t('following-write') }}</li>
-					<li v-if="p == 'drive-read'">{{ $t('drive-read') }}</li>
-					<li v-if="p == 'drive-write'">{{ $t('drive-write') }}</li>
-					<li v-if="p == 'notification-read'">{{ $t('notification-read') }}</li>
-					<li v-if="p == 'notification-write'">{{ $t('notification-write') }}</li>
+					<li v-if="p == 'write:following'">{{ $t('write:following') }}</li>
+					<li v-if="p == 'read:drive'">{{ $t('read:drive') }}</li>
+					<li v-if="p == 'write:drive'">{{ $t('write:drive') }}</li>
+					<li v-if="p == 'read:notifications'">{{ $t('read:notifications') }}</li>
+					<li v-if="p == 'write:notifications'">{{ $t('write:notifications') }}</li>
 				</template>
 			</ul>
 		</section>
diff --git a/src/client/app/common/define-widget.ts b/src/client/app/common/define-widget.ts
index 1efdbb1880b45f9fa5d50ba42b7313774de578a5..632ddf2ed6586c1188d3d3a9841410fadde8431d 100644
--- a/src/client/app/common/define-widget.ts
+++ b/src/client/app/common/define-widget.ts
@@ -45,15 +45,9 @@ export default function <T extends object>(data: {
 			this.$watch('props', () => {
 				this.mergeProps();
 			});
-
-			this.bakeProps();
 		},
 
 		methods: {
-			bakeProps() {
-				this.bakedOldProps = JSON.stringify(this.props);
-			},
-
 			mergeProps() {
 				if (data.props) {
 					const defaultProps = data.props();
@@ -65,17 +59,10 @@ export default function <T extends object>(data: {
 			},
 
 			save() {
-				if (this.bakedOldProps == JSON.stringify(this.props)) return;
-
-				this.bakeProps();
-
 				if (this.platform == 'deck') {
 					this.$store.commit('device/updateDeckColumn', this.column);
 				} else {
-					this.$root.api('i/update_widget', {
-						id: this.id,
-						data: this.props
-					});
+					this.$store.commit('device/updateWidget', this.widget);
 				}
 			}
 		}
diff --git a/src/client/app/common/scripts/note-mixin.ts b/src/client/app/common/scripts/note-mixin.ts
index 5707d1bb4145942252a2bbef4f5e33d3a07df593..67bbe8c0ae622f641249352007c48d6997ffdf2f 100644
--- a/src/client/app/common/scripts/note-mixin.ts
+++ b/src/client/app/common/scripts/note-mixin.ts
@@ -70,8 +70,8 @@ export default (opts: Opts = {}) => ({
 		},
 
 		reactionsCount(): number {
-			return this.appearNote.reactionCounts
-				? sum(Object.values(this.appearNote.reactionCounts))
+			return this.appearNote.reactions
+				? sum(Object.values(this.appearNote.reactions))
 				: 0;
 		},
 
diff --git a/src/client/app/common/scripts/note-subscriber.ts b/src/client/app/common/scripts/note-subscriber.ts
index c2b4dd6df9ac3217d7a5385b3f2f2e0ec34459fd..02d810ded982f7f6e97ff67be71e8038c09964a1 100644
--- a/src/client/app/common/scripts/note-subscriber.ts
+++ b/src/client/app/common/scripts/note-subscriber.ts
@@ -87,16 +87,16 @@ export default prop => ({
 				case 'reacted': {
 					const reaction = body.reaction;
 
-					if (this.$_ns_target.reactionCounts == null) {
-						Vue.set(this.$_ns_target, 'reactionCounts', {});
+					if (this.$_ns_target.reactions == null) {
+						Vue.set(this.$_ns_target, 'reactions', {});
 					}
 
-					if (this.$_ns_target.reactionCounts[reaction] == null) {
-						Vue.set(this.$_ns_target.reactionCounts, reaction, 0);
+					if (this.$_ns_target.reactions[reaction] == null) {
+						Vue.set(this.$_ns_target.reactions, reaction, 0);
 					}
 
 					// Increment the count
-					this.$_ns_target.reactionCounts[reaction]++;
+					this.$_ns_target.reactions[reaction]++;
 
 					if (body.userId == this.$store.state.i.id) {
 						Vue.set(this.$_ns_target, 'myReaction', reaction);
@@ -107,16 +107,16 @@ export default prop => ({
 				case 'unreacted': {
 					const reaction = body.reaction;
 
-					if (this.$_ns_target.reactionCounts == null) {
+					if (this.$_ns_target.reactions == null) {
 						return;
 					}
 
-					if (this.$_ns_target.reactionCounts[reaction] == null) {
+					if (this.$_ns_target.reactions[reaction] == null) {
 						return;
 					}
 
 					// Decrement the count
-					if (this.$_ns_target.reactionCounts[reaction] > 0) this.$_ns_target.reactionCounts[reaction]--;
+					if (this.$_ns_target.reactions[reaction] > 0) this.$_ns_target.reactions[reaction]--;
 
 					if (body.userId == this.$store.state.i.id) {
 						Vue.set(this.$_ns_target, 'myReaction', null);
@@ -125,9 +125,11 @@ export default prop => ({
 				}
 
 				case 'pollVoted': {
-					if (body.userId == this.$store.state.i.id) return;
 					const choice = body.choice;
-					this.$_ns_target.poll.choices.find(c => c.id === choice).votes++;
+					this.$_ns_target.poll.choices[choice].votes++;
+					if (body.userId == this.$store.state.i.id) {
+						Vue.set(this.$_ns_target.poll.choices[choice], 'isVoted', true);
+					}
 					break;
 				}
 
diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue
index dce594e7022557be9d45e6d68a7fd211217fdf6f..c074fb600fead3da11c90210b2caadec83b767b0 100644
--- a/src/client/app/common/views/components/avatar.vue
+++ b/src/client/app/common/views/components/avatar.vue
@@ -55,11 +55,12 @@ export default Vue.extend({
 		},
 		icon(): any {
 			return {
-				backgroundColor: this.lightmode
-					? `rgb(${this.user.avatarColor.slice(0, 3).join(',')})`
-					: this.user.avatarColor && this.user.avatarColor.length == 3
-						? `rgb(${this.user.avatarColor.join(',')})`
-						: null,
+				backgroundColor: this.user.avatarColor ? this.lightmode
+					? this.user.avatarColor
+					: this.user.avatarColor.startsWith('rgb(')
+						? this.user.avatarColor
+						: null
+					: null,
 				backgroundImage: this.lightmode ? null : `url(${this.url})`,
 				borderRadius: this.$store.state.settings.circleIcons ? '100%' : null
 			};
@@ -67,7 +68,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		if (this.user.avatarColor) {
-			this.$el.style.color = `rgb(${this.user.avatarColor.slice(0, 3).join(',')})`;
+			this.$el.style.color = this.user.avatarColor;
 		}
 	},
 	methods: {
diff --git a/src/client/app/common/views/components/games/reversi/reversi.game.vue b/src/client/app/common/views/components/games/reversi/reversi.game.vue
index c6fc36db33d1c7dd82584e17e6326ec8ec25ce07..bd0401f785661e00587624cc5f1e978c650f4969 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.game.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.game.vue
@@ -24,11 +24,11 @@
 
 	<div class="board">
 		<div class="labels-x" v-if="this.$store.state.settings.games.reversi.showBoardLabels">
-			<span v-for="i in game.settings.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
+			<span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
 		</div>
 		<div class="flex">
 			<div class="labels-y" v-if="this.$store.state.settings.games.reversi.showBoardLabels">
-				<div v-for="i in game.settings.map.length">{{ i }}</div>
+				<div v-for="i in game.map.length">{{ i }}</div>
 			</div>
 			<div class="cells" :style="cellsStyle">
 				<div v-for="(stone, i) in o.board"
@@ -46,11 +46,11 @@
 				</div>
 			</div>
 			<div class="labels-y" v-if="this.$store.state.settings.games.reversi.showBoardLabels">
-				<div v-for="i in game.settings.map.length">{{ i }}</div>
+				<div v-for="i in game.map.length">{{ i }}</div>
 			</div>
 		</div>
 		<div class="labels-x" v-if="this.$store.state.settings.games.reversi.showBoardLabels">
-			<span v-for="i in game.settings.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
+			<span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
 		</div>
 	</div>
 
@@ -71,9 +71,9 @@
 	</div>
 
 	<div class="info">
-		<p v-if="game.settings.isLlotheo">{{ $t('is-llotheo') }}</p>
-		<p v-if="game.settings.loopedBoard">{{ $t('looped-map') }}</p>
-		<p v-if="game.settings.canPutEverywhere">{{ $t('can-put-everywhere') }}</p>
+		<p v-if="game.isLlotheo">{{ $t('is-llotheo') }}</p>
+		<p v-if="game.loopedBoard">{{ $t('looped-map') }}</p>
+		<p v-if="game.canPutEverywhere">{{ $t('can-put-everywhere') }}</p>
 	</div>
 </div>
 </template>
@@ -160,8 +160,8 @@ export default Vue.extend({
 
 		cellsStyle(): any {
 			return {
-				'grid-template-rows': `repeat(${this.game.settings.map.length}, 1fr)`,
-				'grid-template-columns': `repeat(${this.game.settings.map[0].length}, 1fr)`
+				'grid-template-rows': `repeat(${this.game.map.length}, 1fr)`,
+				'grid-template-columns': `repeat(${this.game.map[0].length}, 1fr)`
 			};
 		}
 	},
@@ -169,10 +169,10 @@ export default Vue.extend({
 	watch: {
 		logPos(v) {
 			if (!this.game.isEnded) return;
-			this.o = new Reversi(this.game.settings.map, {
-				isLlotheo: this.game.settings.isLlotheo,
-				canPutEverywhere: this.game.settings.canPutEverywhere,
-				loopedBoard: this.game.settings.loopedBoard
+			this.o = new Reversi(this.game.map, {
+				isLlotheo: this.game.isLlotheo,
+				canPutEverywhere: this.game.canPutEverywhere,
+				loopedBoard: this.game.loopedBoard
 			});
 			for (const log of this.logs.slice(0, v)) {
 				this.o.put(log.color, log.pos);
@@ -184,10 +184,10 @@ export default Vue.extend({
 	created() {
 		this.game = this.initGame;
 
-		this.o = new Reversi(this.game.settings.map, {
-			isLlotheo: this.game.settings.isLlotheo,
-			canPutEverywhere: this.game.settings.canPutEverywhere,
-			loopedBoard: this.game.settings.loopedBoard
+		this.o = new Reversi(this.game.map, {
+			isLlotheo: this.game.isLlotheo,
+			canPutEverywhere: this.game.canPutEverywhere,
+			loopedBoard: this.game.loopedBoard
 		});
 
 		for (const log of this.game.logs) {
@@ -286,10 +286,10 @@ export default Vue.extend({
 		onRescue(game) {
 			this.game = game;
 
-			this.o = new Reversi(this.game.settings.map, {
-				isLlotheo: this.game.settings.isLlotheo,
-				canPutEverywhere: this.game.settings.canPutEverywhere,
-				loopedBoard: this.game.settings.loopedBoard
+			this.o = new Reversi(this.game.map, {
+				isLlotheo: this.game.isLlotheo,
+				canPutEverywhere: this.game.canPutEverywhere,
+				loopedBoard: this.game.loopedBoard
 			});
 
 			for (const log of this.game.logs) {
diff --git a/src/client/app/common/views/components/games/reversi/reversi.room.vue b/src/client/app/common/views/components/games/reversi/reversi.room.vue
index d5d148790cc1ec0c5dc4d5882b83e05f56318a66..9ee1a78b869dfd211dc8269b114329ebd0f31203 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.room.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.room.vue
@@ -17,9 +17,9 @@
 			</header>
 
 			<div>
-				<div class="random" v-if="game.settings.map == null"><fa icon="dice"/></div>
-				<div class="board" v-else :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
-					<div v-for="(x, i) in game.settings.map.join('')"
+				<div class="random" v-if="game.map == null"><fa icon="dice"/></div>
+				<div class="board" v-else :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }">
+					<div v-for="(x, i) in game.map.join('')"
 							:data-none="x == ' '"
 							@click="onPixelClick(i, x)">
 						<fa v-if="x == 'b'" :icon="fasCircle"/>
@@ -35,9 +35,9 @@
 			</header>
 
 			<div>
-				<form-radio v-model="game.settings.bw" value="random" @change="updateSettings">{{ $t('random') }}</form-radio>
-				<form-radio v-model="game.settings.bw" :value="1" @change="updateSettings">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user1"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio>
-				<form-radio v-model="game.settings.bw" :value="2" @change="updateSettings">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user2"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio>
+				<form-radio v-model="game.bw" value="random" @change="updateSettings('bw')">{{ $t('random') }}</form-radio>
+				<form-radio v-model="game.bw" :value="1" @change="updateSettings('bw')">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user1"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio>
+				<form-radio v-model="game.bw" :value="2" @change="updateSettings('bw')">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user2"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio>
 			</div>
 		</div>
 
@@ -47,9 +47,9 @@
 			</header>
 
 			<div>
-				<ui-switch v-model="game.settings.isLlotheo" @change="updateSettings">{{ $t('is-llotheo') }}</ui-switch>
-				<ui-switch v-model="game.settings.loopedBoard" @change="updateSettings">{{ $t('looped-map') }}</ui-switch>
-				<ui-switch v-model="game.settings.canPutEverywhere" @change="updateSettings">{{ $t('can-put-everywhere') }}</ui-switch>
+				<ui-switch v-model="game.isLlotheo" @change="updateSettings('isLlotheo')">{{ $t('is-llotheo') }}</ui-switch>
+				<ui-switch v-model="game.loopedBoard" @change="updateSettings('loopedBoard')">{{ $t('looped-map') }}</ui-switch>
+				<ui-switch v-model="game.canPutEverywhere" @change="updateSettings('canPutEverywhere')">{{ $t('can-put-everywhere') }}</ui-switch>
 			</div>
 		</div>
 
@@ -159,8 +159,8 @@ export default Vue.extend({
 		this.connection.on('initForm', this.onInitForm);
 		this.connection.on('message', this.onMessage);
 
-		if (this.game.user1Id != this.$store.state.i.id && this.game.settings.form1) this.form = this.game.settings.form1;
-		if (this.game.user2Id != this.$store.state.i.id && this.game.settings.form2) this.form = this.game.settings.form2;
+		if (this.game.user1Id != this.$store.state.i.id && this.game.form1) this.form = this.game.form1;
+		if (this.game.user2Id != this.$store.state.i.id && this.game.form2) this.form = this.game.form2;
 	},
 
 	beforeDestroy() {
@@ -189,18 +189,19 @@ export default Vue.extend({
 			this.$forceUpdate();
 		},
 
-		updateSettings() {
+		updateSettings(key: string) {
 			this.connection.send('updateSettings', {
-				settings: this.game.settings
+				key: key,
+				value: this.game[key]
 			});
 		},
 
-		onUpdateSettings(settings) {
-			this.game.settings = settings;
-			if (this.game.settings.map == null) {
+		onUpdateSettings({ key, value }) {
+			this.game[key] = value;
+			if (this.game.map == null) {
 				this.mapName = null;
 			} else {
-				const found = Object.values(maps).find(x => x.data.join('') == this.game.settings.map.join(''));
+				const found = Object.values(maps).find(x => x.data.join('') == this.game.map.join(''));
 				this.mapName = found ? found.name : '-Custom-';
 			}
 		},
@@ -224,27 +225,27 @@ export default Vue.extend({
 
 		onMapChange() {
 			if (this.mapName == null) {
-				this.game.settings.map = null;
+				this.game.map = null;
 			} else {
-				this.game.settings.map = Object.values(maps).find(x => x.name == this.mapName).data;
+				this.game.map = Object.values(maps).find(x => x.name == this.mapName).data;
 			}
 			this.$forceUpdate();
 			this.updateSettings();
 		},
 
 		onPixelClick(pos, pixel) {
-			const x = pos % this.game.settings.map[0].length;
-			const y = Math.floor(pos / this.game.settings.map[0].length);
+			const x = pos % this.game.map[0].length;
+			const y = Math.floor(pos / this.game.map[0].length);
 			const newPixel =
 				pixel == ' ' ? '-' :
 				pixel == '-' ? 'b' :
 				pixel == 'b' ? 'w' :
 				' ';
-			const line = this.game.settings.map[y].split('');
+			const line = this.game.map[y].split('');
 			line[x] = newPixel;
-			this.$set(this.game.settings.map, y, line.join(''));
+			this.$set(this.game.map, y, line.join(''));
 			this.$forceUpdate();
-			this.updateSettings();
+			this.updateSettings('map');
 		}
 	}
 });
diff --git a/src/client/app/common/views/components/games/reversi/reversi.vue b/src/client/app/common/views/components/games/reversi/reversi.vue
index b6803cd7f7f2f01c903ce989fdc88f78cd3f2d29..d33471a04981bbeb1a3fd6178247cc323c468125 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.vue
@@ -106,7 +106,7 @@ export default Vue.extend({
 		async nav(game, actualNav = true) {
 			if (this.selfNav) {
 				// 受け取ったゲーム情報が省略されたものなら完全な情報を取得する
-				if (game != null && (game.settings == null || game.settings.map == null)) {
+				if (game != null && game.map == null) {
 					game = await this.$root.api('games/reversi/games/show', {
 						gameId: game.id
 					});
diff --git a/src/client/app/common/views/components/instance.vue b/src/client/app/common/views/components/instance.vue
index 7b8d4f8e0bc488d54a4c765659fcea91d30f05df..497e4976f59b80f48aa25487f1530fd57ad13cd1 100644
--- a/src/client/app/common/views/components/instance.vue
+++ b/src/client/app/common/views/components/instance.vue
@@ -2,7 +2,7 @@
 <div class="nhasjydimbopojusarffqjyktglcuxjy" v-if="meta">
 	<div class="banner" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"></div>
 
-	<h1>{{ meta.name }}</h1>
+	<h1>{{ meta.name || 'Misskey' }}</h1>
 	<p v-html="meta.description || this.$t('@.about')"></p>
 	<router-link to="/">{{ $t('start') }}</router-link>
 </div>
diff --git a/src/client/app/common/views/components/mention.vue b/src/client/app/common/views/components/mention.vue
index 11dddbd52a126ffe13164208e8e03bf09ff453a0..e1f67282b6f1e0483105974524e2beda3e843a03 100644
--- a/src/client/app/common/views/components/mention.vue
+++ b/src/client/app/common/views/components/mention.vue
@@ -33,7 +33,7 @@ export default Vue.extend({
 	},
 	computed: {
 		canonical(): string {
-			return `@${this.username}@${toUnicode(this.host)}`;
+			return this.host === localHost ? `@${this.username}` : `@${this.username}@${toUnicode(this.host)}`;
 		},
 		isMe(): boolean {
 			return this.$store.getters.isSignedIn && this.canonical.toLowerCase() === `@${this.$store.state.i.username}@${toUnicode(localHost)}`.toLowerCase();
diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue
index ba14ba3a44703475da40bab4f95b15280994827b..dc3aaa34f38261ccc288be4037bc75a960c10063 100644
--- a/src/client/app/common/views/components/poll.vue
+++ b/src/client/app/common/views/components/poll.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-poll" :data-done="closed || isVoted">
 	<ul>
-		<li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!closed && !isVoted ? $t('vote-to').replace('{}', choice.text) : ''">
+		<li v-for="(choice, i) in poll.choices" :key="i" @click="vote(i)" :class="{ voted: choice.voted }" :title="!closed && !isVoted ? $t('vote-to').replace('{}', choice.text) : ''">
 			<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
 			<span>
 				<template v-if="choice.isVoted"><fa icon="check"/></template>
@@ -82,12 +82,6 @@ export default Vue.extend({
 				noteId: this.note.id,
 				choice: id
 			}).then(() => {
-				for (const c of this.poll.choices) {
-					if (c.id == id) {
-						c.votes++;
-						Vue.set(c, 'isVoted', true);
-					}
-				}
 				if (!this.showResult) this.showResult = !this.poll.multiple;
 			});
 		}
diff --git a/src/client/app/common/views/components/reactions-viewer.vue b/src/client/app/common/views/components/reactions-viewer.vue
index cf7f88b2f56af9d4571149f8ce5fa28693093cce..46668054b8a1fac44dbaf485aaf331adb4446e9a 100644
--- a/src/client/app/common/views/components/reactions-viewer.vue
+++ b/src/client/app/common/views/components/reactions-viewer.vue
@@ -20,7 +20,7 @@ export default Vue.extend({
 	},
 	computed: {
 		reactions(): any {
-			return this.note.reactionCounts;
+			return this.note.reactions;
 		},
 		isMe(): boolean {
 			return this.$store.getters.isSignedIn && this.$store.state.i.id === this.note.userId;
diff --git a/src/client/app/common/views/components/settings/notification.vue b/src/client/app/common/views/components/settings/notification.vue
index b689544d69f91460dabb7011a0b183140a1b90b3..2554fe633137dcd589f264cdbf08059f049424ea 100644
--- a/src/client/app/common/views/components/settings/notification.vue
+++ b/src/client/app/common/views/components/settings/notification.vue
@@ -2,7 +2,7 @@
 <ui-card>
 	<template #title><fa :icon="['far', 'bell']"/> {{ $t('title') }}</template>
 	<section>
-		<ui-switch v-model="$store.state.i.settings.autoWatch" @change="onChangeAutoWatch">
+		<ui-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch">
 			{{ $t('auto-watch') }}<template #desc>{{ $t('auto-watch-desc') }}</template>
 		</ui-switch>
 		<section>
diff --git a/src/client/app/common/views/components/settings/profile.vue b/src/client/app/common/views/components/settings/profile.vue
index b9837a6966c4dc95d0b03c6c8ef94ec6483218ff..acfc1875a66038bb497ade1d73adab20ccdcc9a5 100644
--- a/src/client/app/common/views/components/settings/profile.vue
+++ b/src/client/app/common/views/components/settings/profile.vue
@@ -158,14 +158,14 @@ export default Vue.extend({
 
 	computed: {
 		alwaysMarkNsfw: {
-			get() { return this.$store.state.i.settings.alwaysMarkNsfw; },
+			get() { return this.$store.state.i.alwaysMarkNsfw; },
 			set(value) { this.$root.api('i/update', { alwaysMarkNsfw: value }); }
 		},
 
 		bannerStyle(): any {
 			if (this.$store.state.i.bannerUrl == null) return {};
 			return {
-				backgroundColor: this.$store.state.i.bannerColor && this.$store.state.i.bannerColor.length == 3 ? `rgb(${ this.$store.state.i.bannerColor.join(',') })` : null,
+				backgroundColor: this.$store.state.i.bannerColor ? this.$store.state.i.bannerColor : null,
 				backgroundImage: `url(${ this.$store.state.i.bannerUrl })`
 			};
 		},
@@ -178,10 +178,10 @@ export default Vue.extend({
 		this.email = this.$store.state.i.email;
 		this.name = this.$store.state.i.name;
 		this.username = this.$store.state.i.username;
-		this.location = this.$store.state.i.profile.location;
+		this.location = this.$store.state.i.location;
 		this.description = this.$store.state.i.description;
 		this.lang = this.$store.state.i.lang;
-		this.birthday = this.$store.state.i.profile.birthday;
+		this.birthday = this.$store.state.i.birthday;
 		this.avatarId = this.$store.state.i.avatarId;
 		this.bannerId = this.$store.state.i.bannerId;
 		this.isCat = this.$store.state.i.isCat;
diff --git a/src/client/app/common/views/components/settings/theme.vue b/src/client/app/common/views/components/settings/theme.vue
index 1dff61e459bb0db63b304c2cc0f5a8c2f4dae30b..3440aacb28cc650bfec9fcb112013e242060c3ba 100644
--- a/src/client/app/common/views/components/settings/theme.vue
+++ b/src/client/app/common/views/components/settings/theme.vue
@@ -130,20 +130,6 @@ import * as tinycolor from 'tinycolor2';
 import * as JSON5 from 'json5';
 import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons';
 
-// 後方互換性のため
-function convertOldThemedefinition(t) {
-	const t2 = {
-		id: t.meta.id,
-		name: t.meta.name,
-		author: t.meta.author,
-		base: t.meta.base,
-		vars: t.meta.vars,
-		props: t
-	};
-	delete t2.props.meta;
-	return t2;
-}
-
 export default Vue.extend({
 	i18n: i18n('common/views/components/theme.vue'),
 	components: {
@@ -231,20 +217,6 @@ export default Vue.extend({
 		}
 	},
 
-	beforeCreate() {
-		// migrate old theme definitions
-		// 後方互換性のため
-		this.$store.commit('device/set', {
-			key: 'themes', value: this.$store.state.device.themes.map(t => {
-				if (t.id == null) {
-					return convertOldThemedefinition(t);
-				} else {
-					return t;
-				}
-			})
-		});
-	},
-
 	methods: {
 		install(code) {
 			let theme;
@@ -259,11 +231,6 @@ export default Vue.extend({
 				return;
 			}
 
-			// 後方互換性のため
-			if (theme.id == null && theme.meta != null) {
-				theme = convertOldThemedefinition(theme);
-			}
-
 			if (theme.id == null) {
 				this.$root.dialog({
 					type: 'error',
diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue
index 16e1afaa9431d40f92aeeaf489877f37512900fb..45c2eabd45d29adbedd3a4f7ae4eeed208b76d2c 100644
--- a/src/client/app/common/views/components/signup.vue
+++ b/src/client/app/common/views/components/signup.vue
@@ -4,7 +4,7 @@
 		<ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required styl="fill">
 			<span>{{ $t('invitation-code') }}</span>
 			<template #prefix><fa icon="id-card-alt"/></template>
-			<template #desc v-html="this.$t('invitation-info').replace('{}', 'mailto:' + meta.maintainer.email)"></template>
+			<template #desc v-html="this.$t('invitation-info').replace('{}', 'mailto:' + meta.maintainerEmail)"></template>
 		</ui-input>
 		<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername" styl="fill">
 			<span>{{ $t('username') }}</span>
diff --git a/src/client/app/common/views/components/trends.vue b/src/client/app/common/views/components/trends.vue
index 536d55247cdd6721ff7cb815f678ca8308d9dfb4..cd67cc0092945b28d05643c26becf035069f170b 100644
--- a/src/client/app/common/views/components/trends.vue
+++ b/src/client/app/common/views/components/trends.vue
@@ -4,9 +4,9 @@
 	<p class="empty" v-else-if="stats.length == 0"><fa icon="exclamation-circle"/>{{ $t('empty') }}</p>
 	<!-- トランジションを有効にするとなぜかメモリリークする -->
 	<transition-group v-else tag="div" name="chart">
-		<div v-for="stat in stats" :key="stat.tag">
+		<div v-for="stat in stats" :key="stat.name">
 			<div class="tag">
-				<router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
+				<router-link :to="`/tags/${ encodeURIComponent(stat.name) }`" :title="stat.name">#{{ stat.name }}</router-link>
 				<p>{{ $t('count').replace('{}', stat.usersCount) }}</p>
 			</div>
 			<x-chart class="chart" :src="stat.chart"/>
diff --git a/src/client/app/common/views/components/user-list-editor.vue b/src/client/app/common/views/components/user-list-editor.vue
index 53c945ca0a362e1fb0e0ffba88e3761d7dca5547..8d2e04d045b30522f52c909f316d6983b61c2e04 100644
--- a/src/client/app/common/views/components/user-list-editor.vue
+++ b/src/client/app/common/views/components/user-list-editor.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="cudqjmnl">
 	<ui-card>
-		<template #title><fa :icon="faList"/> {{ list.title }}</template>
+		<template #title><fa :icon="faList"/> {{ list.name }}</template>
 
 		<section>
 			<ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button>
@@ -75,7 +75,7 @@ export default Vue.extend({
 			this.$root.dialog({
 				title: this.$t('rename'),
 				input: {
-					default: this.list.title
+					default: this.list.name
 				}
 			}).then(({ canceled, result: title }) => {
 				if (canceled) return;
@@ -89,7 +89,7 @@ export default Vue.extend({
 		del() {
 			this.$root.dialog({
 				type: 'warning',
-				text: this.$t('delete-are-you-sure').replace('$1', this.list.title),
+				text: this.$t('delete-are-you-sure').replace('$1', this.list.name),
 				showCancelButton: true
 			}).then(({ canceled }) => {
 				if (canceled) return;
diff --git a/src/client/app/common/views/components/user-menu.vue b/src/client/app/common/views/components/user-menu.vue
index 93fd759fd956ba861eaa4284039975d631d92a9a..a95f7a9225d1fb4cb858d1ca9ca41dea7f80a865 100644
--- a/src/client/app/common/views/components/user-menu.vue
+++ b/src/client/app/common/views/components/user-menu.vue
@@ -73,7 +73,7 @@ export default Vue.extend({
 				title: t,
 				select: {
 					items: lists.map(list => ({
-						value: list.id, text: list.title
+						value: list.id, text: list.name
 					}))
 				},
 				showCancelButton: true
diff --git a/src/client/app/common/views/deck/deck.column-core.vue b/src/client/app/common/views/deck/deck.column-core.vue
index 974c58235df9e4957df8d6d751514f1466cdcf7b..e3f92dea16276d15603ee2c9a8e046808d362377 100644
--- a/src/client/app/common/views/deck/deck.column-core.vue
+++ b/src/client/app/common/views/deck/deck.column-core.vue
@@ -3,7 +3,7 @@
 <x-notifications-column v-else-if="column.type == 'notifications'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
 <x-tl-column v-else-if="column.type == 'home'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
 <x-tl-column v-else-if="column.type == 'local'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
-<x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
+<x-tl-column v-else-if="column.type == 'social'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
 <x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
 <x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
 <x-tl-column v-else-if="column.type == 'hashtag'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
diff --git a/src/client/app/common/views/deck/deck.hashtag-tl.vue b/src/client/app/common/views/deck/deck.hashtag-tl.vue
index 07d96f82c4a655e1e12045d5fad180711b2dd17d..b94267b74b77386d9891391b923c87f5431bdb7a 100644
--- a/src/client/app/common/views/deck/deck.hashtag-tl.vue
+++ b/src/client/app/common/views/deck/deck.hashtag-tl.vue
@@ -28,7 +28,7 @@ export default Vue.extend({
 	data() {
 		return {
 			connection: null,
-			makePromise: cursor => this.$root.api('notes/search_by_tag', {
+			makePromise: cursor => this.$root.api('notes/search-by-tag', {
 				limit: fetchLimit + 1,
 				untilId: cursor ? cursor : undefined,
 				withFiles: this.mediaOnly,
diff --git a/src/client/app/common/views/deck/deck.notification.vue b/src/client/app/common/views/deck/deck.notification.vue
index 6a116260e5ef2a23045f6c5d4abf795ab511ad97..3ced7b7e23cb982e5f07ff5397b96643cecf8b1a 100644
--- a/src/client/app/common/views/deck/deck.notification.vue
+++ b/src/client/app/common/views/deck/deck.notification.vue
@@ -62,7 +62,7 @@
 		</div>
 	</div>
 
-	<div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
+	<div class="notification pollVote" v-if="notification.type == 'pollVote'">
 		<mk-avatar class="avatar" :user="notification.user"/>
 		<div>
 			<header>
diff --git a/src/client/app/common/views/deck/deck.tl-column.vue b/src/client/app/common/views/deck/deck.tl-column.vue
index d53aabaea5175952b5c986d45f35ce607a01149a..f6a9ee52862c56757a3426eb90b12e227182e6d5 100644
--- a/src/client/app/common/views/deck/deck.tl-column.vue
+++ b/src/client/app/common/views/deck/deck.tl-column.vue
@@ -3,7 +3,7 @@
 	<template #header>
 		<fa v-if="column.type == 'home'" icon="home"/>
 		<fa v-if="column.type == 'local'" :icon="['far', 'comments']"/>
-		<fa v-if="column.type == 'hybrid'" icon="share-alt"/>
+		<fa v-if="column.type == 'social'" icon="share-alt"/>
 		<fa v-if="column.type == 'global'" icon="globe"/>
 		<fa v-if="column.type == 'list'" icon="list"/>
 		<fa v-if="column.type == 'hashtag'" icon="hashtag"/>
@@ -80,9 +80,9 @@ export default Vue.extend({
 			switch (this.column.type) {
 				case 'home': return this.$t('@deck.home');
 				case 'local': return this.$t('@deck.local');
-				case 'hybrid': return this.$t('@deck.hybrid');
+				case 'social': return this.$t('@deck.social');
 				case 'global': return this.$t('@deck.global');
-				case 'list': return this.column.list.title;
+				case 'list': return this.column.list.name;
 				case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title;
 			}
 		}
diff --git a/src/client/app/common/views/deck/deck.tl.vue b/src/client/app/common/views/deck/deck.tl.vue
index 35cdfa704f794c099d3b278b27e648b52b2730f5..5381cfbd5ead6cfc7e1c34575d159eb17c4a513a 100644
--- a/src/client/app/common/views/deck/deck.tl.vue
+++ b/src/client/app/common/views/deck/deck.tl.vue
@@ -51,7 +51,7 @@ export default Vue.extend({
 			switch (this.src) {
 				case 'home': return this.$root.stream.useSharedConnection('homeTimeline');
 				case 'local': return this.$root.stream.useSharedConnection('localTimeline');
-				case 'hybrid': return this.$root.stream.useSharedConnection('hybridTimeline');
+				case 'social': return this.$root.stream.useSharedConnection('socialTimeline');
 				case 'global': return this.$root.stream.useSharedConnection('globalTimeline');
 			}
 		},
@@ -60,7 +60,7 @@ export default Vue.extend({
 			switch (this.src) {
 				case 'home': return 'notes/timeline';
 				case 'local': return 'notes/local-timeline';
-				case 'hybrid': return 'notes/hybrid-timeline';
+				case 'social': return 'notes/social-timeline';
 				case 'global': return 'notes/global-timeline';
 			}
 		},
@@ -107,7 +107,7 @@ export default Vue.extend({
 
 		this.$root.getMeta().then(meta => {
 			this.disabled = !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin && (
-				meta.disableLocalTimeline && ['local', 'hybrid'].includes(this.src) ||
+				meta.disableLocalTimeline && ['local', 'social'].includes(this.src) ||
 				meta.disableGlobalTimeline && ['global'].includes(this.src));
 		});
 	},
diff --git a/src/client/app/common/views/deck/deck.vue b/src/client/app/common/views/deck/deck.vue
index 8ffb3223f9b709cd14cedd5879e40736121ed3f6..a1bef840082d8f77857bf978c788af5beab627b3 100644
--- a/src/client/app/common/views/deck/deck.vue
+++ b/src/client/app/common/views/deck/deck.vue
@@ -106,16 +106,6 @@ export default Vue.extend({
 				value: deck
 			});
 		}
-
-		// 互換性のため
-		if (this.$store.state.device.deck != null && this.$store.state.device.deck.layout == null) {
-			this.$store.commit('device/set', {
-				key: 'deck',
-				value: Object.assign({}, this.$store.state.device.deck, {
-					layout: this.$store.state.device.deck.columns.map(c => [c.id])
-				})
-			});
-		}
 	},
 
 	mounted() {
@@ -155,11 +145,11 @@ export default Vue.extend({
 					}
 				}, {
 					icon: 'share-alt',
-					text: this.$t('@deck.hybrid'),
+					text: this.$t('@deck.social'),
 					action: () => {
 						this.$store.commit('device/addDeckColumn', {
 							id: uuid(),
-							type: 'hybrid'
+							type: 'social'
 						});
 					}
 				}, {
@@ -199,7 +189,7 @@ export default Vue.extend({
 							title: this.$t('@deck.select-list'),
 							select: {
 								items: lists.map(list => ({
-									value: list.id, text: list.title
+									value: list.id, text: list.name
 								}))
 							},
 							showCancelButton: true
@@ -312,7 +302,7 @@ export default Vue.extend({
 
 		isTlColumn(id) {
 			const column = this.columns.find(c => c.id === id);
-			return ['home', 'local', 'hybrid', 'global', 'list', 'hashtag', 'mentions', 'direct'].includes(column.type);
+			return ['home', 'local', 'social', 'global', 'list', 'hashtag', 'mentions', 'direct'].includes(column.type);
 		}
 	}
 });
diff --git a/src/client/app/common/views/pages/explore.vue b/src/client/app/common/views/pages/explore.vue
index 098bf1f4c49aded31673542543ebf5de3973a12b..67e92af4452bd71b157045f415882b54b89549fe 100644
--- a/src/client/app/common/views/pages/explore.vue
+++ b/src/client/app/common/views/pages/explore.vue
@@ -3,7 +3,7 @@
 	<ui-container :show-header="false" v-if="meta && stats">
 		<div class="kpdsmpnk" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }">
 			<div>
-				<router-link to="/explore" class="title">{{ $t('explore', { host: meta.name }) }}</router-link>
+				<router-link to="/explore" class="title">{{ $t('explore', { host: meta.name || 'Misskey' }) }}</router-link>
 				<span>{{ $t('users-info', { users: num(stats.originalUsersCount) }) }}</span>
 			</div>
 		</div>
@@ -13,8 +13,8 @@
 		<template #header><fa :icon="faHashtag" fixed-width/>{{ $t('popular-tags') }}</template>
 
 		<div class="vxjfqztj">
-			<router-link v-for="tag in tagsLocal" :to="`/explore/tags/${tag.tag}`" :key="'local:' + tag.tag" class="local">{{ tag.tag }}</router-link>
-			<router-link v-for="tag in tagsRemote" :to="`/explore/tags/${tag.tag}`" :key="'remote:' + tag.tag">{{ tag.tag }}</router-link>
+			<router-link v-for="tag in tagsLocal" :to="`/explore/tags/${tag.name}`" :key="'local:' + tag.name" class="local">{{ tag.name }}</router-link>
+			<router-link v-for="tag in tagsRemote" :to="`/explore/tags/${tag.name}`" :key="'remote:' + tag.name">{{ tag.name }}</router-link>
 		</div>
 	</ui-container>
 
diff --git a/src/client/app/common/views/pages/followers.vue b/src/client/app/common/views/pages/followers.vue
index 94d9c9b13c8ef9ba553f3bba70e3effda1898627..67cfb8512f3b5ea8fb964d0d7dac26661aa3d4b5 100644
--- a/src/client/app/common/views/pages/followers.vue
+++ b/src/client/app/common/views/pages/followers.vue
@@ -9,20 +9,30 @@ import Vue from 'vue';
 import parseAcct from '../../../../../misc/acct/parse';
 import i18n from '../../../i18n';
 
+const fetchLimit = 30;
+
 export default Vue.extend({
-	i18n: i18n(''),
+	i18n: i18n(),
 
 	data() {
 		return {
 			makePromise: cursor => this.$root.api('users/followers', {
 				...parseAcct(this.$route.params.user),
-				limit: 30,
-				cursor: cursor ? cursor : undefined
-			}).then(x => {
-				return {
-					users: x.users,
-					cursor: x.next
-				};
+				limit: fetchLimit + 1,
+				untilId: cursor ? cursor : undefined,
+			}).then(followings => {
+				if (followings.length == fetchLimit + 1) {
+					followings.pop();
+					return {
+						users: followings.map(following => following.follower),
+						cursor: followings[followings.length - 1].id
+					};
+				} else {
+					return {
+						users: followings.map(following => following.follower),
+						cursor: null
+					};
+				}
 			}),
 		};
 	},
diff --git a/src/client/app/common/views/pages/following.vue b/src/client/app/common/views/pages/following.vue
index 39739fa3da6b7f92fe4a41d4664807ff25d2fab5..518a63ac1bb6b992a9493eb2e292817e18e6190c 100644
--- a/src/client/app/common/views/pages/following.vue
+++ b/src/client/app/common/views/pages/following.vue
@@ -7,19 +7,32 @@
 <script lang="ts">
 import Vue from 'vue';
 import parseAcct from '../../../../../misc/acct/parse';
+import i18n from '../../../i18n';
+
+const fetchLimit = 30;
 
 export default Vue.extend({
+	i18n: i18n(),
+
 	data() {
 		return {
 			makePromise: cursor => this.$root.api('users/following', {
 				...parseAcct(this.$route.params.user),
-				limit: 30,
-				cursor: cursor ? cursor : undefined
-			}).then(x => {
-				return {
-					users: x.users,
-					cursor: x.next
-				};
+				limit: fetchLimit + 1,
+				untilId: cursor ? cursor : undefined,
+			}).then(followings => {
+				if (followings.length == fetchLimit + 1) {
+					followings.pop();
+					return {
+						users: followings.map(following => following.followee),
+						cursor: followings[followings.length - 1].id
+					};
+				} else {
+					return {
+						users: followings.map(following => following.followee),
+						cursor: null
+					};
+				}
 			}),
 		};
 	},
diff --git a/src/client/app/common/views/pages/share.vue b/src/client/app/common/views/pages/share.vue
index 760350b9211be612383bb682a60fe4d555f62886..0452b25dfc5a6232c11806327d4655a895cb84af 100644
--- a/src/client/app/common/views/pages/share.vue
+++ b/src/client/app/common/views/pages/share.vue
@@ -42,7 +42,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		this.$root.getMeta().then(meta => {
-			this.name = meta.name;
+			this.name = meta.name || 'Misskey';
 		});
 	}
 });
diff --git a/src/client/app/common/views/widgets/server.info.vue b/src/client/app/common/views/widgets/server.info.vue
index f7efb6fa2a700340e213d86f5ff0d285179c3ceb..a97b4ec49678b31b0a6d27ca43fc6ca2e7e7cc89 100644
--- a/src/client/app/common/views/widgets/server.info.vue
+++ b/src/client/app/common/views/widgets/server.info.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="info">
-	<p>Maintainer: <b><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></b></p>
+	<p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p>
 	<p>Machine: {{ meta.machine }}</p>
 	<p>Node: {{ meta.node }}</p>
 	<p>Version: {{ meta.version }} </p>
diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue
index c560e6d97e39c97d57b531db3a7764e98213302b..5b9ff81c0d122658945b3bef34d9d6aed2a3795b 100644
--- a/src/client/app/desktop/views/components/drive.file.vue
+++ b/src/client/app/desktop/views/components/drive.file.vue
@@ -60,7 +60,7 @@ export default Vue.extend({
 			return this.browser.selectedFiles.some(f => f.id == this.file.id);
 		},
 		title(): string {
-			return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.datasize)}`;
+			return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.size)}`;
 		}
 	},
 	methods: {
diff --git a/src/client/app/desktop/views/components/note.vue b/src/client/app/desktop/views/components/note.vue
index 9b36716e8372d6f680a1513297c724fae139e371..585294fc89a37f89b97f3094e9fe9d0844649fc9 100644
--- a/src/client/app/desktop/views/components/note.vue
+++ b/src/client/app/desktop/views/components/note.vue
@@ -54,11 +54,11 @@
 				</button>
 				<button v-if="!isMyNote && appearNote.myReaction == null" class="reactionButton button" @click="react()" ref="reactButton" :title="$t('add-reaction')">
 					<fa icon="plus"/>
-					<p class="count" v-if="Object.values(appearNote.reactionCounts).some(x => x)">{{ Object.values(appearNote.reactionCounts).reduce((a, c) => a + c, 0) }}</p>
+					<p class="count" v-if="Object.values(appearNote.reactions).some(x => x)">{{ Object.values(appearNote.reactions).reduce((a, c) => a + c, 0) }}</p>
 				</button>
 				<button v-if="!isMyNote && appearNote.myReaction != null" class="reactionButton reacted button" @click="undoReact(appearNote)" ref="reactButton" :title="$t('undo-reaction')">
 					<fa icon="minus"/>
-					<p class="count" v-if="Object.values(appearNote.reactionCounts).some(x => x)">{{ Object.values(appearNote.reactionCounts).reduce((a, c) => a + c, 0) }}</p>
+					<p class="count" v-if="Object.values(appearNote.reactions).some(x => x)">{{ Object.values(appearNote.reactions).reduce((a, c) => a + c, 0) }}</p>
 				</button>
 				<button @click="menu()" ref="menuButton" class="button">
 					<fa icon="ellipsis-h"/>
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index 24b6fc3eba2a2c65c58294e23ef0c0287f53bd07..0bf013292688781dafb03f3e557a86afd3cf02b0 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -110,7 +110,7 @@
 						</div>
 					</template>
 
-					<template v-if="notification.type == 'poll_vote'">
+					<template v-if="notification.type == 'pollVote'">
 						<mk-avatar class="avatar" :user="notification.user"/>
 						<div class="text">
 							<p><fa icon="chart-pie"/><a :href="notification.user | userPage" v-user-preview="notification.user.id">
diff --git a/src/client/app/desktop/views/components/user-list-window.vue b/src/client/app/desktop/views/components/user-list-window.vue
index afece9fe8687c03e5bb1728746183ffeb1254856..6764579b206a56f45794e56decfc8e61f942cd74 100644
--- a/src/client/app/desktop/views/components/user-list-window.vue
+++ b/src/client/app/desktop/views/components/user-list-window.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-window ref="window" width="450px" height="500px" @closed="destroyDom">
-	<template #header><fa icon="list"/> {{ list.title }}</template>
+	<template #header><fa icon="list"/> {{ list.name }}</template>
 
 	<x-editor :list="list"/>
 </mk-window>
diff --git a/src/client/app/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue
index 4f0af4a278bcd43d7a61dcad0802aec48ad90976..7afcd6aa3bf7f9112cad3e6ca215c037cd7a9e11 100644
--- a/src/client/app/desktop/views/components/user-lists-window.vue
+++ b/src/client/app/desktop/views/components/user-lists-window.vue
@@ -4,7 +4,7 @@
 
 	<div class="xkxvokkjlptzyewouewmceqcxhpgzprp">
 		<button class="ui" @click="add">{{ $t('create-list') }}</button>
-		<a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.title }}</a>
+		<a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.name }}</a>
 	</div>
 </mk-window>
 </template>
diff --git a/src/client/app/desktop/views/home/home.vue b/src/client/app/desktop/views/home/home.vue
index fb7af5a9ad2b2a888417e48f1a3e508f5eb45d32..d0b2fc10bc58a7b8c0ccb036c4ab2e0541f4cacf 100644
--- a/src/client/app/desktop/views/home/home.vue
+++ b/src/client/app/desktop/views/home/home.vue
@@ -101,7 +101,7 @@ export default Vue.extend({
 	computed: {
 		home(): any[] {
 			if (this.$store.getters.isSignedIn) {
-				return this.$store.state.settings.home || [];
+				return this.$store.state.device.home || [];
 			} else {
 				return [{
 					name: 'instance',
@@ -182,12 +182,8 @@ export default Vue.extend({
 			}
 			//#endregion
 
-			if (this.$store.state.settings.home == null) {
-				this.$root.api('i/update_home', {
-					home: _defaultDesktopHomeWidgets
-				}).then(() => {
-					this.$store.commit('settings/setHome', _defaultDesktopHomeWidgets);
-				});
+			if (this.$store.state.device.home == null) {
+				this.$store.commit('device/setHome', _defaultDesktopHomeWidgets);
 			}
 		}
 	},
@@ -226,7 +222,7 @@ export default Vue.extend({
 		},
 
 		addWidget() {
-			this.$store.dispatch('settings/addHomeWidget', {
+			this.$store.commit('device/addHomeWidget', {
 				name: this.widgetAdderSelected,
 				id: uuid(),
 				place: 'left',
@@ -237,12 +233,9 @@ export default Vue.extend({
 		saveHome() {
 			const left = this.widgets.left;
 			const right = this.widgets.right;
-			this.$store.commit('settings/setHome', left.concat(right));
+			this.$store.commit('device/setHome', left.concat(right));
 			for (const w of left) w.place = 'left';
 			for (const w of right) w.place = 'right';
-			this.$root.api('i/update_home', {
-				home: this.home
-			});
 		},
 
 		done() {
diff --git a/src/client/app/desktop/views/home/tag.vue b/src/client/app/desktop/views/home/tag.vue
index 4f9bc66e7b8b2b14a5afb48a33eff950987419fc..98d89955b34c92a5ddeebed796f619e23f490fab 100644
--- a/src/client/app/desktop/views/home/tag.vue
+++ b/src/client/app/desktop/views/home/tag.vue
@@ -21,7 +21,7 @@ export default Vue.extend({
 	i18n: i18n('desktop/views/pages/tag.vue'),
 	data() {
 		return {
-			makePromise: cursor => this.$root.api('notes/search_by_tag', {
+			makePromise: cursor => this.$root.api('notes/search-by-tag', {
 				limit: limit + 1,
 				offset: cursor ? cursor : undefined,
 				tag: this.$route.params.tag
diff --git a/src/client/app/desktop/views/home/timeline.core.vue b/src/client/app/desktop/views/home/timeline.core.vue
index e306ac873c8c0871ef07cf3d805d8e8581fe9be3..bf07b69dbf866495c38e810dd1ebdc54b17f5421 100644
--- a/src/client/app/desktop/views/home/timeline.core.vue
+++ b/src/client/app/desktop/views/home/timeline.core.vue
@@ -58,7 +58,7 @@ export default Vue.extend({
 		};
 
 		if (this.src == 'tag') {
-			this.endpoint = 'notes/search_by_tag';
+			this.endpoint = 'notes/search-by-tag';
 			this.query = {
 				query: this.tagTl.query
 			};
@@ -77,9 +77,9 @@ export default Vue.extend({
 			this.endpoint = 'notes/local-timeline';
 			this.connection = this.$root.stream.useSharedConnection('localTimeline');
 			this.connection.on('note', prepend);
-		} else if (this.src == 'hybrid') {
-			this.endpoint = 'notes/hybrid-timeline';
-			this.connection = this.$root.stream.useSharedConnection('hybridTimeline');
+		} else if (this.src == 'social') {
+			this.endpoint = 'notes/social-timeline';
+			this.connection = this.$root.stream.useSharedConnection('socialTimeline');
 			this.connection.on('note', prepend);
 		} else if (this.src == 'global') {
 			this.endpoint = 'notes/global-timeline';
diff --git a/src/client/app/desktop/views/home/timeline.vue b/src/client/app/desktop/views/home/timeline.vue
index 0b8ced479500ed237edae4e6b1b5a536cab90759..ccd55d1d7a1c362b6d6909a4241d5c68cab299ee 100644
--- a/src/client/app/desktop/views/home/timeline.vue
+++ b/src/client/app/desktop/views/home/timeline.vue
@@ -6,10 +6,10 @@
 			<header class="zahtxcqi">
 				<span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span>
 				<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span>
-				<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span>
+				<span :data-active="src == 'social'" @click="src = 'social'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('social') }}</span>
 				<span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span>
 				<span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl"><fa icon="hashtag"/> {{ tagTl.title }}</span>
-				<span :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.title }}</span>
+				<span :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.name }}</span>
 				<div class="buttons">
 					<button :data-active="src == 'mentions'" @click="src = 'mentions'" :title="$t('mentions')"><fa icon="at"/><i class="indicator" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></button>
 					<button :data-active="src == 'messages'" @click="src = 'messages'" :title="$t('messages')"><fa :icon="['far', 'envelope']"/><i class="indicator" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></button>
@@ -78,7 +78,7 @@ export default Vue.extend({
 			) && this.src === 'global') this.src = 'local';
 			if (!(
 				this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin
-			) && ['local', 'hybrid'].includes(this.src)) this.src = 'home';
+			) && ['local', 'social'].includes(this.src)) this.src = 'home';
 		});
 
 		if (this.$store.state.device.tl) {
@@ -89,7 +89,7 @@ export default Vue.extend({
 				this.tagTl = this.$store.state.device.tl.arg;
 			}
 		} else if (this.$store.state.i.followingCount == 0) {
-			this.src = 'hybrid';
+			this.src = 'social';
 		}
 	},
 
@@ -143,7 +143,7 @@ export default Vue.extend({
 
 			menu = menu.concat(lists.map(list => ({
 				icon: 'list',
-				text: list.title,
+				text: list.name,
 				action: () => {
 					this.list = list;
 					this.src = 'list';
diff --git a/src/client/app/desktop/views/home/user/user.header.vue b/src/client/app/desktop/views/home/user/user.header.vue
index 85dcd3ddae81222f2f567482b51ec5cef4ad94e4..61c3839c146b0584e04d62c34129404e8ce60960 100644
--- a/src/client/app/desktop/views/home/user/user.header.vue
+++ b/src/client/app/desktop/views/home/user/user.header.vue
@@ -36,8 +36,8 @@
 			</dl>
 		</div>
 		<div class="info">
-			<span class="location" v-if="user.host === null && user.profile.location"><fa icon="map-marker"/> {{ user.profile.location }}</span>
-			<span class="birthday" v-if="user.host === null && user.profile.birthday"><fa icon="birthday-cake"/> {{ user.profile.birthday.replace('-', $t('year')).replace('-', $t('month')) + $t('day') }} ({{ $t('years-old', { age }) }})</span>
+			<span class="location" v-if="user.host === null && user.location"><fa icon="map-marker"/> {{ user.location }}</span>
+			<span class="birthday" v-if="user.host === null && user.birthday"><fa icon="birthday-cake"/> {{ user.birthday.replace('-', $t('year')).replace('-', $t('month')) + $t('day') }} ({{ $t('years-old', { age }) }})</span>
 		</div>
 		<div class="status">
 			<router-link :to="user | userPage()" class="notes-count"><b>{{ user.notesCount | number }}</b>{{ $t('posts') }}</router-link>
@@ -71,7 +71,7 @@ export default Vue.extend({
 		},
 
 		age(): number {
-			return age(this.user.profile.birthday);
+			return age(this.user.birthday);
 		}
 	},
 	mounted() {
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index ddffeae4084785715d328e9d8e4ed5d2e6f22830..5a5cd9c8e679d310439261b0dbeefea36548d32e 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -13,8 +13,8 @@
 		<div class="body">
 			<div class="main block">
 				<div>
-					<h1 v-if="name != 'Misskey'">{{ name }}</h1>
-					<h1 v-else><img svg-inline src="../../../../assets/title.svg" :alt="name"></h1>
+					<h1 v-if="name != null">{{ name }}</h1>
+					<h1 v-else><img svg-inline src="../../../../assets/title.svg" alt="Misskey"></h1>
 
 					<div class="info">
 						<span><b>{{ host }}</b> - <span v-html="$t('powered-by-misskey')"></span></span>
@@ -87,7 +87,7 @@
 					<div>
 						<div v-if="meta" class="body">
 							<p>Version: <b>{{ meta.version }}</b></p>
-							<p>Maintainer: <b><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></b></p>
+							<p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p>
 						</div>
 					</div>
 				</div>
@@ -162,7 +162,7 @@ export default Vue.extend({
 			banner: null,
 			copyright,
 			host: toUnicode(host),
-			name: 'Misskey',
+			name: null,
 			description: '',
 			announcements: [],
 			photos: []
diff --git a/src/client/app/dev/views/new-app.vue b/src/client/app/dev/views/new-app.vue
index d8c128904a6330692eaeb2920c345b77c92ec379..00f2ed60d9edac5920209bc6a089d656fb4b1e02 100644
--- a/src/client/app/dev/views/new-app.vue
+++ b/src/client/app/dev/views/new-app.vue
@@ -15,15 +15,21 @@
 				<b-form-group :description="$t('description')">
 					<b-alert show variant="warning"><fa icon="exclamation-triangle"/> {{ $t('authority-warning') }}</b-alert>
 					<b-form-checkbox-group v-model="permission" stacked>
-						<b-form-checkbox value="account-read">{{ $t('account-read') }}</b-form-checkbox>
-						<b-form-checkbox value="account-write">{{ $t('account-write') }}</b-form-checkbox>
-						<b-form-checkbox value="note-write">{{ $t('note-write') }}</b-form-checkbox>
-						<b-form-checkbox value="reaction-write">{{ $t('reaction-write') }}</b-form-checkbox>
-						<b-form-checkbox value="following-write">{{ $t('following-write') }}</b-form-checkbox>
-						<b-form-checkbox value="drive-read">{{ $t('drive-read') }}</b-form-checkbox>
-						<b-form-checkbox value="drive-write">{{ $t('drive-write') }}</b-form-checkbox>
-						<b-form-checkbox value="notification-read">{{ $t('notification-read') }}</b-form-checkbox>
-						<b-form-checkbox value="notification-write">{{ $t('notification-write') }}</b-form-checkbox>
+						<b-form-checkbox value="read:account">{{ $t('read:account') }}</b-form-checkbox>
+						<b-form-checkbox value="write:account">{{ $t('write:account') }}</b-form-checkbox>
+						<b-form-checkbox value="write:notes">{{ $t('write:notes') }}</b-form-checkbox>
+						<b-form-checkbox value="read:reactions">{{ $t('read:reactions') }}</b-form-checkbox>
+						<b-form-checkbox value="write:reactions">{{ $t('write:reactions') }}</b-form-checkbox>
+						<b-form-checkbox value="read:following">{{ $t('read:following') }}</b-form-checkbox>
+						<b-form-checkbox value="write:following">{{ $t('write:following') }}</b-form-checkbox>
+						<b-form-checkbox value="read:mutes">{{ $t('read:mutes') }}</b-form-checkbox>
+						<b-form-checkbox value="write:mutes">{{ $t('write:mutes') }}</b-form-checkbox>
+						<b-form-checkbox value="read:blocks">{{ $t('read:blocks') }}</b-form-checkbox>
+						<b-form-checkbox value="write:blocks">{{ $t('write:blocks') }}</b-form-checkbox>
+						<b-form-checkbox value="read:drive">{{ $t('read:drive') }}</b-form-checkbox>
+						<b-form-checkbox value="write:drive">{{ $t('write:drive') }}</b-form-checkbox>
+						<b-form-checkbox value="read:notifications">{{ $t('read:notifications') }}</b-form-checkbox>
+						<b-form-checkbox value="write:notifications">{{ $t('write:notifications') }}</b-form-checkbox>
 					</b-form-checkbox-group>
 				</b-form-group>
 			</b-card>
diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts
index 9e191bf43cde84f2e3e5493a03a5cae180c3dd86..8f4b243623776d3b53e27a68a77c418c4e2df2a9 100644
--- a/src/client/app/mios.ts
+++ b/src/client/app/mios.ts
@@ -278,21 +278,6 @@ export default class MiOS extends EventEmitter {
 				});
 			});
 
-			main.on('homeUpdated', x => {
-				this.store.commit('settings/setHome', x);
-			});
-
-			main.on('mobileHomeUpdated', x => {
-				this.store.commit('settings/setMobileHome', x);
-			});
-
-			main.on('widgetUpdated', x => {
-				this.store.commit('settings/updateWidget', {
-					id: x.id,
-					data: x.data
-				});
-			});
-
 			// トークンが再生成されたとき
 			// このままではMisskeyが利用できないので強制的にサインアウトさせる
 			main.on('myTokenRegenerated', () => {
diff --git a/src/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue
index 92f5c1fd195b064fc3fa3f095e9c12748044bdb6..8f724b0f8e2fba3d60c938be7870dc4663f32970 100644
--- a/src/client/app/mobile/views/components/drive.file-detail.vue
+++ b/src/client/app/mobile/views/components/drive.file-detail.vue
@@ -22,7 +22,7 @@
 		<div>
 			<span class="type"><mk-file-type-icon :type="file.type"/> {{ file.type }}</span>
 			<span class="separator"></span>
-			<span class="data-size">{{ file.datasize | bytes }}</span>
+			<span class="data-size">{{ file.size | bytes }}</span>
 			<span class="separator"></span>
 			<span class="created-at" @click="showCreatedAt"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span>
 			<template v-if="file.isSensitive">
diff --git a/src/client/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue
index feca266ede6c0187f16f3be03a1757f32b852b51..ed95537f9cbf8be00309b1302129347bbc0ec24f 100644
--- a/src/client/app/mobile/views/components/drive.file.vue
+++ b/src/client/app/mobile/views/components/drive.file.vue
@@ -10,7 +10,7 @@
 			<footer>
 				<span class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</span>
 				<span class="separator"></span>
-				<span class="data-size">{{ file.datasize | bytes }}</span>
+				<span class="data-size">{{ file.size | bytes }}</span>
 				<span class="separator"></span>
 				<span class="created-at"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span>
 				<template v-if="file.isSensitive">
diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue
index 1b8eceaa6c044f87c6525e21daf573da9caf91e5..8422c7342064b87b1cd5729954becad705a82a2d 100644
--- a/src/client/app/mobile/views/components/notification-preview.vue
+++ b/src/client/app/mobile/views/components/notification-preview.vue
@@ -54,7 +54,7 @@
 		</div>
 	</template>
 
-	<template v-if="notification.type == 'poll_vote'">
+	<template v-if="notification.type == 'pollVote'">
 		<mk-avatar class="avatar" :user="notification.user"/>
 		<div class="text">
 			<p><fa icon="chart-pie"/><mk-user-name :user="notification.user"/></p>
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index 5308d96533b405fbe16f17b84b561aa4b1f203ac..1128a76000b16325741baee24870b6696adb72fe 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -54,7 +54,7 @@
 		</div>
 	</div>
 
-	<div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
+	<div class="notification pollVote" v-if="notification.type == 'pollVote'">
 		<mk-avatar class="avatar" :user="notification.user"/>
 		<div>
 			<header>
diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue
index 4f9f5119ab00cb44a56ef2cf9648bceeb7fc9bcb..1eb7399979e20e1301ecaf4c24220cdddf5673ca 100644
--- a/src/client/app/mobile/views/pages/home.timeline.vue
+++ b/src/client/app/mobile/views/pages/home.timeline.vue
@@ -59,7 +59,7 @@ export default Vue.extend({
 		};
 
 		if (this.src == 'tag') {
-			this.endpoint = 'notes/search_by_tag';
+			this.endpoint = 'notes/search-by-tag';
 			this.query = {
 				query: this.tagTl.query
 			};
@@ -78,9 +78,9 @@ export default Vue.extend({
 			this.endpoint = 'notes/local-timeline';
 			this.connection = this.$root.stream.useSharedConnection('localTimeline');
 			this.connection.on('note', prepend);
-		} else if (this.src == 'hybrid') {
-			this.endpoint = 'notes/hybrid-timeline';
-			this.connection = this.$root.stream.useSharedConnection('hybridTimeline');
+		} else if (this.src == 'social') {
+			this.endpoint = 'notes/social-timeline';
+			this.connection = this.$root.stream.useSharedConnection('socialTimeline');
 			this.connection.on('note', prepend);
 		} else if (this.src == 'global') {
 			this.endpoint = 'notes/global-timeline';
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index 59fae2340b4f0d1a43a506906dc436a738ce8127..7e39441996cf009d6c6b0294f603a37c132972f3 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -5,11 +5,11 @@
 			<span :class="$style.title">
 				<span v-if="src == 'home'"><fa icon="home"/>{{ $t('home') }}</span>
 				<span v-if="src == 'local'"><fa :icon="['far', 'comments']"/>{{ $t('local') }}</span>
-				<span v-if="src == 'hybrid'"><fa icon="share-alt"/>{{ $t('hybrid') }}</span>
+				<span v-if="src == 'social'"><fa icon="share-alt"/>{{ $t('social') }}</span>
 				<span v-if="src == 'global'"><fa icon="globe"/>{{ $t('global') }}</span>
 				<span v-if="src == 'mentions'"><fa icon="at"/>{{ $t('mentions') }}</span>
 				<span v-if="src == 'messages'"><fa :icon="['far', 'envelope']"/>{{ $t('messages') }}</span>
-				<span v-if="src == 'list'"><fa icon="list"/>{{ list.title }}</span>
+				<span v-if="src == 'list'"><fa icon="list"/>{{ list.name }}</span>
 				<span v-if="src == 'tag'"><fa icon="hashtag"/>{{ tagTl.title }}</span>
 			</span>
 			<span style="margin-left:8px">
@@ -32,7 +32,7 @@
 				<div>
 					<span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span>
 					<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span>
-					<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span>
+					<span :data-active="src == 'social'" @click="src = 'social'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('social') }}</span>
 					<span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span>
 					<div class="hr"></div>
 					<span :data-active="src == 'mentions'" @click="src = 'mentions'"><fa icon="at"/> {{ $t('mentions') }}<i class="badge" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></span>
@@ -50,7 +50,7 @@
 		<div class="tl">
 			<x-tl v-if="src == 'home'" ref="tl" key="home" src="home"/>
 			<x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/>
-			<x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
+			<x-tl v-if="src == 'social'" ref="tl" key="social" src="social"/>
 			<x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/>
 			<x-tl v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
 			<x-tl v-if="src == 'messages'" ref="tl" key="messages" src="messages"/>
@@ -120,7 +120,7 @@ export default Vue.extend({
 			) && this.src === 'global') this.src = 'local';
 			if (!(
 				this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin
-			) && ['local', 'hybrid'].includes(this.src)) this.src = 'home';
+			) && ['local', 'social'].includes(this.src)) this.src = 'home';
 		});
 
 		if (this.$store.state.device.tl) {
@@ -131,7 +131,7 @@ export default Vue.extend({
 				this.tagTl = this.$store.state.device.tl.arg;
 			}
 		} else if (this.$store.state.i.followingCount == 0) {
-			this.src = 'hybrid';
+			this.src = 'social';
 		}
 	},
 
diff --git a/src/client/app/mobile/views/pages/tag.vue b/src/client/app/mobile/views/pages/tag.vue
index 318e63a4735775b9e7edc05cf9c52db7228589b2..7a7b90dad003a14ba998882aeec54526d18be3a2 100644
--- a/src/client/app/mobile/views/pages/tag.vue
+++ b/src/client/app/mobile/views/pages/tag.vue
@@ -19,7 +19,7 @@ export default Vue.extend({
 	i18n: i18n('mobile/views/pages/tag.vue'),
 	data() {
 		return {
-			makePromise: cursor => this.$root.api('notes/search_by_tag', {
+			makePromise: cursor => this.$root.api('notes/search-by-tag', {
 				limit: limit + 1,
 				offset: cursor ? cursor : undefined,
 				tag: this.$route.params.tag
diff --git a/src/client/app/mobile/views/pages/user-list.vue b/src/client/app/mobile/views/pages/user-list.vue
index 874bae5d1816e1d3cc1f613913312841da26fa21..68fd0358c47131270efa97baaae43a7fa281040f 100644
--- a/src/client/app/mobile/views/pages/user-list.vue
+++ b/src/client/app/mobile/views/pages/user-list.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-ui>
-	<template #header v-if="!fetching"><fa icon="list"/>{{ list.title }}</template>
+	<template #header v-if="!fetching"><fa icon="list"/>{{ list.name }}</template>
 
 	<main v-if="!fetching">
 		<x-editor :list="list"/>
diff --git a/src/client/app/mobile/views/pages/user-lists.vue b/src/client/app/mobile/views/pages/user-lists.vue
index fd129339fd85c43ee584c087eecee132a953ce34..49006f41f68270ce4473b96b111653600286a3c7 100644
--- a/src/client/app/mobile/views/pages/user-lists.vue
+++ b/src/client/app/mobile/views/pages/user-lists.vue
@@ -5,7 +5,7 @@
 
 	<main>
 		<ul>
-			<li v-for="list in lists" :key="list.id"><router-link :to="`/i/lists/${list.id}`">{{ list.title }}</router-link></li>
+			<li v-for="list in lists" :key="list.id"><router-link :to="`/i/lists/${list.id}`">{{ list.name }}</router-link></li>
 		</ul>
 	</main>
 </mk-ui>
diff --git a/src/client/app/mobile/views/pages/user/index.vue b/src/client/app/mobile/views/pages/user/index.vue
index fe5ef057e7bfa40826d4b0fb3dcae8b470ccdef4..72f2998dbad8a4a56bc1909992bb905400737f3d 100644
--- a/src/client/app/mobile/views/pages/user/index.vue
+++ b/src/client/app/mobile/views/pages/user/index.vue
@@ -36,11 +36,11 @@
 					</dl>
 				</div>
 				<div class="info">
-					<p class="location" v-if="user.host === null && user.profile.location">
-						<fa icon="map-marker"/>{{ user.profile.location }}
+					<p class="location" v-if="user.host === null && user.location">
+						<fa icon="map-marker"/>{{ user.location }}
 					</p>
-					<p class="birthday" v-if="user.host === null && user.profile.birthday">
-						<fa icon="birthday-cake"/>{{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ $t('years-old', { age }) }})
+					<p class="birthday" v-if="user.host === null && user.birthday">
+						<fa icon="birthday-cake"/>{{ user.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ $t('years-old', { age }) }})
 					</p>
 				</div>
 				<div class="status">
@@ -104,7 +104,7 @@ export default Vue.extend({
 	},
 	computed: {
 		age(): number {
-			return age(this.user.profile.birthday);
+			return age(this.user.birthday);
 		},
 		avator(): string {
 			return this.$store.state.device.disableShowingAnimatedImages
diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue
index 1a2b0b6c128179dcf515ffdc508fda7cf1556d23..dd71a918dbe865b44f5383d19d65f109fd0758fd 100644
--- a/src/client/app/mobile/views/pages/welcome.vue
+++ b/src/client/app/mobile/views/pages/welcome.vue
@@ -3,10 +3,10 @@
 	<div class="banner" :style="{ backgroundImage: banner ? `url(${banner})` : null }"></div>
 
 	<div>
-		<img svg-inline src="../../../../assets/title.svg" :alt="name">
+		<img svg-inline src="../../../../assets/title.svg" alt="Misskey">
 		<p class="host">{{ host }}</p>
 		<div class="about">
-			<h2>{{ name }}</h2>
+			<h2>{{ name || 'Misskey' }}</h2>
 			<p v-html="description || this.$t('@.about')"></p>
 			<router-link class="signup" to="/signup">{{ $t('@.signup') }}</router-link>
 		</div>
@@ -62,7 +62,7 @@
 		</article>
 		<div class="info" v-if="meta">
 			<p>Version: <b>{{ meta.version }}</b></p>
-			<p>Maintainer: <b><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></b></p>
+			<p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p>
 		</div>
 		<footer>
 			<small>{{ copyright }}</small>
@@ -87,7 +87,7 @@ export default Vue.extend({
 			stats: null,
 			banner: null,
 			host: toUnicode(host),
-			name: 'Misskey',
+			name: null,
 			description: '',
 			photos: [],
 			announcements: []
diff --git a/src/client/app/mobile/views/pages/widgets.vue b/src/client/app/mobile/views/pages/widgets.vue
index 96dcb977fa38ba27e29f871952a3ebf3d46ca083..2647f96de2610e34e10df59ae361e70a2c4371a9 100644
--- a/src/client/app/mobile/views/pages/widgets.vue
+++ b/src/client/app/mobile/views/pages/widgets.vue
@@ -119,7 +119,7 @@ export default Vue.extend({
 		},
 
 		addWidget() {
-			this.$store.dispatch('settings/addMobileHomeWidget', {
+			this.$store.commit('settings/addMobileHomeWidget', {
 				name: this.widgetAdderSelected,
 				id: uuid(),
 				data: {}
@@ -127,14 +127,11 @@ export default Vue.extend({
 		},
 
 		removeWidget(widget) {
-			this.$store.dispatch('settings/removeMobileHomeWidget', widget);
+			this.$store.commit('settings/removeMobileHomeWidget', widget);
 		},
 
 		saveHome() {
 			this.$store.commit('settings/setMobileHome', this.widgets);
-			this.$root.api('i/update_mobile_home', {
-				home: this.widgets
-			});
 		}
 	}
 });
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
index e49934fc1673d674f493112d34ed5b45ec9fe347..c82981ad245a0bfe3a89432ee5655d76db48d337 100644
--- a/src/client/app/store.ts
+++ b/src/client/app/store.ts
@@ -7,8 +7,6 @@ import { erase } from '../../prelude/array';
 import getNoteSummary from '../../misc/get-note-summary';
 
 const defaultSettings = {
-	home: null,
-	mobileHome: [],
 	keepCw: false,
 	tagTimelines: [],
 	fetchOnScroll: true,
@@ -41,6 +39,8 @@ const defaultSettings = {
 };
 
 const defaultDeviceSettings = {
+	home: null,
+	mobileHome: [],
 	deck: null,
 	deckMode: false,
 	deckColumnAlign: 'center',
@@ -120,7 +120,7 @@ export default (os: MiOS) => new Vuex.Store({
 	actions: {
 		login(ctx, i) {
 			ctx.commit('updateI', i);
-			ctx.dispatch('settings/merge', i.clientSettings);
+			ctx.dispatch('settings/merge', i.clientData);
 		},
 
 		logout(ctx) {
@@ -134,8 +134,8 @@ export default (os: MiOS) => new Vuex.Store({
 				ctx.commit('updateIKeyValue', { key, value });
 			}
 
-			if (me.clientSettings) {
-				ctx.dispatch('settings/merge', me.clientSettings);
+			if (me.clientData) {
+				ctx.dispatch('settings/merge', me.clientData);
 			}
 		},
 	},
@@ -162,6 +162,48 @@ export default (os: MiOS) => new Vuex.Store({
 					state.visibility = visibility;
 				},
 
+				setHome(state, data) {
+					state.home = data;
+				},
+
+				addHomeWidget(state, widget) {
+					state.home.unshift(widget);
+				},
+
+				setMobileHome(state, data) {
+					state.mobileHome = data;
+				},
+
+				updateWidget(state, x) {
+					let w;
+
+					//#region Desktop home
+					if (state.home) {
+						w = state.home.find(w => w.id == x.id);
+						if (w) {
+							w.data = x.data;
+						}
+					}
+					//#endregion
+
+					//#region Mobile home
+					if (state.mobileHome) {
+						w = state.mobileHome.find(w => w.id == x.id);
+						if (w) {
+							w.data = x.data;
+						}
+					}
+					//#endregion
+				},
+
+				addMobileHomeWidget(state, widget) {
+					state.mobileHome.unshift(widget);
+				},
+
+				removeMobileHomeWidget(state, widget) {
+					state.mobileHome = state.mobileHome.filter(w => w.id != widget.id);
+				},
+
 				addDeckColumn(state, column) {
 					if (column.name == undefined) column.name = null;
 					state.deck.columns.push(column);
@@ -301,48 +343,6 @@ export default (os: MiOS) => new Vuex.Store({
 				set(state, x: { key: string; value: any }) {
 					nestedProperty.set(state, x.key, x.value);
 				},
-
-				setHome(state, data) {
-					state.home = data;
-				},
-
-				addHomeWidget(state, widget) {
-					state.home.unshift(widget);
-				},
-
-				setMobileHome(state, data) {
-					state.mobileHome = data;
-				},
-
-				updateWidget(state, x) {
-					let w;
-
-					//#region Desktop home
-					if (state.home) {
-						w = state.home.find(w => w.id == x.id);
-						if (w) {
-							w.data = x.data;
-						}
-					}
-					//#endregion
-
-					//#region Mobile home
-					if (state.mobileHome) {
-						w = state.mobileHome.find(w => w.id == x.id);
-						if (w) {
-							w.data = x.data;
-						}
-					}
-					//#endregion
-				},
-
-				addMobileHomeWidget(state, widget) {
-					state.mobileHome.unshift(widget);
-				},
-
-				removeMobileHomeWidget(state, widget) {
-					state.mobileHome = state.mobileHome.filter(w => w.id != widget.id);
-				},
 			},
 
 			actions: {
@@ -363,30 +363,6 @@ export default (os: MiOS) => new Vuex.Store({
 						});
 					}
 				},
-
-				addHomeWidget(ctx, widget) {
-					ctx.commit('addHomeWidget', widget);
-
-					os.api('i/update_home', {
-						home: ctx.state.home
-					});
-				},
-
-				addMobileHomeWidget(ctx, widget) {
-					ctx.commit('addMobileHomeWidget', widget);
-
-					os.api('i/update_mobile_home', {
-						home: ctx.state.mobileHome
-					});
-				},
-
-				removeMobileHomeWidget(ctx, widget) {
-					ctx.commit('removeMobileHomeWidget', widget);
-
-					os.api('i/update_mobile_home', {
-						home: ctx.state.mobileHome.filter(w => w.id != widget.id)
-					});
-				}
 			}
 		}
 	}
diff --git a/src/config/types.ts b/src/config/types.ts
index 5f30d410c9f7379e9a92ff1794c723c39d1bb78c..d1749c52f721faf4bd39940a31c19065053b1877 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -8,7 +8,7 @@ export type Source = {
 	port: number;
 	https?: { [x: string]: string };
 	disableHsts?: boolean;
-	mongodb: {
+	db: {
 		host: string;
 		port: number;
 		db: string;
@@ -42,6 +42,8 @@ export type Source = {
 	accesslog?: string;
 
 	clusterLimit?: number;
+
+	id: string;
 };
 
 /**
diff --git a/src/crypto_key.cc b/src/crypto_key.cc
deleted file mode 100644
index 658586baedef4011e36a2168744013a109410cbd..0000000000000000000000000000000000000000
--- a/src/crypto_key.cc
+++ /dev/null
@@ -1,111 +0,0 @@
-#include <nan.h>
-#include <openssl/bio.h>
-#include <openssl/buffer.h>
-#include <openssl/crypto.h>
-#include <openssl/pem.h>
-#include <openssl/rsa.h>
-#include <openssl/x509.h>
-
-NAN_METHOD(extractPublic)
-{
-	const auto sourceString = info[0]->ToString(Nan::GetCurrentContext()).ToLocalChecked();
-	if (!sourceString->IsOneByte()) {
-		Nan::ThrowError("Malformed character found");
-		return;
-	}
-
-	size_t sourceLength = sourceString->Length();
-	const auto sourceBuf = new char[sourceLength];
-
-	Nan::DecodeWrite(sourceBuf, sourceLength, sourceString);
-
-	const auto source = BIO_new_mem_buf(sourceBuf, sourceLength);
-	if (source == nullptr) {
-		Nan::ThrowError("Memory allocation failed");
-		delete[] sourceBuf;
-		return;
-	}
-
-	const auto rsa = PEM_read_bio_RSAPrivateKey(source, nullptr, nullptr, nullptr);
-
-	BIO_free(source);
-	delete[] sourceBuf;
-
-	if (rsa == nullptr) {
-		Nan::ThrowError("Decode failed");
-		return;
-	}
-
-	const auto destination = BIO_new(BIO_s_mem());
-	if (destination == nullptr) {
-		Nan::ThrowError("Memory allocation failed");
-		return;
-	}
-
-	const auto result = PEM_write_bio_RSAPublicKey(destination, rsa);
-
-	RSA_free(rsa);
-
-	if (result != 1) {
-		Nan::ThrowError("Public key extraction failed");
-		BIO_free(destination);
-		return;
-	}
-
-	char *pem;
-	const auto pemLength = BIO_get_mem_data(destination, &pem);
-
-	info.GetReturnValue().Set(Nan::Encode(pem, pemLength));
-	BIO_free(destination);
-}
-
-NAN_METHOD(generate)
-{
-	const auto exponent = BN_new();
-	const auto mem = BIO_new(BIO_s_mem());
-	const auto rsa = RSA_new();
-	char *data;
-	long result;
-
-	if (exponent == nullptr || mem == nullptr || rsa == nullptr) {
-		Nan::ThrowError("Memory allocation failed");
-		goto done;
-	}
-
-	result = BN_set_word(exponent, 65537);
-	if (result != 1) {
-		Nan::ThrowError("Exponent setting failed");
-		goto done;
-	}
-
-	result = RSA_generate_key_ex(rsa, 2048, exponent, nullptr);
-	if (result != 1) {
-		Nan::ThrowError("Key generation failed");
-		goto done;
-	}
-
-	result = PEM_write_bio_RSAPrivateKey(mem, rsa, NULL, NULL, 0, NULL, NULL);
-	if (result != 1) {
-		Nan::ThrowError("Key export failed");
-		goto done;
-	}
-
-	result = BIO_get_mem_data(mem, &data);
-	info.GetReturnValue().Set(Nan::Encode(data, result));
-
-done:
-	RSA_free(rsa);
-	BIO_free(mem);
-	BN_free(exponent);
-}
-
-NAN_MODULE_INIT(InitAll)
-{
-	Nan::Set(target, Nan::New<v8::String>("extractPublic").ToLocalChecked(),
-		Nan::GetFunction(Nan::New<v8::FunctionTemplate>(extractPublic)).ToLocalChecked());
-
-	Nan::Set(target, Nan::New<v8::String>("generate").ToLocalChecked(),
-		Nan::GetFunction(Nan::New<v8::FunctionTemplate>(generate)).ToLocalChecked());
-}
-
-NODE_MODULE(crypto_key, InitAll);
diff --git a/src/crypto_key.d.ts b/src/crypto_key.d.ts
deleted file mode 100644
index 9aa81a687ccdd19768cf798086f27c781c007a0b..0000000000000000000000000000000000000000
--- a/src/crypto_key.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export function extractPublic(keypair: string): string;
-export function generate(): string;
diff --git a/src/daemons/notes-stats-child.ts b/src/daemons/notes-stats-child.ts
index 7f54a36bff7cc34d1c3af21bd5ebd5ce395156af..c491aed4cdc3e815e22223b3f26d28b78f440206 100644
--- a/src/daemons/notes-stats-child.ts
+++ b/src/daemons/notes-stats-child.ts
@@ -1,26 +1,28 @@
-import Note from '../models/note';
+import { MoreThanOrEqual, getRepository } from 'typeorm';
+import { Note } from '../models/entities/note';
+import { initDb } from '../db/postgre';
 
 const interval = 5000;
 
-async function tick() {
-	const [all, local] = await Promise.all([Note.count({
-		createdAt: {
-			$gte: new Date(Date.now() - interval)
-		}
-	}), Note.count({
-		createdAt: {
-			$gte: new Date(Date.now() - interval)
-		},
-		'_user.host': null
-	})]);
+initDb().then(() => {
+	const Notes = getRepository(Note);
 
-	const stats = {
-		all, local
-	};
+	async function tick() {
+		const [all, local] = await Promise.all([Notes.count({
+			createdAt: MoreThanOrEqual(new Date(Date.now() - interval))
+		}), Notes.count({
+			createdAt: MoreThanOrEqual(new Date(Date.now() - interval)),
+			userHost: null
+		})]);
 
-	process.send(stats);
-}
+		const stats = {
+			all, local
+		};
 
-tick();
+		process.send(stats);
+	}
 
-setInterval(tick, interval);
+	tick();
+
+	setInterval(tick, interval);
+});
diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts
deleted file mode 100644
index f82ced176597c078f919aeea0703a54bb9f876b8..0000000000000000000000000000000000000000
--- a/src/db/mongodb.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import config from '../config';
-
-const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null;
-const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null;
-
-const uri = `mongodb://${u && p ? `${u}:${p}@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
-
-/**
- * monk
- */
-import mongo from 'monk';
-
-const db = mongo(uri);
-
-export default db;
-
-/**
- * MongoDB native module (officialy)
- */
-import * as mongodb from 'mongodb';
-
-let mdb: mongodb.Db;
-
-const nativeDbConn = async (): Promise<mongodb.Db> => {
-	if (mdb) return mdb;
-
-	const db = await ((): Promise<mongodb.Db> => new Promise((resolve, reject) => {
-		mongodb.MongoClient.connect(uri, { useNewUrlParser: true }, (e: Error, client: any) => {
-			if (e) return reject(e);
-			resolve(client.db(config.mongodb.db));
-		});
-	}))();
-
-	mdb = db;
-
-	return db;
-};
-
-export { nativeDbConn };
diff --git a/src/db/postgre.ts b/src/db/postgre.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bc5ee4ce8cb59ed7b8696e22121aa763f97b3ff7
--- /dev/null
+++ b/src/db/postgre.ts
@@ -0,0 +1,137 @@
+import { createConnection, Logger, getConnection } from 'typeorm';
+import config from '../config';
+import { entities as charts } from '../services/chart/entities';
+import { dbLogger } from './logger';
+import * as highlight from 'cli-highlight';
+
+import { Log } from '../models/entities/log';
+import { User } from '../models/entities/user';
+import { DriveFile } from '../models/entities/drive-file';
+import { DriveFolder } from '../models/entities/drive-folder';
+import { AccessToken } from '../models/entities/access-token';
+import { App } from '../models/entities/app';
+import { PollVote } from '../models/entities/poll-vote';
+import { Note } from '../models/entities/note';
+import { NoteReaction } from '../models/entities/note-reaction';
+import { NoteWatching } from '../models/entities/note-watching';
+import { NoteUnread } from '../models/entities/note-unread';
+import { Notification } from '../models/entities/notification';
+import { Meta } from '../models/entities/meta';
+import { Following } from '../models/entities/following';
+import { Instance } from '../models/entities/instance';
+import { Muting } from '../models/entities/muting';
+import { SwSubscription } from '../models/entities/sw-subscription';
+import { Blocking } from '../models/entities/blocking';
+import { UserList } from '../models/entities/user-list';
+import { UserListJoining } from '../models/entities/user-list-joining';
+import { Hashtag } from '../models/entities/hashtag';
+import { NoteFavorite } from '../models/entities/note-favorite';
+import { AbuseUserReport } from '../models/entities/abuse-user-report';
+import { RegistrationTicket } from '../models/entities/registration-tickets';
+import { MessagingMessage } from '../models/entities/messaging-message';
+import { Signin } from '../models/entities/signin';
+import { AuthSession } from '../models/entities/auth-session';
+import { FollowRequest } from '../models/entities/follow-request';
+import { Emoji } from '../models/entities/emoji';
+import { ReversiGame } from '../models/entities/games/reversi/game';
+import { ReversiMatching } from '../models/entities/games/reversi/matching';
+import { UserNotePining } from '../models/entities/user-note-pinings';
+import { UserServiceLinking } from '../models/entities/user-service-linking';
+import { Poll } from '../models/entities/poll';
+import { UserKeypair } from '../models/entities/user-keypair';
+import { UserPublickey } from '../models/entities/user-publickey';
+
+const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
+
+class MyCustomLogger implements Logger {
+	private highlight(sql: string) {
+		return highlight.highlight(sql, {
+			language: 'sql', ignoreIllegals: true,
+		});
+	}
+
+	public logQuery(query: string, parameters?: any[]) {
+		sqlLogger.info(this.highlight(query));
+	}
+
+	public logQueryError(error: string, query: string, parameters?: any[]) {
+		sqlLogger.error(this.highlight(query));
+	}
+
+	public logQuerySlow(time: number, query: string, parameters?: any[]) {
+		sqlLogger.warn(this.highlight(query));
+	}
+
+	public logSchemaBuild(message: string) {
+		sqlLogger.info(message);
+	}
+
+	public log(message: string) {
+		sqlLogger.info(message);
+	}
+
+	public logMigration(message: string) {
+		sqlLogger.info(message);
+	}
+}
+
+export function initDb(justBorrow = false, sync = false, log = false) {
+	const enableLogging = log || !['production', 'test'].includes(process.env.NODE_ENV);
+
+	try {
+		const conn = getConnection();
+		return Promise.resolve(conn);
+	} catch (e) {}
+
+	return createConnection({
+		type: 'postgres',
+		host: config.db.host,
+		port: config.db.port,
+		username: config.db.user,
+		password: config.db.pass,
+		database: config.db.db,
+		synchronize: process.env.NODE_ENV === 'test' || sync,
+		dropSchema: process.env.NODE_ENV === 'test' && !justBorrow,
+		logging: enableLogging,
+		logger: enableLogging ? new MyCustomLogger() : null,
+		entities: [
+			Meta,
+			Instance,
+			App,
+			AuthSession,
+			AccessToken,
+			User,
+			UserKeypair,
+			UserPublickey,
+			UserList,
+			UserListJoining,
+			UserNotePining,
+			UserServiceLinking,
+			Following,
+			FollowRequest,
+			Muting,
+			Blocking,
+			Note,
+			NoteFavorite,
+			NoteReaction,
+			NoteWatching,
+			NoteUnread,
+			Log,
+			DriveFile,
+			DriveFolder,
+			Poll,
+			PollVote,
+			Notification,
+			Emoji,
+			Hashtag,
+			SwSubscription,
+			AbuseUserReport,
+			RegistrationTicket,
+			MessagingMessage,
+			Signin,
+			ReversiGame,
+			ReversiMatching,
+			...charts as any
+		]
+	});
+}
diff --git a/src/docs/reversi-bot.ja-JP.md b/src/docs/reversi-bot.ja-JP.md
index a389ead5717b5a780f08fbbc6666ce9e4ebeec10..b1f759ade8abc6ad11e0b0fa698d3d3f707544f5 100644
--- a/src/docs/reversi-bot.ja-JP.md
+++ b/src/docs/reversi-bot.ja-JP.md
@@ -42,9 +42,9 @@ Misskeyのリバーシ機能に対応したBotの開発方法をここに記し
 ```
 pos = x + (y * mapWidth)
 ```
-`mapWidth`は、ゲーム情報の`settings.map`から、次のようにして計算できます:
+`mapWidth`は、ゲーム情報の`map`から、次のようにして計算できます:
 ```
-mapWidth = settings.map[0].length
+mapWidth = map[0].length
 ```
 
 ### Pos から X,Y座標 に変換する
@@ -54,7 +54,7 @@ y = Math.floor(pos / mapWidth)
 ```
 
 ## マップ情報
-マップ情報は、ゲーム情報の`settings.map`に入っています。
+マップ情報は、ゲーム情報の`map`に入っています。
 文字列の配列になっており、ひとつひとつの文字がマス情報を表しています。
 それをもとにマップのデザインを知る事が出来ます:
 * `(スペース)` ... マス無し
diff --git a/src/docs/stream.ja-JP.md b/src/docs/stream.ja-JP.md
index 0e9afa73329e5911a0b9a468f02837a7d2162f9b..8a6bf634646ed6d90e22c765275727108ece3f0f 100644
--- a/src/docs/stream.ja-JP.md
+++ b/src/docs/stream.ja-JP.md
@@ -339,7 +339,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
 #### `note`
 ローカルタイムラインに新しい投稿が流れてきたときに発生するイベントです。
 
-## `hybridTimeline`
+## `socialTimeline`
 ソーシャルタイムラインの投稿情報が流れてきます。このチャンネルにパラメータはありません。
 
 ### 流れてくるイベント一覧
diff --git a/src/index.ts b/src/index.ts
index e55ba5115d54f8f245bf83af0aa919d1113525d2..c4a1088c2e238ec8c8872e7a26dd49c13cf79a22 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -6,281 +6,8 @@ Error.stackTraceLimit = Infinity;
 
 require('events').EventEmitter.defaultMaxListeners = 128;
 
-import * as os from 'os';
-import * as cluster from 'cluster';
-import chalk from 'chalk';
-import * as portscanner from 'portscanner';
-import * as isRoot from 'is-root';
-import Xev from 'xev';
+import boot from './boot';
 
-import Logger from './services/logger';
-import serverStats from './daemons/server-stats';
-import notesStats from './daemons/notes-stats';
-import queueStats from './daemons/queue-stats';
-import loadConfig from './config/load';
-import { Config } from './config/types';
-import { lessThan } from './prelude/array';
-import * as pkg from '../package.json';
-import { program } from './argv';
-import { checkMongoDB } from './misc/check-mongodb';
-import { showMachineInfo } from './misc/show-machine-info';
-
-const logger = new Logger('core', 'cyan');
-const bootLogger = logger.createSubLogger('boot', 'magenta', false);
-const clusterLogger = logger.createSubLogger('cluster', 'orange');
-const ev = new Xev();
-
-/**
- * Init process
- */
-function main() {
-	process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`;
-
-	if (program.onlyQueue) {
-		queueMain();
-		return;
-	}
-
-	if (cluster.isMaster || program.disableClustering) {
-		masterMain();
-
-		if (cluster.isMaster) {
-			ev.mount();
-		}
-
-		if (program.daemons) {
-			serverStats();
-			notesStats();
-			queueStats();
-		}
-	}
-
-	if (cluster.isWorker || program.disableClustering) {
-		workerMain();
-	}
-}
-
-function greet() {
-	if (!program.quiet) {
-		//#region Misskey logo
-		const v = `v${pkg.version}`;
-		console.log('  _____ _         _           ');
-		console.log(' |     |_|___ ___| |_ ___ _ _ ');
-		console.log(' | | | | |_ -|_ -| \'_| -_| | |');
-		console.log(' |_|_|_|_|___|___|_,_|___|_  |');
-		console.log(' ' + chalk.gray(v) + ('                        |___|\n'.substr(v.length)));
-		//#endregion
-
-		console.log(' Misskey is maintained by @syuilo, @AyaMorisawa, @mei23, and @acid-chicken.');
-		console.log(chalk.keyword('orange')(' If you like Misskey, please donate to support development. https://www.patreon.com/syuilo'));
-
-		console.log('');
-		console.log(chalk`< ${os.hostname()} {gray (PID: ${process.pid.toString()})} >`);
-	}
-
-	bootLogger.info('Welcome to Misskey!');
-	bootLogger.info(`Misskey v${pkg.version}`, null, true);
-}
-
-/**
- * Init master process
- */
-async function masterMain() {
-	greet();
-
-	let config: Config;
-
-	try {
-		// initialize app
-		config = await init();
-
-		if (config.port == null) {
-			bootLogger.error('The port is not configured. Please configure port.', null, true);
-			process.exit(1);
-		}
-
-		if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) {
-			bootLogger.error('You need root privileges to listen on well-known port on Linux', null, true);
-			process.exit(1);
-		}
-
-		if (!await isPortAvailable(config.port)) {
-			bootLogger.error(`Port ${config.port} is already in use`, null, true);
-			process.exit(1);
-		}
-	} catch (e) {
-		bootLogger.error('Fatal error occurred during initialization', null, true);
-		process.exit(1);
-	}
-
-	bootLogger.succ('Misskey initialized');
-
-	if (!program.disableClustering) {
-		await spawnWorkers(config.clusterLimit);
-	}
-
-	bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
-}
-
-/**
- * Init worker process
- */
-async function workerMain() {
-	// start server
-	await require('./server').default();
-
-	// start job queue
-	require('./queue').default();
-
-	if (cluster.isWorker) {
-		// Send a 'ready' message to parent process
-		process.send('ready');
-	}
-}
-
-async function queueMain() {
-	greet();
-
-	try {
-		// initialize app
-		await init();
-	} catch (e) {
-		bootLogger.error('Fatal error occurred during initialization', null, true);
-		process.exit(1);
-	}
-
-	bootLogger.succ('Misskey initialized');
-
-	// start processor
-	require('./queue').default();
-
-	bootLogger.succ('Queue started', null, true);
-}
-
-const runningNodejsVersion = process.version.slice(1).split('.').map(x => parseInt(x, 10));
-const requiredNodejsVersion = [10, 0, 0];
-const satisfyNodejsVersion = !lessThan(runningNodejsVersion, requiredNodejsVersion);
-
-function isWellKnownPort(port: number): boolean {
-	return port < 1024;
-}
-
-async function isPortAvailable(port: number): Promise<boolean> {
-	return await portscanner.checkPortStatus(port, '127.0.0.1') === 'closed';
-}
-
-function showEnvironment(): void {
-	const env = process.env.NODE_ENV;
-	const logger = bootLogger.createSubLogger('env');
-	logger.info(typeof env == 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`);
-
-	if (env !== 'production') {
-		logger.warn('The environment is not in production mode.');
-		logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true);
-	}
-
-	logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`);
+export default function() {
+	return boot();
 }
-
-/**
- * Init app
- */
-async function init(): Promise<Config> {
-	showEnvironment();
-
-	const nodejsLogger = bootLogger.createSubLogger('nodejs');
-
-	nodejsLogger.info(`Version ${runningNodejsVersion.join('.')}`);
-
-	if (!satisfyNodejsVersion) {
-		nodejsLogger.error(`Node.js version is less than ${requiredNodejsVersion.join('.')}. Please upgrade it.`, null, true);
-		process.exit(1);
-	}
-
-	await showMachineInfo(bootLogger);
-
-	const configLogger = bootLogger.createSubLogger('config');
-	let config;
-
-	try {
-		config = loadConfig();
-	} catch (exception) {
-		if (typeof exception === 'string') {
-			configLogger.error(exception);
-			process.exit(1);
-		}
-		if (exception.code === 'ENOENT') {
-			configLogger.error('Configuration file not found', null, true);
-			process.exit(1);
-		}
-		throw exception;
-	}
-
-	configLogger.succ('Loaded');
-
-	// Try to connect to MongoDB
-	try {
-		await checkMongoDB(config, bootLogger);
-	} catch (e) {
-		bootLogger.error('Cannot connect to database', null, true);
-		process.exit(1);
-	}
-
-	return config;
-}
-
-async function spawnWorkers(limit: number = Infinity) {
-	const workers = Math.min(limit, os.cpus().length);
-	bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
-	await Promise.all([...Array(workers)].map(spawnWorker));
-	bootLogger.succ('All workers started');
-}
-
-function spawnWorker(): Promise<void> {
-	return new Promise(res => {
-		const worker = cluster.fork();
-		worker.on('message', message => {
-			if (message !== 'ready') return;
-			res();
-		});
-	});
-}
-
-//#region Events
-
-// Listen new workers
-cluster.on('fork', worker => {
-	clusterLogger.debug(`Process forked: [${worker.id}]`);
-});
-
-// Listen online workers
-cluster.on('online', worker => {
-	clusterLogger.debug(`Process is now online: [${worker.id}]`);
-});
-
-// Listen for dying workers
-cluster.on('exit', worker => {
-	// Replace the dead worker,
-	// we're not sentimental
-	clusterLogger.error(chalk.red(`[${worker.id}] died :(`));
-	cluster.fork();
-});
-
-// Display detail of unhandled promise rejection
-if (!program.quiet) {
-	process.on('unhandledRejection', console.dir);
-}
-
-// Display detail of uncaught exception
-process.on('uncaughtException', err => {
-	logger.error(err);
-});
-
-// Dying away...
-process.on('exit', code => {
-	logger.info(`The process is going to exit with code ${code}`);
-});
-
-//#endregion
-
-main();
diff --git a/src/init.ts b/src/init.ts
new file mode 100644
index 0000000000000000000000000000000000000000..69c117c140b2cd3120e8b43594df0e9d55480bf5
--- /dev/null
+++ b/src/init.ts
@@ -0,0 +1,16 @@
+import { initDb } from './db/postgre';
+
+async function main() {
+	try {
+		console.log('Connecting database...');
+		await initDb(false, true, true);
+	} catch (e) {
+		console.error('Cannot connect to database', null, true);
+		console.error(e);
+		process.exit(1);
+	}
+
+	console.log('Done :)');
+}
+
+main();
diff --git a/src/mfm/toHtml.ts b/src/mfm/toHtml.ts
index c676ae6ffcd16739b868cda715ff27106fde2550..3cd79876247e37a414cda802c93a623206af6fcd 100644
--- a/src/mfm/toHtml.ts
+++ b/src/mfm/toHtml.ts
@@ -1,10 +1,10 @@
 import { JSDOM } from 'jsdom';
 import config from '../config';
-import { INote } from '../models/note';
 import { intersperse } from '../prelude/array';
 import { MfmForest, MfmTree } from './prelude';
+import { IMentionedRemoteUsers } from '../models/entities/note';
 
-export function toHtml(tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteUsers'] = []) {
+export function toHtml(tokens: MfmForest, mentionedRemoteUsers: IMentionedRemoteUsers = []) {
 	if (tokens == null) {
 		return null;
 	}
diff --git a/src/misc/aid.ts b/src/misc/aid.ts
new file mode 100644
index 0000000000000000000000000000000000000000..aba53ed5fba533ab8edf1d9898d0ba95eb4f8d5d
--- /dev/null
+++ b/src/misc/aid.ts
@@ -0,0 +1,26 @@
+// AID
+// 長さ8の[2000年1月1日からの経過ミリ秒をbase36でエンコードしたもの] + 長さnの[ランダムな文字列]
+
+const CHARS = '0123456789abcdefghijklmnopqrstuvwxyz';
+const TIME2000 = 946684800000;
+
+function getTime(time: number) {
+	time = time - TIME2000;
+	if (time < 0) time = 0;
+
+	return time.toString(36);
+}
+
+function getRandom(length: number) {
+	let str = '';
+
+	for (let i = 0; i < length; i++) {
+		str += CHARS[Math.floor(Math.random() * CHARS.length)];
+	}
+
+	return str;
+}
+
+export function genAid(date: Date, rand: number): string {
+	return getTime(date.getTime()).padStart(8, CHARS[0]) + getRandom(rand);
+}
diff --git a/src/misc/aidc.ts b/src/misc/aidc.ts
new file mode 100644
index 0000000000000000000000000000000000000000..75168ac3072c61a3cf8ee2db14497a13204daca0
--- /dev/null
+++ b/src/misc/aidc.ts
@@ -0,0 +1,26 @@
+// AID(Cheep)
+// 長さ6の[2000年1月1日からの経過秒をbase36でエンコードしたもの] + 長さ3の[ランダムな文字列]
+
+const CHARS = '0123456789abcdefghijklmnopqrstuvwxyz';
+const TIME2000 = 946684800000;
+
+function getTime(time: number) {
+	time = time - TIME2000;
+	if (time < 0) time = 0;
+	time = Math.floor(time / 1000);
+	return time.toString(36);
+}
+
+function getRandom() {
+	let str = '';
+
+	for (let i = 0; i < 3; i++) {
+		str += CHARS[Math.floor(Math.random() * CHARS.length)];
+	}
+
+	return str;
+}
+
+export function genAidc(date: Date): string {
+	return getTime(date.getTime()).padStart(6, CHARS[0]) + getRandom();
+}
diff --git a/src/misc/cafy-id.ts b/src/misc/cafy-id.ts
index bc8fe4ea2bf8cfde3b59503b1296f211de6dd36a..39886611e16921bdbef2fef4c2d1575b0dad0b60 100644
--- a/src/misc/cafy-id.ts
+++ b/src/misc/cafy-id.ts
@@ -1,38 +1,13 @@
-import * as mongo from 'mongodb';
 import { Context } from 'cafy';
-import isObjectId from './is-objectid';
 
-export const isAnId = (x: any) => mongo.ObjectID.isValid(x);
-export const isNotAnId = (x: any) => !isAnId(x);
-export const transform = (x: string | mongo.ObjectID): mongo.ObjectID => {
-	if (x === undefined) return undefined;
-	if (x === null) return null;
-
-	if (isAnId(x) && !isObjectId(x)) {
-		return new mongo.ObjectID(x);
-	} else {
-		return x as mongo.ObjectID;
-	}
-};
-export const transformMany = (xs: (string | mongo.ObjectID)[]): mongo.ObjectID[] => {
-	if (xs == null) return null;
-
-	return xs.map(x => transform(x));
-};
-
-export type ObjectId = mongo.ObjectID;
-
-/**
- * ID
- */
-export default class ID<Maybe = string> extends Context<string | Maybe> {
+export class ID<Maybe = string> extends Context<string | (Maybe extends {} ? string : Maybe)> {
 	public readonly name = 'ID';
 
 	constructor(optional = false, nullable = false) {
 		super(optional, nullable);
 
 		this.push((v: any) => {
-			if (!isObjectId(v) && isNotAnId(v)) {
+			if (typeof v !== 'string') {
 				return new Error('must-be-an-id');
 			}
 			return true;
diff --git a/src/misc/check-mongodb.ts b/src/misc/check-mongodb.ts
deleted file mode 100644
index 8e03db5d4205de35866f12169f0b75ccc2ea3f25..0000000000000000000000000000000000000000
--- a/src/misc/check-mongodb.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { nativeDbConn } from '../db/mongodb';
-import { Config } from '../config/types';
-import Logger from '../services/logger';
-import { lessThan } from '../prelude/array';
-
-const requiredMongoDBVersion = [3, 6];
-
-export function checkMongoDB(config: Config, logger: Logger) {
-	return new Promise((res, rej) => {
-		const mongoDBLogger = logger.createSubLogger('db');
-		const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null;
-		const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null;
-		const uri = `mongodb://${u && p ? `${u}:****@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
-		mongoDBLogger.info(`Connecting to ${uri} ...`);
-
-		nativeDbConn().then(db => {
-			mongoDBLogger.succ('Connectivity confirmed');
-
-			db.admin().serverInfo().then(x => {
-				const version = x.version as string;
-				mongoDBLogger.info(`Version: ${version}`);
-				if (lessThan(version.split('.').map(x => parseInt(x, 10)), requiredMongoDBVersion)) {
-					mongoDBLogger.error(`MongoDB version is less than ${requiredMongoDBVersion.join('.')}. Please upgrade it.`);
-					rej('outdated version');
-				} else {
-					res();
-				}
-			}).catch(err => {
-				mongoDBLogger.error(`Failed to fetch server info: ${err.message}`);
-				rej(err);
-			});
-		}).catch(err => {
-			mongoDBLogger.error(err.message);
-			rej(err);
-		});
-	});
-}
diff --git a/src/misc/fetch-meta.ts b/src/misc/fetch-meta.ts
index 3584a819bf20d8c8ce4a58158ecb071772990293..d1483e9edbf94b7d0a2c1e9dfba080517f67f541 100644
--- a/src/misc/fetch-meta.ts
+++ b/src/misc/fetch-meta.ts
@@ -1,32 +1,15 @@
-import Meta, { IMeta } from '../models/meta';
+import { Meta } from '../models/entities/meta';
+import { Metas } from '../models';
+import { genId } from './gen-id';
 
-const defaultMeta: any = {
-	name: 'Misskey',
-	maintainer: {},
-	langs: [],
-	cacheRemoteFiles: true,
-	localDriveCapacityMb: 256,
-	remoteDriveCapacityMb: 8,
-	hidedTags: [],
-	stats: {
-		originalNotesCount: 0,
-		originalUsersCount: 0
-	},
-	maxNoteTextLength: 1000,
-	enableEmojiReaction: true,
-	enableTwitterIntegration: false,
-	enableGithubIntegration: false,
-	enableDiscordIntegration: false,
-	enableExternalUserRecommendation: false,
-	externalUserRecommendationEngine: 'https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}',
-	externalUserRecommendationTimeout: 300000,
-	mascotImageUrl: '/assets/ai.png',
-	errorImageUrl: 'https://ai.misskey.xyz/aiart/yubitun.png',
-	enableServiceWorker: false
-};
-
-export default async function(): Promise<IMeta> {
-	const meta = await Meta.findOne({});
-
-	return Object.assign({}, defaultMeta, meta);
+export default async function(): Promise<Meta> {
+	const meta = await Metas.findOne();
+	if (meta) {
+		return meta;
+	} else {
+		return Metas.save({
+			id: genId(),
+			hiddenTags: []
+		} as Meta);
+	}
 }
diff --git a/src/misc/fetch-proxy-account.ts b/src/misc/fetch-proxy-account.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d60fa9b313c1d10dd0bba7c232285133327628f5
--- /dev/null
+++ b/src/misc/fetch-proxy-account.ts
@@ -0,0 +1,8 @@
+import fetchMeta from './fetch-meta';
+import { ILocalUser } from '../models/entities/user';
+import { Users } from '../models';
+
+export async function fetchProxyAccount(): Promise<ILocalUser> {
+	const meta = await fetchMeta();
+	return await Users.findOne({ username: meta.proxyAccount, host: null }) as ILocalUser;
+}
diff --git a/src/misc/gen-id.ts b/src/misc/gen-id.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fe901b1fe7bddc876d6ecf85e3e931b1ef086083
--- /dev/null
+++ b/src/misc/gen-id.ts
@@ -0,0 +1,22 @@
+import { ulid } from 'ulid';
+import { genAid } from './aid';
+import { genAidc } from './aidc';
+import { genObjectId } from './object-id';
+import config from '../config';
+
+const metohd = config.id.toLowerCase();
+
+export function genId(date?: Date): string {
+	if (!date || (date > new Date())) date = new Date();
+
+	switch (metohd) {
+		case 'aidc': return genAidc(date);
+		case 'aid1': return genAid(date, 1);
+		case 'aid2': return genAid(date, 2);
+		case 'aid3': return genAid(date, 3);
+		case 'aid4': return genAid(date, 4);
+		case 'ulid': return ulid(date.getTime());
+		case 'objectid': return genObjectId(date);
+		default: throw 'unknown id generation method';
+	}
+}
diff --git a/src/misc/get-drive-file-url.ts b/src/misc/get-drive-file-url.ts
deleted file mode 100644
index 067db8a5d03429d5a275f68e8799a4953877e342..0000000000000000000000000000000000000000
--- a/src/misc/get-drive-file-url.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { IDriveFile } from '../models/drive-file';
-import config from '../config';
-
-export default function(file: IDriveFile, thumbnail = false): string {
-	if (file == null) return null;
-
-	const isImage = file.contentType && file.contentType.startsWith('image/');
-
-	if (file.metadata.withoutChunks) {
-		if (thumbnail) {
-			return file.metadata.thumbnailUrl || file.metadata.webpublicUrl || (isImage ? file.metadata.url : null);
-		} else {
-			return file.metadata.webpublicUrl || file.metadata.url;
-		}
-	} else {
-		if (thumbnail) {
-			return `${config.driveUrl}/${file._id}?thumbnail`;
-		} else {
-			return `${config.driveUrl}/${file._id}?web`;
-		}
-	}
-}
-
-export function getOriginalUrl(file: IDriveFile) {
-	if (file.metadata && file.metadata.url) {
-		return file.metadata.url;
-	}
-
-	const accessKey = file.metadata ? file.metadata.accessKey : null;
-	return `${config.driveUrl}/${file._id}${accessKey ? '?original=' + accessKey : ''}`;
-}
diff --git a/src/misc/get-notification-summary.ts b/src/misc/get-notification-summary.ts
index 71d4973ce9e53be2c5fefd0442785ddf04877e09..b20711c60580c4a9d02559971c03fe0e3871af4b 100644
--- a/src/misc/get-notification-summary.ts
+++ b/src/misc/get-notification-summary.ts
@@ -20,7 +20,7 @@ export default function(notification: any): string {
 			return `引用されました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`;
 		case 'reaction':
 			return `リアクションされました:\n${getUserName(notification.user)} <${getReactionEmoji(notification.reaction)}>「${getNoteSummary(notification.note)}」`;
-		case 'poll_vote':
+		case 'pollVote':
 			return `投票されました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`;
 		default:
 			return `<不明な通知タイプ: ${notification.type}>`;
diff --git a/src/misc/get-user-name.ts b/src/misc/get-user-name.ts
index eab9f87ef03a207acd6db93cff266f7b9d86620a..b6b45118b0cd89f4ec1249561e6c587b05c08960 100644
--- a/src/misc/get-user-name.ts
+++ b/src/misc/get-user-name.ts
@@ -1,5 +1,5 @@
-import { IUser } from '../models/user';
+import { User } from '../models/entities/user';
 
-export default function(user: IUser): string {
+export default function(user: User): string {
 	return user.name || user.username;
 }
diff --git a/src/misc/get-user-summary.ts b/src/misc/get-user-summary.ts
index 09cf5ebadc008cc8409d49a4a846b6e089222eea..9cb06f43ce015ee1b44e38d402af4a2c43766dd7 100644
--- a/src/misc/get-user-summary.ts
+++ b/src/misc/get-user-summary.ts
@@ -1,17 +1,18 @@
-import { IUser, isLocalUser } from '../models/user';
 import getAcct from './acct/render';
 import getUserName from './get-user-name';
+import { User } from '../models/entities/user';
+import { Users } from '../models';
 
 /**
  * ユーザーを表す文字列を取得します。
  * @param user ユーザー
  */
-export default function(user: IUser): string {
+export default function(user: User): string {
 	let string = `${getUserName(user)} (@${getAcct(user)})\n` +
 		`${user.notesCount}投稿、${user.followingCount}フォロー、${user.followersCount}フォロワー\n`;
 
-	if (isLocalUser(user)) {
-		string += `場所: ${user.profile.location}、誕生日: ${user.profile.birthday}\n`;
+	if (Users.isLocalUser(user)) {
+		string += `場所: ${user.location}、誕生日: ${user.birthday}\n`;
 	}
 
 	return string + `「${user.description}」`;
diff --git a/src/misc/is-duplicate-key-value-error.ts b/src/misc/is-duplicate-key-value-error.ts
new file mode 100644
index 0000000000000000000000000000000000000000..23d8ceb1b7592ed722080f64b3c33c190395c039
--- /dev/null
+++ b/src/misc/is-duplicate-key-value-error.ts
@@ -0,0 +1,3 @@
+export function isDuplicateKeyValueError(e: Error): boolean {
+	return e.message.startsWith('duplicate key value');
+}
diff --git a/src/misc/is-objectid.ts b/src/misc/is-objectid.ts
deleted file mode 100644
index a77c4ee2d578c6c960cae2bf4c1a804a58238842..0000000000000000000000000000000000000000
--- a/src/misc/is-objectid.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { ObjectID } from 'mongodb';
-
-export default function(x: any): x is ObjectID {
-	return x && typeof x === 'object' && (x.hasOwnProperty('toHexString') || x.hasOwnProperty('_bsontype'));
-}
diff --git a/src/misc/is-quote.ts b/src/misc/is-quote.ts
index a99b8f64348541131eeca0f3b7d3138e94ee234a..0a2a72f4a0ebcc08d17df2fe36bc33f51a5fa347 100644
--- a/src/misc/is-quote.ts
+++ b/src/misc/is-quote.ts
@@ -1,5 +1,5 @@
-import { INote } from '../models/note';
+import { Note } from '../models/entities/note';
 
-export default function(note: INote): boolean {
-	return note.renoteId != null && (note.text != null || note.poll != null || (note.fileIds != null && note.fileIds.length > 0));
+export default function(note: Note): boolean {
+	return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0));
 }
diff --git a/src/misc/nyaize.ts b/src/misc/nyaize.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8b06300eab5dfa58d65f8df5f5937341f418c778
--- /dev/null
+++ b/src/misc/nyaize.ts
@@ -0,0 +1,9 @@
+export function nyaize(text: string): string {
+	return text
+		// ja-JP
+		.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ')
+		// ko-KR
+		.replace(/[나-낳]/g, (match: string) => String.fromCharCode(
+			match.codePointAt(0)  + '냐'.charCodeAt(0) - '나'.charCodeAt(0)
+		));
+}
diff --git a/src/misc/object-id.ts b/src/misc/object-id.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6f6422e5e43d1bc4686b88437c6653e095baee33
--- /dev/null
+++ b/src/misc/object-id.ts
@@ -0,0 +1,26 @@
+const CHARS = '0123456789abcdef';
+
+function getTime(time: number) {
+	if (time < 0) time = 0;
+	if (time === 0) {
+		return CHARS[0];
+	}
+
+	time = Math.floor(time / 1000);
+
+	return time.toString(16);
+}
+
+function getRandom() {
+	let str = '';
+
+	for (let i = 0; i < 16; i++) {
+		str += CHARS[Math.floor(Math.random() * CHARS.length)];
+	}
+
+	return str;
+}
+
+export function genObjectId(date: Date): string {
+	return getTime(date.getTime()) + getRandom();
+}
diff --git a/src/misc/reaction-lib.ts b/src/misc/reaction-lib.ts
index 7e5a1b0bc0b6a971bf3d355255ed71e431f52f59..20051f028002156948c3cc269ce4b20876221529 100644
--- a/src/misc/reaction-lib.ts
+++ b/src/misc/reaction-lib.ts
@@ -1,6 +1,6 @@
-import Emoji from '../models/emoji';
 import { emojiRegex } from './emoji-regex';
 import fetchMeta from './fetch-meta';
+import { Emojis } from '../models';
 
 const basic10: Record<string, string> = {
 	'👍': 'like',
@@ -49,7 +49,7 @@ export async function toDbReaction(reaction: string, enableEmoji = true): Promis
 
 	const custom = reaction.match(/^:([\w+-]+):$/);
 	if (custom) {
-		const emoji = await Emoji.findOne({
+		const emoji = await Emojis.findOne({
 			host: null,
 			name: custom[1],
 		});
diff --git a/src/misc/should-mute-this-note.ts b/src/misc/should-mute-this-note.ts
index b1d29c6a28d558ffda5c7ebfe1bfc782316a0475..8f606a2943937522ad1e95964530d0a9d2f2de5e 100644
--- a/src/misc/should-mute-this-note.ts
+++ b/src/misc/should-mute-this-note.ts
@@ -1,20 +1,13 @@
-import * as mongo from 'mongodb';
-import isObjectId from './is-objectid';
-
-function toString(id: any) {
-	return isObjectId(id) ? (id as mongo.ObjectID).toHexString() : id;
-}
-
 export default function(note: any, mutedUserIds: string[]): boolean {
-	if (mutedUserIds.includes(toString(note.userId))) {
+	if (mutedUserIds.includes(note.userId)) {
 		return true;
 	}
 
-	if (note.reply != null && mutedUserIds.includes(toString(note.reply.userId))) {
+	if (note.reply != null && mutedUserIds.includes(note.reply.userId)) {
 		return true;
 	}
 
-	if (note.renote != null && mutedUserIds.includes(toString(note.renote.userId))) {
+	if (note.renote != null && mutedUserIds.includes(note.renote.userId)) {
 		return true;
 	}
 
diff --git a/src/models/abuse-user-report.ts b/src/models/abuse-user-report.ts
deleted file mode 100644
index f3900d348dc1d647d6af190a0af8d1255e9d38f1..0000000000000000000000000000000000000000
--- a/src/models/abuse-user-report.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { pack as packUser } from './user';
-
-const AbuseUserReport = db.get<IAbuseUserReport>('abuseUserReports');
-AbuseUserReport.createIndex('userId');
-AbuseUserReport.createIndex('reporterId');
-AbuseUserReport.createIndex(['userId', 'reporterId'], { unique: true });
-export default AbuseUserReport;
-
-export interface IAbuseUserReport {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	userId: mongo.ObjectID;
-	reporterId: mongo.ObjectID;
-	comment: string;
-}
-
-export const packMany = (
-	reports: (string | mongo.ObjectID | IAbuseUserReport)[]
-) => {
-	return Promise.all(reports.map(x => pack(x)));
-};
-
-export const pack = (
-	report: any
-) => new Promise<any>(async (resolve, reject) => {
-	let _report: any;
-
-	if (isObjectId(report)) {
-		_report = await AbuseUserReport.findOne({
-			_id: report
-		});
-	} else if (typeof report === 'string') {
-		_report = await AbuseUserReport.findOne({
-			_id: new mongo.ObjectID(report)
-		});
-	} else {
-		_report = deepcopy(report);
-	}
-
-	// Rename _id to id
-	_report.id = _report._id;
-	delete _report._id;
-
-	_report.reporter = await packUser(_report.reporterId, null, { detail: true });
-	_report.user = await packUser(_report.userId, null, { detail: true });
-
-	resolve(_report);
-});
diff --git a/src/models/access-token.ts b/src/models/access-token.ts
deleted file mode 100644
index 66c5c91c0bb81df0970dea8e314c943e87a7a571..0000000000000000000000000000000000000000
--- a/src/models/access-token.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const AccessToken = db.get<IAccessToken>('accessTokens');
-AccessToken.createIndex('token');
-AccessToken.createIndex('hash');
-export default AccessToken;
-
-export type IAccessToken = {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	appId: mongo.ObjectID;
-	userId: mongo.ObjectID;
-	token: string;
-	hash: string;
-};
diff --git a/src/models/app.ts b/src/models/app.ts
deleted file mode 100644
index 45d50bccda87b444b14830172b6eff537eff1f11..0000000000000000000000000000000000000000
--- a/src/models/app.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import AccessToken from './access-token';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import config from '../config';
-import { dbLogger } from '../db/logger';
-
-const App = db.get<IApp>('apps');
-App.createIndex('secret');
-export default App;
-
-export type IApp = {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	userId: mongo.ObjectID | null;
-	secret: string;
-	name: string;
-	description: string;
-	permission: string[];
-	callbackUrl: string;
-};
-
-/**
- * Pack an app for API response
- */
-export const pack = (
-	app: any,
-	me?: any,
-	options?: {
-		detail?: boolean,
-		includeSecret?: boolean,
-		includeProfileImageIds?: boolean
-	}
-) => new Promise<any>(async (resolve, reject) => {
-	const opts = Object.assign({
-		detail: false,
-		includeSecret: false,
-		includeProfileImageIds: false
-	}, options);
-
-	let _app: any;
-
-	const fields = opts.detail ? {} : {
-		name: true
-	};
-
-	// Populate the app if 'app' is ID
-	if (isObjectId(app)) {
-		_app = await App.findOne({
-			_id: app
-		});
-	} else if (typeof app === 'string') {
-		_app = await App.findOne({
-			_id: new mongo.ObjectID(app)
-		}, { fields });
-	} else {
-		_app = deepcopy(app);
-	}
-
-	// Me
-	if (me && !isObjectId(me)) {
-		if (typeof me === 'string') {
-			me = new mongo.ObjectID(me);
-		} else {
-			me = me._id;
-		}
-	}
-
-	// (データベースの欠損などで)アプリがデータベース上に見つからなかったとき
-	if (_app == null) {
-		dbLogger.warn(`[DAMAGED DB] (missing) pkg: app :: ${app}`);
-		return null;
-	}
-
-	// Rename _id to id
-	_app.id = _app._id;
-	delete _app._id;
-
-	// Visible by only owner
-	if (!opts.includeSecret) {
-		delete _app.secret;
-	}
-
-	_app.iconUrl = _app.icon != null
-		? `${config.driveUrl}/${_app.icon}`
-		: `${config.driveUrl}/app-default.jpg`;
-
-	if (me) {
-		// 既に連携しているか
-		const exist = await AccessToken.count({
-			appId: _app.id,
-			userId: me,
-		}, {
-				limit: 1
-			});
-
-		_app.isAuthorized = exist === 1;
-	}
-
-	resolve(_app);
-});
diff --git a/src/models/auth-session.ts b/src/models/auth-session.ts
deleted file mode 100644
index 428c707470d1bd812024ca9828d110a9f293ca7d..0000000000000000000000000000000000000000
--- a/src/models/auth-session.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { pack as packApp } from './app';
-
-const AuthSession = db.get<IAuthSession>('authSessions');
-export default AuthSession;
-
-export interface IAuthSession {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	appId: mongo.ObjectID;
-	userId: mongo.ObjectID;
-	token: string;
-}
-
-/**
- * Pack an auth session for API response
- *
- * @param {any} session
- * @param {any} me?
- * @return {Promise<any>}
- */
-export const pack = (
-	session: any,
-	me?: any
-) => new Promise<any>(async (resolve, reject) => {
-	let _session: any;
-
-	// TODO: Populate session if it ID
-	_session = deepcopy(session);
-
-	// Me
-	if (me && !isObjectId(me)) {
-		if (typeof me === 'string') {
-			me = new mongo.ObjectID(me);
-		} else {
-			me = me._id;
-		}
-	}
-
-	delete _session._id;
-
-	// Populate app
-	_session.app = await packApp(_session.appId, me);
-
-	resolve(_session);
-});
diff --git a/src/models/blocking.ts b/src/models/blocking.ts
deleted file mode 100644
index 4bdaa741e95828e872a0097ba4bf2f90f0e0e06b..0000000000000000000000000000000000000000
--- a/src/models/blocking.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import * as deepcopy from 'deepcopy';
-import { pack as packUser, IUser } from './user';
-
-const Blocking = db.get<IBlocking>('blocking');
-Blocking.createIndex('blockerId');
-Blocking.createIndex('blockeeId');
-Blocking.createIndex(['blockerId', 'blockeeId'], { unique: true });
-export default Blocking;
-
-export type IBlocking = {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	blockeeId: mongo.ObjectID;
-	blockerId: mongo.ObjectID;
-};
-
-export const packMany = (
-	blockings: (string | mongo.ObjectID | IBlocking)[],
-	me?: string | mongo.ObjectID | IUser
-) => {
-	return Promise.all(blockings.map(x => pack(x, me)));
-};
-
-export const pack = (
-	blocking: any,
-	me?: any
-) => new Promise<any>(async (resolve, reject) => {
-	let _blocking: any;
-
-	// Populate the blocking if 'blocking' is ID
-	if (isObjectId(blocking)) {
-		_blocking = await Blocking.findOne({
-			_id: blocking
-		});
-	} else if (typeof blocking === 'string') {
-		_blocking = await Blocking.findOne({
-			_id: new mongo.ObjectID(blocking)
-		});
-	} else {
-		_blocking = deepcopy(blocking);
-	}
-
-	// Rename _id to id
-	_blocking.id = _blocking._id;
-	delete _blocking._id;
-
-	// Populate blockee
-	_blocking.blockee = await packUser(_blocking.blockeeId, me, {
-		detail: true
-	});
-
-	resolve(_blocking);
-});
diff --git a/src/models/drive-file-thumbnail.ts b/src/models/drive-file-thumbnail.ts
deleted file mode 100644
index bdb3d010e641d3cec856e667922512ebf1c58cbf..0000000000000000000000000000000000000000
--- a/src/models/drive-file-thumbnail.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import * as mongo from 'mongodb';
-import monkDb, { nativeDbConn } from '../db/mongodb';
-
-const DriveFileThumbnail = monkDb.get<IDriveFileThumbnail>('driveFileThumbnails.files');
-DriveFileThumbnail.createIndex('metadata.originalId', { sparse: true, unique: true });
-export default DriveFileThumbnail;
-
-export const DriveFileThumbnailChunk = monkDb.get('driveFileThumbnails.chunks');
-
-export const getDriveFileThumbnailBucket = async (): Promise<mongo.GridFSBucket> => {
-	const db = await nativeDbConn();
-	const bucket = new mongo.GridFSBucket(db, {
-		bucketName: 'driveFileThumbnails'
-	});
-	return bucket;
-};
-
-export type IMetadata = {
-	originalId: mongo.ObjectID;
-};
-
-export type IDriveFileThumbnail = {
-	_id: mongo.ObjectID;
-	uploadDate: Date;
-	md5: string;
-	filename: string;
-	contentType: string;
-	metadata: IMetadata;
-};
diff --git a/src/models/drive-file-webpublic.ts b/src/models/drive-file-webpublic.ts
deleted file mode 100644
index d087c355d39a8bb38ceb5d37a4d375a502d26bb4..0000000000000000000000000000000000000000
--- a/src/models/drive-file-webpublic.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import * as mongo from 'mongodb';
-import monkDb, { nativeDbConn } from '../db/mongodb';
-
-const DriveFileWebpublic = monkDb.get<IDriveFileWebpublic>('driveFileWebpublics.files');
-DriveFileWebpublic.createIndex('metadata.originalId', { sparse: true, unique: true });
-export default DriveFileWebpublic;
-
-export const DriveFileWebpublicChunk = monkDb.get('driveFileWebpublics.chunks');
-
-export const getDriveFileWebpublicBucket = async (): Promise<mongo.GridFSBucket> => {
-	const db = await nativeDbConn();
-	const bucket = new mongo.GridFSBucket(db, {
-		bucketName: 'driveFileWebpublics'
-	});
-	return bucket;
-};
-
-export type IMetadata = {
-	originalId: mongo.ObjectID;
-};
-
-export type IDriveFileWebpublic = {
-	_id: mongo.ObjectID;
-	uploadDate: Date;
-	md5: string;
-	filename: string;
-	contentType: string;
-	metadata: IMetadata;
-};
diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts
deleted file mode 100644
index c31e9a709f2ceed94f042d71a42474d03982f877..0000000000000000000000000000000000000000
--- a/src/models/drive-file.ts
+++ /dev/null
@@ -1,232 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import { pack as packFolder } from './drive-folder';
-import { pack as packUser } from './user';
-import monkDb, { nativeDbConn } from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import getDriveFileUrl, { getOriginalUrl } from '../misc/get-drive-file-url';
-import { dbLogger } from '../db/logger';
-
-const DriveFile = monkDb.get<IDriveFile>('driveFiles.files');
-DriveFile.createIndex('md5');
-DriveFile.createIndex('metadata.uri');
-DriveFile.createIndex('metadata.userId');
-DriveFile.createIndex('metadata.folderId');
-DriveFile.createIndex('metadata._user.host');
-export default DriveFile;
-
-export const DriveFileChunk = monkDb.get('driveFiles.chunks');
-
-export const getDriveFileBucket = async (): Promise<mongo.GridFSBucket> => {
-	const db = await nativeDbConn();
-	const bucket = new mongo.GridFSBucket(db, {
-		bucketName: 'driveFiles'
-	});
-	return bucket;
-};
-
-export type IMetadata = {
-	properties: any;
-	userId: mongo.ObjectID;
-	_user: any;
-	folderId: mongo.ObjectID;
-	comment: string;
-
-	/**
-	 * リモートインスタンスから取得した場合の元URL
-	 */
-	uri?: string;
-
-	/**
-	 * URL for web(生成されている場合) or original
-	 * * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ
-	 */
-	url?: string;
-
-	/**
-	 * URL for thumbnail (thumbnailがなければなし)
-	 * * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ
-	 */
-	thumbnailUrl?: string;
-
-	/**
-	 * URL for original (web用が生成されてない場合はurlがoriginalを指す)
-	 * * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ
-	 */
-	webpublicUrl?: string;
-
-	accessKey?: string;
-
-	src?: string;
-	deletedAt?: Date;
-
-	/**
-	 * このファイルの中身データがMongoDB内に保存されていないか否か
-	 * オブジェクトストレージを利用している or リモートサーバーへの直リンクである
-	 * な場合は true になります
-	 */
-	withoutChunks?: boolean;
-
-	storage?: string;
-
-	/***
-	 * ObjectStorage の格納先の情報
-	 */
-	storageProps?: IStorageProps;
-	isSensitive?: boolean;
-
-	/**
-	 * このファイルが添付された投稿のID一覧
-	 */
-	attachedNoteIds?: mongo.ObjectID[];
-
-	/**
-	 * 外部の(信頼されていない)URLへの直リンクか否か
-	 */
-	isRemote?: boolean;
-};
-
-export type IStorageProps = {
-	/**
-	 * ObjectStorage key for original
-	 */
-	key: string;
-
-	/***
-	 * ObjectStorage key for thumbnail (thumbnailがなければなし)
-	 */
-	thumbnailKey?: string;
-
-	/***
-	 * ObjectStorage key for webpublic (webpublicがなければなし)
-	 */
-	webpublicKey?: string;
-
-	id?: string;
-};
-
-export type IDriveFile = {
-	_id: mongo.ObjectID;
-	uploadDate: Date;
-	md5: string;
-	filename: string;
-	contentType: string;
-	metadata: IMetadata;
-
-	/**
-	 * ファイルサイズ
-	 */
-	length: number;
-};
-
-export function validateFileName(name: string): boolean {
-	return (
-		(name.trim().length > 0) &&
-		(name.length <= 200) &&
-		(name.indexOf('\\') === -1) &&
-		(name.indexOf('/') === -1) &&
-		(name.indexOf('..') === -1)
-	);
-}
-
-export const packMany = (
-	files: any[],
-	options?: {
-		detail?: boolean
-		self?: boolean,
-		withUser?: boolean,
-	}
-) => {
-	return Promise.all(files.map(f => pack(f, options)));
-};
-
-/**
- * Pack a drive file for API response
- */
-export const pack = (
-	file: any,
-	options?: {
-		detail?: boolean,
-		self?: boolean,
-		withUser?: boolean,
-	}
-) => new Promise<any>(async (resolve, reject) => {
-	const opts = Object.assign({
-		detail: false,
-		self: false
-	}, options);
-
-	let _file: any;
-
-	// Populate the file if 'file' is ID
-	if (isObjectId(file)) {
-		_file = await DriveFile.findOne({
-			_id: file
-		});
-	} else if (typeof file === 'string') {
-		_file = await DriveFile.findOne({
-			_id: new mongo.ObjectID(file)
-		});
-	} else {
-		_file = deepcopy(file);
-	}
-
-	// (データベースの欠損などで)ファイルがデータベース上に見つからなかったとき
-	if (_file == null) {
-		dbLogger.warn(`[DAMAGED DB] (missing) pkg: driveFile :: ${file}`);
-		return resolve(null);
-	}
-
-	// rendered target
-	let _target: any = {};
-
-	_target.id = _file._id;
-	_target.createdAt = _file.uploadDate;
-	_target.name = _file.filename;
-	_target.type = _file.contentType;
-	_target.datasize = _file.length;
-	_target.md5 = _file.md5;
-
-	_target = Object.assign(_target, _file.metadata);
-
-	_target.url = getDriveFileUrl(_file);
-	_target.thumbnailUrl = getDriveFileUrl(_file, true);
-	_target.isRemote = _file.metadata.isRemote;
-
-	if (_target.properties == null) _target.properties = {};
-
-	if (opts.detail) {
-		if (_target.folderId) {
-			// Populate folder
-			_target.folder = await packFolder(_target.folderId, {
-				detail: true
-			});
-		}
-
-		/*
-		if (_target.tags) {
-			// Populate tags
-			_target.tags = await _target.tags.map(async (tag: any) =>
-				await serializeDriveTag(tag)
-			);
-		}
-		*/
-	}
-
-	if (opts.withUser) {
-		// Populate user
-		_target.user = await packUser(_file.metadata.userId);
-	}
-
-	delete _target.withoutChunks;
-	delete _target.storage;
-	delete _target.storageProps;
-	delete _target.isRemote;
-	delete _target._user;
-
-	if (opts.self) {
-		_target.url = getOriginalUrl(_file);
-	}
-
-	resolve(_target);
-});
diff --git a/src/models/drive-folder.ts b/src/models/drive-folder.ts
deleted file mode 100644
index b0f6e4273e2c6372a03bfb9b7460fece32366439..0000000000000000000000000000000000000000
--- a/src/models/drive-folder.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import DriveFile from './drive-file';
-
-const DriveFolder = db.get<IDriveFolder>('driveFolders');
-DriveFolder.createIndex('userId');
-export default DriveFolder;
-
-export type IDriveFolder = {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	name: string;
-	userId: mongo.ObjectID;
-	parentId: mongo.ObjectID;
-};
-
-export function isValidFolderName(name: string): boolean {
-	return (
-		(name.trim().length > 0) &&
-		(name.length <= 200)
-	);
-}
-
-/**
- * Pack a drive folder for API response
- */
-export const pack = (
-	folder: any,
-	options?: {
-		detail: boolean
-	}
-) => new Promise<any>(async (resolve, reject) => {
-	const opts = Object.assign({
-		detail: false
-	}, options);
-
-	let _folder: any;
-
-	// Populate the folder if 'folder' is ID
-	if (isObjectId(folder)) {
-		_folder = await DriveFolder.findOne({ _id: folder });
-	} else if (typeof folder === 'string') {
-		_folder = await DriveFolder.findOne({ _id: new mongo.ObjectID(folder) });
-	} else {
-		_folder = deepcopy(folder);
-	}
-
-	// Rename _id to id
-	_folder.id = _folder._id;
-	delete _folder._id;
-
-	if (opts.detail) {
-		const childFoldersCount = await DriveFolder.count({
-			parentId: _folder.id
-		});
-
-		const childFilesCount = await DriveFile.count({
-			'metadata.folderId': _folder.id
-		});
-
-		_folder.foldersCount = childFoldersCount;
-		_folder.filesCount = childFilesCount;
-	}
-
-	if (opts.detail && _folder.parentId) {
-		// Populate parent folder
-		_folder.parent = await pack(_folder.parentId, {
-			detail: true
-		});
-	}
-
-	resolve(_folder);
-});
diff --git a/src/models/emoji.ts b/src/models/emoji.ts
deleted file mode 100644
index cbf939222efb96ceade1a6b643b8fc52256f105e..0000000000000000000000000000000000000000
--- a/src/models/emoji.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const Emoji = db.get<IEmoji>('emoji');
-Emoji.createIndex('name');
-Emoji.createIndex('host');
-Emoji.createIndex(['name', 'host'], { unique: true });
-
-export default Emoji;
-
-export type IEmoji = {
-	_id: mongo.ObjectID;
-	name: string;
-	host: string;
-	url: string;
-	aliases?: string[];
-	updatedAt?: Date;
-	/** AP object id */
-	uri?: string;
-	type?: string;
-};
diff --git a/src/models/entities/abuse-user-report.ts b/src/models/entities/abuse-user-report.ts
new file mode 100644
index 0000000000000000000000000000000000000000..43ab56023a2e80bd85224e21085e50ed320d23c4
--- /dev/null
+++ b/src/models/entities/abuse-user-report.ts
@@ -0,0 +1,41 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'reporterId'], { unique: true })
+export class AbuseUserReport {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the AbuseUserReport.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column(id())
+	public reporterId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public reporter: User | null;
+
+	@Column('varchar', {
+		length: 512,
+	})
+	public comment: string;
+}
diff --git a/src/models/entities/access-token.ts b/src/models/entities/access-token.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d08930cf5ab6c6282c28ded52f0f5682609eb804
--- /dev/null
+++ b/src/models/entities/access-token.ts
@@ -0,0 +1,45 @@
+import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn, RelationId } from 'typeorm';
+import { User } from './user';
+import { App } from './app';
+import { id } from '../id';
+
+@Entity()
+export class AccessToken {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the AccessToken.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column('varchar', {
+		length: 128
+	})
+	public token: string;
+
+	@Index()
+	@Column('varchar', {
+		length: 128
+	})
+	public hash: string;
+
+	@RelationId((self: AccessToken) => self.user)
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column(id())
+	public appId: App['id'];
+
+	@ManyToOne(type => App, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public app: App | null;
+}
diff --git a/src/models/entities/app.ts b/src/models/entities/app.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d0c89000fce2d3b317227d8b636d108a5164e26b
--- /dev/null
+++ b/src/models/entities/app.ts
@@ -0,0 +1,60 @@
+import { Entity, PrimaryColumn, Column, Index, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class App {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the App.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The owner ID.'
+	})
+	public userId: User['id'] | null;
+
+	@ManyToOne(type => User, {
+		onDelete: 'SET NULL',
+		nullable: true,
+	})
+	public user: User | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 64,
+		comment: 'The secret key of the App.'
+	})
+	public secret: string;
+
+	@Column('varchar', {
+		length: 128,
+		comment: 'The name of the App.'
+	})
+	public name: string;
+
+	@Column('varchar', {
+		length: 512,
+		comment: 'The description of the App.'
+	})
+	public description: string;
+
+	@Column('varchar', {
+		length: 64, array: true,
+		comment: 'The permission of the App.'
+	})
+	public permission: string[];
+
+	@Column('varchar', {
+		length: 256, nullable: true,
+		comment: 'The callbackUrl of the App.'
+	})
+	public callbackUrl: string | null;
+}
diff --git a/src/models/entities/auth-session.ts b/src/models/entities/auth-session.ts
new file mode 100644
index 0000000000000000000000000000000000000000..83f83656301381d4adac9a9b3872db0f759252df
--- /dev/null
+++ b/src/models/entities/auth-session.ts
@@ -0,0 +1,39 @@
+import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
+import { User } from './user';
+import { App } from './app';
+import { id } from '../id';
+
+@Entity()
+export class AuthSession {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the AuthSession.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column('varchar', {
+		length: 128
+	})
+	public token: string;
+
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column(id())
+	public appId: App['id'];
+
+	@ManyToOne(type => App, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public app: App | null;
+}
diff --git a/src/models/entities/blocking.ts b/src/models/entities/blocking.ts
new file mode 100644
index 0000000000000000000000000000000000000000..48487cb086bfccec019ca17860349197abbc308a
--- /dev/null
+++ b/src/models/entities/blocking.ts
@@ -0,0 +1,42 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['blockerId', 'blockeeId'], { unique: true })
+export class Blocking {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Blocking.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The blockee user ID.'
+	})
+	public blockeeId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public blockee: User | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The blocker user ID.'
+	})
+	public blockerId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public blocker: User | null;
+}
diff --git a/src/models/entities/drive-file.ts b/src/models/entities/drive-file.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a8f8c69e56bc251913e5aabaa97214f883206ce9
--- /dev/null
+++ b/src/models/entities/drive-file.ts
@@ -0,0 +1,154 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { DriveFolder } from './drive-folder';
+import { id } from '../id';
+
+@Entity()
+export class DriveFile {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the DriveFile.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The owner ID.'
+	})
+	public userId: User['id'] | null;
+
+	@ManyToOne(type => User, {
+		onDelete: 'SET NULL'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: 'The host of owner. It will be null if the user in local.'
+	})
+	public userHost: string | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 32,
+		comment: 'The MD5 hash of the DriveFile.'
+	})
+	public md5: string;
+
+	@Column('varchar', {
+		length: 256,
+		comment: 'The file name of the DriveFile.'
+	})
+	public name: string;
+
+	@Index()
+	@Column('varchar', {
+		length: 128,
+		comment: 'The content type (MIME) of the DriveFile.'
+	})
+	public type: string;
+
+	@Column('integer', {
+		comment: 'The file size (bytes) of the DriveFile.'
+	})
+	public size: number;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: 'The comment of the DriveFile.'
+	})
+	public comment: string | null;
+
+	@Column('jsonb', {
+		default: {},
+		comment: 'The any properties of the DriveFile. For example, it includes image width/height.'
+	})
+	public properties: Record<string, any>;
+
+	@Column('boolean')
+	public storedInternal: boolean;
+
+	@Column('varchar', {
+		length: 512,
+		comment: 'The URL of the DriveFile.'
+	})
+	public url: string;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: 'The URL of the thumbnail of the DriveFile.'
+	})
+	public thumbnailUrl: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: 'The URL of the webpublic of the DriveFile.'
+	})
+	public webpublicUrl: string | null;
+
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 256,
+	})
+	public accessKey: string;
+
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 256, nullable: true,
+	})
+	public thumbnailAccessKey: string | null;
+
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 256, nullable: true,
+	})
+	public webpublicAccessKey: string | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: 'The URI of the DriveFile. it will be null when the DriveFile is local.'
+	})
+	public uri: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+	})
+	public src: string | null;
+
+	@Index()
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The parent folder ID. If null, it means the DriveFile is located in root.'
+	})
+	public folderId: DriveFolder['id'] | null;
+
+	@ManyToOne(type => DriveFolder, {
+		onDelete: 'SET NULL'
+	})
+	@JoinColumn()
+	public folder: DriveFolder | null;
+
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the DriveFile is NSFW.'
+	})
+	public isSensitive: boolean;
+
+	/**
+	 * 外部の(信頼されていない)URLへの直リンクか否か
+	 */
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the DriveFile is direct link to remote server.'
+	})
+	public isRemote: boolean;
+}
diff --git a/src/models/entities/drive-folder.ts b/src/models/entities/drive-folder.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a80d075855a24861241a4b3f958e7c13db19ce52
--- /dev/null
+++ b/src/models/entities/drive-folder.ts
@@ -0,0 +1,49 @@
+import { JoinColumn, ManyToOne, Entity, PrimaryColumn, Index, Column } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class DriveFolder {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the DriveFolder.'
+	})
+	public createdAt: Date;
+
+	@Column('varchar', {
+		length: 128,
+		comment: 'The name of the DriveFolder.'
+	})
+	public name: string;
+
+	@Index()
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The owner ID.'
+	})
+	public userId: User['id'] | null;
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The parent folder ID. If null, it means the DriveFolder is located in root.'
+	})
+	public parentId: DriveFolder['id'] | null;
+
+	@ManyToOne(type => DriveFolder, {
+		onDelete: 'SET NULL'
+	})
+	@JoinColumn()
+	public parent: DriveFolder | null;
+}
diff --git a/src/models/entities/emoji.ts b/src/models/entities/emoji.ts
new file mode 100644
index 0000000000000000000000000000000000000000..da04da897eb52c4d57aa92a6d4feaaed82a8a7d9
--- /dev/null
+++ b/src/models/entities/emoji.ts
@@ -0,0 +1,46 @@
+import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+@Index(['name', 'host'], { unique: true })
+export class Emoji {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		nullable: true
+	})
+	public updatedAt: Date | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 128
+	})
+	public name: string;
+
+	@Index()
+	@Column('varchar', {
+		length: 128, nullable: true
+	})
+	public host: string | null;
+
+	@Column('varchar', {
+		length: 256,
+	})
+	public url: string;
+
+	@Column('varchar', {
+		length: 256, nullable: true
+	})
+	public uri: string | null;
+
+	@Column('varchar', {
+		length: 64, nullable: true
+	})
+	public type: string | null;
+
+	@Column('varchar', {
+		array: true, length: 128, default: '{}'
+	})
+	public aliases: string[];
+}
diff --git a/src/models/entities/follow-request.ts b/src/models/entities/follow-request.ts
new file mode 100644
index 0000000000000000000000000000000000000000..80a71fe482ec48a6a819c68ae399cdf0003b4958
--- /dev/null
+++ b/src/models/entities/follow-request.ts
@@ -0,0 +1,85 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['followerId', 'followeeId'], { unique: true })
+export class FollowRequest {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the FollowRequest.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The followee user ID.'
+	})
+	public followeeId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public followee: User | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The follower user ID.'
+	})
+	public followerId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public follower: User | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: 'id of Follow Activity.'
+	})
+	public requestId: string | null;
+
+	//#region Denormalized fields
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followerHost: string | null;
+
+	@Column('varchar', {
+		length: 256, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followerInbox: string | null;
+
+	@Column('varchar', {
+		length: 256, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followerSharedInbox: string | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followeeHost: string | null;
+
+	@Column('varchar', {
+		length: 256, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followeeInbox: string | null;
+
+	@Column('varchar', {
+		length: 256, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followeeSharedInbox: string | null;
+	//#endregion
+}
diff --git a/src/models/entities/following.ts b/src/models/entities/following.ts
new file mode 100644
index 0000000000000000000000000000000000000000..963873d112d6d66476291fb5452a365aa5738196
--- /dev/null
+++ b/src/models/entities/following.ts
@@ -0,0 +1,80 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['followerId', 'followeeId'], { unique: true })
+export class Following {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Following.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The followee user ID.'
+	})
+	public followeeId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public followee: User | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The follower user ID.'
+	})
+	public followerId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public follower: User | null;
+
+	//#region Denormalized fields
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followerHost: string | null;
+
+	@Column('varchar', {
+		length: 256, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followerInbox: string | null;
+
+	@Column('varchar', {
+		length: 256, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followerSharedInbox: string | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followeeHost: string | null;
+
+	@Column('varchar', {
+		length: 256, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followeeInbox: string | null;
+
+	@Column('varchar', {
+		length: 256, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followeeSharedInbox: string | null;
+	//#endregion
+}
diff --git a/src/models/entities/games/reversi/game.ts b/src/models/entities/games/reversi/game.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9deacaf5c6dc3f90ef54457cf708c03af9ff52dc
--- /dev/null
+++ b/src/models/entities/games/reversi/game.ts
@@ -0,0 +1,133 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from '../../user';
+import { id } from '../../../id';
+
+@Entity()
+export class ReversiGame {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the ReversiGame.'
+	})
+	public createdAt: Date;
+
+	@Column('timestamp with time zone', {
+		nullable: true,
+		comment: 'The started date of the ReversiGame.'
+	})
+	public startedAt: Date | null;
+
+	@Column(id())
+	public user1Id: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user1: User | null;
+
+	@Column(id())
+	public user2Id: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user2: User | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public user1Accepted: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public user2Accepted: boolean;
+
+	/**
+	 * どちらのプレイヤーが先行(黒)か
+	 * 1 ... user1
+	 * 2 ... user2
+	 */
+	@Column('integer', {
+		nullable: true,
+	})
+	public black: number | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public isStarted: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public isEnded: boolean;
+
+	@Column({
+		...id(),
+		nullable: true
+	})
+	public winnerId: User['id'] | null;
+
+	@Column({
+		...id(),
+		nullable: true
+	})
+	public surrendered: User['id'] | null;
+
+	@Column('jsonb', {
+		default: [],
+	})
+	public logs: {
+		at: Date;
+		color: boolean;
+		pos: number;
+	}[];
+
+	@Column('varchar', {
+		array: true, length: 64,
+	})
+	public map: string[];
+
+	@Column('varchar', {
+		length: 32
+	})
+	public bw: string;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public isLlotheo: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public canPutEverywhere: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public loopedBoard: boolean;
+
+	@Column('jsonb', {
+		nullable: true, default: null,
+	})
+	public form1: any | null;
+
+	@Column('jsonb', {
+		nullable: true, default: null,
+	})
+	public form2: any | null;
+
+	/**
+	 * ログのposを文字列としてすべて連結したもののCRC32値
+	 */
+	@Column('varchar', {
+		length: 32, nullable: true
+	})
+	public crc32: string | null;
+}
diff --git a/src/models/entities/games/reversi/matching.ts b/src/models/entities/games/reversi/matching.ts
new file mode 100644
index 0000000000000000000000000000000000000000..477a29316e95eca708c014e03585aa4eb421ea00
--- /dev/null
+++ b/src/models/entities/games/reversi/matching.ts
@@ -0,0 +1,35 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from '../../user';
+import { id } from '../../../id';
+
+@Entity()
+export class ReversiMatching {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the ReversiMatching.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public parentId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public parent: User | null;
+
+	@Index()
+	@Column(id())
+	public childId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public child: User | null;
+}
diff --git a/src/models/entities/hashtag.ts b/src/models/entities/hashtag.ts
new file mode 100644
index 0000000000000000000000000000000000000000..842cdaa5626ee748b787db671aa540548c08aadc
--- /dev/null
+++ b/src/models/entities/hashtag.ts
@@ -0,0 +1,87 @@
+import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class Hashtag {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 128
+	})
+	public name: string;
+
+	@Column({
+		...id(),
+		array: true,
+	})
+	public mentionedUserIds: User['id'][];
+
+	@Index()
+	@Column('integer', {
+		default: 0
+	})
+	public mentionedUsersCount: number;
+
+	@Column({
+		...id(),
+		array: true,
+	})
+	public mentionedLocalUserIds: User['id'][];
+
+	@Index()
+	@Column('integer', {
+		default: 0
+	})
+	public mentionedLocalUsersCount: number;
+
+	@Column({
+		...id(),
+		array: true,
+	})
+	public mentionedRemoteUserIds: User['id'][];
+
+	@Index()
+	@Column('integer', {
+		default: 0
+	})
+	public mentionedRemoteUsersCount: number;
+
+	@Column({
+		...id(),
+		array: true,
+	})
+	public attachedUserIds: User['id'][];
+
+	@Index()
+	@Column('integer', {
+		default: 0
+	})
+	public attachedUsersCount: number;
+
+	@Column({
+		...id(),
+		array: true,
+	})
+	public attachedLocalUserIds: User['id'][];
+
+	@Index()
+	@Column('integer', {
+		default: 0
+	})
+	public attachedLocalUsersCount: number;
+
+	@Column({
+		...id(),
+		array: true,
+	})
+	public attachedRemoteUserIds: User['id'][];
+
+	@Index()
+	@Column('integer', {
+		default: 0
+	})
+	public attachedRemoteUsersCount: number;
+}
diff --git a/src/models/entities/instance.ts b/src/models/entities/instance.ts
new file mode 100644
index 0000000000000000000000000000000000000000..977054263c9e32ea08d458b1f72c42fabda4391f
--- /dev/null
+++ b/src/models/entities/instance.ts
@@ -0,0 +1,132 @@
+import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class Instance {
+	@PrimaryColumn(id())
+	public id: string;
+
+	/**
+	 * このインスタンスを捕捉した日時
+	 */
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The caught date of the Instance.'
+	})
+	public caughtAt: Date;
+
+	/**
+	 * ホスト
+	 */
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 128,
+		comment: 'The host of the Instance.'
+	})
+	public host: string;
+
+	/**
+	 * インスタンスのシステム (MastodonとかMisskeyとかPleromaとか)
+	 */
+	@Column('varchar', {
+		length: 64, nullable: true,
+		comment: 'The system of the Instance.'
+	})
+	public system: string | null;
+
+	/**
+	 * インスタンスのユーザー数
+	 */
+	@Column('integer', {
+		default: 0,
+		comment: 'The count of the users of the Instance.'
+	})
+	public usersCount: number;
+
+	/**
+	 * インスタンスの投稿数
+	 */
+	@Column('integer', {
+		default: 0,
+		comment: 'The count of the notes of the Instance.'
+	})
+	public notesCount: number;
+
+	/**
+	 * このインスタンスのユーザーからフォローされている、自インスタンスのユーザーの数
+	 */
+	@Column('integer', {
+		default: 0,
+	})
+	public followingCount: number;
+
+	/**
+	 * このインスタンスのユーザーをフォローしている、自インスタンスのユーザーの数
+	 */
+	@Column('integer', {
+		default: 0,
+	})
+	public followersCount: number;
+
+	/**
+	 * ドライブ使用量
+	 */
+	@Column('integer', {
+		default: 0,
+	})
+	public driveUsage: number;
+
+	/**
+	 * ドライブのファイル数
+	 */
+	@Column('integer', {
+		default: 0,
+	})
+	public driveFiles: number;
+
+	/**
+	 * 直近のリクエスト送信日時
+	 */
+	@Column('timestamp with time zone', {
+		nullable: true,
+	})
+	public latestRequestSentAt: Date | null;
+
+	/**
+	 * 直近のリクエスト送信時のHTTPステータスコード
+	 */
+	@Column('integer', {
+		nullable: true,
+	})
+	public latestStatus: number | null;
+
+	/**
+	 * 直近のリクエスト受信日時
+	 */
+	@Column('timestamp with time zone', {
+		nullable: true,
+	})
+	public latestRequestReceivedAt: Date | null;
+
+	/**
+	 * このインスタンスと最後にやり取りした日時
+	 */
+	@Column('timestamp with time zone')
+	public lastCommunicatedAt: Date;
+
+	/**
+	 * このインスタンスと不通かどうか
+	 */
+	@Column('boolean', {
+		default: false
+	})
+	public isNotResponding: boolean;
+
+	/**
+	 * このインスタンスが閉鎖済みとしてマークされているか
+	 */
+	@Column('boolean', {
+		default: false
+	})
+	public isMarkedAsClosed: boolean;
+}
diff --git a/src/models/entities/log.ts b/src/models/entities/log.ts
new file mode 100644
index 0000000000000000000000000000000000000000..99e1e8947ec7f2c36d14e0bdeaeaa72143087c5d
--- /dev/null
+++ b/src/models/entities/log.ts
@@ -0,0 +1,46 @@
+import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class Log {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Log.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column('varchar', {
+		length: 64, array: true, default: '{}'
+	})
+	public domain: string[];
+
+	@Index()
+	@Column('enum', {
+		enum: ['error', 'warning', 'info', 'success', 'debug']
+	})
+	public level: string;
+
+	@Column('varchar', {
+		length: 8
+	})
+	public worker: string;
+
+	@Column('varchar', {
+		length: 128
+	})
+	public machine: string;
+
+	@Column('varchar', {
+		length: 1024
+	})
+	public message: string;
+
+	@Column('jsonb', {
+		default: {}
+	})
+	public data: Record<string, any>;
+}
diff --git a/src/models/entities/messaging-message.ts b/src/models/entities/messaging-message.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d3c3eab3a22f1f411226fc83f35f5c1a05dbe238
--- /dev/null
+++ b/src/models/entities/messaging-message.ts
@@ -0,0 +1,64 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { DriveFile } from './drive-file';
+import { id } from '../id';
+
+@Entity()
+export class MessagingMessage {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the MessagingMessage.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The sender user ID.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The recipient user ID.'
+	})
+	public recipientId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public recipient: User | null;
+
+	@Column('varchar', {
+		length: 4096, nullable: true
+	})
+	public text: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public isRead: boolean;
+
+	@Column({
+		...id(),
+		nullable: true,
+	})
+	public fileId: DriveFile['id'] | null;
+
+	@ManyToOne(type => DriveFile, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public file: DriveFile | null;
+}
diff --git a/src/models/entities/meta.ts b/src/models/entities/meta.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c34f5b690438a56e0d4d9c3ec1056eb4e6838313
--- /dev/null
+++ b/src/models/entities/meta.ts
@@ -0,0 +1,264 @@
+import { Entity, Column, PrimaryColumn } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class Meta {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('varchar', {
+		length: 128, nullable: true
+	})
+	public name: string | null;
+
+	@Column('varchar', {
+		length: 1024, nullable: true
+	})
+	public description: string | null;
+
+	/**
+	 * メンテナの名前
+	 */
+	@Column('varchar', {
+		length: 128, nullable: true
+	})
+	public maintainerName: string | null;
+
+	/**
+	 * メンテナの連絡先
+	 */
+	@Column('varchar', {
+		length: 128, nullable: true
+	})
+	public maintainerEmail: string | null;
+
+	@Column('jsonb', {
+		default: [],
+	})
+	public announcements: Record<string, any>[];
+
+	@Column('boolean', {
+		default: false,
+	})
+	public disableRegistration: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public disableLocalTimeline: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public disableGlobalTimeline: boolean;
+
+	@Column('boolean', {
+		default: true,
+	})
+	public enableEmojiReaction: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public useStarForReactionFallback: boolean;
+
+	@Column('varchar', {
+		length: 64, array: true, default: '{}'
+	})
+	public langs: string[];
+
+	@Column('varchar', {
+		length: 256, array: true, default: '{}'
+	})
+	public hiddenTags: string[];
+
+	@Column('varchar', {
+		length: 256, array: true, default: '{}'
+	})
+	public blockedHosts: string[];
+
+	@Column('varchar', {
+		length: 256,
+		nullable: true,
+		default: '/assets/ai.png'
+	})
+	public mascotImageUrl: string | null;
+
+	@Column('varchar', {
+		length: 256,
+		nullable: true
+	})
+	public bannerUrl: string | null;
+
+	@Column('varchar', {
+		length: 256,
+		nullable: true,
+		default: 'https://ai.misskey.xyz/aiart/yubitun.png'
+	})
+	public errorImageUrl: string | null;
+
+	@Column('varchar', {
+		length: 256,
+		nullable: true
+	})
+	public iconUrl: string | null;
+
+	@Column('boolean', {
+		default: true,
+	})
+	public cacheRemoteFiles: boolean;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public proxyAccount: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public enableRecaptcha: boolean;
+
+	@Column('varchar', {
+		length: 64,
+		nullable: true
+	})
+	public recaptchaSiteKey: string | null;
+
+	@Column('varchar', {
+		length: 64,
+		nullable: true
+	})
+	public recaptchaSecretKey: string | null;
+
+	@Column('integer', {
+		default: 1024,
+		comment: 'Drive capacity of a local user (MB)'
+	})
+	public localDriveCapacityMb: number;
+
+	@Column('integer', {
+		default: 32,
+		comment: 'Drive capacity of a remote user (MB)'
+	})
+	public remoteDriveCapacityMb: number;
+
+	@Column('integer', {
+		default: 500,
+		comment: 'Max allowed note text length in characters'
+	})
+	public maxNoteTextLength: number;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public summalyProxy: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public enableEmail: boolean;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public email: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public smtpSecure: boolean;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public smtpHost: string | null;
+
+	@Column('integer', {
+		nullable: true
+	})
+	public smtpPort: number | null;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public smtpUser: string | null;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public smtpPass: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public enableServiceWorker: boolean;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public swPublicKey: string | null;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public swPrivateKey: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public enableTwitterIntegration: boolean;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public twitterConsumerKey: string | null;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public twitterConsumerSecret: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public enableGithubIntegration: boolean;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public githubClientId: string | null;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public githubClientSecret: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public enableDiscordIntegration: boolean;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public discordClientId: string | null;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public discordClientSecret: string | null;
+}
diff --git a/src/models/entities/muting.ts b/src/models/entities/muting.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0084213bcc06093146aaaf5b676aa6e49f5ee569
--- /dev/null
+++ b/src/models/entities/muting.ts
@@ -0,0 +1,42 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['muterId', 'muteeId'], { unique: true })
+export class Muting {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Muting.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The mutee user ID.'
+	})
+	public muteeId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public mutee: User | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The muter user ID.'
+	})
+	public muterId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public muter: User | null;
+}
diff --git a/src/models/entities/note-favorite.ts b/src/models/entities/note-favorite.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0713c3ae56a18436d54fbb9e24d18b430bf6c0ff
--- /dev/null
+++ b/src/models/entities/note-favorite.ts
@@ -0,0 +1,35 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { Note } from './note';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class NoteFavorite {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the NoteFavorite.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column(id())
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+}
diff --git a/src/models/entities/note-reaction.ts b/src/models/entities/note-reaction.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1ce5d841fb7caa7d5bcd9b7bd213258a1bb44e38
--- /dev/null
+++ b/src/models/entities/note-reaction.ts
@@ -0,0 +1,42 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Note } from './note';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class NoteReaction {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the NoteReaction.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column(id())
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+
+	@Column('varchar', {
+		length: 32
+	})
+	public reaction: string;
+}
diff --git a/src/models/entities/note-unread.ts b/src/models/entities/note-unread.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2d18728256780125ee8cdfc08b84ab5514bee5f8
--- /dev/null
+++ b/src/models/entities/note-unread.ts
@@ -0,0 +1,43 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Note } from './note';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class NoteUnread {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column(id())
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+
+	@Column({
+		...id(),
+		comment: '[Denormalized]'
+	})
+	public noteUserId: User['id'];
+
+	/**
+	 * ダイレクト投稿か
+	 */
+	@Column('boolean')
+	public isSpecified: boolean;
+}
diff --git a/src/models/entities/note-watching.ts b/src/models/entities/note-watching.ts
new file mode 100644
index 0000000000000000000000000000000000000000..741a1c0c8bcd2e1108a9a1d4da96ae078b05a476
--- /dev/null
+++ b/src/models/entities/note-watching.ts
@@ -0,0 +1,52 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Note } from './note';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class NoteWatching {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the NoteWatching.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The watcher ID.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The target Note ID.'
+	})
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+
+	//#region Denormalized fields
+	@Index()
+	@Column({
+		...id(),
+		comment: '[Denormalized]'
+	})
+	public noteUserId: Note['userId'];
+	//#endregion
+}
diff --git a/src/models/entities/note.ts b/src/models/entities/note.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0bcb9b4a447f8ab0c4f72c0ad7addde74fea91d7
--- /dev/null
+++ b/src/models/entities/note.ts
@@ -0,0 +1,236 @@
+import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { App } from './app';
+import { DriveFile } from './drive-file';
+import { id } from '../id';
+
+@Entity()
+export class Note {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Note.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		nullable: true,
+		comment: 'The updated date of the Note.'
+	})
+	public updatedAt: Date | null;
+
+	@Index()
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The ID of reply target.'
+	})
+	public replyId: Note['id'] | null;
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public reply: Note | null;
+
+	@Index()
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The ID of renote target.'
+	})
+	public renoteId: Note['id'] | null;
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public renote: Note | null;
+
+	@Column({
+		type: 'text', nullable: true
+	})
+	public text: string | null;
+
+	@Column('varchar', {
+		length: 256, nullable: true
+	})
+	public name: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true
+	})
+	public cw: string | null;
+
+	@Column({
+		...id(),
+		nullable: true
+	})
+	public appId: App['id'] | null;
+
+	@ManyToOne(type => App, {
+		onDelete: 'SET NULL'
+	})
+	@JoinColumn()
+	public app: App | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The ID of author.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('boolean', {
+		default: false
+	})
+	public viaMobile: boolean;
+
+	@Column('boolean', {
+		default: false
+	})
+	public localOnly: boolean;
+
+	@Column('integer', {
+		default: 0
+	})
+	public renoteCount: number;
+
+	@Column('integer', {
+		default: 0
+	})
+	public repliesCount: number;
+
+	@Column('jsonb', {
+		default: {}
+	})
+	public reactions: Record<string, number>;
+
+	/**
+	 * public ... 公開
+	 * home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す
+	 * followers ... フォロワーのみ
+	 * specified ... visibleUserIds で指定したユーザーのみ
+	 */
+	@Column('enum', { enum: ['public', 'home', 'followers', 'specified'] })
+	public visibility: 'public' | 'home' | 'followers' | 'specified';
+
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 256, nullable: true,
+		comment: 'The URI of a note. it will be null when the note is local.'
+	})
+	public uri: string | null;
+
+	@Column('integer', {
+		default: 0
+	})
+	public score: number;
+
+	@Column({
+		...id(),
+		array: true, default: '{}'
+	})
+	public fileIds: DriveFile['id'][];
+
+	@Column('varchar', {
+		length: 256, array: true, default: '{}'
+	})
+	public attachedFileTypes: string[];
+
+	@Index()
+	@Column({
+		...id(),
+		array: true, default: '{}'
+	})
+	public visibleUserIds: User['id'][];
+
+	@Index()
+	@Column({
+		...id(),
+		array: true, default: '{}'
+	})
+	public mentions: User['id'][];
+
+	@Column('text', {
+		default: '[]'
+	})
+	public mentionedRemoteUsers: string;
+
+	@Column('varchar', {
+		length: 128, array: true, default: '{}'
+	})
+	public emojis: string[];
+
+	@Index()
+	@Column('varchar', {
+		length: 128, array: true, default: '{}'
+	})
+	public tags: string[];
+
+	@Column('boolean', {
+		default: false
+	})
+	public hasPoll: boolean;
+
+	@Column('jsonb', {
+		nullable: true, default: {}
+	})
+	public geo: any | null;
+
+	//#region Denormalized fields
+	@Index()
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public userHost: string | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public userInbox: string | null;
+
+	@Column({
+		...id(),
+		nullable: true,
+		comment: '[Denormalized]'
+	})
+	public replyUserId: User['id'] | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public replyUserHost: string | null;
+
+	@Column({
+		...id(),
+		nullable: true,
+		comment: '[Denormalized]'
+	})
+	public renoteUserId: User['id'] | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public renoteUserHost: string | null;
+	//#endregion
+}
+
+export type IMentionedRemoteUsers = {
+	uri: string;
+	username: string;
+	host: string;
+}[];
diff --git a/src/models/entities/notification.ts b/src/models/entities/notification.ts
new file mode 100644
index 0000000000000000000000000000000000000000..627a57bececeedd972c6de0c9405ca435ce386e6
--- /dev/null
+++ b/src/models/entities/notification.ts
@@ -0,0 +1,94 @@
+import { Entity, Index, JoinColumn, ManyToOne, Column, PrimaryColumn } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+import { Note } from './note';
+
+@Entity()
+export class Notification {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Notification.'
+	})
+	public createdAt: Date;
+
+	/**
+	 * 通知の受信者
+	 */
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The ID of recipient user of the Notification.'
+	})
+	public notifieeId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public notifiee: User | null;
+
+	/**
+	 * 通知の送信者(initiator)
+	 */
+	@Column({
+		...id(),
+		comment: 'The ID of sender user of the Notification.'
+	})
+	public notifierId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public notifier: User | null;
+
+	/**
+	 * 通知の種類。
+	 * follow - フォローされた
+	 * mention - 投稿で自分が言及された
+	 * reply - (自分または自分がWatchしている)投稿が返信された
+	 * renote - (自分または自分がWatchしている)投稿がRenoteされた
+	 * quote - (自分または自分がWatchしている)投稿が引用Renoteされた
+	 * reaction - (自分または自分がWatchしている)投稿にリアクションされた
+	 * pollVote - (自分または自分がWatchしている)投稿の投票に投票された
+	 */
+	@Column('varchar', {
+		length: 32,
+		comment: 'The type of the Notification.'
+	})
+	public type: string;
+
+	/**
+	 * 通知が読まれたかどうか
+	 */
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the Notification is read.'
+	})
+	public isRead: boolean;
+
+	@Column({
+		...id(),
+		nullable: true
+	})
+	public noteId: Note['id'] | null;
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true
+	})
+	public reaction: string;
+
+	@Column('integer', {
+		nullable: true
+	})
+	public choice: number;
+}
diff --git a/src/models/entities/poll-vote.ts b/src/models/entities/poll-vote.ts
new file mode 100644
index 0000000000000000000000000000000000000000..709376f909cee1fded3c11e43c52d54e2799f3ac
--- /dev/null
+++ b/src/models/entities/poll-vote.ts
@@ -0,0 +1,40 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Note } from './note';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId', 'choice'], { unique: true })
+export class PollVote {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the PollVote.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column(id())
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+
+	@Column('integer')
+	public choice: number;
+}
diff --git a/src/models/entities/poll.ts b/src/models/entities/poll.ts
new file mode 100644
index 0000000000000000000000000000000000000000..204f102f5103f14b80a284f0d4290f42f476100a
--- /dev/null
+++ b/src/models/entities/poll.ts
@@ -0,0 +1,67 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
+import { id } from '../id';
+import { Note } from './note';
+import { User } from './user';
+
+@Entity()
+export class Poll {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index({ unique: true })
+	@Column(id())
+	public noteId: Note['id'];
+
+	@OneToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+
+	@Column('timestamp with time zone', {
+		nullable: true
+	})
+	public expiresAt: Date | null;
+
+	@Column('boolean')
+	public multiple: boolean;
+
+	@Column('varchar', {
+		length: 128, array: true, default: '{}'
+	})
+	public choices: string[];
+
+	@Column('integer', {
+		array: true,
+	})
+	public votes: number[];
+
+	//#region Denormalized fields
+	@Column('enum', {
+		enum: ['public', 'home', 'followers', 'specified'],
+		comment: '[Denormalized]'
+	})
+	public noteVisibility: 'public' | 'home' | 'followers' | 'specified';
+
+	@Index()
+	@Column({
+		...id(),
+		comment: '[Denormalized]'
+	})
+	public userId: User['id'];
+
+	@Index()
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public userHost: string | null;
+	//#endregion
+}
+
+export type IPoll = {
+	choices: string[];
+	votes?: number[];
+	multiple: boolean;
+	expiresAt: Date;
+};
diff --git a/src/models/entities/registration-tickets.ts b/src/models/entities/registration-tickets.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d962f78a78c9ef9f17c068e3ffe1d2dbd2947ff5
--- /dev/null
+++ b/src/models/entities/registration-tickets.ts
@@ -0,0 +1,17 @@
+import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class RegistrationTicket {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone')
+	public createdAt: Date;
+
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 64,
+	})
+	public code: string;
+}
diff --git a/src/models/entities/signin.ts b/src/models/entities/signin.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7e047084b10a2b8789502196ae6ef08d4434740f
--- /dev/null
+++ b/src/models/entities/signin.ts
@@ -0,0 +1,35 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class Signin {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Signin.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('varchar', {
+		length: 128,
+	})
+	public ip: string;
+
+	@Column('jsonb')
+	public headers: Record<string, any>;
+
+	@Column('boolean')
+	public success: boolean;
+}
diff --git a/src/models/entities/sw-subscription.ts b/src/models/entities/sw-subscription.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f0f2a69f1b0fa14dd574d0cda0a031d42922c2f0
--- /dev/null
+++ b/src/models/entities/sw-subscription.ts
@@ -0,0 +1,37 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class SwSubscription {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone')
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('varchar', {
+		length: 256,
+	})
+	public endpoint: string;
+
+	@Column('varchar', {
+		length: 256,
+	})
+	public auth: string;
+
+	@Column('varchar', {
+		length: 128,
+	})
+	public publickey: string;
+}
diff --git a/src/models/entities/user-keypair.ts b/src/models/entities/user-keypair.ts
new file mode 100644
index 0000000000000000000000000000000000000000..06b98d2536ddc19ebe91ca8d621ead4e32a03202
--- /dev/null
+++ b/src/models/entities/user-keypair.ts
@@ -0,0 +1,24 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class UserKeypair {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index({ unique: true })
+	@Column(id())
+	public userId: User['id'];
+
+	@OneToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('varchar', {
+		length: 4096,
+	})
+	public keyPem: string;
+}
diff --git a/src/models/entities/user-list-joining.ts b/src/models/entities/user-list-joining.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8af4efb6a73937fc45ff88462c073ef68e022a74
--- /dev/null
+++ b/src/models/entities/user-list-joining.ts
@@ -0,0 +1,41 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { UserList } from './user-list';
+import { id } from '../id';
+
+@Entity()
+export class UserListJoining {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the UserListJoining.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The user ID.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The list ID.'
+	})
+	public userListId: UserList['id'];
+
+	@ManyToOne(type => UserList, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public userList: UserList | null;
+}
diff --git a/src/models/entities/user-list.ts b/src/models/entities/user-list.ts
new file mode 100644
index 0000000000000000000000000000000000000000..35a83ef8c318b656b6caf9a15dc545261068cf65
--- /dev/null
+++ b/src/models/entities/user-list.ts
@@ -0,0 +1,33 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class UserList {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the UserList.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The owner ID.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('varchar', {
+		length: 128,
+		comment: 'The name of the UserList.'
+	})
+	public name: string;
+}
diff --git a/src/models/entities/user-note-pinings.ts b/src/models/entities/user-note-pinings.ts
new file mode 100644
index 0000000000000000000000000000000000000000..04a6f8f6452fbd126fc6f14104fd02320ce33c27
--- /dev/null
+++ b/src/models/entities/user-note-pinings.ts
@@ -0,0 +1,35 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { Note } from './note';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class UserNotePining {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the UserNotePinings.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column(id())
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+}
diff --git a/src/models/entities/user-publickey.ts b/src/models/entities/user-publickey.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6c019f331327c0166835a033dd8b4ca811d43a26
--- /dev/null
+++ b/src/models/entities/user-publickey.ts
@@ -0,0 +1,30 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class UserPublickey {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index({ unique: true })
+	@Column(id())
+	public userId: User['id'];
+
+	@OneToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 256,
+	})
+	public keyId: string;
+
+	@Column('varchar', {
+		length: 4096,
+	})
+	public keyPem: string;
+}
diff --git a/src/models/entities/user-service-linking.ts b/src/models/entities/user-service-linking.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3d99554e1e5bd4823300a182e4c47f946b97279d
--- /dev/null
+++ b/src/models/entities/user-service-linking.ts
@@ -0,0 +1,108 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class UserServiceLinking {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index({ unique: true })
+	@Column(id())
+	public userId: User['id'];
+
+	@OneToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public twitter: boolean;
+
+	@Column('varchar', {
+		length: 64, nullable: true, default: null,
+	})
+	public twitterAccessToken: string | null;
+
+	@Column('varchar', {
+		length: 64, nullable: true, default: null,
+	})
+	public twitterAccessTokenSecret: string | null;
+
+	@Column('varchar', {
+		length: 64, nullable: true, default: null,
+	})
+	public twitterUserId: string | null;
+
+	@Column('varchar', {
+		length: 64, nullable: true, default: null,
+	})
+	public twitterScreenName: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public github: boolean;
+
+	@Column('varchar', {
+		length: 64, nullable: true, default: null,
+	})
+	public githubAccessToken: string | null;
+
+	@Column('integer', {
+		nullable: true, default: null,
+	})
+	public githubId: number | null;
+
+	@Column('varchar', {
+		length: 64, nullable: true, default: null,
+	})
+	public githubLogin: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public discord: boolean;
+
+	@Column('varchar', {
+		length: 64, nullable: true, default: null,
+	})
+	public discordAccessToken: string | null;
+
+	@Column('varchar', {
+		length: 64, nullable: true, default: null,
+	})
+	public discordRefreshToken: string | null;
+
+	@Column('integer', {
+		nullable: true, default: null,
+	})
+	public discordExpiresDate: number | null;
+
+	@Column('varchar', {
+		length: 64, nullable: true, default: null,
+	})
+	public discordId: string | null;
+
+	@Column('varchar', {
+		length: 64, nullable: true, default: null,
+	})
+	public discordUsername: string | null;
+
+	@Column('varchar', {
+		length: 64, nullable: true, default: null,
+	})
+	public discordDiscriminator: string | null;
+
+	//#region Denormalized fields
+	@Index()
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public userHost: string | null;
+	//#endregion
+}
diff --git a/src/models/entities/user.ts b/src/models/entities/user.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1ef98cadc2a6c8a9f3285a294e3de11c3d977cc0
--- /dev/null
+++ b/src/models/entities/user.ts
@@ -0,0 +1,297 @@
+import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
+import { DriveFile } from './drive-file';
+import { id } from '../id';
+
+@Entity()
+@Index(['usernameLower', 'host'], { unique: true })
+export class User {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the User.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		nullable: true,
+		comment: 'The updated date of the User.'
+	})
+	public updatedAt: Date | null;
+
+	@Column('timestamp with time zone', {
+		nullable: true
+	})
+	public lastFetchedAt: Date | null;
+
+	@Column('varchar', {
+		length: 128,
+		comment: 'The username of the User.'
+	})
+	public username: string;
+
+	@Index()
+	@Column('varchar', {
+		length: 128, select: false,
+		comment: 'The username (lowercased) of the User.'
+	})
+	public usernameLower: string;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: 'The name of the User.'
+	})
+	public name: string | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: 'The location of the User.'
+	})
+	public location: string | null;
+
+	@Column('char', {
+		length: 10, nullable: true,
+		comment: 'The birthday (YYYY-MM-DD) of the User.'
+	})
+	public birthday: string | null;
+
+	@Column('integer', {
+		default: 0,
+		comment: 'The count of followers.'
+	})
+	public followersCount: number;
+
+	@Column('integer', {
+		default: 0,
+		comment: 'The count of following.'
+	})
+	public followingCount: number;
+
+	@Column('integer', {
+		default: 0,
+		comment: 'The count of notes.'
+	})
+	public notesCount: number;
+
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The ID of avatar DriveFile.'
+	})
+	public avatarId: DriveFile['id'] | null;
+
+	@OneToOne(type => DriveFile, {
+		onDelete: 'SET NULL'
+	})
+	@JoinColumn()
+	public avatar: DriveFile | null;
+
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The ID of banner DriveFile.'
+	})
+	public bannerId: DriveFile['id'] | null;
+
+	@OneToOne(type => DriveFile, {
+		onDelete: 'SET NULL'
+	})
+	@JoinColumn()
+	public banner: DriveFile | null;
+
+	@Column('varchar', {
+		length: 1024, nullable: true,
+		comment: 'The description (bio) of the User.'
+	})
+	public description: string | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 128, array: true, default: '{}'
+	})
+	public tags: string[];
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: 'The email address of the User.'
+	})
+	public email: string | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+	})
+	public emailVerifyCode: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public emailVerified: boolean;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+	})
+	public twoFactorTempSecret: string | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+	})
+	public twoFactorSecret: string | null;
+
+	@Column('varchar', {
+		length: 256, nullable: true,
+	})
+	public avatarUrl: string | null;
+
+	@Column('varchar', {
+		length: 256, nullable: true,
+	})
+	public bannerUrl: string | null;
+
+	@Column('varchar', {
+		length: 32, nullable: true,
+	})
+	public avatarColor: string | null;
+
+	@Column('varchar', {
+		length: 32, nullable: true,
+	})
+	public bannerColor: string | null;
+
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the User is suspended.'
+	})
+	public isSuspended: boolean;
+
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the User is silenced.'
+	})
+	public isSilenced: boolean;
+
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the User is locked.'
+	})
+	public isLocked: boolean;
+
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the User is a bot.'
+	})
+	public isBot: boolean;
+
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the User is a cat.'
+	})
+	public isCat: boolean;
+
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the User is the admin.'
+	})
+	public isAdmin: boolean;
+
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the User is a moderator.'
+	})
+	public isModerator: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public isVerified: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public twoFactorEnabled: boolean;
+
+	@Column('varchar', {
+		length: 128, array: true, default: '{}'
+	})
+	public emojis: string[];
+
+	@Index()
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: 'The host of the User. It will be null if the origin of the user is local.'
+	})
+	public host: string | null;
+
+	@Column('varchar', {
+		length: 256, nullable: true,
+		comment: 'The inbox of the User. It will be null if the origin of the user is local.'
+	})
+	public inbox: string | null;
+
+	@Column('varchar', {
+		length: 256, nullable: true,
+		comment: 'The sharedInbox of the User. It will be null if the origin of the user is local.'
+	})
+	public sharedInbox: string | null;
+
+	@Column('varchar', {
+		length: 256, nullable: true,
+		comment: 'The featured of the User. It will be null if the origin of the user is local.'
+	})
+	public featured: string | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 256, nullable: true,
+		comment: 'The URI of the User. It will be null if the origin of the user is local.'
+	})
+	public uri: string | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: 'The password hash of the User. It will be null if the origin of the user is local.'
+	})
+	public password: string | null;
+
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 32, nullable: true, unique: true,
+		comment: 'The native access token of the User. It will be null if the origin of the user is local.'
+	})
+	public token: string | null;
+
+	@Column('jsonb', {
+		default: {},
+		comment: 'The client-specific data of the User.'
+	})
+	public clientData: Record<string, any>;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public autoWatch: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public autoAcceptFollowed: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public alwaysMarkNsfw: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public carefulBot: boolean;
+}
+
+export interface ILocalUser extends User {
+	host: null;
+}
+
+export interface IRemoteUser extends User {
+	host: string;
+}
diff --git a/src/models/favorite.ts b/src/models/favorite.ts
deleted file mode 100644
index 2008edbfaf47625ccd38200fdd1ed272d07e8cd1..0000000000000000000000000000000000000000
--- a/src/models/favorite.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { pack as packNote } from './note';
-import { dbLogger } from '../db/logger';
-
-const Favorite = db.get<IFavorite>('favorites');
-Favorite.createIndex('userId');
-Favorite.createIndex(['userId', 'noteId'], { unique: true });
-export default Favorite;
-
-export type IFavorite = {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	userId: mongo.ObjectID;
-	noteId: mongo.ObjectID;
-};
-
-export const packMany = (
-	favorites: any[],
-	me: any
-) => {
-	return Promise.all(favorites.map(f => pack(f, me)));
-};
-
-/**
- * Pack a favorite for API response
- */
-export const pack = (
-	favorite: any,
-	me: any
-) => new Promise<any>(async (resolve, reject) => {
-	let _favorite: any;
-
-	// Populate the favorite if 'favorite' is ID
-	if (isObjectId(favorite)) {
-		_favorite = await Favorite.findOne({
-			_id: favorite
-		});
-	} else if (typeof favorite === 'string') {
-		_favorite = await Favorite.findOne({
-			_id: new mongo.ObjectID(favorite)
-		});
-	} else {
-		_favorite = deepcopy(favorite);
-	}
-
-	// Rename _id to id
-	_favorite.id = _favorite._id;
-	delete _favorite._id;
-
-	// Populate note
-	_favorite.note = await packNote(_favorite.noteId, me, {
-		detail: true
-	});
-
-	// (データベースの不具合などで)投稿が見つからなかったら
-	if (_favorite.note == null) {
-		dbLogger.warn(`[DAMAGED DB] (missing) pkg: favorite -> note :: ${_favorite.id} (note ${_favorite.noteId})`);
-		return resolve(null);
-	}
-
-	resolve(_favorite);
-});
diff --git a/src/models/follow-request.ts b/src/models/follow-request.ts
deleted file mode 100644
index 4f75c63a32285da131504c57b665441df5218680..0000000000000000000000000000000000000000
--- a/src/models/follow-request.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { pack as packUser } from './user';
-
-const FollowRequest = db.get<IFollowRequest>('followRequests');
-FollowRequest.createIndex('followerId');
-FollowRequest.createIndex('followeeId');
-FollowRequest.createIndex(['followerId', 'followeeId'], { unique: true });
-export default FollowRequest;
-
-export type IFollowRequest = {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	followeeId: mongo.ObjectID;
-	followerId: mongo.ObjectID;
-	requestId?: string;	// id of Follow Activity
-
-	// 非正規化
-	_followee: {
-		host: string;
-		inbox?: string;
-		sharedInbox?: string;
-	},
-	_follower: {
-		host: string;
-		inbox?: string;
-		sharedInbox?: string;
-	}
-};
-
-/**
- * Pack a request for API response
- */
-export const pack = (
-	request: any,
-	me?: any
-) => new Promise<any>(async (resolve, reject) => {
-	let _request: any;
-
-	// Populate the request if 'request' is ID
-	if (isObjectId(request)) {
-		_request = await FollowRequest.findOne({
-			_id: request
-		});
-	} else if (typeof request === 'string') {
-		_request = await FollowRequest.findOne({
-			_id: new mongo.ObjectID(request)
-		});
-	} else {
-		_request = deepcopy(request);
-	}
-
-	// Rename _id to id
-	_request.id = _request._id;
-	delete _request._id;
-
-	// Populate follower
-	_request.follower = await packUser(_request.followerId, me);
-
-	// Populate followee
-	_request.followee = await packUser(_request.followeeId, me);
-
-	resolve(_request);
-});
diff --git a/src/models/following.ts b/src/models/following.ts
deleted file mode 100644
index 12cc27211bc87d267021bd55c08574e818c37a0b..0000000000000000000000000000000000000000
--- a/src/models/following.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const Following = db.get<IFollowing>('following');
-Following.createIndex('followerId');
-Following.createIndex('followeeId');
-Following.createIndex(['followerId', 'followeeId'], { unique: true });
-export default Following;
-
-export type IFollowing = {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	followeeId: mongo.ObjectID;
-	followerId: mongo.ObjectID;
-
-	// 非正規化
-	_followee: {
-		host: string;
-		inbox?: string;
-		sharedInbox?: string;
-	},
-	_follower: {
-		host: string;
-		inbox?: string;
-		sharedInbox?: string;
-	}
-};
diff --git a/src/models/games/reversi/game.ts b/src/models/games/reversi/game.ts
deleted file mode 100644
index 57c493cff50687b9307f2b9a2faaf979dfa95955..0000000000000000000000000000000000000000
--- a/src/models/games/reversi/game.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../../../db/mongodb';
-import isObjectId from '../../../misc/is-objectid';
-import { IUser, pack as packUser } from '../../user';
-
-const ReversiGame = db.get<IReversiGame>('reversiGames');
-export default ReversiGame;
-
-export interface IReversiGame {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	startedAt: Date;
-	user1Id: mongo.ObjectID;
-	user2Id: mongo.ObjectID;
-	user1Accepted: boolean;
-	user2Accepted: boolean;
-
-	/**
-	 * どちらのプレイヤーが先行(黒)か
-	 * 1 ... user1
-	 * 2 ... user2
-	 */
-	black: number;
-
-	isStarted: boolean;
-	isEnded: boolean;
-	winnerId: mongo.ObjectID;
-	surrendered: mongo.ObjectID;
-	logs: {
-		at: Date;
-		color: boolean;
-		pos: number;
-	}[];
-	settings: {
-		map: string[];
-		bw: string | number;
-		isLlotheo: boolean;
-		canPutEverywhere: boolean;
-		loopedBoard: boolean;
-	};
-	form1: any;
-	form2: any;
-
-	// ログのposを文字列としてすべて連結したもののCRC32値
-	crc32: string;
-}
-
-/**
- * Pack an reversi game for API response
- */
-export const pack = (
-	game: any,
-	me?: string | mongo.ObjectID | IUser,
-	options?: {
-		detail?: boolean
-	}
-) => new Promise<any>(async (resolve, reject) => {
-	const opts = Object.assign({
-		detail: true
-	}, options);
-
-	let _game: any;
-
-	// Populate the game if 'game' is ID
-	if (isObjectId(game)) {
-		_game = await ReversiGame.findOne({
-			_id: game
-		});
-	} else if (typeof game === 'string') {
-		_game = await ReversiGame.findOne({
-			_id: new mongo.ObjectID(game)
-		});
-	} else {
-		_game = deepcopy(game);
-	}
-
-	// Me
-	const meId: mongo.ObjectID = me
-		? isObjectId(me)
-			? me as mongo.ObjectID
-			: typeof me === 'string'
-				? new mongo.ObjectID(me)
-				: (me as IUser)._id
-		: null;
-
-	// Rename _id to id
-	_game.id = _game._id;
-	delete _game._id;
-
-	if (opts.detail === false) {
-		delete _game.logs;
-		delete _game.settings.map;
-	} else {
-		// 互換性のため
-		if (_game.settings.map.hasOwnProperty('size')) {
-			_game.settings.map = _game.settings.map.data.match(new RegExp(`.{1,${_game.settings.map.size}}`, 'g'));
-		}
-	}
-
-	// Populate user
-	_game.user1 = await packUser(_game.user1Id, meId);
-	_game.user2 = await packUser(_game.user2Id, meId);
-	if (_game.winnerId) {
-		_game.winner = await packUser(_game.winnerId, meId);
-	} else {
-		_game.winner = null;
-	}
-
-	resolve(_game);
-});
diff --git a/src/models/games/reversi/matching.ts b/src/models/games/reversi/matching.ts
deleted file mode 100644
index ba2ac1bc051e55bd359a9f2c484cf700b1836894..0000000000000000000000000000000000000000
--- a/src/models/games/reversi/matching.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../../../db/mongodb';
-import isObjectId from '../../../misc/is-objectid';
-import { IUser, pack as packUser } from '../../user';
-
-const Matching = db.get<IMatching>('reversiMatchings');
-export default Matching;
-
-export interface IMatching {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	parentId: mongo.ObjectID;
-	childId: mongo.ObjectID;
-}
-
-/**
- * Pack an reversi matching for API response
- */
-export const pack = (
-	matching: any,
-	me?: string | mongo.ObjectID | IUser
-) => new Promise<any>(async (resolve, reject) => {
-
-	// Me
-	const meId: mongo.ObjectID = me
-		? isObjectId(me)
-			? me as mongo.ObjectID
-			: typeof me === 'string'
-				? new mongo.ObjectID(me)
-				: (me as IUser)._id
-		: null;
-
-	const _matching = deepcopy(matching);
-
-	// Rename _id to id
-	_matching.id = _matching._id;
-	delete _matching._id;
-
-	// Populate user
-	_matching.parent = await packUser(_matching.parentId, meId);
-	_matching.child = await packUser(_matching.childId, meId);
-
-	resolve(_matching);
-});
diff --git a/src/models/hashtag.ts b/src/models/hashtag.ts
deleted file mode 100644
index c1de42086e168bbcd109c17a7afa2b0c2b482531..0000000000000000000000000000000000000000
--- a/src/models/hashtag.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const Hashtag = db.get<IHashtags>('hashtags');
-Hashtag.createIndex('tag', { unique: true });
-Hashtag.createIndex('mentionedUsersCount');
-Hashtag.createIndex('mentionedLocalUsersCount');
-Hashtag.createIndex('mentionedRemoteUsersCount');
-Hashtag.createIndex('attachedUsersCount');
-Hashtag.createIndex('attachedLocalUsersCount');
-Hashtag.createIndex('attachedRemoteUsersCount');
-export default Hashtag;
-
-// 後方互換性のため
-Hashtag.findOne({ attachedUserIds: { $exists: false }}).then(h => {
-	if (h != null) {
-		Hashtag.update({}, {
-			$rename: {
-				mentionedUserIdsCount: 'mentionedUsersCount'
-			},
-			$set: {
-				mentionedLocalUserIds: [],
-				mentionedLocalUsersCount: 0,
-				attachedUserIds: [],
-				attachedUsersCount: 0,
-				attachedLocalUserIds: [],
-				attachedLocalUsersCount: 0,
-			}
-		}, {
-			multi: true
-		});
-	}
-});
-Hashtag.findOne({ attachedRemoteUserIds: { $exists: false }}).then(h => {
-	if (h != null) {
-		Hashtag.update({}, {
-			$set: {
-				mentionedRemoteUserIds: [],
-				mentionedRemoteUsersCount: 0,
-				attachedRemoteUserIds: [],
-				attachedRemoteUsersCount: 0,
-			}
-		}, {
-			multi: true
-		});
-	}
-});
-
-export interface IHashtags {
-	tag: string;
-	mentionedUserIds: mongo.ObjectID[];
-	mentionedUsersCount: number;
-	mentionedLocalUserIds: mongo.ObjectID[];
-	mentionedLocalUsersCount: number;
-	mentionedRemoteUserIds: mongo.ObjectID[];
-	mentionedRemoteUsersCount: number;
-	attachedUserIds: mongo.ObjectID[];
-	attachedUsersCount: number;
-	attachedLocalUserIds: mongo.ObjectID[];
-	attachedLocalUsersCount: number;
-	attachedRemoteUserIds: mongo.ObjectID[];
-	attachedRemoteUsersCount: number;
-}
diff --git a/src/models/id.ts b/src/models/id.ts
new file mode 100644
index 0000000000000000000000000000000000000000..be2cccfe3b415e37cf1df00bf9e7d4b10275155a
--- /dev/null
+++ b/src/models/id.ts
@@ -0,0 +1,4 @@
+export const id = () => ({
+	type: 'varchar' as 'varchar',
+	length: 32
+});
diff --git a/src/models/index.ts b/src/models/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f88bb8d6366137396329962ab328154ce0a811d3
--- /dev/null
+++ b/src/models/index.ts
@@ -0,0 +1,74 @@
+import { getRepository, getCustomRepository } from 'typeorm';
+import { Instance } from './entities/instance';
+import { Emoji } from './entities/emoji';
+import { Poll } from './entities/poll';
+import { PollVote } from './entities/poll-vote';
+import { Meta } from './entities/meta';
+import { SwSubscription } from './entities/sw-subscription';
+import { NoteWatching } from './entities/note-watching';
+import { UserListJoining } from './entities/user-list-joining';
+import { Hashtag } from './entities/hashtag';
+import { NoteUnread } from './entities/note-unread';
+import { RegistrationTicket } from './entities/registration-tickets';
+import { UserRepository } from './repositories/user';
+import { NoteRepository } from './repositories/note';
+import { DriveFileRepository } from './repositories/drive-file';
+import { DriveFolderRepository } from './repositories/drive-folder';
+import { Log } from './entities/log';
+import { AccessToken } from './entities/access-token';
+import { UserNotePining } from './entities/user-note-pinings';
+import { SigninRepository } from './repositories/signin';
+import { MessagingMessageRepository } from './repositories/messaging-message';
+import { ReversiGameRepository } from './repositories/games/reversi/game';
+import { UserListRepository } from './repositories/user-list';
+import { FollowRequestRepository } from './repositories/follow-request';
+import { MutingRepository } from './repositories/muting';
+import { BlockingRepository } from './repositories/blocking';
+import { NoteReactionRepository } from './repositories/note-reaction';
+import { UserServiceLinking } from './entities/user-service-linking';
+import { NotificationRepository } from './repositories/notification';
+import { NoteFavoriteRepository } from './repositories/note-favorite';
+import { ReversiMatchingRepository } from './repositories/games/reversi/matching';
+import { UserPublickey } from './entities/user-publickey';
+import { UserKeypair } from './entities/user-keypair';
+import { AppRepository } from './repositories/app';
+import { FollowingRepository } from './repositories/following';
+import { AbuseUserReportRepository } from './repositories/abuse-user-report';
+import { AuthSessionRepository } from './repositories/auth-session';
+
+export const Apps = getCustomRepository(AppRepository);
+export const Notes = getCustomRepository(NoteRepository);
+export const NoteFavorites = getCustomRepository(NoteFavoriteRepository);
+export const NoteWatchings = getRepository(NoteWatching);
+export const NoteReactions = getCustomRepository(NoteReactionRepository);
+export const NoteUnreads = getRepository(NoteUnread);
+export const Polls = getRepository(Poll);
+export const PollVotes = getRepository(PollVote);
+export const Users = getCustomRepository(UserRepository);
+export const UserKeypairs = getRepository(UserKeypair);
+export const UserPublickeys = getRepository(UserPublickey);
+export const UserLists = getCustomRepository(UserListRepository);
+export const UserListJoinings = getRepository(UserListJoining);
+export const UserNotePinings = getRepository(UserNotePining);
+export const UserServiceLinkings = getRepository(UserServiceLinking);
+export const Followings = getCustomRepository(FollowingRepository);
+export const FollowRequests = getCustomRepository(FollowRequestRepository);
+export const Instances = getRepository(Instance);
+export const Emojis = getRepository(Emoji);
+export const DriveFiles = getCustomRepository(DriveFileRepository);
+export const DriveFolders = getCustomRepository(DriveFolderRepository);
+export const Notifications = getCustomRepository(NotificationRepository);
+export const Metas = getRepository(Meta);
+export const Mutings = getCustomRepository(MutingRepository);
+export const Blockings = getCustomRepository(BlockingRepository);
+export const SwSubscriptions = getRepository(SwSubscription);
+export const Hashtags = getRepository(Hashtag);
+export const AbuseUserReports = getCustomRepository(AbuseUserReportRepository);
+export const RegistrationTickets = getRepository(RegistrationTicket);
+export const AuthSessions = getCustomRepository(AuthSessionRepository);
+export const AccessTokens = getRepository(AccessToken);
+export const Signins = getCustomRepository(SigninRepository);
+export const MessagingMessages = getCustomRepository(MessagingMessageRepository);
+export const ReversiGames = getCustomRepository(ReversiGameRepository);
+export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository);
+export const Logs = getRepository(Log);
diff --git a/src/models/instance.ts b/src/models/instance.ts
deleted file mode 100644
index cdce570a4be13b782272add1d3f2164b949e0c59..0000000000000000000000000000000000000000
--- a/src/models/instance.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const Instance = db.get<IInstance>('instances');
-Instance.createIndex('host', { unique: true });
-export default Instance;
-
-export interface IInstance {
-	_id: mongo.ObjectID;
-
-	/**
-	 * ホスト
-	 */
-	host: string;
-
-	/**
-	 * このインスタンスを捕捉した日時
-	 */
-	caughtAt: Date;
-
-	/**
-	 * このインスタンスのシステム (MastodonとかMisskeyとかPleromaとか)
-	 */
-	system: string;
-
-	/**
-	 * このインスタンスのユーザー数
-	 */
-	usersCount: number;
-
-	/**
-	 * このインスタンスから受け取った投稿数
-	 */
-	notesCount: number;
-
-	/**
-	 * このインスタンスのユーザーからフォローされている、自インスタンスのユーザーの数
-	 */
-	followingCount: number;
-
-	/**
-	 * このインスタンスのユーザーをフォローしている、自インスタンスのユーザーの数
-	 */
-	followersCount: number;
-
-	/**
-	 * ドライブ使用量
-	 */
-	driveUsage: number;
-
-	/**
-	 * ドライブのファイル数
-	 */
-	driveFiles: number;
-
-	/**
-	 * 直近のリクエスト送信日時
-	 */
-	latestRequestSentAt?: Date;
-
-	/**
-	 * 直近のリクエスト送信時のHTTPステータスコード
-	 */
-	latestStatus?: number;
-
-	/**
-	 * 直近のリクエスト受信日時
-	 */
-	latestRequestReceivedAt?: Date;
-
-	/**
-	 * このインスタンスと不通かどうか
-	 */
-	isNotResponding: boolean;
-
-	/**
-	 * このインスタンスと最後にやり取りした日時
-	 */
-	lastCommunicatedAt: Date;
-
-	/**
-	 * このインスタンスをブロックしているか
-	 */
-	isBlocked: boolean;
-
-	/**
-	 * このインスタンスが閉鎖済みとしてマークされているか
-	 */
-	isMarkedAsClosed: boolean;
-}
diff --git a/src/models/log.ts b/src/models/log.ts
deleted file mode 100644
index 6f79e83c78c2f3df6709672354a79943bad2f9d4..0000000000000000000000000000000000000000
--- a/src/models/log.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const Log = db.get<ILog>('logs');
-Log.createIndex('createdAt', { expireAfterSeconds: 3600 * 24 * 3 });
-Log.createIndex('level');
-Log.createIndex('domain');
-export default Log;
-
-export interface ILog {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	machine: string;
-	worker: string;
-	domain: string[];
-	level: string;
-	message: string;
-	data: any;
-}
diff --git a/src/models/messaging-message.ts b/src/models/messaging-message.ts
deleted file mode 100644
index 67abb4d111f0ef7cfc00ab57e358dddaae37fe2e..0000000000000000000000000000000000000000
--- a/src/models/messaging-message.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import { pack as packUser } from './user';
-import { pack as packFile } from './drive-file';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { length } from 'stringz';
-
-const MessagingMessage = db.get<IMessagingMessage>('messagingMessages');
-MessagingMessage.createIndex('userId');
-MessagingMessage.createIndex('recipientId');
-export default MessagingMessage;
-
-export interface IMessagingMessage {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	text: string;
-	userId: mongo.ObjectID;
-	recipientId: mongo.ObjectID;
-	isRead: boolean;
-	fileId: mongo.ObjectID;
-}
-
-export function isValidText(text: string): boolean {
-	return length(text.trim()) <= 1000 && text.trim() != '';
-}
-
-/**
- * Pack a messaging message for API response
- */
-export const pack = (
-	message: any,
-	me?: any,
-	options?: {
-		populateRecipient: boolean
-	}
-) => new Promise<any>(async (resolve, reject) => {
-	const opts = options || {
-		populateRecipient: true
-	};
-
-	let _message: any;
-
-	// Populate the message if 'message' is ID
-	if (isObjectId(message)) {
-		_message = await MessagingMessage.findOne({
-			_id: message
-		});
-	} else if (typeof message === 'string') {
-		_message = await MessagingMessage.findOne({
-			_id: new mongo.ObjectID(message)
-		});
-	} else {
-		_message = deepcopy(message);
-	}
-
-	// Rename _id to id
-	_message.id = _message._id;
-	delete _message._id;
-
-	// Populate user
-	_message.user = await packUser(_message.userId, me);
-
-	if (_message.fileId) {
-		// Populate file
-		_message.file = await packFile(_message.fileId);
-	}
-
-	if (opts.populateRecipient) {
-		// Populate recipient
-		_message.recipient = await packUser(_message.recipientId, me);
-	}
-
-	resolve(_message);
-});
diff --git a/src/models/meta.ts b/src/models/meta.ts
deleted file mode 100644
index 5ca0f01236449c5bf3d546bccb350096ee5bc8a8..0000000000000000000000000000000000000000
--- a/src/models/meta.ts
+++ /dev/null
@@ -1,257 +0,0 @@
-import db from '../db/mongodb';
-import config from '../config';
-import User from './user';
-import { transform } from '../misc/cafy-id';
-
-const Meta = db.get<IMeta>('meta');
-export default Meta;
-
-// 後方互換性のため。
-// 過去のMisskeyではインスタンス名や紹介を設定ファイルに記述していたのでそれを移行
-if ((config as any).name) {
-	Meta.findOne({}).then(m => {
-		if (m != null && m.name == null) {
-			Meta.update({}, {
-				$set: {
-					name: (config as any).name
-				}
-			});
-		}
-	});
-}
-if ((config as any).description) {
-	Meta.findOne({}).then(m => {
-		if (m != null && m.description == null) {
-			Meta.update({}, {
-				$set: {
-					description: (config as any).description
-				}
-			});
-		}
-	});
-}
-if ((config as any).localDriveCapacityMb) {
-	Meta.findOne({}).then(m => {
-		if (m != null && m.localDriveCapacityMb == null) {
-			Meta.update({}, {
-				$set: {
-					localDriveCapacityMb: (config as any).localDriveCapacityMb
-				}
-			});
-		}
-	});
-}
-if ((config as any).remoteDriveCapacityMb) {
-	Meta.findOne({}).then(m => {
-		if (m != null && m.remoteDriveCapacityMb == null) {
-			Meta.update({}, {
-				$set: {
-					remoteDriveCapacityMb: (config as any).remoteDriveCapacityMb
-				}
-			});
-		}
-	});
-}
-if ((config as any).preventCacheRemoteFiles) {
-	Meta.findOne({}).then(m => {
-		if (m != null && m.cacheRemoteFiles == null) {
-			Meta.update({}, {
-				$set: {
-					cacheRemoteFiles: !(config as any).preventCacheRemoteFiles
-				}
-			});
-		}
-	});
-}
-if ((config as any).recaptcha) {
-	Meta.findOne({}).then(m => {
-		if (m != null && m.enableRecaptcha == null) {
-			Meta.update({}, {
-				$set: {
-					enableRecaptcha: (config as any).recaptcha != null,
-					recaptchaSiteKey: (config as any).recaptcha.site_key,
-					recaptchaSecretKey: (config as any).recaptcha.secret_key,
-				}
-			});
-		}
-	});
-}
-if ((config as any).ghost) {
-	Meta.findOne({}).then(async m => {
-		if (m != null && m.proxyAccount == null) {
-			const account = await User.findOne({ _id: transform((config as any).ghost) });
-			Meta.update({}, {
-				$set: {
-					proxyAccount: account.username
-				}
-			});
-		}
-	});
-}
-if ((config as any).maintainer) {
-	Meta.findOne({}).then(m => {
-		if (m != null && m.maintainer == null) {
-			Meta.update({}, {
-				$set: {
-					maintainer: (config as any).maintainer
-				}
-			});
-		}
-	});
-}
-if ((config as any).twitter) {
-	Meta.findOne({}).then(m => {
-		if (m != null && m.enableTwitterIntegration == null) {
-			Meta.update({}, {
-				$set: {
-					enableTwitterIntegration: true,
-					twitterConsumerKey: (config as any).twitter.consumer_key,
-					twitterConsumerSecret: (config as any).twitter.consumer_secret
-				}
-			});
-		}
-	});
-}
-if ((config as any).github) {
-	Meta.findOne({}).then(m => {
-		if (m != null && m.enableGithubIntegration == null) {
-			Meta.update({}, {
-				$set: {
-					enableGithubIntegration: true,
-					githubClientId: (config as any).github.client_id,
-					githubClientSecret: (config as any).github.client_secret
-				}
-			});
-		}
-	});
-}
-if ((config as any).user_recommendation) {
-	Meta.findOne({}).then(m => {
-		if (m != null && m.enableExternalUserRecommendation == null) {
-			Meta.update({}, {
-				$set: {
-					enableExternalUserRecommendation: true,
-					externalUserRecommendationEngine: (config as any).user_recommendation.engine,
-					externalUserRecommendationTimeout: (config as any).user_recommendation.timeout
-				}
-			});
-		}
-	});
-}
-if ((config as any).sw) {
-	Meta.findOne({}).then(m => {
-		if (m != null && m.enableServiceWorker == null) {
-			Meta.update({}, {
-				$set: {
-					enableServiceWorker: true,
-					swPublicKey: (config as any).sw.public_key,
-					swPrivateKey: (config as any).sw.private_key
-				}
-			});
-		}
-	});
-}
-Meta.findOne({}).then(m => {
-	if (m != null && (m as any).broadcasts != null) {
-		Meta.update({}, {
-			$rename: {
-				broadcasts: 'announcements'
-			}
-		});
-	}
-});
-
-export type IMeta = {
-	name?: string;
-	description?: string;
-
-	/**
-	 * メンテナ情報
-	 */
-	maintainer: {
-		/**
-		 * メンテナの名前
-		 */
-		name: string;
-
-		/**
-		 * メンテナの連絡先
-		 */
-		email?: string;
-	};
-
-	langs?: string[];
-
-	announcements?: any[];
-
-	stats?: {
-		notesCount: number;
-		originalNotesCount: number;
-		usersCount: number;
-		originalUsersCount: number;
-	};
-
-	disableRegistration?: boolean;
-	disableLocalTimeline?: boolean;
-	disableGlobalTimeline?: boolean;
-	enableEmojiReaction?: boolean;
-	useStarForReactionFallback?: boolean;
-	hidedTags?: string[];
-	mascotImageUrl?: string;
-	bannerUrl?: string;
-	errorImageUrl?: string;
-	iconUrl?: string;
-
-	cacheRemoteFiles?: boolean;
-
-	proxyAccount?: string;
-
-	enableRecaptcha?: boolean;
-	recaptchaSiteKey?: string;
-	recaptchaSecretKey?: string;
-
-	/**
-	 * Drive capacity of a local user (MB)
-	 */
-	localDriveCapacityMb?: number;
-
-	/**
-	 * Drive capacity of a remote user (MB)
-	 */
-	remoteDriveCapacityMb?: number;
-
-	/**
-	 * Max allowed note text length in characters
-	 */
-	maxNoteTextLength?: number;
-
-	summalyProxy?: string;
-
-	enableTwitterIntegration?: boolean;
-	twitterConsumerKey?: string;
-	twitterConsumerSecret?: string;
-
-	enableGithubIntegration?: boolean;
-	githubClientId?: string;
-	githubClientSecret?: string;
-
-	enableDiscordIntegration?: boolean;
-	discordClientId?: string;
-	discordClientSecret?: string;
-
-	enableExternalUserRecommendation?: boolean;
-	externalUserRecommendationEngine?: string;
-	externalUserRecommendationTimeout?: number;
-
-	enableEmail?: boolean;
-	email?: string;
-	smtpSecure?: boolean;
-	smtpHost?: string;
-	smtpPort?: number;
-	smtpUser?: string;
-	smtpPass?: string;
-
-	enableServiceWorker?: boolean;
-	swPublicKey?: string;
-	swPrivateKey?: string;
-};
diff --git a/src/models/mute.ts b/src/models/mute.ts
deleted file mode 100644
index 52775e13ca5fd5147861dcf352c8c8548c3fed06..0000000000000000000000000000000000000000
--- a/src/models/mute.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import * as deepcopy from 'deepcopy';
-import { pack as packUser, IUser } from './user';
-
-const Mute = db.get<IMute>('mute');
-Mute.createIndex('muterId');
-Mute.createIndex('muteeId');
-Mute.createIndex(['muterId', 'muteeId'], { unique: true });
-export default Mute;
-
-export interface IMute {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	muterId: mongo.ObjectID;
-	muteeId: mongo.ObjectID;
-}
-
-export const packMany = (
-	mutes: (string | mongo.ObjectID | IMute)[],
-	me?: string | mongo.ObjectID | IUser
-) => {
-	return Promise.all(mutes.map(x => pack(x, me)));
-};
-
-export const pack = (
-	mute: any,
-	me?: any
-) => new Promise<any>(async (resolve, reject) => {
-	let _mute: any;
-
-	// Populate the mute if 'mute' is ID
-	if (isObjectId(mute)) {
-		_mute = await Mute.findOne({
-			_id: mute
-		});
-	} else if (typeof mute === 'string') {
-		_mute = await Mute.findOne({
-			_id: new mongo.ObjectID(mute)
-		});
-	} else {
-		_mute = deepcopy(mute);
-	}
-
-	// Rename _id to id
-	_mute.id = _mute._id;
-	delete _mute._id;
-
-	// Populate mutee
-	_mute.mutee = await packUser(_mute.muteeId, me, {
-		detail: true
-	});
-
-	resolve(_mute);
-});
diff --git a/src/models/note-reaction.ts b/src/models/note-reaction.ts
deleted file mode 100644
index 89b7529350f4a204bc5e9394b2a00c95271c70a4..0000000000000000000000000000000000000000
--- a/src/models/note-reaction.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { pack as packUser } from './user';
-
-const NoteReaction = db.get<INoteReaction>('noteReactions');
-NoteReaction.createIndex('noteId');
-NoteReaction.createIndex('userId');
-NoteReaction.createIndex(['userId', 'noteId'], { unique: true });
-export default NoteReaction;
-
-export interface INoteReaction {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	noteId: mongo.ObjectID;
-	userId: mongo.ObjectID;
-	reaction: string;
-}
-
-/**
- * Pack a reaction for API response
- */
-export const pack = (
-	reaction: any,
-	me?: any
-) => new Promise<any>(async (resolve, reject) => {
-	let _reaction: any;
-
-	// Populate the reaction if 'reaction' is ID
-	if (isObjectId(reaction)) {
-		_reaction = await NoteReaction.findOne({
-			_id: reaction
-		});
-	} else if (typeof reaction === 'string') {
-		_reaction = await NoteReaction.findOne({
-			_id: new mongo.ObjectID(reaction)
-		});
-	} else {
-		_reaction = deepcopy(reaction);
-	}
-
-	// Rename _id to id
-	_reaction.id = _reaction._id;
-	delete _reaction._id;
-
-	// Populate user
-	_reaction.user = await packUser(_reaction.userId, me);
-
-	resolve(_reaction);
-});
diff --git a/src/models/note-unread.ts b/src/models/note-unread.ts
deleted file mode 100644
index dd08640d85f4332efa2c6a63fbcf7e500fbf491e..0000000000000000000000000000000000000000
--- a/src/models/note-unread.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const NoteUnread = db.get<INoteUnread>('noteUnreads');
-NoteUnread.createIndex('userId');
-NoteUnread.createIndex('noteId');
-NoteUnread.createIndex(['userId', 'noteId'], { unique: true });
-export default NoteUnread;
-
-export interface INoteUnread {
-	_id: mongo.ObjectID;
-	noteId: mongo.ObjectID;
-	userId: mongo.ObjectID;
-	isSpecified: boolean;
-
-	_note: {
-		userId: mongo.ObjectID;
-	};
-}
diff --git a/src/models/note-watching.ts b/src/models/note-watching.ts
deleted file mode 100644
index 83aaf8ad0661141a617fad414d2d31abed72e087..0000000000000000000000000000000000000000
--- a/src/models/note-watching.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const NoteWatching = db.get<INoteWatching>('noteWatching');
-NoteWatching.createIndex('userId');
-NoteWatching.createIndex('noteId');
-NoteWatching.createIndex(['userId', 'noteId'], { unique: true });
-export default NoteWatching;
-
-export interface INoteWatching {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	userId: mongo.ObjectID;
-	noteId: mongo.ObjectID;
-}
diff --git a/src/models/note.ts b/src/models/note.ts
deleted file mode 100644
index 8c71c1940c3f58a2d61bee3c6b3d0d5788e30016..0000000000000000000000000000000000000000
--- a/src/models/note.ts
+++ /dev/null
@@ -1,418 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import rap from '@prezzemolo/rap';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { length } from 'stringz';
-import { IUser, pack as packUser } from './user';
-import { pack as packApp } from './app';
-import PollVote from './poll-vote';
-import NoteReaction from './note-reaction';
-import { packMany as packFileMany, IDriveFile } from './drive-file';
-import Following from './following';
-import Emoji from './emoji';
-import { dbLogger } from '../db/logger';
-import { unique, concat } from '../prelude/array';
-
-const Note = db.get<INote>('notes');
-Note.createIndex('uri', { sparse: true, unique: true });
-Note.createIndex('userId');
-Note.createIndex('mentions');
-Note.createIndex('visibleUserIds');
-Note.createIndex('replyId');
-Note.createIndex('renoteId');
-Note.createIndex('tagsLower');
-Note.createIndex('_user.host');
-Note.createIndex('_files._id');
-Note.createIndex('_files.contentType');
-Note.createIndex({ createdAt: -1 });
-Note.createIndex({ score: -1 }, { sparse: true });
-export default Note;
-
-export function isValidCw(text: string): boolean {
-	return length(text.trim()) <= 100;
-}
-
-export type INote = {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	deletedAt: Date;
-	updatedAt?: Date;
-	fileIds: mongo.ObjectID[];
-	replyId: mongo.ObjectID;
-	renoteId: mongo.ObjectID;
-	poll: IPoll;
-	name?: string;
-	text: string;
-	tags: string[];
-	tagsLower: string[];
-	emojis: string[];
-	cw: string;
-	userId: mongo.ObjectID;
-	appId: mongo.ObjectID;
-	viaMobile: boolean;
-	localOnly: boolean;
-	renoteCount: number;
-	repliesCount: number;
-	reactionCounts: Record<string, number>;
-	mentions: mongo.ObjectID[];
-	mentionedRemoteUsers: {
-		uri: string;
-		username: string;
-		host: string;
-	}[];
-
-	/**
-	 * public ... 公開
-	 * home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す
-	 * followers ... フォロワーのみ
-	 * specified ... visibleUserIds で指定したユーザーのみ
-	 */
-	visibility: 'public' | 'home' | 'followers' | 'specified';
-
-	visibleUserIds: mongo.ObjectID[];
-
-	geo: {
-		coordinates: number[];
-		altitude: number;
-		accuracy: number;
-		altitudeAccuracy: number;
-		heading: number;
-		speed: number;
-	};
-
-	uri: string;
-
-	/**
-	 * 人気の投稿度合いを表すスコア
-	 */
-	score: number;
-
-	// 非正規化
-	_reply?: {
-		userId: mongo.ObjectID;
-	};
-	_renote?: {
-		userId: mongo.ObjectID;
-	};
-	_user: {
-		host: string;
-		inbox?: string;
-	};
-	_files?: IDriveFile[];
-};
-
-export type IPoll = {
-	choices: IChoice[];
-	multiple?: boolean;
-	expiresAt?: Date;
-};
-
-export type IChoice = {
-	id: number;
-	text: string;
-	votes: number;
-};
-
-export const hideNote = async (packedNote: any, meId: mongo.ObjectID) => {
-	let hide = false;
-
-	// visibility が private かつ投稿者のIDが自分のIDではなかったら非表示(後方互換性のため)
-	if (packedNote.visibility == 'private' && (meId == null || !meId.equals(packedNote.userId))) {
-		hide = true;
-	}
-
-	// visibility が specified かつ自分が指定されていなかったら非表示
-	if (packedNote.visibility == 'specified') {
-		if (meId == null) {
-			hide = true;
-		} else if (meId.equals(packedNote.userId)) {
-			hide = false;
-		} else {
-			// 指定されているかどうか
-			const specified = packedNote.visibleUserIds.some((id: any) => meId.equals(id));
-
-			if (specified) {
-				hide = false;
-			} else {
-				hide = true;
-			}
-		}
-	}
-
-	// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
-	if (packedNote.visibility == 'followers') {
-		if (meId == null) {
-			hide = true;
-		} else if (meId.equals(packedNote.userId)) {
-			hide = false;
-		} else if (packedNote.reply && meId.equals(packedNote.reply.userId)) {
-			// 自分の投稿に対するリプライ
-			hide = false;
-		} else if (packedNote.mentions && packedNote.mentions.some((id: any) => meId.equals(id))) {
-			// 自分へのメンション
-			hide = false;
-		} else {
-			// フォロワーかどうか
-			const following = await Following.findOne({
-				followeeId: packedNote.userId,
-				followerId: meId
-			});
-
-			if (following == null) {
-				hide = true;
-			} else {
-				hide = false;
-			}
-		}
-	}
-
-	if (hide) {
-		packedNote.fileIds = [];
-		packedNote.files = [];
-		packedNote.text = null;
-		packedNote.poll = null;
-		packedNote.cw = null;
-		packedNote.tags = [];
-		packedNote.geo = null;
-		packedNote.isHidden = true;
-	}
-};
-
-export const packMany = (
-	notes: (string | mongo.ObjectID | INote)[],
-	me?: string | mongo.ObjectID | IUser,
-	options?: {
-		detail?: boolean;
-		skipHide?: boolean;
-	}
-) => {
-	return Promise.all(notes.map(n => pack(n, me, options)));
-};
-
-/**
- * Pack a note for API response
- *
- * @param note target
- * @param me? serializee
- * @param options? serialize options
- * @return response
- */
-export const pack = async (
-	note: string | mongo.ObjectID | INote,
-	me?: string | mongo.ObjectID | IUser,
-	options?: {
-		detail?: boolean;
-		skipHide?: boolean;
-	}
-) => {
-	const opts = Object.assign({
-		detail: true,
-		skipHide: false
-	}, options);
-
-	// Me
-	const meId: mongo.ObjectID = me
-		? isObjectId(me)
-			? me as mongo.ObjectID
-			: typeof me === 'string'
-				? new mongo.ObjectID(me)
-				: (me as IUser)._id
-		: null;
-
-	let _note: any;
-
-	// Populate the note if 'note' is ID
-	if (isObjectId(note)) {
-		_note = await Note.findOne({
-			_id: note
-		});
-	} else if (typeof note === 'string') {
-		_note = await Note.findOne({
-			_id: new mongo.ObjectID(note)
-		});
-	} else {
-		_note = deepcopy(note);
-	}
-
-	// (データベースの欠損などで)投稿がデータベース上に見つからなかったとき
-	if (_note == null) {
-		dbLogger.warn(`[DAMAGED DB] (missing) pkg: note :: ${note}`);
-		return null;
-	}
-
-	const id = _note._id;
-
-	// Some counts
-	_note.renoteCount = _note.renoteCount || 0;
-	_note.repliesCount = _note.repliesCount || 0;
-	_note.reactionCounts = _note.reactionCounts || {};
-
-	// _note._userを消す前か、_note.userを解決した後でないとホストがわからない
-	if (_note._user) {
-		const host = _note._user.host;
-		// 互換性のため。(古いMisskeyではNoteにemojisが無い)
-		if (_note.emojis == null) {
-			_note.emojis = Emoji.find({
-				host: host
-			}, {
-				fields: { _id: false }
-			});
-		} else {
-			_note.emojis = unique(concat([_note.emojis, Object.keys(_note.reactionCounts).map(x => x.replace(/:/g, ''))]));
-
-			_note.emojis = Emoji.find({
-				name: { $in: _note.emojis },
-				host: host
-			}, {
-				fields: { _id: false }
-			});
-		}
-	}
-
-	// Rename _id to id
-	_note.id = _note._id;
-	delete _note._id;
-
-	delete _note.prev;
-	delete _note.next;
-	delete _note.tagsLower;
-	delete _note.score;
-	delete _note._user;
-	delete _note._reply;
-	delete _note._renote;
-	delete _note._files;
-	delete _note._replyIds;
-	delete _note.mentionedRemoteUsers;
-
-	if (_note.geo) delete _note.geo.type;
-
-	// Populate user
-	_note.user = packUser(_note.userId, meId);
-
-	// Populate app
-	if (_note.appId) {
-		_note.app = packApp(_note.appId);
-	}
-
-	// Populate files
-	_note.files = packFileMany(_note.fileIds || []);
-
-	// 後方互換性のため
-	_note.mediaIds = _note.fileIds;
-	_note.media = _note.files;
-
-	// When requested a detailed note data
-	if (opts.detail) {
-		if (_note.replyId) {
-			// Populate reply to note
-			_note.reply = pack(_note.replyId, meId, {
-				detail: false
-			});
-		}
-
-		if (_note.renoteId) {
-			// Populate renote
-			_note.renote = pack(_note.renoteId, meId, {
-				detail: _note.text == null
-			});
-		}
-
-		// Poll
-		if (meId && _note.poll) {
-			_note.poll = (async poll => {
-				if (poll.multiple) {
-					const votes = await PollVote.find({
-						userId: meId,
-						noteId: id
-					});
-
-					const myChoices = (poll.choices as IChoice[]).filter(x => votes.some(y => x.id == y.choice));
-					for (const myChoice of myChoices) {
-						(myChoice as any).isVoted = true;
-					}
-
-					return poll;
-				} else {
-					poll.multiple = false;
-				}
-
-				const vote = await PollVote
-					.findOne({
-						userId: meId,
-						noteId: id
-					});
-
-				if (vote) {
-					const myChoice = (poll.choices as IChoice[])
-						.filter(x => x.id == vote.choice)[0] as any;
-
-					myChoice.isVoted = true;
-				}
-
-				return poll;
-			})(_note.poll);
-		}
-
-		if (meId) {
-			// Fetch my reaction
-			_note.myReaction = (async () => {
-				const reaction = await NoteReaction
-					.findOne({
-						userId: meId,
-						noteId: id,
-						deletedAt: { $exists: false }
-					});
-
-				if (reaction) {
-					return reaction.reaction;
-				}
-
-				return null;
-			})();
-		}
-	}
-
-	// resolve promises in _note object
-	_note = await rap(_note);
-
-	//#region (データベースの欠損などで)参照しているデータがデータベース上に見つからなかったとき
-	if (_note.user == null) {
-		dbLogger.warn(`[DAMAGED DB] (missing) pkg: note -> user :: ${_note.id} (user ${_note.userId})`);
-		return null;
-	}
-
-	if (opts.detail) {
-		if (_note.replyId != null && _note.reply == null) {
-			dbLogger.warn(`[DAMAGED DB] (missing) pkg: note -> reply :: ${_note.id} (reply ${_note.replyId})`);
-			return null;
-		}
-
-		if (_note.renoteId != null && _note.renote == null) {
-			dbLogger.warn(`[DAMAGED DB] (missing) pkg: note -> renote :: ${_note.id} (renote ${_note.renoteId})`);
-			return null;
-		}
-	}
-	//#endregion
-
-	if (_note.name) {
-		_note.text = `【${_note.name}】\n${_note.text}`;
-	}
-
-	if (_note.user.isCat && _note.text) {
-		_note.text = (_note.text
-			// ja-JP
-			.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ')
-			// ko-KR
-			.replace(/[나-낳]/g, (match: string) => String.fromCharCode(
-				match.codePointAt(0)  + '냐'.charCodeAt(0) - '나'.charCodeAt(0)
-			))
-		);
-	}
-
-	if (!opts.skipHide) {
-		await hideNote(_note, meId);
-	}
-
-	return _note;
-};
diff --git a/src/models/notification.ts b/src/models/notification.ts
deleted file mode 100644
index 75456af57b34036abad50d771a1e1216a8b81dcf..0000000000000000000000000000000000000000
--- a/src/models/notification.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { IUser, pack as packUser } from './user';
-import { pack as packNote } from './note';
-import { dbLogger } from '../db/logger';
-
-const Notification = db.get<INotification>('notifications');
-Notification.createIndex('notifieeId');
-export default Notification;
-
-export interface INotification {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-
-	/**
-	 * 通知の受信者
-	 */
-	notifiee?: IUser;
-
-	/**
-	 * 通知の受信者
-	 */
-	notifieeId: mongo.ObjectID;
-
-	/**
-	 * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー
-	 */
-	notifier?: IUser;
-
-	/**
-	 * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー
-	 */
-	notifierId: mongo.ObjectID;
-
-	/**
-	 * 通知の種類。
-	 * follow - フォローされた
-	 * mention - 投稿で自分が言及された
-	 * reply - (自分または自分がWatchしている)投稿が返信された
-	 * renote - (自分または自分がWatchしている)投稿がRenoteされた
-	 * quote - (自分または自分がWatchしている)投稿が引用Renoteされた
-	 * reaction - (自分または自分がWatchしている)投稿にリアクションされた
-	 * poll_vote - (自分または自分がWatchしている)投稿の投票に投票された
-	 */
-	type: 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'poll_vote';
-
-	/**
-	 * 通知が読まれたかどうか
-	 */
-	isRead: boolean;
-}
-
-export const packMany = (
-	notifications: any[]
-) => {
-	return Promise.all(notifications.map(n => pack(n)));
-};
-
-/**
- * Pack a notification for API response
- */
-export const pack = (notification: any) => new Promise<any>(async (resolve, reject) => {
-	let _notification: any;
-
-	// Populate the notification if 'notification' is ID
-	if (isObjectId(notification)) {
-		_notification = await Notification.findOne({
-			_id: notification
-		});
-	} else if (typeof notification === 'string') {
-		_notification = await Notification.findOne({
-			_id: new mongo.ObjectID(notification)
-		});
-	} else {
-		_notification = deepcopy(notification);
-	}
-
-	// Rename _id to id
-	_notification.id = _notification._id;
-	delete _notification._id;
-
-	// Rename notifierId to userId
-	_notification.userId = _notification.notifierId;
-	delete _notification.notifierId;
-
-	const me = _notification.notifieeId;
-	delete _notification.notifieeId;
-
-	// Populate notifier
-	_notification.user = await packUser(_notification.userId, me);
-
-	switch (_notification.type) {
-		case 'follow':
-		case 'receiveFollowRequest':
-			// nope
-			break;
-		case 'mention':
-		case 'reply':
-		case 'renote':
-		case 'quote':
-		case 'reaction':
-		case 'poll_vote':
-			// Populate note
-			_notification.note = await packNote(_notification.noteId, me);
-
-			// (データベースの不具合などで)投稿が見つからなかったら
-			if (_notification.note == null) {
-				dbLogger.warn(`[DAMAGED DB] (missing) pkg: notification -> note :: ${_notification.id} (note ${_notification.noteId})`);
-				return resolve(null);
-			}
-			break;
-		default:
-			dbLogger.error(`Unknown type: ${_notification.type}`);
-			break;
-	}
-
-	resolve(_notification);
-});
diff --git a/src/models/poll-vote.ts b/src/models/poll-vote.ts
deleted file mode 100644
index e6178cbc262064481dfe7906efde1e2db7009c11..0000000000000000000000000000000000000000
--- a/src/models/poll-vote.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const PollVote = db.get<IPollVote>('pollVotes');
-PollVote.dropIndex(['userId', 'noteId'], { unique: true }).catch(() => {});
-PollVote.createIndex('userId');
-PollVote.createIndex('noteId');
-PollVote.createIndex(['userId', 'noteId', 'choice'], { unique: true });
-export default PollVote;
-
-export interface IPollVote {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	userId: mongo.ObjectID;
-	noteId: mongo.ObjectID;
-	choice: number;
-}
diff --git a/src/models/registration-tickets.ts b/src/models/registration-tickets.ts
deleted file mode 100644
index 846acefedf132df42a4bab6edd51dd830de2d682..0000000000000000000000000000000000000000
--- a/src/models/registration-tickets.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const RegistrationTicket = db.get<IRegistrationTicket>('registrationTickets');
-RegistrationTicket.createIndex('code', { unique: true });
-export default RegistrationTicket;
-
-export interface IRegistrationTicket {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	code: string;
-}
diff --git a/src/models/repositories/abuse-user-report.ts b/src/models/repositories/abuse-user-report.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c72a582c04613ce2422ff1ebd40d0b653e4990ef
--- /dev/null
+++ b/src/models/repositories/abuse-user-report.ts
@@ -0,0 +1,32 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '..';
+import rap from '@prezzemolo/rap';
+import { AbuseUserReport } from '../entities/abuse-user-report';
+
+@EntityRepository(AbuseUserReport)
+export class AbuseUserReportRepository extends Repository<AbuseUserReport> {
+	public packMany(
+		reports: any[],
+	) {
+		return Promise.all(reports.map(x => this.pack(x)));
+	}
+
+	public async pack(
+		src: AbuseUserReport['id'] | AbuseUserReport,
+	) {
+		const report = typeof src === 'object' ? src : await this.findOne(src);
+
+		return await rap({
+			id: report.id,
+			createdAt: report.createdAt,
+			reporterId: report.reporterId,
+			userId: report.userId,
+			reporter: Users.pack(report.reporter || report.reporterId, null, {
+				detail: true
+			}),
+			user: Users.pack(report.user || report.userId, null, {
+				detail: true
+			}),
+		});
+	}
+}
diff --git a/src/models/repositories/app.ts b/src/models/repositories/app.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2e3323baf8ad55e286bc405d643adfc50f183400
--- /dev/null
+++ b/src/models/repositories/app.ts
@@ -0,0 +1,36 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { App } from '../entities/app';
+import { AccessTokens } from '..';
+
+@EntityRepository(App)
+export class AppRepository extends Repository<App> {
+	public async pack(
+		src: App['id'] | App,
+		me?: any,
+		options?: {
+			detail?: boolean,
+			includeSecret?: boolean,
+			includeProfileImageIds?: boolean
+		}
+	) {
+		const opts = Object.assign({
+			detail: false,
+			includeSecret: false,
+			includeProfileImageIds: false
+		}, options);
+
+		const app = typeof src === 'object' ? src : await this.findOne(src);
+
+		return {
+			id: app.id,
+			name: app.name,
+			...(opts.includeSecret ? { secret: app.secret } : {}),
+			...(me ? {
+				isAuthorized: await AccessTokens.count({
+					appId: app.id,
+					userId: me,
+				}).then(count => count > 0)
+			} : {})
+		};
+	}
+}
diff --git a/src/models/repositories/auth-session.ts b/src/models/repositories/auth-session.ts
new file mode 100644
index 0000000000000000000000000000000000000000..76e3ddf9ab11cf47e9a709a3f19a91d91e54c3e6
--- /dev/null
+++ b/src/models/repositories/auth-session.ts
@@ -0,0 +1,19 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Apps } from '..';
+import rap from '@prezzemolo/rap';
+import { AuthSession } from '../entities/auth-session';
+
+@EntityRepository(AuthSession)
+export class AuthSessionRepository extends Repository<AuthSession> {
+	public async pack(
+		src: AuthSession['id'] | AuthSession,
+		me?: any
+	) {
+		const session = typeof src === 'object' ? src : await this.findOne(src);
+
+		return await rap({
+			id: session.id,
+			app: Apps.pack(session.appId, me)
+		});
+	}
+}
diff --git a/src/models/repositories/blocking.ts b/src/models/repositories/blocking.ts
new file mode 100644
index 0000000000000000000000000000000000000000..81f3866131e326c14859296107ff3a4197308a8a
--- /dev/null
+++ b/src/models/repositories/blocking.ts
@@ -0,0 +1,28 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '..';
+import rap from '@prezzemolo/rap';
+import { Blocking } from '../entities/blocking';
+
+@EntityRepository(Blocking)
+export class BlockingRepository extends Repository<Blocking> {
+	public packMany(
+		blockings: any[],
+		me: any
+	) {
+		return Promise.all(blockings.map(x => this.pack(x, me)));
+	}
+
+	public async pack(
+		src: Blocking['id'] | Blocking,
+		me?: any
+	) {
+		const blocking = typeof src === 'object' ? src : await this.findOne(src);
+
+		return await rap({
+			id: blocking.id,
+			blockee: Users.pack(blocking.blockeeId, me, {
+				detail: true
+			})
+		});
+	}
+}
diff --git a/src/models/repositories/drive-file.ts b/src/models/repositories/drive-file.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fe0ca72bfbb3759cbc80e4a0a6f6cf288f0f7774
--- /dev/null
+++ b/src/models/repositories/drive-file.ts
@@ -0,0 +1,113 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { DriveFile } from '../entities/drive-file';
+import { Users, DriveFolders } from '..';
+import rap from '@prezzemolo/rap';
+import { User } from '../entities/user';
+
+@EntityRepository(DriveFile)
+export class DriveFileRepository extends Repository<DriveFile> {
+	public validateFileName(name: string): boolean {
+		return (
+			(name.trim().length > 0) &&
+			(name.length <= 200) &&
+			(name.indexOf('\\') === -1) &&
+			(name.indexOf('/') === -1) &&
+			(name.indexOf('..') === -1)
+		);
+	}
+
+	public getPublicUrl(file: DriveFile, thumbnail = false): string {
+		if (thumbnail) {
+			return file.thumbnailUrl || file.webpublicUrl || file.url;
+		} else {
+			return file.webpublicUrl || file.thumbnailUrl || file.url;
+		}
+	}
+
+	public async clacDriveUsageOf(user: User['id'] | User): Promise<number> {
+		const id = typeof user === 'object' ? user.id : user;
+
+		const { sum } = await this
+			.createQueryBuilder('file')
+			.where('file.userId = :id', { id: id })
+			.select('SUM(file.size)', 'sum')
+			.getRawOne();
+
+		return parseInt(sum, 10) || 0;
+	}
+
+	public async clacDriveUsageOfHost(host: string): Promise<number> {
+		const { sum } = await this
+			.createQueryBuilder('file')
+			.where('file.userHost = :host', { host: host })
+			.select('SUM(file.size)', 'sum')
+			.getRawOne();
+
+		return parseInt(sum, 10) || 0;
+	}
+
+	public async clacDriveUsageOfLocal(): Promise<number> {
+		const { sum } = await this
+			.createQueryBuilder('file')
+			.where('file.userHost IS NULL')
+			.select('SUM(file.size)', 'sum')
+			.getRawOne();
+
+		return parseInt(sum, 10) || 0;
+	}
+
+	public async clacDriveUsageOfRemote(): Promise<number> {
+		const { sum } = await this
+			.createQueryBuilder('file')
+			.where('file.userHost IS NOT NULL')
+			.select('SUM(file.size)', 'sum')
+			.getRawOne();
+
+		return parseInt(sum, 10) || 0;
+	}
+
+	public packMany(
+		files: any[],
+		options?: {
+			detail?: boolean
+			self?: boolean,
+			withUser?: boolean,
+		}
+	) {
+		return Promise.all(files.map(f => this.pack(f, options)));
+	}
+
+	public async pack(
+		src: DriveFile['id'] | DriveFile,
+		options?: {
+			detail?: boolean,
+			self?: boolean,
+			withUser?: boolean,
+		}
+	) {
+		const opts = Object.assign({
+			detail: false,
+			self: false
+		}, options);
+
+		const file = typeof src === 'object' ? src : await this.findOne(src);
+
+		return await rap({
+			id: file.id,
+			createdAt: file.createdAt,
+			name: file.name,
+			type: file.type,
+			md5: file.md5,
+			size: file.size,
+			isSensitive: file.isSensitive,
+			properties: file.properties,
+			url: opts.self ? file.url : this.getPublicUrl(file, false),
+			thumbnailUrl: this.getPublicUrl(file, true),
+			folderId: file.folderId,
+			folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, {
+				detail: true
+			}) : null,
+			user: opts.withUser ? Users.pack(file.userId) : null
+		});
+	}
+}
diff --git a/src/models/repositories/drive-folder.ts b/src/models/repositories/drive-folder.ts
new file mode 100644
index 0000000000000000000000000000000000000000..faf0f353aa795c80f04ce6bc03c00b4d80d78c65
--- /dev/null
+++ b/src/models/repositories/drive-folder.ts
@@ -0,0 +1,49 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { DriveFolders, DriveFiles } from '..';
+import rap from '@prezzemolo/rap';
+import { DriveFolder } from '../entities/drive-folder';
+
+@EntityRepository(DriveFolder)
+export class DriveFolderRepository extends Repository<DriveFolder> {
+	public validateFolderName(name: string): boolean {
+		return (
+			(name.trim().length > 0) &&
+			(name.length <= 200)
+		);
+	}
+
+	public async pack(
+		src: DriveFolder['id'] | DriveFolder,
+		options?: {
+			detail: boolean
+		}
+	): Promise<Record<string, any>> {
+		const opts = Object.assign({
+			detail: false
+		}, options);
+
+		const folder = typeof src === 'object' ? src : await this.findOne(src);
+
+		return await rap({
+			id: folder.id,
+			createdAt: folder.createdAt,
+			name: folder.name,
+			parentId: folder.parentId,
+
+			...(opts.detail ? {
+				foldersCount: DriveFolders.count({
+					parentId: folder.id
+				}),
+				filesCount: DriveFiles.count({
+					folderId: folder.id
+				}),
+
+				...(folder.parentId ? {
+					parent: this.pack(folder.parentId, {
+						detail: true
+					})
+				} : {})
+			} : {})
+		});
+	}
+}
diff --git a/src/models/repositories/follow-request.ts b/src/models/repositories/follow-request.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bead093b21850f2b1e269f311d4793d50fd7f5a4
--- /dev/null
+++ b/src/models/repositories/follow-request.ts
@@ -0,0 +1,19 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { FollowRequest } from '../entities/follow-request';
+import { Users } from '..';
+
+@EntityRepository(FollowRequest)
+export class FollowRequestRepository extends Repository<FollowRequest> {
+	public async pack(
+		src: FollowRequest['id'] | FollowRequest,
+		me?: any
+	) {
+		const request = typeof src === 'object' ? src : await this.findOne(src);
+
+		return {
+			id: request.id,
+			follower: await Users.pack(request.followerId, me),
+			followee: await Users.pack(request.followeeId, me),
+		};
+	}
+}
diff --git a/src/models/repositories/following.ts b/src/models/repositories/following.ts
new file mode 100644
index 0000000000000000000000000000000000000000..02253d272d9e4386795241c5e02cf56f4d397393
--- /dev/null
+++ b/src/models/repositories/following.ts
@@ -0,0 +1,44 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '..';
+import rap from '@prezzemolo/rap';
+import { Following } from '../entities/following';
+
+@EntityRepository(Following)
+export class FollowingRepository extends Repository<Following> {
+	public packMany(
+		followings: any[],
+		me?: any,
+		opts?: {
+			populateFollowee?: boolean;
+			populateFollower?: boolean;
+		}
+	) {
+		return Promise.all(followings.map(x => this.pack(x, me, opts)));
+	}
+
+	public async pack(
+		src: Following['id'] | Following,
+		me?: any,
+		opts?: {
+			populateFollowee?: boolean;
+			populateFollower?: boolean;
+		}
+	) {
+		const following = typeof src === 'object' ? src : await this.findOne(src);
+
+		if (opts == null) opts = {};
+
+		return await rap({
+			id: following.id,
+			createdAt: following.createdAt,
+			followeeId: following.followeeId,
+			followerId: following.followerId,
+			followee: opts.populateFollowee ? Users.pack(following.followee || following.followeeId, me, {
+				detail: true
+			}) : null,
+			follower: opts.populateFollower ? Users.pack(following.follower || following.followerId, me, {
+				detail: true
+			}) : null,
+		});
+	}
+}
diff --git a/src/models/repositories/games/reversi/game.ts b/src/models/repositories/games/reversi/game.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f0cb6ff905d7b556e8dae794eaa522bd454fe9b5
--- /dev/null
+++ b/src/models/repositories/games/reversi/game.ts
@@ -0,0 +1,49 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '../../..';
+import { ReversiGame } from '../../../entities/games/reversi/game';
+
+@EntityRepository(ReversiGame)
+export class ReversiGameRepository extends Repository<ReversiGame> {
+	public async pack(
+		src: ReversiGame['id'] | ReversiGame,
+		me?: any,
+		options?: {
+			detail?: boolean
+		}
+	) {
+		const opts = Object.assign({
+			detail: true
+		}, options);
+
+		const game = typeof src === 'object' ? src : await this.findOne(src);
+		const meId = me ? typeof me === 'string' ? me : me.id : null;
+
+		return {
+			id: game.id,
+			createdAt: game.createdAt,
+			startedAt: game.startedAt,
+			isStarted: game.isStarted,
+			isEnded: game.isEnded,
+			form1: game.form1,
+			form2: game.form2,
+			user1Accepted: game.user1Accepted,
+			user2Accepted: game.user2Accepted,
+			user1Id: game.user1Id,
+			user2Id: game.user2Id,
+			user1: await Users.pack(game.user1Id, meId),
+			user2: await Users.pack(game.user2Id, meId),
+			winnerId: game.winnerId,
+			winner: game.winnerId ? await Users.pack(game.winnerId, meId) : null,
+			surrendered: game.surrendered,
+			black: game.black,
+			bw: game.bw,
+			isLlotheo: game.isLlotheo,
+			canPutEverywhere: game.canPutEverywhere,
+			loopedBoard: game.loopedBoard,
+			...(opts.detail ? {
+				logs: game.logs,
+				map: game.map,
+			} : {})
+		};
+	}
+}
diff --git a/src/models/repositories/games/reversi/matching.ts b/src/models/repositories/games/reversi/matching.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3612ac5c47f83edf9f88f4c54d0b27b731b166bc
--- /dev/null
+++ b/src/models/repositories/games/reversi/matching.ts
@@ -0,0 +1,27 @@
+import { EntityRepository, Repository } from 'typeorm';
+import rap from '@prezzemolo/rap';
+import { ReversiMatching } from '../../../entities/games/reversi/matching';
+import { Users } from '../../..';
+
+@EntityRepository(ReversiMatching)
+export class ReversiMatchingRepository extends Repository<ReversiMatching> {
+	public async pack(
+		src: ReversiMatching['id'] | ReversiMatching,
+		me: any
+	) {
+		const matching = typeof src === 'object' ? src : await this.findOne(src);
+
+		return await rap({
+			id: matching.id,
+			createdAt: matching.createdAt,
+			parentId: matching.parentId,
+			parent: Users.pack(matching.parentId, me, {
+				detail: true
+			}),
+			childId: matching.childId,
+			child: Users.pack(matching.childId, me, {
+				detail: true
+			})
+		});
+	}
+}
diff --git a/src/models/repositories/messaging-message.ts b/src/models/repositories/messaging-message.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b87b30388a6e8685fca0a7915cbb0137f25bebdf
--- /dev/null
+++ b/src/models/repositories/messaging-message.ts
@@ -0,0 +1,37 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { MessagingMessage } from '../entities/messaging-message';
+import { Users, DriveFiles } from '..';
+
+@EntityRepository(MessagingMessage)
+export class MessagingMessageRepository extends Repository<MessagingMessage> {
+	public isValidText(text: string): boolean {
+		return text.trim().length <= 1000 && text.trim() != '';
+	}
+
+	public async pack(
+		src: MessagingMessage['id'] | MessagingMessage,
+		me?: any,
+		options?: {
+			populateRecipient: boolean
+		}
+	) {
+		const opts = options || {
+			populateRecipient: true
+		};
+
+		const message = typeof src === 'object' ? src : await this.findOne(src);
+
+		return {
+			id: message.id,
+			createdAt: message.createdAt,
+			text: message.text,
+			userId: message.userId,
+			user: await Users.pack(message.user || message.userId, me),
+			recipientId: message.recipientId,
+			recipient: opts.populateRecipient ? await Users.pack(message.recipient || message.recipientId, me) : null,
+			fileId: message.fileId,
+			file: message.fileId ? await DriveFiles.pack(message.fileId) : null,
+			isRead: message.isRead
+		};
+	}
+}
diff --git a/src/models/repositories/muting.ts b/src/models/repositories/muting.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cd98cb4fece28ab24d0aaff7d6fd54b16894c3e9
--- /dev/null
+++ b/src/models/repositories/muting.ts
@@ -0,0 +1,28 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '..';
+import rap from '@prezzemolo/rap';
+import { Muting } from '../entities/muting';
+
+@EntityRepository(Muting)
+export class MutingRepository extends Repository<Muting> {
+	public packMany(
+		mutings: any[],
+		me: any
+	) {
+		return Promise.all(mutings.map(x => this.pack(x, me)));
+	}
+
+	public async pack(
+		src: Muting['id'] | Muting,
+		me?: any
+	) {
+		const muting = typeof src === 'object' ? src : await this.findOne(src);
+
+		return await rap({
+			id: muting.id,
+			mutee: Users.pack(muting.muteeId, me, {
+				detail: true
+			})
+		});
+	}
+}
diff --git a/src/models/repositories/note-favorite.ts b/src/models/repositories/note-favorite.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4526461e6937cee64ce46183df095e52f39efca8
--- /dev/null
+++ b/src/models/repositories/note-favorite.ts
@@ -0,0 +1,25 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { NoteFavorite } from '../entities/note-favorite';
+import { Notes } from '..';
+
+@EntityRepository(NoteFavorite)
+export class NoteFavoriteRepository extends Repository<NoteFavorite> {
+	public packMany(
+		favorites: any[],
+		me: any
+	) {
+		return Promise.all(favorites.map(x => this.pack(x, me)));
+	}
+
+	public async pack(
+		src: NoteFavorite['id'] | NoteFavorite,
+		me?: any
+	) {
+		const favorite = typeof src === 'object' ? src : await this.findOne(src);
+
+		return {
+			id: favorite.id,
+			note: await Notes.pack(favorite.note || favorite.noteId, me),
+		};
+	}
+}
diff --git a/src/models/repositories/note-reaction.ts b/src/models/repositories/note-reaction.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7189da8e202e8103442472d9e63786795e837e22
--- /dev/null
+++ b/src/models/repositories/note-reaction.ts
@@ -0,0 +1,18 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { NoteReaction } from '../entities/note-reaction';
+import { Users } from '..';
+
+@EntityRepository(NoteReaction)
+export class NoteReactionRepository extends Repository<NoteReaction> {
+	public async pack(
+		src: NoteReaction['id'] | NoteReaction,
+		me?: any
+	) {
+		const reaction = typeof src === 'object' ? src : await this.findOne(src);
+
+		return {
+			id: reaction.id,
+			user: await Users.pack(reaction.userId, me),
+		};
+	}
+}
diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4df0135115384168a7a47f0080f3f63b7ab0445a
--- /dev/null
+++ b/src/models/repositories/note.ts
@@ -0,0 +1,210 @@
+import { EntityRepository, Repository, In } from 'typeorm';
+import { Note } from '../entities/note';
+import { User } from '../entities/user';
+import { unique, concat } from '../../prelude/array';
+import { nyaize } from '../../misc/nyaize';
+import { Emojis, Users, Apps, PollVotes, DriveFiles, NoteReactions, Followings, Polls } from '..';
+import rap from '@prezzemolo/rap';
+
+@EntityRepository(Note)
+export class NoteRepository extends Repository<Note> {
+	public validateCw(x: string) {
+		return x.trim().length <= 100;
+	}
+
+	private async hideNote(packedNote: any, meId: User['id']) {
+		let hide = false;
+
+		// visibility が specified かつ自分が指定されていなかったら非表示
+		if (packedNote.visibility == 'specified') {
+			if (meId == null) {
+				hide = true;
+			} else if (meId === packedNote.userId) {
+				hide = false;
+			} else {
+				// 指定されているかどうか
+				const specified = packedNote.visibleUserIds.some((id: any) => meId === id);
+
+				if (specified) {
+					hide = false;
+				} else {
+					hide = true;
+				}
+			}
+		}
+
+		// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
+		if (packedNote.visibility == 'followers') {
+			if (meId == null) {
+				hide = true;
+			} else if (meId === packedNote.userId) {
+				hide = false;
+			} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
+				// 自分の投稿に対するリプライ
+				hide = false;
+			} else if (packedNote.mentions && packedNote.mentions.some((id: any) => meId === id)) {
+				// 自分へのメンション
+				hide = false;
+			} else {
+				// フォロワーかどうか
+				const following = await Followings.findOne({
+					followeeId: packedNote.userId,
+					followerId: meId
+				});
+
+				if (following == null) {
+					hide = true;
+				} else {
+					hide = false;
+				}
+			}
+		}
+
+		if (hide) {
+			packedNote.visibleUserIds = null;
+			packedNote.fileIds = [];
+			packedNote.files = [];
+			packedNote.text = null;
+			packedNote.poll = null;
+			packedNote.cw = null;
+			packedNote.tags = [];
+			packedNote.geo = null;
+			packedNote.isHidden = true;
+		}
+	}
+
+	public packMany(
+		notes: (Note['id'] | Note)[],
+		me?: User['id'] | User,
+		options?: {
+			detail?: boolean;
+			skipHide?: boolean;
+		}
+	) {
+		return Promise.all(notes.map(n => this.pack(n, me, options)));
+	}
+
+	public async pack(
+		src: Note['id'] | Note,
+		me?: User['id'] | User,
+		options?: {
+			detail?: boolean;
+			skipHide?: boolean;
+		}
+	): Promise<Record<string, any>> {
+		const opts = Object.assign({
+			detail: true,
+			skipHide: false
+		}, options);
+
+		const meId = me ? typeof me === 'string' ? me : me.id : null;
+		const note = typeof src === 'object' ? src : await this.findOne(src);
+		const host = note.userHost;
+
+		async function populatePoll() {
+			const poll = await Polls.findOne({ noteId: note.id });
+			const choices = poll.choices.map(c => ({
+				text: c,
+				votes: poll.votes[poll.choices.indexOf(c)],
+				isVoted: false
+			}));
+
+			if (poll.multiple) {
+				const votes = await PollVotes.find({
+					userId: meId,
+					noteId: note.id
+				});
+
+				const myChoices = votes.map(v => v.choice);
+				for (const myChoice of myChoices) {
+					choices[myChoice].isVoted = true;
+				}
+			} else {
+				const vote = await PollVotes.findOne({
+					userId: meId,
+					noteId: note.id
+				});
+
+				if (vote) {
+					choices[vote.choice].isVoted = true;
+				}
+			}
+
+			return {
+				multiple: poll.multiple,
+				expiresAt: poll.expiresAt,
+				choices
+			};
+		}
+
+		async function populateMyReaction() {
+			const reaction = await NoteReactions.findOne({
+				userId: meId,
+				noteId: note.id,
+			});
+
+			if (reaction) {
+				return reaction.reaction;
+			}
+
+			return null;
+		}
+
+		let text = note.text;
+
+		if (note.name) {
+			text = `【${note.name}】\n${note.text}`;
+		}
+
+		const reactionEmojis = unique(concat([note.emojis, Object.keys(note.reactions)]));
+
+		const packed = await rap({
+			id: note.id,
+			createdAt: note.createdAt,
+			app: note.appId ? Apps.pack(note.appId) : null,
+			userId: note.userId,
+			user: Users.pack(note.user || note.userId, meId),
+			text: text,
+			cw: note.cw,
+			visibility: note.visibility,
+			visibleUserIds: note.visibleUserIds,
+			viaMobile: note.viaMobile,
+			reactions: note.reactions,
+			emojis: reactionEmojis.length > 0 ? Emojis.find({
+				name: In(reactionEmojis),
+				host: host
+			}) : [],
+			tags: note.tags,
+			fileIds: note.fileIds,
+			files: DriveFiles.packMany(note.fileIds),
+			replyId: note.replyId,
+			renoteId: note.renoteId,
+
+			...(opts.detail ? {
+				reply: note.replyId ? this.pack(note.replyId, meId, {
+					detail: false
+				}) : null,
+
+				renote: note.renoteId ? this.pack(note.renoteId, meId, {
+					detail: false
+				}) : null,
+
+				poll: note.hasPoll ? populatePoll() : null,
+
+				...(meId ? {
+					myReaction: populateMyReaction()
+				} : {})
+			} : {})
+		});
+
+		if (packed.user.isCat && packed.text) {
+			packed.text = nyaize(packed.text);
+		}
+
+		if (!opts.skipHide) {
+			await this.hideNote(packed, meId);
+		}
+
+		return packed;
+	}
+}
diff --git a/src/models/repositories/notification.ts b/src/models/repositories/notification.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9bc569cd3f716f9ae479030e04122ef644556852
--- /dev/null
+++ b/src/models/repositories/notification.ts
@@ -0,0 +1,47 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users, Notes } from '..';
+import rap from '@prezzemolo/rap';
+import { Notification } from '../entities/notification';
+
+@EntityRepository(Notification)
+export class NotificationRepository extends Repository<Notification> {
+	public packMany(
+		notifications: any[],
+	) {
+		return Promise.all(notifications.map(x => this.pack(x)));
+	}
+
+	public async pack(
+		src: Notification['id'] | Notification,
+	) {
+		const notification = typeof src === 'object' ? src : await this.findOne(src);
+
+		return await rap({
+			id: notification.id,
+			createdAt: notification.createdAt,
+			type: notification.type,
+			userId: notification.notifierId,
+			user: Users.pack(notification.notifier || notification.notifierId),
+			...(notification.type === 'mention' ? {
+				note: Notes.pack(notification.note || notification.noteId),
+			} : {}),
+			...(notification.type === 'reply' ? {
+				note: Notes.pack(notification.note || notification.noteId),
+			} : {}),
+			...(notification.type === 'renote' ? {
+				note: Notes.pack(notification.note || notification.noteId),
+			} : {}),
+			...(notification.type === 'quote' ? {
+				note: Notes.pack(notification.note || notification.noteId),
+			} : {}),
+			...(notification.type === 'reaction' ? {
+				note: Notes.pack(notification.note || notification.noteId),
+				reaction: notification.reaction
+			} : {}),
+			...(notification.type === 'pollVote' ? {
+				note: Notes.pack(notification.note || notification.noteId),
+				choice: notification.choice
+			} : {})
+		});
+	}
+}
diff --git a/src/models/repositories/signin.ts b/src/models/repositories/signin.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f5b90c0e9ebbf86dc46dc918051b928f369a50ad
--- /dev/null
+++ b/src/models/repositories/signin.ts
@@ -0,0 +1,11 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Signin } from '../entities/signin';
+
+@EntityRepository(Signin)
+export class SigninRepository extends Repository<Signin> {
+	public async pack(
+		src: any,
+	) {
+		return src;
+	}
+}
diff --git a/src/models/repositories/user-list.ts b/src/models/repositories/user-list.ts
new file mode 100644
index 0000000000000000000000000000000000000000..921c18ca7aa5027d53f0f398e02444bc7f2698f0
--- /dev/null
+++ b/src/models/repositories/user-list.ts
@@ -0,0 +1,16 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { UserList } from '../entities/user-list';
+
+@EntityRepository(UserList)
+export class UserListRepository extends Repository<UserList> {
+	public async pack(
+		src: any,
+	) {
+		const userList = typeof src === 'object' ? src : await this.findOne(src);
+
+		return {
+			id: userList.id,
+			name: userList.name
+		};
+	}
+}
diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7c4cc545cf518b8899a47a4383cd8caec5d3a919
--- /dev/null
+++ b/src/models/repositories/user.ts
@@ -0,0 +1,198 @@
+import { EntityRepository, Repository, In } from 'typeorm';
+import { User, ILocalUser, IRemoteUser } from '../entities/user';
+import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings } from '..';
+import rap from '@prezzemolo/rap';
+
+@EntityRepository(User)
+export class UserRepository extends Repository<User> {
+	public async getRelation(me: User['id'], target: User['id']) {
+		const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
+			Followings.findOne({
+				followerId: me,
+				followeeId: target
+			}),
+			Followings.findOne({
+				followerId: target,
+				followeeId: me
+			}),
+			FollowRequests.findOne({
+				followerId: me,
+				followeeId: target
+			}),
+			FollowRequests.findOne({
+				followerId: target,
+				followeeId: me
+			}),
+			Blockings.findOne({
+				blockerId: me,
+				blockeeId: target
+			}),
+			Blockings.findOne({
+				blockerId: target,
+				blockeeId: me
+			}),
+			Mutings.findOne({
+				muterId: me,
+				muteeId: target
+			})
+		]);
+
+		return {
+			id: target,
+			isFollowing: following1 != null,
+			hasPendingFollowRequestFromYou: followReq1 != null,
+			hasPendingFollowRequestToYou: followReq2 != null,
+			isFollowed: following2 != null,
+			isBlocking: toBlocking != null,
+			isBlocked: fromBlocked != null,
+			isMuted: mute != null
+		};
+	}
+
+	public packMany(
+		users: (User['id'] | User)[],
+		me?: User['id'] | User,
+		options?: {
+			detail?: boolean,
+			includeSecrets?: boolean,
+			includeHasUnreadNotes?: boolean
+		}
+	) {
+		return Promise.all(users.map(u => this.pack(u, me, options)));
+	}
+
+	public async pack(
+		src: User['id'] | User,
+		me?: User['id'] | User,
+		options?: {
+			detail?: boolean,
+			includeSecrets?: boolean,
+			includeHasUnreadNotes?: boolean
+		}
+	): Promise<Record<string, any>> {
+		const opts = Object.assign({
+			detail: false,
+			includeSecrets: false
+		}, options);
+
+		const user = typeof src === 'object' ? src : await this.findOne(src);
+		const meId = me ? typeof me === 'string' ? me : me.id : null;
+
+		const relation = meId && (meId !== user.id) && opts.detail ? await this.getRelation(meId, user.id) : null;
+		const pins = opts.detail ? await UserNotePinings.find({ userId: user.id }) : [];
+
+		return await rap({
+			id: user.id,
+			name: user.name,
+			username: user.username,
+			host: user.host,
+			avatarUrl: user.avatarUrl,
+			bannerUrl: user.bannerUrl,
+			avatarColor: user.avatarColor,
+			bannerColor: user.bannerColor,
+			isAdmin: user.isAdmin,
+
+			// カスタム絵文字添付
+			emojis: user.emojis.length > 0 ? Emojis.find({
+				where: {
+					name: In(user.emojis),
+					host: user.host
+				},
+				select: ['name', 'host', 'url', 'aliases']
+			}) : [],
+
+			...(opts.includeHasUnreadNotes ? {
+				hasUnreadSpecifiedNotes: NoteUnreads.count({
+					where: { userId: user.id, isSpecified: true },
+					take: 1
+				}).then(count => count > 0),
+				hasUnreadMentions: NoteUnreads.count({
+					where: { userId: user.id },
+					take: 1
+				}).then(count => count > 0),
+			} : {}),
+
+			...(opts.detail ? {
+				description: user.description,
+				location: user.location,
+				birthday: user.birthday,
+				followersCount: user.followersCount,
+				followingCount: user.followingCount,
+				notesCount: user.notesCount,
+				pinnedNoteIds: pins.map(pin => pin.noteId),
+				pinnedNotes: Notes.packMany(pins.map(pin => pin.noteId), meId, {
+					detail: true
+				}),
+			} : {}),
+
+			...(opts.detail && meId === user.id ? {
+				avatarId: user.avatarId,
+				bannerId: user.bannerId,
+				autoWatch: user.autoWatch,
+				alwaysMarkNsfw: user.alwaysMarkNsfw,
+				carefulBot: user.carefulBot,
+				hasUnreadMessagingMessage: MessagingMessages.count({
+					where: {
+						recipientId: user.id,
+						isRead: false
+					},
+					take: 1
+				}).then(count => count > 0),
+				hasUnreadNotification: Notifications.count({
+					where: {
+						userId: user.id,
+						isRead: false
+					},
+					take: 1
+				}).then(count => count > 0),
+				pendingReceivedFollowRequestsCount: FollowRequests.count({
+					followeeId: user.id
+				}),
+			} : {}),
+
+			...(relation ? {
+				isFollowing: relation.isFollowing,
+				isFollowed: relation.isFollowed,
+				hasPendingFollowRequestFromYou: relation.hasPendingFollowRequestFromYou,
+				hasPendingFollowRequestToYou: relation.hasPendingFollowRequestToYou,
+				isBlocking: relation.isBlocking,
+				isBlocked: relation.isBlocked,
+				isMuted: relation.isMuted,
+			} : {})
+		});
+	}
+
+	public isLocalUser(user: User): user is ILocalUser {
+		return user.host === null;
+	}
+
+	public isRemoteUser(user: User): user is IRemoteUser {
+		return !this.isLocalUser(user);
+	}
+
+	//#region Validators
+	public validateUsername(username: string, remote = false): boolean {
+		return typeof username == 'string' && (remote ? /^\w([\w-]*\w)?$/ : /^\w{1,20}$/).test(username);
+	}
+
+	public validatePassword(password: string): boolean {
+		return typeof password == 'string' && password != '';
+	}
+
+	public isValidName(name?: string): boolean {
+		return name === null || (typeof name == 'string' && name.length < 50 && name.trim() != '');
+	}
+
+	public isValidDescription(description: string): boolean {
+		return typeof description == 'string' && description.length < 500 && description.trim() != '';
+	}
+
+	public isValidLocation(location: string): boolean {
+		return typeof location == 'string' && location.length < 50 && location.trim() != '';
+	}
+
+	public isValidBirthday(birthday: string): boolean {
+		return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday);
+	}
+	//#endregion
+}
diff --git a/src/models/signin.ts b/src/models/signin.ts
deleted file mode 100644
index d8b05c0e3060dcc91ea16616c15e9dfb67a1f331..0000000000000000000000000000000000000000
--- a/src/models/signin.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-
-const Signin = db.get<ISignin>('signin');
-export default Signin;
-
-export interface ISignin {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	userId: mongo.ObjectID;
-	ip: string;
-	headers: any;
-	success: boolean;
-}
-
-/**
- * Pack a signin record for API response
- *
- * @param {any} record
- * @return {Promise<any>}
- */
-export const pack = (
-	record: any
-) => new Promise<any>(async (resolve, reject) => {
-
-	const _record = deepcopy(record);
-
-	// Rename _id to id
-	_record.id = _record._id;
-	delete _record._id;
-
-	resolve(_record);
-});
diff --git a/src/models/sw-subscription.ts b/src/models/sw-subscription.ts
deleted file mode 100644
index 743d0d2dd9a94dacf3c14032102b91bfb11fa59b..0000000000000000000000000000000000000000
--- a/src/models/sw-subscription.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import * as mongo from 'mongodb';
-import db from '../db/mongodb';
-
-const SwSubscription = db.get<ISwSubscription>('swSubscriptions');
-export default SwSubscription;
-
-export interface ISwSubscription {
-	_id: mongo.ObjectID;
-	userId: mongo.ObjectID;
-	endpoint: string;
-	auth: string;
-	publickey: string;
-}
diff --git a/src/models/user-list.ts b/src/models/user-list.ts
deleted file mode 100644
index e7dd74bdd176cfab1ce15e7c544384e2c3cb0331..0000000000000000000000000000000000000000
--- a/src/models/user-list.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-
-const UserList = db.get<IUserList>('userList');
-export default UserList;
-
-export interface IUserList {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	title: string;
-	userId: mongo.ObjectID;
-	userIds: mongo.ObjectID[];
-}
-
-export const pack = (
-	userList: string | mongo.ObjectID | IUserList
-) => new Promise<any>(async (resolve, reject) => {
-	let _userList: any;
-
-	if (isObjectId(userList)) {
-		_userList = await UserList.findOne({
-			_id: userList
-		});
-	} else if (typeof userList === 'string') {
-		_userList = await UserList.findOne({
-			_id: new mongo.ObjectID(userList)
-		});
-	} else {
-		_userList = deepcopy(userList);
-	}
-
-	if (!_userList) throw `invalid userList arg ${userList}`;
-
-	// Rename _id to id
-	_userList.id = _userList._id;
-	delete _userList._id;
-
-	resolve(_userList);
-});
diff --git a/src/models/user.ts b/src/models/user.ts
deleted file mode 100644
index 0c3f7b5508792e4282f3dffb6955875c2fb68f6c..0000000000000000000000000000000000000000
--- a/src/models/user.ts
+++ /dev/null
@@ -1,438 +0,0 @@
-import * as mongo from 'mongodb';
-import * as deepcopy from 'deepcopy';
-import rap from '@prezzemolo/rap';
-import db from '../db/mongodb';
-import isObjectId from '../misc/is-objectid';
-import { packMany as packNoteMany } from './note';
-import Following from './following';
-import Blocking from './blocking';
-import Mute from './mute';
-import { getFriendIds } from '../server/api/common/get-friends';
-import config from '../config';
-import FollowRequest from './follow-request';
-import fetchMeta from '../misc/fetch-meta';
-import Emoji from './emoji';
-import { dbLogger } from '../db/logger';
-
-const User = db.get<IUser>('users');
-
-User.createIndex('createdAt');
-User.createIndex('updatedAt');
-User.createIndex('followersCount');
-User.createIndex('tags');
-User.createIndex('isSuspended');
-User.createIndex('username');
-User.createIndex('usernameLower');
-User.createIndex('host');
-User.createIndex(['username', 'host'], { unique: true });
-User.createIndex(['usernameLower', 'host'], { unique: true });
-User.createIndex('token', { sparse: true, unique: true });
-User.createIndex('uri', { sparse: true, unique: true });
-
-export default User;
-
-type IUserBase = {
-	_id: mongo.ObjectID;
-	createdAt: Date;
-	updatedAt?: Date;
-	deletedAt?: Date;
-	followersCount: number;
-	followingCount: number;
-	name?: string;
-	notesCount: number;
-	username: string;
-	usernameLower: string;
-	avatarId: mongo.ObjectID;
-	bannerId: mongo.ObjectID;
-	avatarUrl?: string;
-	bannerUrl?: string;
-	avatarColor?: any;
-	bannerColor?: any;
-	wallpaperId: mongo.ObjectID;
-	wallpaperUrl?: string;
-	data: any;
-	description: string;
-	lang?: string;
-	pinnedNoteIds: mongo.ObjectID[];
-	emojis?: string[];
-	tags?: string[];
-
-	isDeleted: boolean;
-
-	/**
-	 * 凍結されているか否か
-	 */
-	isSuspended: boolean;
-
-	/**
-	 * サイレンスされているか否か
-	 */
-	isSilenced: boolean;
-
-	/**
-	 * 鍵アカウントか否か
-	 */
-	isLocked: boolean;
-
-	/**
-	 * Botか否か
-	 */
-	isBot: boolean;
-
-	/**
-	 * Botからのフォローを承認制にするか
-	 */
-	carefulBot: boolean;
-
-	/**
-	 * フォローしているユーザーからのフォローリクエストを自動承認するか
-	 */
-	autoAcceptFollowed: boolean;
-
-	/**
-	 * このアカウントに届いているフォローリクエストの数
-	 */
-	pendingReceivedFollowRequestsCount: number;
-
-	host: string;
-};
-
-export interface ILocalUser extends IUserBase {
-	host: null;
-	keypair: string;
-	email: string;
-	emailVerified?: boolean;
-	emailVerifyCode?: string;
-	password: string;
-	token: string;
-	twitter: {
-		accessToken: string;
-		accessTokenSecret: string;
-		userId: string;
-		screenName: string;
-	};
-	github: {
-		accessToken: string;
-		id: string;
-		login: string;
-	};
-	discord: {
-		accessToken: string;
-		refreshToken: string;
-		expiresDate: number;
-		id: string;
-		username: string;
-		discriminator: string;
-	};
-	profile: {
-		location: string;
-		birthday: string; // 'YYYY-MM-DD'
-		tags: string[];
-	};
-	fields?: {
-		name: string;
-		value: string;
-	}[];
-	isCat: boolean;
-	isAdmin?: boolean;
-	isModerator?: boolean;
-	isVerified?: boolean;
-	twoFactorSecret: string;
-	twoFactorEnabled: boolean;
-	twoFactorTempSecret?: string;
-	clientSettings: any;
-	settings: {
-		autoWatch: boolean;
-		alwaysMarkNsfw?: boolean;
-	};
-	hasUnreadNotification: boolean;
-	hasUnreadMessagingMessage: boolean;
-}
-
-export interface IRemoteUser extends IUserBase {
-	inbox: string;
-	sharedInbox?: string;
-	featured?: string;
-	endpoints: string[];
-	uri: string;
-	url?: string;
-	publicKey: {
-		id: string;
-		publicKeyPem: string;
-	};
-	lastFetchedAt: Date;
-	isAdmin: false;
-	isModerator: false;
-}
-
-export type IUser = ILocalUser | IRemoteUser;
-
-export const isLocalUser = (user: any): user is ILocalUser =>
-	user.host === null;
-
-export const isRemoteUser = (user: any): user is IRemoteUser =>
-	!isLocalUser(user);
-
-//#region Validators
-export function validateUsername(username: string, remote = false): boolean {
-	return typeof username == 'string' && (remote ? /^\w([\w-]*\w)?$/ : /^\w{1,20}$/).test(username);
-}
-
-export function validatePassword(password: string): boolean {
-	return typeof password == 'string' && password != '';
-}
-
-export function isValidName(name?: string): boolean {
-	return name === null || (typeof name == 'string' && name.length < 50 && name.trim() != '');
-}
-
-export function isValidDescription(description: string): boolean {
-	return typeof description == 'string' && description.length < 500 && description.trim() != '';
-}
-
-export function isValidLocation(location: string): boolean {
-	return typeof location == 'string' && location.length < 50 && location.trim() != '';
-}
-
-export function isValidBirthday(birthday: string): boolean {
-	return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday);
-}
-//#endregion
-
-export async function getRelation(me: mongo.ObjectId, target: mongo.ObjectId) {
-	const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
-		Following.findOne({
-			followerId: me,
-			followeeId: target
-		}),
-		Following.findOne({
-			followerId: target,
-			followeeId: me
-		}),
-		FollowRequest.findOne({
-			followerId: me,
-			followeeId: target
-		}),
-		FollowRequest.findOne({
-			followerId: target,
-			followeeId: me
-		}),
-		Blocking.findOne({
-			blockerId: me,
-			blockeeId: target
-		}),
-		Blocking.findOne({
-			blockerId: target,
-			blockeeId: me
-		}),
-		Mute.findOne({
-			muterId: me,
-			muteeId: target
-		})
-	]);
-
-	return {
-		id: target,
-		isFollowing: following1 !== null,
-		hasPendingFollowRequestFromYou: followReq1 !== null,
-		hasPendingFollowRequestToYou: followReq2 !== null,
-		isFollowed: following2 !== null,
-		isBlocking: toBlocking !== null,
-		isBlocked: fromBlocked !== null,
-		isMuted: mute !== null
-	};
-}
-
-/**
- * Pack a user for API response
- *
- * @param user target
- * @param me? serializee
- * @param options? serialize options
- * @return Packed user
- */
-export const pack = (
-	user: string | mongo.ObjectID | IUser,
-	me?: string | mongo.ObjectID | IUser,
-	options?: {
-		detail?: boolean,
-		includeSecrets?: boolean,
-		includeHasUnreadNotes?: boolean
-	}
-) => new Promise<any>(async (resolve, reject) => {
-	const opts = Object.assign({
-		detail: false,
-		includeSecrets: false
-	}, options);
-
-	let _user: any;
-
-	const fields = opts.detail ? {} : {
-		name: true,
-		username: true,
-		host: true,
-		avatarColor: true,
-		avatarUrl: true,
-		emojis: true,
-		isCat: true,
-		isBot: true,
-		isAdmin: true,
-		isVerified: true
-	};
-
-	// Populate the user if 'user' is ID
-	if (isObjectId(user)) {
-		_user = await User.findOne({
-			_id: user
-		}, { fields });
-	} else if (typeof user === 'string') {
-		_user = await User.findOne({
-			_id: new mongo.ObjectID(user)
-		}, { fields });
-	} else {
-		_user = deepcopy(user);
-	}
-
-	// (データベースの欠損などで)ユーザーがデータベース上に見つからなかったとき
-	if (_user == null) {
-		dbLogger.warn(`user not found on database: ${user}`);
-		return resolve(null);
-	}
-
-	// Me
-	const meId: mongo.ObjectID = me
-		? isObjectId(me)
-			? me as mongo.ObjectID
-			: typeof me === 'string'
-				? new mongo.ObjectID(me)
-				: (me as IUser)._id
-		: null;
-
-	// Rename _id to id
-	_user.id = _user._id;
-	delete _user._id;
-
-	delete _user.usernameLower;
-	delete _user.emailVerifyCode;
-
-	if (_user.host == null) {
-		// Remove private properties
-		delete _user.keypair;
-		delete _user.password;
-		delete _user.token;
-		delete _user.twoFactorTempSecret;
-		delete _user.two_factor_temp_secret; // 後方互換性のため
-		delete _user.twoFactorSecret;
-		if (_user.twitter) {
-			delete _user.twitter.accessToken;
-			delete _user.twitter.accessTokenSecret;
-		}
-		if (_user.github) {
-			delete _user.github.accessToken;
-		}
-		if (_user.discord) {
-			delete _user.discord.accessToken;
-			delete _user.discord.refreshToken;
-			delete _user.discord.expiresDate;
-		}
-
-		// Visible via only the official client
-		if (!opts.includeSecrets) {
-			delete _user.email;
-			delete _user.emailVerified;
-			delete _user.settings;
-			delete _user.clientSettings;
-		}
-
-		if (!opts.detail) {
-			delete _user.twoFactorEnabled;
-		}
-	} else {
-		delete _user.publicKey;
-	}
-
-	if (_user.avatarUrl == null) {
-		_user.avatarUrl = `${config.driveUrl}/default-avatar.jpg`;
-	}
-
-	if (!meId || !meId.equals(_user.id) || !opts.detail) {
-		delete _user.avatarId;
-		delete _user.bannerId;
-		delete _user.hasUnreadMessagingMessage;
-		delete _user.hasUnreadNotification;
-	}
-
-	if (meId && !meId.equals(_user.id) && opts.detail) {
-		const relation = await getRelation(meId, _user.id);
-
-		_user.isFollowing = relation.isFollowing;
-		_user.isFollowed = relation.isFollowed;
-		_user.hasPendingFollowRequestFromYou = relation.hasPendingFollowRequestFromYou;
-		_user.hasPendingFollowRequestToYou = relation.hasPendingFollowRequestToYou;
-		_user.isBlocking = relation.isBlocking;
-		_user.isBlocked = relation.isBlocked;
-		_user.isMuted = relation.isMuted;
-	}
-
-	if (opts.detail) {
-		if (_user.pinnedNoteIds) {
-			// Populate pinned notes
-			_user.pinnedNotes = packNoteMany(_user.pinnedNoteIds, meId, {
-				detail: true
-			});
-		}
-
-		if (meId && !meId.equals(_user.id)) {
-			const myFollowingIds = await getFriendIds(meId);
-
-			// Get following you know count
-			_user.followingYouKnowCount = Following.count({
-				followeeId: { $in: myFollowingIds },
-				followerId: _user.id
-			});
-
-			// Get followers you know count
-			_user.followersYouKnowCount = Following.count({
-				followeeId: _user.id,
-				followerId: { $in: myFollowingIds }
-			});
-		}
-	}
-
-	if (!opts.includeHasUnreadNotes) {
-		delete _user.hasUnreadSpecifiedNotes;
-		delete _user.hasUnreadMentions;
-	}
-
-	// カスタム絵文字添付
-	if (_user.emojis) {
-		_user.emojis = Emoji.find({
-			name: { $in: _user.emojis },
-			host: _user.host
-		}, {
-			fields: { _id: false }
-		});
-	}
-
-	// resolve promises in _user object
-	_user = await rap(_user);
-
-	resolve(_user);
-});
-
-/*
-function img(url) {
-	return {
-		thumbnail: {
-			large: `${url}`,
-			medium: '',
-			small: ''
-		}
-	};
-}
-*/
-
-export async function fetchProxyAccount(): Promise<ILocalUser> {
-	const meta = await fetchMeta();
-	return await User.findOne({ username: meta.proxyAccount, host: null }) as ILocalUser;
-}
diff --git a/src/queue/index.ts b/src/queue/index.ts
index d8328a1d5758c92ca2dfe215c176a25b0aedcf41..728c43c6aced18a7493e0bf9d8c6d9fa0236efdc 100644
--- a/src/queue/index.ts
+++ b/src/queue/index.ts
@@ -2,14 +2,14 @@ import * as Queue from 'bull';
 import * as httpSignature from 'http-signature';
 
 import config from '../config';
-import { ILocalUser } from '../models/user';
+import { ILocalUser } from '../models/entities/user';
 import { program } from '../argv';
 
 import processDeliver from './processors/deliver';
 import processInbox from './processors/inbox';
 import processDb from './processors/db';
 import { queueLogger } from './logger';
-import { IDriveFile } from '../models/drive-file';
+import { DriveFile } from '../models/entities/drive-file';
 
 function initializeQueue(name: string) {
 	return new Queue(name, config.redis != null ? {
@@ -83,15 +83,6 @@ export function inbox(activity: any, signature: httpSignature.IParsedSignature)
 	});
 }
 
-export function createDeleteNotesJob(user: ILocalUser) {
-	return dbQueue.add('deleteNotes', {
-		user: user
-	}, {
-		removeOnComplete: true,
-		removeOnFail: true
-	});
-}
-
 export function createDeleteDriveFilesJob(user: ILocalUser) {
 	return dbQueue.add('deleteDriveFiles', {
 		user: user
@@ -146,7 +137,7 @@ export function createExportUserListsJob(user: ILocalUser) {
 	});
 }
 
-export function createImportFollowingJob(user: ILocalUser, fileId: IDriveFile['_id']) {
+export function createImportFollowingJob(user: ILocalUser, fileId: DriveFile['id']) {
 	return dbQueue.add('importFollowing', {
 		user: user,
 		fileId: fileId
@@ -156,7 +147,7 @@ export function createImportFollowingJob(user: ILocalUser, fileId: IDriveFile['_
 	});
 }
 
-export function createImportUserListsJob(user: ILocalUser, fileId: IDriveFile['_id']) {
+export function createImportUserListsJob(user: ILocalUser, fileId: DriveFile['id']) {
 	return dbQueue.add('importUserLists', {
 		user: user,
 		fileId: fileId
diff --git a/src/queue/processors/db/delete-drive-files.ts b/src/queue/processors/db/delete-drive-files.ts
index 3de960a25e3ab425d9cf4df70c27602bed6854bd..5f347fb58818732ead7bb9392110eb759ccdb1d2 100644
--- a/src/queue/processors/db/delete-drive-files.ts
+++ b/src/queue/processors/db/delete-drive-files.ts
@@ -1,18 +1,17 @@
 import * as Bull from 'bull';
-import * as mongo from 'mongodb';
 
 import { queueLogger } from '../../logger';
-import User from '../../../models/user';
-import DriveFile from '../../../models/drive-file';
 import deleteFile from '../../../services/drive/delete-file';
+import { Users, DriveFiles } from '../../../models';
+import { MoreThan } from 'typeorm';
 
 const logger = queueLogger.createSubLogger('delete-drive-files');
 
 export async function deleteDriveFiles(job: Bull.Job, done: any): Promise<void> {
-	logger.info(`Deleting drive files of ${job.data.user._id} ...`);
+	logger.info(`Deleting drive files of ${job.data.user.id} ...`);
 
-	const user = await User.findOne({
-		_id: new mongo.ObjectID(job.data.user._id.toString())
+	const user = await Users.findOne({
+		id: job.data.user.id
 	});
 
 	let deletedCount = 0;
@@ -20,13 +19,14 @@ export async function deleteDriveFiles(job: Bull.Job, done: any): Promise<void>
 	let cursor: any = null;
 
 	while (!ended) {
-		const files = await DriveFile.find({
-			userId: user._id,
-			...(cursor ? { _id: { $gt: cursor } } : {})
-		}, {
-			limit: 100,
-			sort: {
-				_id: 1
+		const files = await DriveFiles.find({
+			where: {
+				userId: user.id,
+				...(cursor ? { id: MoreThan(cursor) } : {})
+			},
+			take: 100,
+			order: {
+				id: 1
 			}
 		});
 
@@ -36,20 +36,20 @@ export async function deleteDriveFiles(job: Bull.Job, done: any): Promise<void>
 			break;
 		}
 
-		cursor = files[files.length - 1]._id;
+		cursor = files[files.length - 1].id;
 
 		for (const file of files) {
 			await deleteFile(file);
 			deletedCount++;
 		}
 
-		const total = await DriveFile.count({
-			userId: user._id,
+		const total = await DriveFiles.count({
+			userId: user.id,
 		});
 
 		job.progress(deletedCount / total);
 	}
 
-	logger.succ(`All drive files (${deletedCount}) of ${user._id} has been deleted.`);
+	logger.succ(`All drive files (${deletedCount}) of ${user.id} has been deleted.`);
 	done();
 }
diff --git a/src/queue/processors/db/delete-notes.ts b/src/queue/processors/db/delete-notes.ts
deleted file mode 100644
index 021db8062e7e40b9549b08f50d23c37050262989..0000000000000000000000000000000000000000
--- a/src/queue/processors/db/delete-notes.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import * as Bull from 'bull';
-import * as mongo from 'mongodb';
-
-import { queueLogger } from '../../logger';
-import Note from '../../../models/note';
-import deleteNote from '../../../services/note/delete';
-import User from '../../../models/user';
-
-const logger = queueLogger.createSubLogger('delete-notes');
-
-export async function deleteNotes(job: Bull.Job, done: any): Promise<void> {
-	logger.info(`Deleting notes of ${job.data.user._id} ...`);
-
-	const user = await User.findOne({
-		_id: new mongo.ObjectID(job.data.user._id.toString())
-	});
-
-	let deletedCount = 0;
-	let ended = false;
-	let cursor: any = null;
-
-	while (!ended) {
-		const notes = await Note.find({
-			userId: user._id,
-			...(cursor ? { _id: { $gt: cursor } } : {})
-		}, {
-			limit: 100,
-			sort: {
-				_id: 1
-			}
-		});
-
-		if (notes.length === 0) {
-			ended = true;
-			job.progress(100);
-			break;
-		}
-
-		cursor = notes[notes.length - 1]._id;
-
-		for (const note of notes) {
-			await deleteNote(user, note, true);
-			deletedCount++;
-		}
-
-		const total = await Note.count({
-			userId: user._id,
-		});
-
-		job.progress(deletedCount / total);
-	}
-
-	logger.succ(`All notes (${deletedCount}) of ${user._id} has been deleted.`);
-	done();
-}
diff --git a/src/queue/processors/db/export-blocking.ts b/src/queue/processors/db/export-blocking.ts
index 7f32c06472ebc5909afb3ce8cac217df52961975..c12aa4fca30839234c49efa5d2dc652a3d2ba8fe 100644
--- a/src/queue/processors/db/export-blocking.ts
+++ b/src/queue/processors/db/export-blocking.ts
@@ -1,22 +1,21 @@
 import * as Bull from 'bull';
 import * as tmp from 'tmp';
 import * as fs from 'fs';
-import * as mongo from 'mongodb';
 
 import { queueLogger } from '../../logger';
 import addFile from '../../../services/drive/add-file';
-import User from '../../../models/user';
 import dateFormat = require('dateformat');
-import Blocking from '../../../models/blocking';
 import { getFullApAccount } from '../../../misc/convert-host';
+import { Users, Blockings } from '../../../models';
+import { MoreThan } from 'typeorm';
 
 const logger = queueLogger.createSubLogger('export-blocking');
 
 export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
-	logger.info(`Exporting blocking of ${job.data.user._id} ...`);
+	logger.info(`Exporting blocking of ${job.data.user.id} ...`);
 
-	const user = await User.findOne({
-		_id: new mongo.ObjectID(job.data.user._id.toString())
+	const user = await Users.findOne({
+		id: job.data.user.id
 	});
 
 	// Create temp file
@@ -36,13 +35,14 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
 	let cursor: any = null;
 
 	while (!ended) {
-		const blockings = await Blocking.find({
-			blockerId: user._id,
-			...(cursor ? { _id: { $gt: cursor } } : {})
-		}, {
-			limit: 100,
-			sort: {
-				_id: 1
+		const blockings = await Blockings.find({
+			where: {
+				blockerId: user.id,
+				...(cursor ? { id: MoreThan(cursor) } : {})
+			},
+			take: 100,
+			order: {
+				id: 1
 			}
 		});
 
@@ -52,10 +52,10 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
 			break;
 		}
 
-		cursor = blockings[blockings.length - 1]._id;
+		cursor = blockings[blockings.length - 1].id;
 
 		for (const block of blockings) {
-			const u = await User.findOne({ _id: block.blockeeId }, { fields: { username: true, host: true } });
+			const u = await Users.findOne({ id: block.blockeeId });
 			const content = getFullApAccount(u.username, u.host);
 			await new Promise((res, rej) => {
 				stream.write(content + '\n', err => {
@@ -70,8 +70,8 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
 			exportedCount++;
 		}
 
-		const total = await Blocking.count({
-			blockerId: user._id,
+		const total = await Blockings.count({
+			blockerId: user.id,
 		});
 
 		job.progress(exportedCount / total);
@@ -83,7 +83,7 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
 	const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
 	const driveFile = await addFile(user, path, fileName);
 
-	logger.succ(`Exported to: ${driveFile._id}`);
+	logger.succ(`Exported to: ${driveFile.id}`);
 	cleanup();
 	done();
 }
diff --git a/src/queue/processors/db/export-following.ts b/src/queue/processors/db/export-following.ts
index 019414072a01a6cde5666cbde8bbc3037b5d0c6c..fb30df79fe67ddee5ba88bd5e6f6dcaf0539bd8d 100644
--- a/src/queue/processors/db/export-following.ts
+++ b/src/queue/processors/db/export-following.ts
@@ -1,22 +1,21 @@
 import * as Bull from 'bull';
 import * as tmp from 'tmp';
 import * as fs from 'fs';
-import * as mongo from 'mongodb';
 
 import { queueLogger } from '../../logger';
 import addFile from '../../../services/drive/add-file';
-import User from '../../../models/user';
 import dateFormat = require('dateformat');
-import Following from '../../../models/following';
 import { getFullApAccount } from '../../../misc/convert-host';
+import { Users, Followings } from '../../../models';
+import { MoreThan } from 'typeorm';
 
 const logger = queueLogger.createSubLogger('export-following');
 
 export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
-	logger.info(`Exporting following of ${job.data.user._id} ...`);
+	logger.info(`Exporting following of ${job.data.user.id} ...`);
 
-	const user = await User.findOne({
-		_id: new mongo.ObjectID(job.data.user._id.toString())
+	const user = await Users.findOne({
+		id: job.data.user.id
 	});
 
 	// Create temp file
@@ -36,13 +35,14 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
 	let cursor: any = null;
 
 	while (!ended) {
-		const followings = await Following.find({
-			followerId: user._id,
-			...(cursor ? { _id: { $gt: cursor } } : {})
-		}, {
-			limit: 100,
-			sort: {
-				_id: 1
+		const followings = await Followings.find({
+			where: {
+				followerId: user.id,
+				...(cursor ? { id: MoreThan(cursor) } : {})
+			},
+			take: 100,
+			order: {
+				id: 1
 			}
 		});
 
@@ -52,10 +52,10 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
 			break;
 		}
 
-		cursor = followings[followings.length - 1]._id;
+		cursor = followings[followings.length - 1].id;
 
 		for (const following of followings) {
-			const u = await User.findOne({ _id: following.followeeId }, { fields: { username: true, host: true } });
+			const u = await Users.findOne({ id: following.followeeId });
 			const content = getFullApAccount(u.username, u.host);
 			await new Promise((res, rej) => {
 				stream.write(content + '\n', err => {
@@ -70,8 +70,8 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
 			exportedCount++;
 		}
 
-		const total = await Following.count({
-			followerId: user._id,
+		const total = await Followings.count({
+			followerId: user.id,
 		});
 
 		job.progress(exportedCount / total);
@@ -83,7 +83,7 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
 	const fileName = 'following-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
 	const driveFile = await addFile(user, path, fileName);
 
-	logger.succ(`Exported to: ${driveFile._id}`);
+	logger.succ(`Exported to: ${driveFile.id}`);
 	cleanup();
 	done();
 }
diff --git a/src/queue/processors/db/export-mute.ts b/src/queue/processors/db/export-mute.ts
index 5ded7cf651985f396c69f1ed816e422d830b96a4..3aed526dc571e3367099d1cd8794bf43c97fab31 100644
--- a/src/queue/processors/db/export-mute.ts
+++ b/src/queue/processors/db/export-mute.ts
@@ -1,22 +1,21 @@
 import * as Bull from 'bull';
 import * as tmp from 'tmp';
 import * as fs from 'fs';
-import * as mongo from 'mongodb';
 
 import { queueLogger } from '../../logger';
 import addFile from '../../../services/drive/add-file';
-import User from '../../../models/user';
 import dateFormat = require('dateformat');
-import Mute from '../../../models/mute';
 import { getFullApAccount } from '../../../misc/convert-host';
+import { Users, Mutings } from '../../../models';
+import { MoreThan } from 'typeorm';
 
 const logger = queueLogger.createSubLogger('export-mute');
 
 export async function exportMute(job: Bull.Job, done: any): Promise<void> {
-	logger.info(`Exporting mute of ${job.data.user._id} ...`);
+	logger.info(`Exporting mute of ${job.data.user.id} ...`);
 
-	const user = await User.findOne({
-		_id: new mongo.ObjectID(job.data.user._id.toString())
+	const user = await Users.findOne({
+		id: job.data.user.id
 	});
 
 	// Create temp file
@@ -36,13 +35,14 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> {
 	let cursor: any = null;
 
 	while (!ended) {
-		const mutes = await Mute.find({
-			muterId: user._id,
-			...(cursor ? { _id: { $gt: cursor } } : {})
-		}, {
-			limit: 100,
-			sort: {
-				_id: 1
+		const mutes = await Mutings.find({
+			where: {
+				muterId: user.id,
+				...(cursor ? { id: MoreThan(cursor) } : {})
+			},
+			take: 100,
+			order: {
+				id: 1
 			}
 		});
 
@@ -52,10 +52,10 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> {
 			break;
 		}
 
-		cursor = mutes[mutes.length - 1]._id;
+		cursor = mutes[mutes.length - 1].id;
 
 		for (const mute of mutes) {
-			const u = await User.findOne({ _id: mute.muteeId }, { fields: { username: true, host: true } });
+			const u = await Users.findOne({ id: mute.muteeId });
 			const content = getFullApAccount(u.username, u.host);
 			await new Promise((res, rej) => {
 				stream.write(content + '\n', err => {
@@ -70,8 +70,8 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> {
 			exportedCount++;
 		}
 
-		const total = await Mute.count({
-			muterId: user._id,
+		const total = await Mutings.count({
+			muterId: user.id,
 		});
 
 		job.progress(exportedCount / total);
@@ -83,7 +83,7 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> {
 	const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
 	const driveFile = await addFile(user, path, fileName);
 
-	logger.succ(`Exported to: ${driveFile._id}`);
+	logger.succ(`Exported to: ${driveFile.id}`);
 	cleanup();
 	done();
 }
diff --git a/src/queue/processors/db/export-notes.ts b/src/queue/processors/db/export-notes.ts
index 8f3cdc5b997f5e1592a5bf06bb4a2ce1757d4601..92867ad82e38009c587ffcc753f7f8ee087b81f6 100644
--- a/src/queue/processors/db/export-notes.ts
+++ b/src/queue/processors/db/export-notes.ts
@@ -1,21 +1,22 @@
 import * as Bull from 'bull';
 import * as tmp from 'tmp';
 import * as fs from 'fs';
-import * as mongo from 'mongodb';
 
 import { queueLogger } from '../../logger';
-import Note, { INote } from '../../../models/note';
 import addFile from '../../../services/drive/add-file';
-import User from '../../../models/user';
 import dateFormat = require('dateformat');
+import { Users, Notes, Polls } from '../../../models';
+import { MoreThan } from 'typeorm';
+import { Note } from '../../../models/entities/note';
+import { Poll } from '../../../models/entities/poll';
 
 const logger = queueLogger.createSubLogger('export-notes');
 
 export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
-	logger.info(`Exporting notes of ${job.data.user._id} ...`);
+	logger.info(`Exporting notes of ${job.data.user.id} ...`);
 
-	const user = await User.findOne({
-		_id: new mongo.ObjectID(job.data.user._id.toString())
+	const user = await Users.findOne({
+		id: job.data.user.id
 	});
 
 	// Create temp file
@@ -46,13 +47,14 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
 	let cursor: any = null;
 
 	while (!ended) {
-		const notes = await Note.find({
-			userId: user._id,
-			...(cursor ? { _id: { $gt: cursor } } : {})
-		}, {
-			limit: 100,
-			sort: {
-				_id: 1
+		const notes = await Notes.find({
+			where: {
+				userId: user.id,
+				...(cursor ? { id: MoreThan(cursor) } : {})
+			},
+			take: 100,
+			order: {
+				id: 1
 			}
 		});
 
@@ -62,10 +64,14 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
 			break;
 		}
 
-		cursor = notes[notes.length - 1]._id;
+		cursor = notes[notes.length - 1].id;
 
 		for (const note of notes) {
-			const content = JSON.stringify(serialize(note));
+			let poll: Poll;
+			if (note.hasPoll) {
+				poll = await Polls.findOne({ noteId: note.id });
+			}
+			const content = JSON.stringify(serialize(note, poll));
 			await new Promise((res, rej) => {
 				stream.write(exportedNotesCount === 0 ? content : ',\n' + content, err => {
 					if (err) {
@@ -79,8 +85,8 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
 			exportedNotesCount++;
 		}
 
-		const total = await Note.count({
-			userId: user._id,
+		const total = await Notes.count({
+			userId: user.id,
 		});
 
 		job.progress(exportedNotesCount / total);
@@ -103,20 +109,20 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
 	const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.json';
 	const driveFile = await addFile(user, path, fileName);
 
-	logger.succ(`Exported to: ${driveFile._id}`);
+	logger.succ(`Exported to: ${driveFile.id}`);
 	cleanup();
 	done();
 }
 
-function serialize(note: INote): any {
+function serialize(note: Note, poll: Poll): any {
 	return {
-		id: note._id,
+		id: note.id,
 		text: note.text,
 		createdAt: note.createdAt,
 		fileIds: note.fileIds,
 		replyId: note.replyId,
 		renoteId: note.renoteId,
-		poll: note.poll,
+		poll: poll,
 		cw: note.cw,
 		viaMobile: note.viaMobile,
 		visibility: note.visibility,
diff --git a/src/queue/processors/db/export-user-lists.ts b/src/queue/processors/db/export-user-lists.ts
index dfbf152ec061c432a8eb656f34eb6bf035d80946..f3987cb0d2c4a86f76e705983233590b8eaa1693 100644
--- a/src/queue/processors/db/export-user-lists.ts
+++ b/src/queue/processors/db/export-user-lists.ts
@@ -1,26 +1,25 @@
 import * as Bull from 'bull';
 import * as tmp from 'tmp';
 import * as fs from 'fs';
-import * as mongo from 'mongodb';
 
 import { queueLogger } from '../../logger';
 import addFile from '../../../services/drive/add-file';
-import User from '../../../models/user';
 import dateFormat = require('dateformat');
-import UserList from '../../../models/user-list';
 import { getFullApAccount } from '../../../misc/convert-host';
+import { Users, UserLists, UserListJoinings } from '../../../models';
+import { In } from 'typeorm';
 
 const logger = queueLogger.createSubLogger('export-user-lists');
 
 export async function exportUserLists(job: Bull.Job, done: any): Promise<void> {
-	logger.info(`Exporting user lists of ${job.data.user._id} ...`);
+	logger.info(`Exporting user lists of ${job.data.user.id} ...`);
 
-	const user = await User.findOne({
-		_id: new mongo.ObjectID(job.data.user._id.toString())
+	const user = await Users.findOne({
+		id: job.data.user.id
 	});
 
-	const lists = await UserList.find({
-		userId: user._id
+	const lists = await UserLists.find({
+		userId: user.id
 	});
 
 	// Create temp file
@@ -36,18 +35,14 @@ export async function exportUserLists(job: Bull.Job, done: any): Promise<void> {
 	const stream = fs.createWriteStream(path, { flags: 'a' });
 
 	for (const list of lists) {
-		const users = await User.find({
-			_id: { $in: list.userIds }
-		}, {
-			fields: {
-				username: true,
-				host: true
-			}
+		const joinings = await UserListJoinings.find({ userListId: list.id });
+		const users = await Users.find({
+			id: In(joinings.map(j => j.userId))
 		});
 
 		for (const u of users) {
 			const acct = getFullApAccount(u.username, u.host);
-			const content = `${list.title},${acct}`;
+			const content = `${list.name},${acct}`;
 			await new Promise((res, rej) => {
 				stream.write(content + '\n', err => {
 					if (err) {
@@ -67,7 +62,7 @@ export async function exportUserLists(job: Bull.Job, done: any): Promise<void> {
 	const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
 	const driveFile = await addFile(user, path, fileName);
 
-	logger.succ(`Exported to: ${driveFile._id}`);
+	logger.succ(`Exported to: ${driveFile.id}`);
 	cleanup();
 	done();
 }
diff --git a/src/queue/processors/db/import-following.ts b/src/queue/processors/db/import-following.ts
index 069afa74c466ac736a3e91d09c6951e75dc0e299..2e646c1869445bbfe44e356f9542d2a08596c8ec 100644
--- a/src/queue/processors/db/import-following.ts
+++ b/src/queue/processors/db/import-following.ts
@@ -1,32 +1,27 @@
 import * as Bull from 'bull';
-import * as mongo from 'mongodb';
 
 import { queueLogger } from '../../logger';
-import User from '../../../models/user';
 import follow from '../../../services/following/create';
-import DriveFile from '../../../models/drive-file';
-import { getOriginalUrl } from '../../../misc/get-drive-file-url';
 import parseAcct from '../../../misc/acct/parse';
 import resolveUser from '../../../remote/resolve-user';
 import { downloadTextFile } from '../../../misc/download-text-file';
 import { isSelfHost, toDbHost } from '../../../misc/convert-host';
+import { Users, DriveFiles } from '../../../models';
 
 const logger = queueLogger.createSubLogger('import-following');
 
 export async function importFollowing(job: Bull.Job, done: any): Promise<void> {
-	logger.info(`Importing following of ${job.data.user._id} ...`);
+	logger.info(`Importing following of ${job.data.user.id} ...`);
 
-	const user = await User.findOne({
-		_id: new mongo.ObjectID(job.data.user._id.toString())
+	const user = await Users.findOne({
+		id: job.data.user.id
 	});
 
-	const file = await DriveFile.findOne({
-		_id: new mongo.ObjectID(job.data.fileId.toString())
+	const file = await DriveFiles.findOne({
+		id: job.data.fileId
 	});
 
-	const url = getOriginalUrl(file);
-
-	const csv = await downloadTextFile(url);
+	const csv = await downloadTextFile(file.url);
 
 	let linenum = 0;
 
@@ -36,10 +31,10 @@ export async function importFollowing(job: Bull.Job, done: any): Promise<void> {
 		try {
 			const { username, host } = parseAcct(line.trim());
 
-			let target = isSelfHost(host) ? await User.findOne({
+			let target = isSelfHost(host) ? await Users.findOne({
 				host: null,
 				usernameLower: username.toLowerCase()
-			}) : await User.findOne({
+			}) : await Users.findOne({
 				host: toDbHost(host),
 				usernameLower: username.toLowerCase()
 			});
@@ -55,9 +50,9 @@ export async function importFollowing(job: Bull.Job, done: any): Promise<void> {
 			}
 
 			// skip myself
-			if (target._id.equals(job.data.user._id)) continue;
+			if (target.id === job.data.user.id) continue;
 
-			logger.info(`Follow[${linenum}] ${target._id} ...`);
+			logger.info(`Follow[${linenum}] ${target.id} ...`);
 
 			follow(user, target);
 		} catch (e) {
diff --git a/src/queue/processors/db/import-user-lists.ts b/src/queue/processors/db/import-user-lists.ts
index 50d3c649d4003a8cb424387b43814164f7331b32..8be578589621679031c055ac841647a8738889d4 100644
--- a/src/queue/processors/db/import-user-lists.ts
+++ b/src/queue/processors/db/import-user-lists.ts
@@ -1,62 +1,59 @@
 import * as Bull from 'bull';
-import * as mongo from 'mongodb';
 
 import { queueLogger } from '../../logger';
-import User from '../../../models/user';
-import UserList from '../../../models/user-list';
-import DriveFile from '../../../models/drive-file';
-import { getOriginalUrl } from '../../../misc/get-drive-file-url';
 import parseAcct from '../../../misc/acct/parse';
 import resolveUser from '../../../remote/resolve-user';
 import { pushUserToUserList } from '../../../services/user-list/push';
 import { downloadTextFile } from '../../../misc/download-text-file';
 import { isSelfHost, toDbHost } from '../../../misc/convert-host';
+import { DriveFiles, Users, UserLists, UserListJoinings } from '../../../models';
+import { genId } from '../../../misc/gen-id';
 
 const logger = queueLogger.createSubLogger('import-user-lists');
 
 export async function importUserLists(job: Bull.Job, done: any): Promise<void> {
-	logger.info(`Importing user lists of ${job.data.user._id} ...`);
+	logger.info(`Importing user lists of ${job.data.user.id} ...`);
 
-	const user = await User.findOne({
-		_id: new mongo.ObjectID(job.data.user._id.toString())
+	const user = await Users.findOne({
+		id: job.data.user.id
 	});
 
-	const file = await DriveFile.findOne({
-		_id: new mongo.ObjectID(job.data.fileId.toString())
+	const file = await DriveFiles.findOne({
+		id: job.data.fileId
 	});
 
-	const url = getOriginalUrl(file);
-
-	const csv = await downloadTextFile(url);
+	const csv = await downloadTextFile(file.url);
 
 	for (const line of csv.trim().split('\n')) {
 		const listName = line.split(',')[0].trim();
 		const { username, host } = parseAcct(line.split(',')[1].trim());
 
-		let list = await UserList.findOne({
-			userId: user._id,
-			title: listName
+		let list = await UserLists.findOne({
+			userId: user.id,
+			name: listName
 		});
 
 		if (list == null) {
-			list = await UserList.insert({
+			list = await UserLists.save({
+				id: genId(),
 				createdAt: new Date(),
-				userId: user._id,
-				title: listName,
+				userId: user.id,
+				name: listName,
 				userIds: []
 			});
 		}
 
-		let target = isSelfHost(host) ? await User.findOne({
+		let target = isSelfHost(host) ? await Users.findOne({
 			host: null,
 			usernameLower: username.toLowerCase()
-		}) : await User.findOne({
+		}) : await Users.findOne({
 			host: toDbHost(host),
 			usernameLower: username.toLowerCase()
 		});
 
 		if (host == null && target == null) continue;
-		if (list.userIds.some(id => id.equals(target._id))) continue;
+
+		if (await UserListJoinings.findOne({ userListId: list.id, userId: target.id }) != null) continue;
 
 		if (target == null) {
 			target = await resolveUser(username, host);
diff --git a/src/queue/processors/db/index.ts b/src/queue/processors/db/index.ts
index 1bc9a9af7ce19bfe7c648aa761dafbfe756e4d86..921cdf7ab14e51f93bfa42e450db6242ee9679d8 100644
--- a/src/queue/processors/db/index.ts
+++ b/src/queue/processors/db/index.ts
@@ -1,5 +1,4 @@
 import * as Bull from 'bull';
-import { deleteNotes } from './delete-notes';
 import { deleteDriveFiles } from './delete-drive-files';
 import { exportNotes } from './export-notes';
 import { exportFollowing } from './export-following';
@@ -10,7 +9,6 @@ import { importFollowing } from './import-following';
 import { importUserLists } from './import-user-lists';
 
 const jobs = {
-	deleteNotes,
 	deleteDriveFiles,
 	exportNotes,
 	exportFollowing,
diff --git a/src/queue/processors/deliver.ts b/src/queue/processors/deliver.ts
index 28d3a17f6b5091501f6889c49b6536a5e41ab9d5..b9701c0c65ac1e8f66852c81a7c090d7d42636f7 100644
--- a/src/queue/processors/deliver.ts
+++ b/src/queue/processors/deliver.ts
@@ -1,9 +1,9 @@
 import * as Bull from 'bull';
 import request from '../../remote/activitypub/request';
 import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc';
-import Instance from '../../models/instance';
-import instanceChart from '../../services/chart/instance';
 import Logger from '../../services/logger';
+import { Instances } from '../../models';
+import { instanceChart } from '../../services/chart';
 
 const logger = new Logger('deliver');
 
@@ -21,13 +21,11 @@ export default async (job: Bull.Job) => {
 
 		// Update stats
 		registerOrFetchInstanceDoc(host).then(i => {
-			Instance.update({ _id: i._id }, {
-				$set: {
-					latestRequestSentAt: new Date(),
-					latestStatus: 200,
-					lastCommunicatedAt: new Date(),
-					isNotResponding: false
-				}
+			Instances.update(i.id, {
+				latestRequestSentAt: new Date(),
+				latestStatus: 200,
+				lastCommunicatedAt: new Date(),
+				isNotResponding: false
 			});
 
 			instanceChart.requestSent(i.host, true);
@@ -37,12 +35,10 @@ export default async (job: Bull.Job) => {
 	} catch (res) {
 		// Update stats
 		registerOrFetchInstanceDoc(host).then(i => {
-			Instance.update({ _id: i._id }, {
-				$set: {
-					latestRequestSentAt: new Date(),
-					latestStatus: res != null && res.hasOwnProperty('statusCode') ? res.statusCode : null,
-					isNotResponding: true
-				}
+			Instances.update(i.id, {
+				latestRequestSentAt: new Date(),
+				latestStatus: res != null && res.hasOwnProperty('statusCode') ? res.statusCode : null,
+				isNotResponding: true
 			});
 
 			instanceChart.requestSent(i.host, false);
diff --git a/src/queue/processors/inbox.ts b/src/queue/processors/inbox.ts
index 436f3335c8422fb257b4a12c2c3f0e06a4ee70b4..16badabcf7eeb32445f6cf6802e9cf0bfac6ddde 100644
--- a/src/queue/processors/inbox.ts
+++ b/src/queue/processors/inbox.ts
@@ -1,7 +1,7 @@
 import * as Bull from 'bull';
 import * as httpSignature from 'http-signature';
 import parseAcct from '../../misc/acct/parse';
-import User, { IRemoteUser } from '../../models/user';
+import { IRemoteUser } from '../../models/entities/user';
 import perform from '../../remote/activitypub/perform';
 import { resolvePerson, updatePerson } from '../../remote/activitypub/models/person';
 import { toUnicode } from 'punycode';
@@ -9,8 +9,10 @@ import { URL } from 'url';
 import { publishApLogStream } from '../../services/stream';
 import Logger from '../../services/logger';
 import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc';
-import Instance from '../../models/instance';
-import instanceChart from '../../services/chart/instance';
+import { Instances, Users, UserPublickeys } from '../../models';
+import { instanceChart } from '../../services/chart';
+import { UserPublickey } from '../../models/entities/user-publickey';
+import fetchMeta from '../../misc/fetch-meta';
 
 const logger = new Logger('inbox');
 
@@ -28,6 +30,7 @@ export default async (job: Bull.Job): Promise<void> => {
 
 	const keyIdLower = signature.keyId.toLowerCase();
 	let user: IRemoteUser;
+	let key: UserPublickey;
 
 	if (keyIdLower.startsWith('acct:')) {
 		const { username, host } = parseAcct(keyIdLower.slice('acct:'.length));
@@ -46,13 +49,17 @@ export default async (job: Bull.Job): Promise<void> => {
 
 		// ブロックしてたら中断
 		// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
-		const instance = await Instance.findOne({ host: host.toLowerCase() });
-		if (instance && instance.isBlocked) {
+		const meta = await fetchMeta();
+		if (meta.blockedHosts.includes(host.toLowerCase())) {
 			logger.info(`Blocked request: ${host}`);
 			return;
 		}
 
-		user = await User.findOne({ usernameLower: username, host: host.toLowerCase() }) as IRemoteUser;
+		user = await Users.findOne({ usernameLower: username, host: host.toLowerCase() }) as IRemoteUser;
+
+		key = await UserPublickeys.findOne({
+			userId: user.id
+		});
 	} else {
 		// アクティビティ内のホストの検証
 		const host = toUnicode(new URL(signature.keyId).hostname.toLowerCase());
@@ -65,16 +72,17 @@ export default async (job: Bull.Job): Promise<void> => {
 
 		// ブロックしてたら中断
 		// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
-		const instance = await Instance.findOne({ host: host.toLowerCase() });
-		if (instance && instance.isBlocked) {
-			logger.warn(`Blocked request: ${host}`);
+		const meta = await fetchMeta();
+		if (meta.blockedHosts.includes(host.toLowerCase())) {
+			logger.info(`Blocked request: ${host}`);
 			return;
 		}
 
-		user = await User.findOne({
-			host: { $ne: null },
-			'publicKey.id': signature.keyId
-		}) as IRemoteUser;
+		key = await UserPublickeys.findOne({
+			keyId: signature.keyId
+		});
+
+		user = await Users.findOne(key.userId) as IRemoteUser;
 	}
 
 	// Update Person activityの場合は、ここで署名検証/更新処理まで実施して終了
@@ -82,7 +90,7 @@ export default async (job: Bull.Job): Promise<void> => {
 		if (activity.object && activity.object.type === 'Person') {
 			if (user == null) {
 				logger.warn('Update activity received, but user not registed.');
-			} else if (!httpSignature.verifySignature(signature, user.publicKey.publicKeyPem)) {
+			} else if (!httpSignature.verifySignature(signature, key.keyPem)) {
 				logger.warn('Update activity received, but signature verification failed.');
 			} else {
 				updatePerson(activity.actor, null, activity.object);
@@ -92,15 +100,15 @@ export default async (job: Bull.Job): Promise<void> => {
 	}
 
 	// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する
-	if (user === null) {
+	if (user == null) {
 		user = await resolvePerson(activity.actor) as IRemoteUser;
 	}
 
-	if (user === null) {
+	if (user == null) {
 		throw new Error('failed to resolve user');
 	}
 
-	if (!httpSignature.verifySignature(signature, user.publicKey.publicKeyPem)) {
+	if (!httpSignature.verifySignature(signature, key.keyPem)) {
 		logger.error('signature verification failed');
 		return;
 	}
@@ -116,12 +124,10 @@ export default async (job: Bull.Job): Promise<void> => {
 
 	// Update stats
 	registerOrFetchInstanceDoc(user.host).then(i => {
-		Instance.update({ _id: i._id }, {
-			$set: {
-				latestRequestReceivedAt: new Date(),
-				lastCommunicatedAt: new Date(),
-				isNotResponding: false
-			}
+		Instances.update(i.id, {
+			latestRequestReceivedAt: new Date(),
+			lastCommunicatedAt: new Date(),
+			isNotResponding: false
 		});
 
 		instanceChart.requestReceived(i.host);
diff --git a/src/remote/activitypub/kernel/accept/follow.ts b/src/remote/activitypub/kernel/accept/follow.ts
index 07c820c28a2f6d5e10a799d271f76c572d147929..816fcbadbfe5b95a733679e52f395f029fce8f00 100644
--- a/src/remote/activitypub/kernel/accept/follow.ts
+++ b/src/remote/activitypub/kernel/accept/follow.ts
@@ -1,8 +1,8 @@
-import * as mongo from 'mongodb';
-import User, { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
 import config from '../../../../config';
 import accept from '../../../../services/following/requests/accept';
 import { IFollow } from '../../type';
+import { Users } from '../../../../models';
 
 export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
 	const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id;
@@ -11,11 +11,11 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
 		return null;
 	}
 
-	const follower = await User.findOne({
-		_id: new mongo.ObjectID(id.split('/').pop())
+	const follower = await Users.findOne({
+		id: id.split('/').pop()
 	});
 
-	if (follower === null) {
+	if (follower == null) {
 		throw new Error('follower not found');
 	}
 
diff --git a/src/remote/activitypub/kernel/accept/index.ts b/src/remote/activitypub/kernel/accept/index.ts
index 443c1935d68a210c1407a299ae022ec1656b87fa..5a27ce1d4d193a58a0e0fee63b44fc2fcb63272b 100644
--- a/src/remote/activitypub/kernel/accept/index.ts
+++ b/src/remote/activitypub/kernel/accept/index.ts
@@ -1,5 +1,5 @@
 import Resolver from '../../resolver';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
 import acceptFollow from './follow';
 import { IAccept, IFollow } from '../../type';
 import { apLogger } from '../../logger';
diff --git a/src/remote/activitypub/kernel/add/index.ts b/src/remote/activitypub/kernel/add/index.ts
index eb2dba5b21f2f241f19e43472c8d090a65e8b16f..d16f0a4a0d33239b7a663c55beb7a7949a0023af 100644
--- a/src/remote/activitypub/kernel/add/index.ts
+++ b/src/remote/activitypub/kernel/add/index.ts
@@ -1,4 +1,4 @@
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
 import { IAdd } from '../../type';
 import { resolveNote } from '../../models/note';
 import { addPinned } from '../../../../services/i/pin';
@@ -14,7 +14,7 @@ export default async (actor: IRemoteUser, activity: IAdd): Promise<void> => {
 
 	if (activity.target === actor.featured) {
 		const note = await resolveNote(activity.object);
-		await addPinned(actor, note._id);
+		await addPinned(actor, note.id);
 		return;
 	}
 
diff --git a/src/remote/activitypub/kernel/announce/index.ts b/src/remote/activitypub/kernel/announce/index.ts
index 5f738da6c76104bdd1170d39ef64dd23ed320d16..ebd5a27b929fd7670d35ebf870706449e83d65e5 100644
--- a/src/remote/activitypub/kernel/announce/index.ts
+++ b/src/remote/activitypub/kernel/announce/index.ts
@@ -1,5 +1,5 @@
 import Resolver from '../../resolver';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
 import announceNote from './note';
 import { IAnnounce, INote } from '../../type';
 import { apLogger } from '../../logger';
diff --git a/src/remote/activitypub/kernel/announce/note.ts b/src/remote/activitypub/kernel/announce/note.ts
index 912936bef823dc1b00546b761f15f1bf84e5dbeb..403fc66bedcc76d715baff62d0b6c455b921b9c3 100644
--- a/src/remote/activitypub/kernel/announce/note.ts
+++ b/src/remote/activitypub/kernel/announce/note.ts
@@ -1,12 +1,12 @@
 import Resolver from '../../resolver';
 import post from '../../../../services/note/create';
-import { IRemoteUser, IUser } from '../../../../models/user';
+import { IRemoteUser, User } from '../../../../models/entities/user';
 import { IAnnounce, INote } from '../../type';
 import { fetchNote, resolveNote } from '../../models/note';
 import { resolvePerson } from '../../models/person';
 import { apLogger } from '../../logger';
 import { extractDbHost } from '../../../../misc/convert-host';
-import Instance from '../../../../models/instance';
+import fetchMeta from '../../../../misc/fetch-meta';
 
 const logger = apLogger;
 
@@ -27,8 +27,8 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
 
 	// アナウンス先をブロックしてたら中断
 	// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
-	const instance = await Instance.findOne({ host: extractDbHost(uri) });
-	if (instance && instance.isBlocked) return;
+	const meta = await fetchMeta();
+	if (meta.blockedHosts.includes(extractDbHost(uri))) return;
 
 	// 既に同じURIを持つものが登録されていないかチェック
 	const exist = await fetchNote(uri);
@@ -55,7 +55,7 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
 	//#region Visibility
 	const visibility = getVisibility(activity.to, activity.cc, actor);
 
-	let visibleUsers: IUser[] = [];
+	let visibleUsers: User[] = [];
 	if (visibility == 'specified') {
 		visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri)));
 	}
diff --git a/src/remote/activitypub/kernel/block/index.ts b/src/remote/activitypub/kernel/block/index.ts
index a10163016cab022a552fc9e66303713677b14512..48e251dd9b4adfcc004684f75eb8434f2e8189f6 100644
--- a/src/remote/activitypub/kernel/block/index.ts
+++ b/src/remote/activitypub/kernel/block/index.ts
@@ -1,9 +1,9 @@
-import * as mongo from 'mongodb';
-import User, { IRemoteUser } from '../../../../models/user';
 import config from '../../../../config';
 import { IBlock } from '../../type';
 import block from '../../../../services/blocking/create';
 import { apLogger } from '../../logger';
+import { Users } from '../../../../models';
+import { IRemoteUser } from '../../../../models/entities/user';
 
 const logger = apLogger;
 
@@ -18,11 +18,9 @@ export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => {
 		return null;
 	}
 
-	const blockee = await User.findOne({
-		_id: new mongo.ObjectID(id.split('/').pop())
-	});
+	const blockee = await Users.findOne(id.split('/').pop());
 
-	if (blockee === null) {
+	if (blockee == null) {
 		throw new Error('blockee not found');
 	}
 
diff --git a/src/remote/activitypub/kernel/create/image.ts b/src/remote/activitypub/kernel/create/image.ts
index 9c19abbcc41ee75b39b316ff9085e0386a88f246..7720e8f1bd37d121bdbc0abe2d92968dafa4f8e6 100644
--- a/src/remote/activitypub/kernel/create/image.ts
+++ b/src/remote/activitypub/kernel/create/image.ts
@@ -1,4 +1,4 @@
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
 import { createImage } from '../../models/image';
 
 export default async function(actor: IRemoteUser, image: any): Promise<void> {
diff --git a/src/remote/activitypub/kernel/create/index.ts b/src/remote/activitypub/kernel/create/index.ts
index 6e314d0b821ec7558e32939f778e49c56af1772f..0326b591f8f6cb53b8df9342cef19cc5e3843ef7 100644
--- a/src/remote/activitypub/kernel/create/index.ts
+++ b/src/remote/activitypub/kernel/create/index.ts
@@ -1,5 +1,5 @@
 import Resolver from '../../resolver';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
 import createImage from './image';
 import createNote from './note';
 import { ICreate } from '../../type';
diff --git a/src/remote/activitypub/kernel/create/note.ts b/src/remote/activitypub/kernel/create/note.ts
index 0f874b9fbf9ae7c3fd93b3152b410f6128d6589e..70e61bdf1b7b4be159bb44a41b421387fd3b37e6 100644
--- a/src/remote/activitypub/kernel/create/note.ts
+++ b/src/remote/activitypub/kernel/create/note.ts
@@ -1,5 +1,5 @@
 import Resolver from '../../resolver';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
 import { createNote, fetchNote } from '../../models/note';
 
 /**
diff --git a/src/remote/activitypub/kernel/delete/index.ts b/src/remote/activitypub/kernel/delete/index.ts
index c9c385b1fa5157476b318cd066c0cb491e8ec5c0..fab5e7ab64e722ab1df8055598091f5978d86358 100644
--- a/src/remote/activitypub/kernel/delete/index.ts
+++ b/src/remote/activitypub/kernel/delete/index.ts
@@ -1,9 +1,9 @@
 import Resolver from '../../resolver';
 import deleteNote from './note';
-import Note from '../../../../models/note';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
 import { IDelete } from '../../type';
 import { apLogger } from '../../logger';
+import { Notes } from '../../../../models';
 
 /**
  * 削除アクティビティを捌きます
@@ -27,7 +27,7 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise<void> => {
 		break;
 
 	case 'Tombstone':
-		const note = await Note.findOne({ uri });
+		const note = await Notes.findOne({ uri });
 		if (note != null) {
 			deleteNote(actor, uri);
 		}
diff --git a/src/remote/activitypub/kernel/delete/note.ts b/src/remote/activitypub/kernel/delete/note.ts
index f67919c56bcc4524749ccddd30cf03bf0f37e8df..b146e68a071e09c063a43bb7c312042cb9c5aec5 100644
--- a/src/remote/activitypub/kernel/delete/note.ts
+++ b/src/remote/activitypub/kernel/delete/note.ts
@@ -1,20 +1,20 @@
-import Note from '../../../../models/note';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
 import deleteNode from '../../../../services/note/delete';
 import { apLogger } from '../../logger';
+import { Notes } from '../../../../models';
 
 const logger = apLogger;
 
 export default async function(actor: IRemoteUser, uri: string): Promise<void> {
 	logger.info(`Deleting the Note: ${uri}`);
 
-	const note = await Note.findOne({ uri });
+	const note = await Notes.findOne({ uri });
 
 	if (note == null) {
 		throw new Error('note not found');
 	}
 
-	if (!note.userId.equals(actor._id)) {
+	if (note.userId !== actor.id) {
 		throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません');
 	}
 
diff --git a/src/remote/activitypub/kernel/follow.ts b/src/remote/activitypub/kernel/follow.ts
index e2db70b20d30d598719811a0a8ddebcc94f8859a..e6c8833f3a2acbdde30ffbc68271d18ef45e0d98 100644
--- a/src/remote/activitypub/kernel/follow.ts
+++ b/src/remote/activitypub/kernel/follow.ts
@@ -1,8 +1,8 @@
-import * as mongo from 'mongodb';
-import User, { IRemoteUser } from '../../../models/user';
+import { IRemoteUser } from '../../../models/entities/user';
 import config from '../../../config';
 import follow from '../../../services/following/create';
 import { IFollow } from '../type';
+import { Users } from '../../../models';
 
 export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
 	const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
@@ -11,11 +11,9 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
 		return null;
 	}
 
-	const followee = await User.findOne({
-		_id: new mongo.ObjectID(id.split('/').pop())
-	});
+	const followee = await Users.findOne(id.split('/').pop());
 
-	if (followee === null) {
+	if (followee == null) {
 		throw new Error('followee not found');
 	}
 
diff --git a/src/remote/activitypub/kernel/index.ts b/src/remote/activitypub/kernel/index.ts
index 4f7a5c91fdf54af35a6aa99dd41eb4d735971924..4a57d0675ed0e307c75056a81ed4a8d64adf4498 100644
--- a/src/remote/activitypub/kernel/index.ts
+++ b/src/remote/activitypub/kernel/index.ts
@@ -1,5 +1,5 @@
 import { Object } from '../type';
-import { IRemoteUser } from '../../../models/user';
+import { IRemoteUser } from '../../../models/entities/user';
 import create from './create';
 import performDeleteActivity from './delete';
 import performUpdateActivity from './update';
diff --git a/src/remote/activitypub/kernel/like.ts b/src/remote/activitypub/kernel/like.ts
index ed35da81332f430ed11d9860d93972c31ee85f93..86dd8fb33d0d718981e04ac6be7a81a07ba8c48d 100644
--- a/src/remote/activitypub/kernel/like.ts
+++ b/src/remote/activitypub/kernel/like.ts
@@ -1,8 +1,7 @@
-import * as mongo from 'mongodb';
-import Note from '../../../models/note';
-import { IRemoteUser } from '../../../models/user';
+import { IRemoteUser } from '../../../models/entities/user';
 import { ILike } from '../type';
 import create from '../../../services/note/reaction/create';
+import { Notes } from '../../../models';
 
 export default async (actor: IRemoteUser, activity: ILike) => {
 	const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
@@ -10,10 +9,10 @@ export default async (actor: IRemoteUser, activity: ILike) => {
 	// Transform:
 	// https://misskey.ex/notes/xxxx to
 	// xxxx
-	const noteId = new mongo.ObjectID(id.split('/').pop());
+	const noteId = id.split('/').pop();
 
-	const note = await Note.findOne({ _id: noteId });
-	if (note === null) {
+	const note = await Notes.findOne(noteId);
+	if (note == null) {
 		throw new Error();
 	}
 
diff --git a/src/remote/activitypub/kernel/reject/follow.ts b/src/remote/activitypub/kernel/reject/follow.ts
index 35cd2ec0c9d31b537d183e311e3ff8d9d93f3fad..b06ae6fb9690a61e989a230c50676f54bdc85f3e 100644
--- a/src/remote/activitypub/kernel/reject/follow.ts
+++ b/src/remote/activitypub/kernel/reject/follow.ts
@@ -1,8 +1,8 @@
-import * as mongo from 'mongodb';
-import User, { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
 import config from '../../../../config';
 import reject from '../../../../services/following/requests/reject';
 import { IFollow } from '../../type';
+import { Users } from '../../../../models';
 
 export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
 	const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id;
@@ -11,11 +11,9 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
 		return null;
 	}
 
-	const follower = await User.findOne({
-		_id: new mongo.ObjectID(id.split('/').pop())
-	});
+	const follower = await Users.findOne(id.split('/').pop());
 
-	if (follower === null) {
+	if (follower == null) {
 		throw new Error('follower not found');
 	}
 
diff --git a/src/remote/activitypub/kernel/reject/index.ts b/src/remote/activitypub/kernel/reject/index.ts
index c3585abbb6a689a4c37cf0917085515633b8dfca..8ece5cf174bf9aa6cfa9883ca607f4c3368c7e13 100644
--- a/src/remote/activitypub/kernel/reject/index.ts
+++ b/src/remote/activitypub/kernel/reject/index.ts
@@ -1,5 +1,5 @@
 import Resolver from '../../resolver';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
 import rejectFollow from './follow';
 import { IReject, IFollow } from '../../type';
 import { apLogger } from '../../logger';
diff --git a/src/remote/activitypub/kernel/remove/index.ts b/src/remote/activitypub/kernel/remove/index.ts
index 91b207c80dcfbb226d5633d87e40ae95bfcf55f5..ae33be59dc28bf2b5d1376e1f66506d08ecdf17d 100644
--- a/src/remote/activitypub/kernel/remove/index.ts
+++ b/src/remote/activitypub/kernel/remove/index.ts
@@ -1,4 +1,4 @@
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
 import { IRemove } from '../../type';
 import { resolveNote } from '../../models/note';
 import { removePinned } from '../../../../services/i/pin';
@@ -14,7 +14,7 @@ export default async (actor: IRemoteUser, activity: IRemove): Promise<void> => {
 
 	if (activity.target === actor.featured) {
 		const note = await resolveNote(activity.object);
-		await removePinned(actor, note._id);
+		await removePinned(actor, note.id);
 		return;
 	}
 
diff --git a/src/remote/activitypub/kernel/undo/block.ts b/src/remote/activitypub/kernel/undo/block.ts
index 4a22ac79241b5532ca870d7bdd92b75ba739652c..c916a0073790be80607a7a24ce6b83d0517fcbd4 100644
--- a/src/remote/activitypub/kernel/undo/block.ts
+++ b/src/remote/activitypub/kernel/undo/block.ts
@@ -1,9 +1,9 @@
-import * as mongo from 'mongodb';
-import User, { IRemoteUser } from '../../../../models/user';
 import config from '../../../../config';
 import { IBlock } from '../../type';
 import unblock from '../../../../services/blocking/delete';
 import { apLogger } from '../../logger';
+import { IRemoteUser } from '../../../../models/entities/user';
+import { Users } from '../../../../models';
 
 const logger = apLogger;
 
@@ -18,11 +18,9 @@ export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => {
 		return null;
 	}
 
-	const blockee = await User.findOne({
-		_id: new mongo.ObjectID(id.split('/').pop())
-	});
+	const blockee = await Users.findOne(id.split('/').pop());
 
-	if (blockee === null) {
+	if (blockee == null) {
 		throw new Error('blockee not found');
 	}
 
diff --git a/src/remote/activitypub/kernel/undo/follow.ts b/src/remote/activitypub/kernel/undo/follow.ts
index af06aa5b31ecbed78ade3264128eb7a70b11bc28..cc63a740b14eb76adb35019d14d1bd8fe174bb8e 100644
--- a/src/remote/activitypub/kernel/undo/follow.ts
+++ b/src/remote/activitypub/kernel/undo/follow.ts
@@ -1,11 +1,9 @@
-import * as mongo from 'mongodb';
-import User, { IRemoteUser } from '../../../../models/user';
 import config from '../../../../config';
 import unfollow from '../../../../services/following/delete';
 import cancelRequest from '../../../../services/following/requests/cancel';
 import { IFollow } from '../../type';
-import FollowRequest from '../../../../models/follow-request';
-import Following from '../../../../models/following';
+import { IRemoteUser } from '../../../../models/entities/user';
+import { Users, FollowRequests, Followings } from '../../../../models';
 
 export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
 	const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
@@ -14,11 +12,9 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
 		return null;
 	}
 
-	const followee = await User.findOne({
-		_id: new mongo.ObjectID(id.split('/').pop())
-	});
+	const followee = await Users.findOne(id.split('/').pop());
 
-	if (followee === null) {
+	if (followee == null) {
 		throw new Error('followee not found');
 	}
 
@@ -26,14 +22,14 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
 		throw new Error('フォロー解除しようとしているユーザーはローカルユーザーではありません');
 	}
 
-	const req = await FollowRequest.findOne({
-		followerId: actor._id,
-		followeeId: followee._id
+	const req = await FollowRequests.findOne({
+		followerId: actor.id,
+		followeeId: followee.id
 	});
 
-	const following = await Following.findOne({
-		followerId: actor._id,
-		followeeId: followee._id
+	const following = await Followings.findOne({
+		followerId: actor.id,
+		followeeId: followee.id
 	});
 
 	if (req) {
diff --git a/src/remote/activitypub/kernel/undo/index.ts b/src/remote/activitypub/kernel/undo/index.ts
index 80b44fae04ea51a3173969d2329b847db3f1513b..6376ab93a82c8f8ad39c1592eacf66ec284162bc 100644
--- a/src/remote/activitypub/kernel/undo/index.ts
+++ b/src/remote/activitypub/kernel/undo/index.ts
@@ -1,4 +1,4 @@
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
 import { IUndo, IFollow, IBlock, ILike } from '../../type';
 import unfollow from './follow';
 import unblock from './block';
diff --git a/src/remote/activitypub/kernel/undo/like.ts b/src/remote/activitypub/kernel/undo/like.ts
index b324ec854cb46f94ac4756b61e74b56be35dc773..f337a0173e4fa9454f460c9dc02831be2a64c9d9 100644
--- a/src/remote/activitypub/kernel/undo/like.ts
+++ b/src/remote/activitypub/kernel/undo/like.ts
@@ -1,8 +1,7 @@
-import * as mongo from 'mongodb';
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
 import { ILike } from '../../type';
-import Note from '../../../../models/note';
 import deleteReaction from '../../../../services/note/reaction/delete';
+import { Notes } from '../../../../models';
 
 /**
  * Process Undo.Like activity
@@ -10,10 +9,10 @@ import deleteReaction from '../../../../services/note/reaction/delete';
 export default async (actor: IRemoteUser, activity: ILike): Promise<void> => {
 	const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
 
-	const noteId = new mongo.ObjectID(id.split('/').pop());
+	const noteId = id.split('/').pop();
 
-	const note = await Note.findOne({ _id: noteId });
-	if (note === null) {
+	const note = await Notes.findOne(noteId);
+	if (note == null) {
 		throw 'note not found';
 	}
 
diff --git a/src/remote/activitypub/kernel/update/index.ts b/src/remote/activitypub/kernel/update/index.ts
index 49b730391a0a9d45f4e39fba491cb68984a493d3..b8dff733952f47fe233efbcb96dfaed947c63b5b 100644
--- a/src/remote/activitypub/kernel/update/index.ts
+++ b/src/remote/activitypub/kernel/update/index.ts
@@ -1,4 +1,4 @@
-import { IRemoteUser } from '../../../../models/user';
+import { IRemoteUser } from '../../../../models/entities/user';
 import { IUpdate, IObject } from '../../type';
 import { apLogger } from '../../logger';
 import { updateQuestion } from '../../models/question';
diff --git a/src/remote/activitypub/misc/get-note-html.ts b/src/remote/activitypub/misc/get-note-html.ts
index 967ee655440ea5913833620277e684349dccf29a..dba915fee9cb3644e3c45209c7084ac8acbfbffa 100644
--- a/src/remote/activitypub/misc/get-note-html.ts
+++ b/src/remote/activitypub/misc/get-note-html.ts
@@ -1,9 +1,9 @@
-import { INote } from '../../../models/note';
+import { Note } from '../../../models/entities/note';
 import { toHtml } from '../../../mfm/toHtml';
 import { parse } from '../../../mfm/parse';
 
-export default function(note: INote) {
-	let html = toHtml(parse(note.text), note.mentionedRemoteUsers);
+export default function(note: Note) {
+	let html = toHtml(parse(note.text), JSON.parse(note.mentionedRemoteUsers));
 	if (html == null) html = '<p>.</p>';
 
 	return html;
diff --git a/src/remote/activitypub/models/image.ts b/src/remote/activitypub/models/image.ts
index bd97d13d27d81ab4ee9cf81354608a39cd306bd6..87095acd886aff7bc1d8a2c53c0b648585dc1247 100644
--- a/src/remote/activitypub/models/image.ts
+++ b/src/remote/activitypub/models/image.ts
@@ -1,16 +1,17 @@
 import uploadFromUrl from '../../../services/drive/upload-from-url';
-import { IRemoteUser } from '../../../models/user';
-import DriveFile, { IDriveFile } from '../../../models/drive-file';
+import { IRemoteUser } from '../../../models/entities/user';
 import Resolver from '../resolver';
 import fetchMeta from '../../../misc/fetch-meta';
 import { apLogger } from '../logger';
+import { DriveFile } from '../../../models/entities/drive-file';
+import { DriveFiles } from '../../../models';
 
 const logger = apLogger;
 
 /**
  * Imageを作成します。
  */
-export async function createImage(actor: IRemoteUser, value: any): Promise<IDriveFile> {
+export async function createImage(actor: IRemoteUser, value: any): Promise<DriveFile> {
 	// 投稿者が凍結されていたらスキップ
 	if (actor.isSuspended) {
 		return null;
@@ -39,18 +40,16 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv
 		throw e;
 	}
 
-	if (file.metadata.isRemote) {
+	if (file.isRemote) {
 		// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
 		// URLを更新する
-		if (file.metadata.url !== image.url) {
-			file = await DriveFile.findOneAndUpdate({ _id: file._id }, {
-				$set: {
-					'metadata.url': image.url,
-					'metadata.uri': image.url
-				}
-			}, {
-				returnNewDocument: true
+		if (file.url !== image.url) {
+			await DriveFiles.update({ id: file.id }, {
+				url: image.url,
+				uri: image.url
 			});
+
+			file = DriveFiles.findOne(file.id);
 		}
 	}
 
@@ -63,7 +62,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv
  * Misskeyに対象のImageが登録されていればそれを返し、そうでなければ
  * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
  */
-export async function resolveImage(actor: IRemoteUser, value: any): Promise<IDriveFile> {
+export async function resolveImage(actor: IRemoteUser, value: any): Promise<DriveFile> {
 	// TODO
 
 	// リモートサーバーからフェッチしてきて登録
diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts
index 6251621527cb0a891f73233670b7001cad95700a..cd587c51cf4ff1757ed31e0608fa61d3a60991ed 100644
--- a/src/remote/activitypub/models/note.ts
+++ b/src/remote/activitypub/models/note.ts
@@ -1,26 +1,27 @@
-import * as mongo from 'mongodb';
 import * as promiseLimit from 'promise-limit';
 
 import config from '../../../config';
 import Resolver from '../resolver';
-import Note, { INote } from '../../../models/note';
 import post from '../../../services/note/create';
-import { INote as INoteActivityStreamsObject, IObject } from '../type';
 import { resolvePerson, updatePerson } from './person';
 import { resolveImage } from './image';
-import { IRemoteUser, IUser } from '../../../models/user';
+import { IRemoteUser, User } from '../../../models/entities/user';
 import { fromHtml } from '../../../mfm/fromHtml';
-import Emoji, { IEmoji } from '../../../models/emoji';
 import { ITag, extractHashtags } from './tag';
 import { toUnicode } from 'punycode';
 import { unique, concat, difference } from '../../../prelude/array';
 import { extractPollFromQuestion } from './question';
 import vote from '../../../services/note/polls/vote';
 import { apLogger } from '../logger';
-import { IDriveFile } from '../../../models/drive-file';
+import { DriveFile } from '../../../models/entities/drive-file';
 import { deliverQuestionUpdate } from '../../../services/note/polls/update';
-import Instance from '../../../models/instance';
 import { extractDbHost } from '../../../misc/convert-host';
+import { Notes, Emojis, Polls } from '../../../models';
+import { Note } from '../../../models/entities/note';
+import { IObject, INote } from '../type';
+import { Emoji } from '../../../models/entities/emoji';
+import { genId } from '../../../misc/gen-id';
+import fetchMeta from '../../../misc/fetch-meta';
 
 const logger = apLogger;
 
@@ -29,17 +30,17 @@ const logger = apLogger;
  *
  * Misskeyに対象のNoteが登録されていればそれを返します。
  */
-export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<INote> {
+export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<Note> {
 	const uri = typeof value == 'string' ? value : value.id;
 
 	// URIがこのサーバーを指しているならデータベースからフェッチ
 	if (uri.startsWith(config.url + '/')) {
-		const id = new mongo.ObjectID(uri.split('/').pop());
-		return await Note.findOne({ _id: id });
+		const id = uri.split('/').pop();
+		return await Notes.findOne(id);
 	}
 
 	//#region このサーバーに既に登録されていたらそれを返す
-	const exist = await Note.findOne({ uri });
+	const exist = await Notes.findOne({ uri });
 
 	if (exist) {
 		return exist;
@@ -52,7 +53,7 @@ export async function fetchNote(value: string | IObject, resolver?: Resolver): P
 /**
  * Noteを作成します。
  */
-export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<INote> {
+export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<Note> {
 	if (resolver == null) resolver = new Resolver();
 
 	const object: any = await resolver.resolve(value);
@@ -68,7 +69,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
 		return null;
 	}
 
-	const note: INoteActivityStreamsObject = object;
+	const note: INote = object;
 
 	logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
 
@@ -87,7 +88,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
 	note.cc = note.cc == null ? [] : typeof note.cc == 'string' ? [note.cc] : note.cc;
 
 	let visibility = 'public';
-	let visibleUsers: IUser[] = [];
+	let visibleUsers: User[] = [];
 	if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) {
 		if (note.cc.includes('https://www.w3.org/ns/activitystreams#Public')) {
 			visibility = 'home';
@@ -113,12 +114,12 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
 	note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : [];
 	const files = note.attachment
 		.map(attach => attach.sensitive = note.sensitive)
-		? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<IDriveFile>)))
+		? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<DriveFile>)))
 			.filter(image => image != null)
 		: [];
 
 	// リプライ
-	const reply: INote = note.inReplyTo
+	const reply: Note = note.inReplyTo
 		? await resolveNote(note.inReplyTo, resolver).catch(e => {
 			// 4xxの場合はリプライしてないことにする
 			if (e.statusCode >= 400 && e.statusCode < 500) {
@@ -131,7 +132,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
 		: null;
 
 	// 引用
-	let quote: INote;
+	let quote: Note;
 
 	if (note._misskey_quote && typeof note._misskey_quote == 'string') {
 		quote = await resolveNote(note._misskey_quote).catch(e => {
@@ -151,22 +152,23 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
 	const text = note._misskey_content || fromHtml(note.content);
 
 	// vote
-	if (reply && reply.poll) {
+	if (reply && reply.hasPoll) {
+		const poll = await Polls.findOne({ noteId: reply.id });
 		const tryCreateVote = async (name: string, index: number): Promise<null> => {
-			if (reply.poll.expiresAt && Date.now() > new Date(reply.poll.expiresAt).getTime()) {
+			if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) {
 				logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
 			} else if (index >= 0) {
 				logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
 				await vote(actor, reply, index);
 
 				// リモートフォロワーにUpdate配信
-				deliverQuestionUpdate(reply._id);
+				deliverQuestionUpdate(reply.id);
 			}
 			return null;
 		};
 
 		if (note.name) {
-			return await tryCreateVote(note.name, reply.poll.choices.findIndex(x => x.text === note.name));
+			return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name));
 		}
 
 		// 後方互換性のため
@@ -181,7 +183,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
 
 	const emojis = await extractEmojis(note.tag, actor.host).catch(e => {
 		logger.info(`extractEmojis: ${e}`);
-		return [] as IEmoji[];
+		return [] as Emoji[];
 	});
 
 	const apEmojis = emojis.map(emoji => emoji.name);
@@ -222,13 +224,13 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
  * Misskeyに対象のNoteが登録されていればそれを返し、そうでなければ
  * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
  */
-export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<INote> {
+export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note> {
 	const uri = typeof value == 'string' ? value : value.id;
 
 	// ブロックしてたら中断
 	// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
-	const instance = await Instance.findOne({ host: extractDbHost(uri) });
-	if (instance && instance.isBlocked) throw { statusCode: 451 };
+	const meta = await fetchMeta();
+	if (meta.blockedHosts.includes(extractDbHost(uri))) throw { statusCode: 451 };
 
 	//#region このサーバーに既に登録されていたらそれを返す
 	const exist = await fetchNote(uri);
@@ -255,7 +257,7 @@ export async function extractEmojis(tags: ITag[], host_: string) {
 		eomjiTags.map(async tag => {
 			const name = tag.name.replace(/^:/, '').replace(/:$/, '');
 
-			const exists = await Emoji.findOne({
+			const exists = await Emojis.findOne({
 				host,
 				name
 			});
@@ -263,31 +265,37 @@ export async function extractEmojis(tags: ITag[], host_: string) {
 			if (exists) {
 				if ((tag.updated != null && exists.updatedAt == null)
 					|| (tag.id != null && exists.uri == null)
-					|| (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)) {
-						return await Emoji.findOneAndUpdate({
-							host,
-							name,
-						}, {
-							$set: {
-								uri: tag.id,
-								url: tag.icon.url,
-								updatedAt: new Date(tag.updated),
-							}
-						});
+					|| (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)
+				) {
+					await Emojis.update({
+						host,
+						name,
+					}, {
+						uri: tag.id,
+						url: tag.icon.url,
+						updatedAt: new Date(tag.updated),
+					});
+
+					return await Emojis.findOne({
+						host,
+						name
+					});
 				}
+
 				return exists;
 			}
 
 			logger.info(`register emoji host=${host}, name=${name}`);
 
-			return await Emoji.insert({
+			return await Emojis.save({
+				id: genId(),
 				host,
 				name,
 				uri: tag.id,
 				url: tag.icon.url,
 				updatedAt: tag.updated ? new Date(tag.updated) : undefined,
 				aliases: []
-			});
+			} as Emoji);
 		})
 	);
 }
@@ -298,7 +306,7 @@ async function extractMentionedUsers(actor: IRemoteUser, to: string[], cc: strin
 
 	const limit = promiseLimit(2);
 	const users = await Promise.all(
-		uris.map(uri => limit(() => resolvePerson(uri, null, resolver).catch(() => null)) as Promise<IUser>)
+		uris.map(uri => limit(() => resolvePerson(uri, null, resolver).catch(() => null)) as Promise<User>)
 	);
 
 	return users.filter(x => x != null);
diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts
index d27c9379886aaf91925f504c65388c53ac3f272a..51a9efa10b091e1b2174f9b0ec5375c79fb755e1 100644
--- a/src/remote/activitypub/models/person.ts
+++ b/src/remote/activitypub/models/person.ts
@@ -1,29 +1,29 @@
-import * as mongo from 'mongodb';
 import * as promiseLimit from 'promise-limit';
 import { toUnicode } from 'punycode';
 
 import config from '../../../config';
-import User, { validateUsername, isValidName, IUser, IRemoteUser, isRemoteUser } from '../../../models/user';
 import Resolver from '../resolver';
 import { resolveImage } from './image';
 import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type';
-import { IDriveFile } from '../../../models/drive-file';
-import Meta from '../../../models/meta';
+import { DriveFile } from '../../../models/entities/drive-file';
 import { fromHtml } from '../../../mfm/fromHtml';
-import usersChart from '../../../services/chart/users';
-import instanceChart from '../../../services/chart/instance';
 import { URL } from 'url';
 import { resolveNote, extractEmojis } from './note';
 import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
-import Instance from '../../../models/instance';
-import getDriveFileUrl from '../../../misc/get-drive-file-url';
-import { IEmoji } from '../../../models/emoji';
 import { ITag, extractHashtags } from './tag';
-import Following from '../../../models/following';
 import { IIdentifier } from './identifier';
 import { apLogger } from '../logger';
-import { INote } from '../../../models/note';
+import { Note } from '../../../models/entities/note';
 import { updateHashtag } from '../../../services/update-hashtag';
+import { Users, UserNotePinings, Instances, DriveFiles, Followings, UserServiceLinkings, UserPublickeys } from '../../../models';
+import { User, IRemoteUser } from '../../../models/entities/user';
+import { Emoji } from '../../../models/entities/emoji';
+import { UserNotePining } from '../../../models/entities/user-note-pinings';
+import { genId } from '../../../misc/gen-id';
+import { UserServiceLinking } from '../../../models/entities/user-service-linking';
+import { instanceChart, usersChart } from '../../../services/chart';
+import { UserPublickey } from '../../../models/entities/user-publickey';
+import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error';
 const logger = apLogger;
 
 /**
@@ -50,11 +50,11 @@ function validatePerson(x: any, uri: string) {
 		return new Error('invalid person: inbox is not a string');
 	}
 
-	if (!validateUsername(x.preferredUsername, true)) {
+	if (!Users.validateUsername(x.preferredUsername, true)) {
 		return new Error('invalid person: invalid username');
 	}
 
-	if (!isValidName(x.name == '' ? null : x.name)) {
+	if (!Users.isValidName(x.name == '' ? null : x.name)) {
 		return new Error('invalid person: invalid name');
 	}
 
@@ -84,17 +84,17 @@ function validatePerson(x: any, uri: string) {
  *
  * Misskeyに対象のPersonが登録されていればそれを返します。
  */
-export async function fetchPerson(uri: string, resolver?: Resolver): Promise<IUser> {
+export async function fetchPerson(uri: string, resolver?: Resolver): Promise<User> {
 	if (typeof uri !== 'string') throw 'uri is not string';
 
 	// URIがこのサーバーを指しているならデータベースからフェッチ
 	if (uri.startsWith(config.url + '/')) {
-		const id = new mongo.ObjectID(uri.split('/').pop());
-		return await User.findOne({ _id: id });
+		const id = uri.split('/').pop();
+		return await Users.findOne(id);
 	}
 
 	//#region このサーバーに既に登録されていたらそれを返す
-	const exist = await User.findOne({ uri });
+	const exist = await Users.findOne({ uri });
 
 	if (exist) {
 		return exist;
@@ -107,7 +107,7 @@ export async function fetchPerson(uri: string, resolver?: Resolver): Promise<IUs
 /**
  * Personを作成します。
  */
-export async function createPerson(uri: string, resolver?: Resolver): Promise<IUser> {
+export async function createPerson(uri: string, resolver?: Resolver): Promise<User> {
 	if (typeof uri !== 'string') throw 'uri is not string';
 
 	if (resolver == null) resolver = new Resolver();
@@ -124,21 +124,6 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
 
 	logger.info(`Creating the Person: ${person.id}`);
 
-	const [followersCount = 0, followingCount = 0, notesCount = 0] = await Promise.all([
-		resolver.resolve(person.followers).then(
-			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
-			() => undefined
-		),
-		resolver.resolve(person.following).then(
-			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
-			() => undefined
-		),
-		resolver.resolve(person.outbox).then(
-			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
-			() => undefined
-		)
-	]);
-
 	const host = toUnicode(new URL(object.id).hostname.toLowerCase());
 
 	const { fields, services } = analyzeAttachments(person.attachment);
@@ -150,24 +135,18 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
 	// Create user
 	let user: IRemoteUser;
 	try {
-		user = await User.insert({
+		user = await Users.save({
+			id: genId(),
 			avatarId: null,
 			bannerId: null,
-			createdAt: Date.parse(person.published) || null,
+			createdAt: Date.parse(person.published) || new Date(),
 			lastFetchedAt: new Date(),
 			description: fromHtml(person.summary),
-			followersCount,
-			followingCount,
-			notesCount,
 			name: person.name,
 			isLocked: person.manuallyApprovesFollowers,
 			username: person.preferredUsername,
 			usernameLower: person.preferredUsername.toLowerCase(),
 			host,
-			publicKey: {
-				id: person.publicKey.id,
-				publicKeyPem: person.publicKey.publicKeyPem
-			},
 			inbox: person.inbox,
 			sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
 			featured: person.featured,
@@ -179,10 +158,22 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
 			tags,
 			isBot,
 			isCat: (person as any).isCat === true
-		}) as IRemoteUser;
+		} as Partial<User>) as IRemoteUser;
+
+		await UserPublickeys.save({
+			id: genId(),
+			userId: user.id,
+			keyId: person.publicKey.id,
+			keyPem: person.publicKey.publicKeyPem
+		} as UserPublickey);
+
+		await UserServiceLinkings.save({
+			id: genId(),
+			userId: user.id,
+		} as UserServiceLinking);
 	} catch (e) {
 		// duplicate key error
-		if (e.code === 11000) {
+		if (isDuplicateKeyValueError(e)) {
 			throw new Error('already registered');
 		}
 
@@ -190,33 +181,25 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
 		throw e;
 	}
 
+	await UserServiceLinkings.save({
+		id: genId(),
+		userId: user.id
+	} as UserServiceLinking);
+
 	// Register host
 	registerOrFetchInstanceDoc(host).then(i => {
-		Instance.update({ _id: i._id }, {
-			$inc: {
-				usersCount: 1
-			}
-		});
-
+		Instances.increment({ id: i.id }, 'usersCount', 1);
 		instanceChart.newUser(i.host);
 	});
 
-	//#region Increment users count
-	Meta.update({}, {
-		$inc: {
-			'stats.usersCount': 1
-		}
-	}, { upsert: true });
-
 	usersChart.update(user, true);
-	//#endregion
 
 	// ハッシュタグ更新
 	for (const tag of tags) updateHashtag(user, tag, true, true);
 	for (const tag of (user.tags || []).filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false);
 
 	//#region アイコンとヘッダー画像をフェッチ
-	const [avatar, banner] = (await Promise.all<IDriveFile>([
+	const [avatar, banner] = (await Promise.all<DriveFile>([
 		person.icon,
 		person.image
 	].map(img =>
@@ -225,22 +208,20 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
 			: resolveImage(user, img).catch(() => null)
 	)));
 
-	const avatarId = avatar ? avatar._id : null;
-	const bannerId = banner ? banner._id : null;
-	const avatarUrl = getDriveFileUrl(avatar, true);
-	const bannerUrl = getDriveFileUrl(banner, false);
-	const avatarColor = avatar && avatar.metadata.properties.avgColor ? avatar.metadata.properties.avgColor : null;
-	const bannerColor = banner && avatar.metadata.properties.avgColor ? banner.metadata.properties.avgColor : null;
-
-	await User.update({ _id: user._id }, {
-		$set: {
-			avatarId,
-			bannerId,
-			avatarUrl,
-			bannerUrl,
-			avatarColor,
-			bannerColor
-		}
+	const avatarId = avatar ? avatar.id : null;
+	const bannerId = banner ? banner.id : null;
+	const avatarUrl = DriveFiles.getPublicUrl(avatar);
+	const bannerUrl = DriveFiles.getPublicUrl(banner);
+	const avatarColor = avatar && avatar.properties.avgColor ? avatar.properties.avgColor : null;
+	const bannerColor = banner && avatar.properties.avgColor ? banner.properties.avgColor : null;
+
+	await Users.update(user.id, {
+		avatarId,
+		bannerId,
+		avatarUrl,
+		bannerUrl,
+		avatarColor,
+		bannerColor
 	});
 
 	user.avatarId = avatarId;
@@ -254,19 +235,17 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
 	//#region カスタム絵文字取得
 	const emojis = await extractEmojis(person.tag, host).catch(e => {
 		logger.info(`extractEmojis: ${e}`);
-		return [] as IEmoji[];
+		return [] as Emoji[];
 	});
 
 	const emojiNames = emojis.map(emoji => emoji.name);
 
-	await User.update({ _id: user._id }, {
-		$set: {
-			emojis: emojiNames
-		}
+	await Users.update(user.id, {
+		emojis: emojiNames
 	});
 	//#endregion
 
-	await updateFeatured(user._id).catch(err => logger.error(err));
+	await updateFeatured(user.id).catch(err => logger.error(err));
 
 	return user;
 }
@@ -287,7 +266,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
 	}
 
 	//#region このサーバーに既に登録されているか
-	const exist = await User.findOne({ uri }) as IRemoteUser;
+	const exist = await Users.findOne({ uri }) as IRemoteUser;
 
 	if (exist == null) {
 		return;
@@ -295,10 +274,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
 	//#endregion
 
 	// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
-	await User.update({ _id: exist._id }, {
-		$set: {
-			lastFetchedAt: new Date(),
-		},
+	await Users.update(exist.id, {
+		lastFetchedAt: new Date(),
 	});
 
 	if (resolver == null) resolver = new Resolver();
@@ -315,23 +292,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
 
 	logger.info(`Updating the Person: ${person.id}`);
 
-	const [followersCount = 0, followingCount = 0, notesCount = 0] = await Promise.all([
-		resolver.resolve(person.followers).then(
-			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
-			() => undefined
-		),
-		resolver.resolve(person.following).then(
-			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
-			() => undefined
-		),
-		resolver.resolve(person.outbox).then(
-			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
-			() => undefined
-		)
-	]);
-
 	// アイコンとヘッダー画像をフェッチ
-	const [avatar, banner] = (await Promise.all<IDriveFile>([
+	const [avatar, banner] = (await Promise.all<DriveFile>([
 		person.icon,
 		person.image
 	].map(img =>
@@ -343,7 +305,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
 	// カスタム絵文字取得
 	const emojis = await extractEmojis(person.tag, exist.host).catch(e => {
 		logger.info(`extractEmojis: ${e}`);
-		return [] as IEmoji[];
+		return [] as Emoji[];
 	});
 
 	const emojiNames = emojis.map(emoji => emoji.name);
@@ -359,40 +321,45 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
 		featured: person.featured,
 		emojis: emojiNames,
 		description: fromHtml(person.summary),
-		followersCount,
-		followingCount,
-		notesCount,
 		name: person.name,
 		url: person.url,
 		endpoints: person.endpoints,
 		fields,
-		...services,
 		tags,
 		isBot: object.type == 'Service',
 		isCat: (person as any).isCat === true,
 		isLocked: person.manuallyApprovesFollowers,
-		createdAt: Date.parse(person.published) || null,
-		publicKey: {
-			id: person.publicKey.id,
-			publicKeyPem: person.publicKey.publicKeyPem
-		},
-	} as any;
+		createdAt: new Date(Date.parse(person.published)) || null,
+	} as Partial<User>;
 
 	if (avatar) {
-		updates.avatarId = avatar._id;
-		updates.avatarUrl = getDriveFileUrl(avatar, true);
-		updates.avatarColor = avatar.metadata.properties.avgColor ? avatar.metadata.properties.avgColor : null;
+		updates.avatarId = avatar.id;
+		updates.avatarUrl = DriveFiles.getPublicUrl(avatar);
+		updates.avatarColor = avatar.properties.avgColor ? avatar.properties.avgColor : null;
 	}
 
 	if (banner) {
-		updates.bannerId = banner._id;
-		updates.bannerUrl = getDriveFileUrl(banner, true);
-		updates.bannerColor = banner.metadata.properties.avgColor ? banner.metadata.properties.avgColor : null;
+		updates.bannerId = banner.id;
+		updates.bannerUrl = DriveFiles.getPublicUrl(banner);
+		updates.bannerColor = banner.properties.avgColor ? banner.properties.avgColor : null;
 	}
 
 	// Update user
-	await User.update({ _id: exist._id }, {
-		$set: updates
+	await Users.update(exist.id, updates);
+
+	await UserPublickeys.update({ userId: exist.id }, {
+		keyId: person.publicKey.id,
+		keyPem: person.publicKey.publicKeyPem
+	});
+
+	await UserServiceLinkings.update({ userId: exist.id }, {
+		twitterUserId: services.twitter.userId,
+		twitterScreenName: services.twitter.screenName,
+		githubId: services.github.id,
+		githubLogin: services.github.login,
+		discordId: services.discord.id,
+		discordUsername: services.discord.username,
+		discordDiscriminator: services.discord.discriminator,
 	});
 
 	// ハッシュタグ更新
@@ -400,17 +367,13 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
 	for (const tag of (exist.tags || []).filter(x => !tags.includes(x))) updateHashtag(exist, tag, true, false);
 
 	// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
-	await Following.update({
-		followerId: exist._id
-	}, {
-		$set: {
-			'_follower.sharedInbox': person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined)
-		}
+	await Followings.update({
+		followerId: exist.id
 	}, {
-		multi: true
+		followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined)
 	});
 
-	await updateFeatured(exist._id).catch(err => logger.error(err));
+	await updateFeatured(exist.id).catch(err => logger.error(err));
 }
 
 /**
@@ -419,7 +382,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
  * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ
  * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
  */
-export async function resolvePerson(uri: string, verifier?: string, resolver?: Resolver): Promise<IUser> {
+export async function resolvePerson(uri: string, verifier?: string, resolver?: Resolver): Promise<User> {
 	if (typeof uri !== 'string') throw 'uri is not string';
 
 	//#region このサーバーに既に登録されていたらそれを返す
@@ -492,9 +455,9 @@ export function analyzeAttachments(attachments: ITag[]) {
 	return { fields, services };
 }
 
-export async function updateFeatured(userId: mongo.ObjectID) {
-	const user = await User.findOne({ _id: userId });
-	if (!isRemoteUser(user)) return;
+export async function updateFeatured(userId: User['id']) {
+	const user = await Users.findOne(userId);
+	if (!Users.isRemoteUser(user)) return;
 	if (!user.featured) return;
 
 	logger.info(`Updating the featured: ${user.uri}`);
@@ -515,11 +478,14 @@ export async function updateFeatured(userId: mongo.ObjectID) {
 	const featuredNotes = await Promise.all(items
 		.filter(item => item.type === 'Note')
 		.slice(0, 5)
-		.map(item => limit(() => resolveNote(item, resolver)) as Promise<INote>));
-
-	await User.update({ _id: user._id }, {
-		$set: {
-			pinnedNoteIds: featuredNotes.filter(note => note != null).map(note => note._id)
-		}
-	});
+		.map(item => limit(() => resolveNote(item, resolver)) as Promise<Note>));
+
+	for (const note of featuredNotes.filter(note => note != null)) {
+		UserNotePinings.save({
+			id: genId(),
+			createdAt: new Date(),
+			userId: user.id,
+			noteId: note.id
+		} as UserNotePining);
+	}
 }
diff --git a/src/remote/activitypub/models/question.ts b/src/remote/activitypub/models/question.ts
index c073684349ccadde6777c3be9152240fba191c08..a5091a6d96d3cca6dd99f55c25a4aa3c9f8fa761 100644
--- a/src/remote/activitypub/models/question.ts
+++ b/src/remote/activitypub/models/question.ts
@@ -1,8 +1,9 @@
 import config from '../../../config';
-import Note, { IChoice, IPoll } from '../../../models/note';
 import Resolver from '../resolver';
 import { IQuestion } from '../type';
 import { apLogger } from '../logger';
+import { Notes, Polls } from '../../../models';
+import { IPoll } from '../../../models/entities/poll';
 
 export async function extractPollFromQuestion(source: string | IQuestion): Promise<IPoll> {
 	const question = typeof source === 'string' ? await new Resolver().resolve(source) as IQuestion : source;
@@ -14,14 +15,14 @@ export async function extractPollFromQuestion(source: string | IQuestion): Promi
 	}
 
 	const choices = question[multiple ? 'anyOf' : 'oneOf']
-		.map((x, i) => ({
-			id: i,
-			text: x.name,
-			votes: x.replies && x.replies.totalItems || x._misskey_votes || 0,
-		} as IChoice));
+		.map((x, i) => x.name);
+
+	const votes = question[multiple ? 'anyOf' : 'oneOf']
+		.map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0);
 
 	return {
 		choices,
+		votes,
 		multiple,
 		expiresAt
 	};
@@ -39,9 +40,11 @@ export async function updateQuestion(value: any) {
 	if (uri.startsWith(config.url + '/')) throw 'uri points local';
 
 	//#region このサーバーに既に登録されているか
-	const note = await Note.findOne({ uri });
-
+	const note = await Notes.findOne({ uri });
 	if (note == null) throw 'Question is not registed';
+
+	const poll = await Polls.findOne({ noteId: note.id });
+	if (poll == null) throw 'Question is not registed';
 	//#endregion
 
 	// resolve new Question object
@@ -52,27 +55,25 @@ export async function updateQuestion(value: any) {
 	if (question.type !== 'Question') throw 'object is not a Question';
 
 	const apChoices = question.oneOf || question.anyOf;
-	const dbChoices = note.poll.choices;
 
 	let changed = false;
 
-	for (const db of dbChoices) {
-		const oldCount = db.votes;
-		const newCount = apChoices.filter(ap => ap.name === db.text)[0].replies.totalItems;
+	for (const choice of poll.choices) {
+		const oldCount = poll.votes[poll.choices.indexOf(choice)];
+		const newCount = apChoices.filter(ap => ap.name === choice)[0].replies.totalItems;
 
 		if (oldCount != newCount) {
 			changed = true;
-			db.votes = newCount;
+			poll.votes[poll.choices.indexOf(choice)] = newCount;
 		}
 	}
 
-	await Note.update({
-		_id: note._id
-	}, {
-		$set: {
-			'poll.choices': dbChoices,
-			updatedAt: new Date(),
-		}
+	await Notes.update(note.id, {
+		updatedAt: new Date(),
+	});
+
+	await Polls.update(poll.id, {
+		votes: poll.votes
 	});
 
 	return changed;
diff --git a/src/remote/activitypub/perform.ts b/src/remote/activitypub/perform.ts
index 2e4f53adf5bb62156f6e163d24c8bcf74768862e..425adaec9686aa5769145d6c44b0fddf7a655a8c 100644
--- a/src/remote/activitypub/perform.ts
+++ b/src/remote/activitypub/perform.ts
@@ -1,5 +1,5 @@
 import { Object } from './type';
-import { IRemoteUser } from '../../models/user';
+import { IRemoteUser } from '../../models/entities/user';
 import kernel from './kernel';
 
 export default async (actor: IRemoteUser, activity: Object): Promise<void> => {
diff --git a/src/remote/activitypub/renderer/accept.ts b/src/remote/activitypub/renderer/accept.ts
index fdbdff3f12b5ce861a72b475ef5353ad5f451840..21b462907455a4c4f8d0d78db9240056df192684 100644
--- a/src/remote/activitypub/renderer/accept.ts
+++ b/src/remote/activitypub/renderer/accept.ts
@@ -1,8 +1,8 @@
 import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
+import { ILocalUser } from '../../../models/entities/user';
 
 export default (object: any, user: ILocalUser) => ({
 	type: 'Accept',
-	actor: `${config.url}/users/${user._id}`,
+	actor: `${config.url}/users/${user.id}`,
 	object
 });
diff --git a/src/remote/activitypub/renderer/add.ts b/src/remote/activitypub/renderer/add.ts
index 4d6fe392aa3c051a21f449e4a384154f504fbc53..46f937f61dbe6d017c2dc45fb13757bea0b70576 100644
--- a/src/remote/activitypub/renderer/add.ts
+++ b/src/remote/activitypub/renderer/add.ts
@@ -1,9 +1,9 @@
 import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
+import { ILocalUser } from '../../../models/entities/user';
 
 export default (user: ILocalUser, target: any, object: any) => ({
 	type: 'Add',
-	actor: `${config.url}/users/${user._id}`,
+	actor: `${config.url}/users/${user.id}`,
 	target,
 	object
 });
diff --git a/src/remote/activitypub/renderer/announce.ts b/src/remote/activitypub/renderer/announce.ts
index f6f2f9bdcd39b886dccbdf1b100f3d595e6525ac..11e7be449b9261a1e3c61ae597c5d2ba173914cb 100644
--- a/src/remote/activitypub/renderer/announce.ts
+++ b/src/remote/activitypub/renderer/announce.ts
@@ -1,7 +1,7 @@
 import config from '../../../config';
-import { INote } from '../../../models/note';
+import { Note } from '../../../models/entities/note';
 
-export default (object: any, note: INote) => {
+export default (object: any, note: Note) => {
 	const attributedTo = `${config.url}/users/${note.userId}`;
 
 	let to: string[] = [];
@@ -18,7 +18,7 @@ export default (object: any, note: INote) => {
 	}
 
 	return {
-		id: `${config.url}/notes/${note._id}/activity`,
+		id: `${config.url}/notes/${note.id}/activity`,
 		actor: `${config.url}/users/${note.userId}`,
 		type: 'Announce',
 		published: note.createdAt.toISOString(),
diff --git a/src/remote/activitypub/renderer/block.ts b/src/remote/activitypub/renderer/block.ts
index 694f3a1418e8cb7ec6b1e3766884fb597bfa3875..946c45a813d8171846fd833a4e25240d6012425e 100644
--- a/src/remote/activitypub/renderer/block.ts
+++ b/src/remote/activitypub/renderer/block.ts
@@ -1,8 +1,8 @@
 import config from '../../../config';
-import { ILocalUser, IRemoteUser } from '../../../models/user';
+import { ILocalUser, IRemoteUser } from '../../../models/entities/user';
 
 export default (blocker?: ILocalUser, blockee?: IRemoteUser) => ({
 	type: 'Block',
-	actor: `${config.url}/users/${blocker._id}`,
+	actor: `${config.url}/users/${blocker.id}`,
 	object: blockee.uri
 });
diff --git a/src/remote/activitypub/renderer/create.ts b/src/remote/activitypub/renderer/create.ts
index 1ee1418fce317c61d7becdeb21ef51e3bd58bf92..e1fc0515c8e34292f5e8f89617c0935b349a33aa 100644
--- a/src/remote/activitypub/renderer/create.ts
+++ b/src/remote/activitypub/renderer/create.ts
@@ -1,9 +1,9 @@
 import config from '../../../config';
-import { INote } from '../../../models/note';
+import { Note } from '../../../models/entities/note';
 
-export default (object: any, note: INote) => {
+export default (object: any, note: Note) => {
 	const activity = {
-		id: `${config.url}/notes/${note._id}/activity`,
+		id: `${config.url}/notes/${note.id}/activity`,
 		actor: `${config.url}/users/${note.userId}`,
 		type: 'Create',
 		published: note.createdAt.toISOString(),
diff --git a/src/remote/activitypub/renderer/delete.ts b/src/remote/activitypub/renderer/delete.ts
index e090e1c88618298aee55e4398bf87cb1c732a6ae..a98c97e6e915cecd41aef734d781a4f2d9ebec96 100644
--- a/src/remote/activitypub/renderer/delete.ts
+++ b/src/remote/activitypub/renderer/delete.ts
@@ -1,8 +1,8 @@
 import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
+import { ILocalUser } from '../../../models/entities/user';
 
 export default (object: any, user: ILocalUser) => ({
 	type: 'Delete',
-	actor: `${config.url}/users/${user._id}`,
+	actor: `${config.url}/users/${user.id}`,
 	object
 });
diff --git a/src/remote/activitypub/renderer/document.ts b/src/remote/activitypub/renderer/document.ts
index 17721e9417308e76c96dbc090e8a5f3b61447e55..4f6ea8c4eea05b70c3a60257e5e1892e185ee3a9 100644
--- a/src/remote/activitypub/renderer/document.ts
+++ b/src/remote/activitypub/renderer/document.ts
@@ -1,8 +1,8 @@
-import { IDriveFile } from '../../../models/drive-file';
-import getDriveFileUrl from '../../../misc/get-drive-file-url';
+import { DriveFile } from '../../../models/entities/drive-file';
+import { DriveFiles } from '../../../models';
 
-export default (file: IDriveFile) => ({
+export default (file: DriveFile) => ({
 	type: 'Document',
-	mediaType: file.contentType,
-	url: getDriveFileUrl(file)
+	mediaType: file.type,
+	url: DriveFiles.getPublicUrl(file)
 });
diff --git a/src/remote/activitypub/renderer/emoji.ts b/src/remote/activitypub/renderer/emoji.ts
index 1a05b4e89e78589c57c25336bd6f7ab5d31c4216..947a96df37c8333c227fe426e8d2bb05c08e882b 100644
--- a/src/remote/activitypub/renderer/emoji.ts
+++ b/src/remote/activitypub/renderer/emoji.ts
@@ -1,7 +1,7 @@
-import { IEmoji } from '../../../models/emoji';
 import config from '../../../config';
+import { Emoji } from '../../../models/entities/emoji';
 
-export default (emoji: IEmoji) => ({
+export default (emoji: Emoji) => ({
 	id: `${config.url}/emojis/${emoji.name}`,
 	type: 'Emoji',
 	name: `:${emoji.name}:`,
diff --git a/src/remote/activitypub/renderer/follow-user.ts b/src/remote/activitypub/renderer/follow-user.ts
index 9a488d392b4fb053d74b2ae60ee42baa902cc775..9446be3c8618f5d92a510d472d2ff28ac4b7af32 100644
--- a/src/remote/activitypub/renderer/follow-user.ts
+++ b/src/remote/activitypub/renderer/follow-user.ts
@@ -1,16 +1,12 @@
 import config from '../../../config';
-import * as mongo from 'mongodb';
-import User, { isLocalUser } from '../../../models/user';
+import { Users } from '../../../models';
+import { User } from '../../../models/entities/user';
 
 /**
  * Convert (local|remote)(Follower|Followee)ID to URL
  * @param id Follower|Followee ID
  */
-export default async function renderFollowUser(id: mongo.ObjectID): Promise<any> {
-
-	const user = await User.findOne({
-		_id: id
-	});
-
-	return isLocalUser(user) ? `${config.url}/users/${user._id}` : user.uri;
+export default async function renderFollowUser(id: User['id']): Promise<any> {
+	const user = await Users.findOne(id);
+	return Users.isLocalUser(user) ? `${config.url}/users/${user.id}` : user.uri;
 }
diff --git a/src/remote/activitypub/renderer/follow.ts b/src/remote/activitypub/renderer/follow.ts
index 98d4cdd020aac11062ca4bed78bed1e6f3e52b04..400b15ec7bedba180bbccd5821232ad3c41f62b6 100644
--- a/src/remote/activitypub/renderer/follow.ts
+++ b/src/remote/activitypub/renderer/follow.ts
@@ -1,11 +1,12 @@
 import config from '../../../config';
-import { IUser, isLocalUser } from '../../../models/user';
+import { User } from '../../../models/entities/user';
+import { Users } from '../../../models';
 
-export default (follower: IUser, followee: IUser, requestId?: string) => {
+export default (follower: User, followee: User, requestId?: string) => {
 	const follow = {
 		type: 'Follow',
-		actor: isLocalUser(follower) ? `${config.url}/users/${follower._id}` : follower.uri,
-		object: isLocalUser(followee) ? `${config.url}/users/${followee._id}` : followee.uri
+		actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri,
+		object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri
 	} as any;
 
 	if (requestId) follow.id = requestId;
diff --git a/src/remote/activitypub/renderer/image.ts b/src/remote/activitypub/renderer/image.ts
index ec637b9521dde0ac304d535a0d89ce842c268272..ce98f98c629da4914894f821f206e6e817c7b44c 100644
--- a/src/remote/activitypub/renderer/image.ts
+++ b/src/remote/activitypub/renderer/image.ts
@@ -1,8 +1,8 @@
-import { IDriveFile } from '../../../models/drive-file';
-import getDriveFileUrl from '../../../misc/get-drive-file-url';
+import { DriveFile } from '../../../models/entities/drive-file';
+import { DriveFiles } from '../../../models';
 
-export default (file: IDriveFile) => ({
+export default (file: DriveFile) => ({
 	type: 'Image',
-	url: getDriveFileUrl(file),
-	sensitive: file.metadata.isSensitive
+	url: DriveFiles.getPublicUrl(file),
+	sensitive: file.isSensitive
 });
diff --git a/src/remote/activitypub/renderer/key.ts b/src/remote/activitypub/renderer/key.ts
index 0d5e52557c2da519eeed2fb01044382fd4d24c52..fb5975a6c42832ca5cdb44feb4a38e38066356df 100644
--- a/src/remote/activitypub/renderer/key.ts
+++ b/src/remote/activitypub/renderer/key.ts
@@ -1,10 +1,11 @@
+import { createPublicKey } from 'crypto';
 import config from '../../../config';
-import { extractPublic } from '../../../crypto_key';
-import { ILocalUser } from '../../../models/user';
+import { ILocalUser } from '../../../models/entities/user';
+import { UserKeypair } from '../../../models/entities/user-keypair';
 
-export default (user: ILocalUser) => ({
-	id: `${config.url}/users/${user._id}/publickey`,
+export default (user: ILocalUser, key: UserKeypair) => ({
+	id: `${config.url}/users/${user.id}/publickey`,
 	type: 'Key',
-	owner: `${config.url}/users/${user._id}`,
-	publicKeyPem: extractPublic(user.keypair)
+	owner: `${config.url}/users/${user.id}`,
+	publicKeyPem: createPublicKey(key.keyPem)
 });
diff --git a/src/remote/activitypub/renderer/like.ts b/src/remote/activitypub/renderer/like.ts
index 523cb4f1ad3a64d931525ac34c43a90df1b09ae4..01f10ec0a905632f183186442a70d1b9e1293aec 100644
--- a/src/remote/activitypub/renderer/like.ts
+++ b/src/remote/activitypub/renderer/like.ts
@@ -1,10 +1,10 @@
 import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
-import { INote } from '../../../models/note';
+import { ILocalUser } from '../../../models/entities/user';
+import { Note } from '../../../models/entities/note';
 
-export default (user: ILocalUser, note: INote, reaction: string) => ({
+export default (user: ILocalUser, note: Note, reaction: string) => ({
 	type: 'Like',
-	actor: `${config.url}/users/${user._id}`,
-	object: note.uri ? note.uri : `${config.url}/notes/${note._id}`,
+	actor: `${config.url}/users/${user.id}`,
+	object: note.uri ? note.uri : `${config.url}/notes/${note.id}`,
 	_misskey_reaction: reaction
 });
diff --git a/src/remote/activitypub/renderer/mention.ts b/src/remote/activitypub/renderer/mention.ts
index 8d12e6d8bf230b7d38b196747e0363e4af0e4179..889be5d85d1dccf2d219bdde5a67aa901e45a605 100644
--- a/src/remote/activitypub/renderer/mention.ts
+++ b/src/remote/activitypub/renderer/mention.ts
@@ -1,8 +1,9 @@
-import { IUser, isRemoteUser } from '../../../models/user';
 import config from '../../../config';
+import { User, ILocalUser } from '../../../models/entities/user';
+import { Users } from '../../../models';
 
-export default (mention: IUser) => ({
+export default (mention: User) => ({
 	type: 'Mention',
-	href: isRemoteUser(mention) ? mention.uri : `${config.url}/@${mention.username}`,
-	name: isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${mention.username}`,
+	href: Users.isRemoteUser(mention) ? mention.uri : `${config.url}/@${(mention as ILocalUser).username}`,
+	name: Users.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as ILocalUser).username}`,
 });
diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
index 8b349526e174e2e155e9508b271e6c2e3368d086..5b36366b289f7cb4d14f1344f8727ab1591a5642 100644
--- a/src/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -3,29 +3,27 @@ import renderHashtag from './hashtag';
 import renderMention from './mention';
 import renderEmoji from './emoji';
 import config from '../../../config';
-import DriveFile, { IDriveFile } from '../../../models/drive-file';
-import Note, { INote } from '../../../models/note';
-import User from '../../../models/user';
 import toHtml from '../misc/get-note-html';
-import Emoji, { IEmoji } from '../../../models/emoji';
-
-export default async function renderNote(note: INote, dive = true): Promise<any> {
-	const promisedFiles: Promise<IDriveFile[]> = note.fileIds
-		? DriveFile.find({ _id: { $in: note.fileIds } })
+import { Note, IMentionedRemoteUsers } from '../../../models/entities/note';
+import { DriveFile } from '../../../models/entities/drive-file';
+import { DriveFiles, Notes, Users, Emojis, Polls } from '../../../models';
+import { In } from 'typeorm';
+import { Emoji } from '../../../models/entities/emoji';
+import { Poll } from '../../../models/entities/poll';
+
+export default async function renderNote(note: Note, dive = true): Promise<any> {
+	const promisedFiles: Promise<DriveFile[]> = note.fileIds.length > 1
+		? DriveFiles.find({ id: In(note.fileIds) })
 		: Promise.resolve([]);
 
 	let inReplyTo;
-	let inReplyToNote: INote;
+	let inReplyToNote: Note;
 
 	if (note.replyId) {
-		inReplyToNote = await Note.findOne({
-			_id: note.replyId,
-		});
+		inReplyToNote = await Notes.findOne(note.replyId);
 
 		if (inReplyToNote !== null) {
-			const inReplyToUser = await User.findOne({
-				_id: inReplyToNote.userId,
-			});
+			const inReplyToUser = await Users.findOne(inReplyToNote.userId);
 
 			if (inReplyToUser !== null) {
 				if (inReplyToNote.uri) {
@@ -34,7 +32,7 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
 					if (dive) {
 						inReplyTo = await renderNote(inReplyToNote, false);
 					} else {
-						inReplyTo = `${config.url}/notes/${inReplyToNote._id}`;
+						inReplyTo = `${config.url}/notes/${inReplyToNote.id}`;
 					}
 				}
 			}
@@ -46,24 +44,20 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
 	let quote;
 
 	if (note.renoteId) {
-		const renote = await Note.findOne({
-			_id: note.renoteId,
-		});
+		const renote = await Notes.findOne(note.renoteId);
 
 		if (renote) {
-			quote = renote.uri ? renote.uri : `${config.url}/notes/${renote._id}`;
+			quote = renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`;
 		}
 	}
 
-	const user = await User.findOne({
-		_id: note.userId
+	const user = await Users.findOne({
+		id: note.userId
 	});
 
-	const attributedTo = `${config.url}/users/${user._id}`;
+	const attributedTo = `${config.url}/users/${user.id}`;
 
-	const mentions = note.mentionedRemoteUsers && note.mentionedRemoteUsers.length > 0
-		? note.mentionedRemoteUsers.map(x => x.uri)
-		: [];
+	const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
 
 	let to: string[] = [];
 	let cc: string[] = [];
@@ -81,10 +75,8 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
 		to = mentions;
 	}
 
-	const mentionedUsers = note.mentions ? await User.find({
-		_id: {
-			$in: note.mentions
-		}
+	const mentionedUsers = note.mentions.length > 0 ? await Users.find({
+		id: In(note.mentions)
 	}) : [];
 
 	const hashtagTags = (note.tags || []).map(tag => renderHashtag(tag));
@@ -93,23 +85,28 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
 	const files = await promisedFiles;
 
 	let text = note.text;
+	let poll: Poll;
+
+	if (note.hasPoll) {
+		poll = await Polls.findOne({ noteId: note.id });
+	}
 
 	let question: string;
-	if (note.poll != null) {
+	if (poll) {
 		if (text == null) text = '';
-		const url = `${config.url}/notes/${note._id}`;
+		const url = `${config.url}/notes/${note.id}`;
 		// TODO: i18n
 		text += `\n[リモートで結果を表示](${url})`;
 
-		question = `${config.url}/questions/${note._id}`;
+		question = `${config.url}/questions/${note.id}`;
 	}
 
 	let apText = text;
 	if (apText == null) apText = '';
 
 	// Provides choices as text for AP
-	if (note.poll != null) {
-		const cs = note.poll.choices.map(c => `${c.id}: ${c.text}`);
+	if (poll) {
+		const cs = poll.choices.map((c, i) => `${i}: ${c}`);
 		apText += '\n----------------------------------------\n';
 		apText += cs.join('\n');
 		apText += '\n----------------------------------------\n';
@@ -135,31 +132,25 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
 		...apemojis,
 	];
 
-	const {
-		choices = [],
-		expiresAt = null,
-		multiple = false
-	} = note.poll || {};
-
-	const asPoll = note.poll ? {
+	const asPoll = poll ? {
 		type: 'Question',
 		content: toHtml(Object.assign({}, note, {
 			text: text
 		})),
 		_misskey_fallback_content: content,
-		[expiresAt && expiresAt < new Date() ? 'closed' : 'endTime']: expiresAt,
-		[multiple ? 'anyOf' : 'oneOf']: choices.map(({ text, votes }) => ({
+		[poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt,
+		[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
 			type: 'Note',
 			name: text,
 			replies: {
 				type: 'Collection',
-				totalItems: votes
+				totalItems: poll.votes[i]
 			}
 		}))
 	} : {};
 
 	return {
-		id: `${config.url}/notes/${note._id}`,
+		id: `${config.url}/notes/${note.id}`,
 		type: 'Note',
 		attributedTo,
 		summary,
@@ -172,17 +163,17 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
 		cc,
 		inReplyTo,
 		attachment: files.map(renderDocument),
-		sensitive: files.some(file => file.metadata.isSensitive),
+		sensitive: files.some(file => file.isSensitive),
 		tag,
 		...asPoll
 	};
 }
 
-export async function getEmojis(names: string[]): Promise<IEmoji[]> {
-	if (names == null || names.length < 1) return [];
+export async function getEmojis(names: string[]): Promise<Emoji[]> {
+	if (names == null || names.length === 0) return [];
 
 	const emojis = await Promise.all(
-		names.map(name => Emoji.findOne({
+		names.map(name => Emojis.findOne({
 			name,
 			host: null
 		}))
diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts
index 77e60cd61ae5c48f66bef70681616ba4c372f980..4c6b518eb608a218b12bf94c6573223b7eed6e98 100644
--- a/src/remote/activitypub/renderer/person.ts
+++ b/src/remote/activitypub/renderer/person.ts
@@ -1,21 +1,22 @@
 import renderImage from './image';
 import renderKey from './key';
 import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
+import { ILocalUser } from '../../../models/entities/user';
 import { toHtml } from '../../../mfm/toHtml';
 import { parse } from '../../../mfm/parse';
-import DriveFile from '../../../models/drive-file';
 import { getEmojis } from './note';
 import renderEmoji from './emoji';
 import { IIdentifier } from '../models/identifier';
 import renderHashtag from './hashtag';
+import { DriveFiles, UserServiceLinkings, UserKeypairs } from '../../../models';
 
-export default async (user: ILocalUser) => {
-	const id = `${config.url}/users/${user._id}`;
+export async function renderPerson(user: ILocalUser) {
+	const id = `${config.url}/users/${user.id}`;
 
-	const [avatar, banner] = await Promise.all([
-		DriveFile.findOne({ _id: user.avatarId }),
-		DriveFile.findOne({ _id: user.bannerId })
+	const [avatar, banner, links] = await Promise.all([
+		DriveFiles.findOne(user.avatarId),
+		DriveFiles.findOne(user.bannerId),
+		UserServiceLinkings.findOne({ userId: user.id })
 	]);
 
 	const attachment: {
@@ -26,41 +27,41 @@ export default async (user: ILocalUser) => {
 		identifier?: IIdentifier
 	}[] = [];
 
-	if (user.twitter) {
+	if (links.twitter) {
 		attachment.push({
 			type: 'PropertyValue',
 			name: 'Twitter',
-			value: `<a href="https://twitter.com/intent/user?user_id=${user.twitter.userId}" rel="me nofollow noopener" target="_blank"><span>@${user.twitter.screenName}</span></a>`,
+			value: `<a href="https://twitter.com/intent/user?user_id=${links.twitterUserId}" rel="me nofollow noopener" target="_blank"><span>@${links.twitterScreenName}</span></a>`,
 			identifier: {
 				type: 'PropertyValue',
 				name: 'misskey:authentication:twitter',
-				value: `${user.twitter.userId}@${user.twitter.screenName}`
+				value: `${links.twitterUserId}@${links.twitterScreenName}`
 			}
 		});
 	}
 
-	if (user.github) {
+	if (links.github) {
 		attachment.push({
 			type: 'PropertyValue',
 			name: 'GitHub',
-			value: `<a href="https://github.com/${user.github.login}" rel="me nofollow noopener" target="_blank"><span>@${user.github.login}</span></a>`,
+			value: `<a href="https://github.com/${links.githubLogin}" rel="me nofollow noopener" target="_blank"><span>@${links.githubLogin}</span></a>`,
 			identifier: {
 				type: 'PropertyValue',
 				name: 'misskey:authentication:github',
-				value: `${user.github.id}@${user.github.login}`
+				value: `${links.githubId}@${links.githubLogin}`
 			}
 		});
 	}
 
-	if (user.discord) {
+	if (links.discord) {
 		attachment.push({
 			type: 'PropertyValue',
 			name: 'Discord',
-			value: `<a href="https://discordapp.com/users/${user.discord.id}" rel="me nofollow noopener" target="_blank"><span>${user.discord.username}#${user.discord.discriminator}</span></a>`,
+			value: `<a href="https://discordapp.com/users/${links.discordId}" rel="me nofollow noopener" target="_blank"><span>${links.discordUsername}#${links.discordDiscriminator}</span></a>`,
 			identifier: {
 				type: 'PropertyValue',
 				name: 'misskey:authentication:discord',
-				value: `${user.discord.id}@${user.discord.username}#${user.discord.discriminator}`
+				value: `${links.discordId}@${links.discordUsername}#${links.discordDiscriminator}`
 			}
 		});
 	}
@@ -75,6 +76,10 @@ export default async (user: ILocalUser) => {
 		...hashtagTags,
 	];
 
+	const keypair = await UserKeypairs.findOne({
+		userId: user.id
+	});
+
 	return {
 		type: user.isBot ? 'Service' : 'Person',
 		id,
@@ -93,8 +98,8 @@ export default async (user: ILocalUser) => {
 		image: user.bannerId && renderImage(banner),
 		tag,
 		manuallyApprovesFollowers: user.isLocked,
-		publicKey: renderKey(user),
+		publicKey: renderKey(user, keypair),
 		isCat: user.isCat,
 		attachment: attachment.length ? attachment : undefined
 	};
-};
+}
diff --git a/src/remote/activitypub/renderer/question.ts b/src/remote/activitypub/renderer/question.ts
index cf0bf387c8b2a87aac66ea675a5a9f23e6d34929..6ade10d1bfa48153d40a3d7c954889d6cdc6ca94 100644
--- a/src/remote/activitypub/renderer/question.ts
+++ b/src/remote/activitypub/renderer/question.ts
@@ -1,19 +1,20 @@
 import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
-import { INote } from '../../../models/note';
+import { ILocalUser } from '../../../models/entities/user';
+import { Note } from '../../../models/entities/note';
+import { Poll } from '../../../models/entities/poll';
 
-export default async function renderQuestion(user: ILocalUser, note: INote) {
+export default async function renderQuestion(user: ILocalUser, note: Note, poll: Poll) {
 	const question = {
 		type: 'Question',
-		id: `${config.url}/questions/${note._id}`,
-		actor: `${config.url}/users/${user._id}`,
+		id: `${config.url}/questions/${note.id}`,
+		actor: `${config.url}/users/${user.id}`,
 		content:  note.text || '',
-		[note.poll.multiple ? 'anyOf' : 'oneOf']: note.poll.choices.map(c => ({
-			name: c.text,
-			_misskey_votes: c.votes,
+		[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
+			name: text,
+			_misskey_votes: poll.votes[i],
 			replies: {
 				type: 'Collection',
-				totalItems: c.votes
+				totalItems: poll.votes[i]
 			}
 		}))
 	};
diff --git a/src/remote/activitypub/renderer/reject.ts b/src/remote/activitypub/renderer/reject.ts
index 6d7d23708ac8d92952be644dabed36dbcb2b0514..c4e0ba0d0adc6db72c69320de208414c9751dccf 100644
--- a/src/remote/activitypub/renderer/reject.ts
+++ b/src/remote/activitypub/renderer/reject.ts
@@ -1,8 +1,8 @@
 import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
+import { ILocalUser } from '../../../models/entities/user';
 
 export default (object: any, user: ILocalUser) => ({
 	type: 'Reject',
-	actor: `${config.url}/users/${user._id}`,
+	actor: `${config.url}/users/${user.id}`,
 	object
 });
diff --git a/src/remote/activitypub/renderer/remove.ts b/src/remote/activitypub/renderer/remove.ts
index ed840be751be48b5b03a9f77b90b1e1f70c24b92..1b9a6b8c05fda5f1f50ac1d06ba58329551b60c4 100644
--- a/src/remote/activitypub/renderer/remove.ts
+++ b/src/remote/activitypub/renderer/remove.ts
@@ -1,9 +1,9 @@
 import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
+import { ILocalUser } from '../../../models/entities/user';
 
 export default (user: ILocalUser, target: any, object: any) => ({
 	type: 'Remove',
-	actor: `${config.url}/users/${user._id}`,
+	actor: `${config.url}/users/${user.id}`,
 	target,
 	object
 });
diff --git a/src/remote/activitypub/renderer/undo.ts b/src/remote/activitypub/renderer/undo.ts
index dbcf5732be01feca9ec77373c98c6eaa5603a22d..2ff6b61b905c743b08205e0d08be976f4310abe5 100644
--- a/src/remote/activitypub/renderer/undo.ts
+++ b/src/remote/activitypub/renderer/undo.ts
@@ -1,8 +1,8 @@
 import config from '../../../config';
-import { ILocalUser, IUser } from '../../../models/user';
+import { ILocalUser, User } from '../../../models/entities/user';
 
-export default (object: any, user: ILocalUser | IUser) => ({
+export default (object: any, user: ILocalUser | User) => ({
 	type: 'Undo',
-	actor: `${config.url}/users/${user._id}`,
+	actor: `${config.url}/users/${user.id}`,
 	object
 });
diff --git a/src/remote/activitypub/renderer/update.ts b/src/remote/activitypub/renderer/update.ts
index cf9acc9acb9a64dc5871d92ac33e039e915cfe31..c1d5ba29b243d262aa5fc106ed5fccb65945dd66 100644
--- a/src/remote/activitypub/renderer/update.ts
+++ b/src/remote/activitypub/renderer/update.ts
@@ -1,10 +1,10 @@
 import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
+import { ILocalUser } from '../../../models/entities/user';
 
 export default (object: any, user: ILocalUser) => {
 	const activity = {
-		id: `${config.url}/users/${user._id}#updates/${new Date().getTime()}`,
-		actor: `${config.url}/users/${user._id}`,
+		id: `${config.url}/users/${user.id}#updates/${new Date().getTime()}`,
+		actor: `${config.url}/users/${user.id}`,
 		type: 'Update',
 		to: [ 'https://www.w3.org/ns/activitystreams#Public' ],
 		object
diff --git a/src/remote/activitypub/renderer/vote.ts b/src/remote/activitypub/renderer/vote.ts
index 014b76765b8958e29d091f40600f688285b56555..8929c03460bac78865e088423db6df709ff60616 100644
--- a/src/remote/activitypub/renderer/vote.ts
+++ b/src/remote/activitypub/renderer/vote.ts
@@ -1,22 +1,23 @@
 import config from '../../../config';
-import { INote } from '../../../models/note';
-import { IRemoteUser, ILocalUser } from '../../../models/user';
-import { IPollVote } from '../../../models/poll-vote';
+import { Note } from '../../../models/entities/note';
+import { IRemoteUser, ILocalUser } from '../../../models/entities/user';
+import { PollVote } from '../../../models/entities/poll-vote';
+import { Poll } from '../../../models/entities/poll';
 
-export default async function renderVote(user: ILocalUser, vote: IPollVote, pollNote: INote, pollOwner: IRemoteUser): Promise<any> {
+export default async function renderVote(user: ILocalUser, vote: PollVote, note: Note, poll: Poll, pollOwner: IRemoteUser): Promise<any> {
 	return {
-		id: `${config.url}/users/${user._id}#votes/${vote._id}/activity`,
-		actor: `${config.url}/users/${user._id}`,
+		id: `${config.url}/users/${user.id}#votes/${vote.id}/activity`,
+		actor: `${config.url}/users/${user.id}`,
 		type: 'Create',
 		to: [pollOwner.uri],
 		published: new Date().toISOString(),
 		object: {
-			id: `${config.url}/users/${user._id}#votes/${vote._id}`,
+			id: `${config.url}/users/${user.id}#votes/${vote.id}`,
 			type: 'Note',
-			attributedTo: `${config.url}/users/${user._id}`,
+			attributedTo: `${config.url}/users/${user.id}`,
 			to: [pollOwner.uri],
-			inReplyTo: pollNote.uri,
-			name: pollNote.poll.choices.find(x => x.id === vote.choice).text
+			inReplyTo: note.uri,
+			name: poll.choices[vote.choice]
 		}
 	};
 }
diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts
index 08dd7a6ba9b33e4112518c29fcca59c65825afa7..a089ed371c7705df753b4dada410dbe5f496628a 100644
--- a/src/remote/activitypub/request.ts
+++ b/src/remote/activitypub/request.ts
@@ -7,10 +7,11 @@ import * as promiseAny from 'promise-any';
 import { toUnicode } from 'punycode';
 
 import config from '../../config';
-import { ILocalUser } from '../../models/user';
+import { ILocalUser } from '../../models/entities/user';
 import { publishApLogStream } from '../../services/stream';
 import { apLogger } from './logger';
-import Instance from '../../models/instance';
+import { UserKeypairs } from '../../models';
+import fetchMeta from '../../misc/fetch-meta';
 
 export const logger = apLogger.createSubLogger('deliver');
 
@@ -23,8 +24,8 @@ export default async (user: ILocalUser, url: string, object: any) => {
 
 	// ブロックしてたら中断
 	// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
-	const instance = await Instance.findOne({ host: toUnicode(host) });
-	if (instance && instance.isBlocked) return;
+	const meta = await fetchMeta();
+	if (meta.blockedHosts.includes(toUnicode(host))) return;
 
 	const data = JSON.stringify(object);
 
@@ -35,6 +36,10 @@ export default async (user: ILocalUser, url: string, object: any) => {
 	const addr = await resolveAddr(hostname);
 	if (!addr) return;
 
+	const keypair = await UserKeypairs.findOne({
+		userId: user.id
+	});
+
 	const _ = new Promise((resolve, reject) => {
 		const req = request({
 			protocol,
@@ -62,8 +67,8 @@ export default async (user: ILocalUser, url: string, object: any) => {
 
 		sign(req, {
 			authorizationHeaderName: 'Signature',
-			key: user.keypair,
-			keyId: `${config.url}/users/${user._id}/publickey`,
+			key: keypair.keyPem,
+			keyId: `${config.url}/users/${user.id}/publickey`,
 			headers: ['date', 'host', 'digest']
 		});
 
diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts
index 05152993e4d4cde034a9cf482543b1d76572f26b..e8d0be638a4fc461c9cc497859d82e60ccd7a763 100644
--- a/src/remote/activitypub/resolver.ts
+++ b/src/remote/activitypub/resolver.ts
@@ -64,7 +64,7 @@ export default class Resolver {
 			json: true
 		});
 
-		if (object === null || (
+		if (object == null || (
 			Array.isArray(object['@context']) ?
 				!object['@context'].includes('https://www.w3.org/ns/activitystreams') :
 				object['@context'] !== 'https://www.w3.org/ns/activitystreams'
diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts
index 400293da89f52ad1edf77e1dc7dadb2c4ed0e55d..be846ab2799adb907788794a9030ce765f207959 100644
--- a/src/remote/resolve-user.ts
+++ b/src/remote/resolve-user.ts
@@ -1,20 +1,21 @@
 import { toUnicode, toASCII } from 'punycode';
-import User, { IUser, IRemoteUser } from '../models/user';
 import webFinger from './webfinger';
 import config from '../config';
 import { createPerson, updatePerson } from './activitypub/models/person';
 import { URL } from 'url';
 import { remoteLogger } from './logger';
 import chalk from 'chalk';
+import { User, IRemoteUser } from '../models/entities/user';
+import { Users } from '../models';
 
 const logger = remoteLogger.createSubLogger('resolve-user');
 
-export default async (username: string, _host: string, option?: any, resync?: boolean): Promise<IUser> => {
+export default async (username: string, _host: string, option?: any, resync = false): Promise<User> => {
 	const usernameLower = username.toLowerCase();
 
 	if (_host == null) {
 		logger.info(`return local user: ${usernameLower}`);
-		return await User.findOne({ usernameLower, host: null });
+		return await Users.findOne({ usernameLower, host: null });
 	}
 
 	const configHostAscii = toASCII(config.host).toLowerCase();
@@ -25,14 +26,14 @@ export default async (username: string, _host: string, option?: any, resync?: bo
 
 	if (configHost == host) {
 		logger.info(`return local user: ${usernameLower}`);
-		return await User.findOne({ usernameLower, host: null });
+		return await Users.findOne({ usernameLower, host: null });
 	}
 
-	const user = await User.findOne({ usernameLower, host }, option);
+	const user = await Users.findOne({ usernameLower, host }, option);
 
 	const acctLower = `${usernameLower}@${hostAscii}`;
 
-	if (user === null) {
+	if (user == null) {
 		const self = await resolveSelf(acctLower);
 
 		logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
@@ -54,13 +55,11 @@ export default async (username: string, _host: string, option?: any, resync?: bo
 				throw new Error(`Invalied uri`);
 			}
 
-			await User.update({
+			await Users.update({
 				usernameLower,
 				host: host
 			}, {
-				$set: {
-					uri: self.href
-				}
+				uri: self.href
 			});
 		} else {
 			logger.info(`uri is fine: ${acctLower}`);
@@ -69,7 +68,7 @@ export default async (username: string, _host: string, option?: any, resync?: bo
 		await updatePerson(self.href);
 
 		logger.info(`return resynced remote user: ${acctLower}`);
-		return await User.findOne({ uri: self.href });
+		return await Users.findOne({ uri: self.href });
 	}
 
 	logger.info(`return existing remote user: ${acctLower}`);
diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
index df5f5b141daed3ca51a3e47f677269f9672c9dfd..3b39977d47b63e0521648518929155d1d8e7c1fe 100644
--- a/src/server/activitypub.ts
+++ b/src/server/activitypub.ts
@@ -1,15 +1,11 @@
-import { ObjectID } from 'mongodb';
 import * as Router from 'koa-router';
 import * as json from 'koa-json-body';
 import * as httpSignature from 'http-signature';
 
 import { renderActivity } from '../remote/activitypub/renderer';
-import Note from '../models/note';
-import User, { isLocalUser, ILocalUser, IUser } from '../models/user';
-import Emoji from '../models/emoji';
 import renderNote from '../remote/activitypub/renderer/note';
 import renderKey from '../remote/activitypub/renderer/key';
-import renderPerson from '../remote/activitypub/renderer/person';
+import { renderPerson } from '../remote/activitypub/renderer/person';
 import renderEmoji from '../remote/activitypub/renderer/emoji';
 import Outbox, { packActivity } from './activitypub/outbox';
 import Followers from './activitypub/followers';
@@ -18,6 +14,9 @@ import Featured from './activitypub/featured';
 import renderQuestion from '../remote/activitypub/renderer/question';
 import { inbox as processInbox } from '../queue';
 import { isSelfHost } from '../misc/convert-host';
+import { Notes, Users, Emojis, UserKeypairs, Polls } from '../models';
+import { ILocalUser, User } from '../models/entities/user';
+import { In } from 'typeorm';
 
 // Init router
 const router = new Router();
@@ -64,25 +63,20 @@ router.post('/users/:user/inbox', json(), inbox);
 router.get('/notes/:note', async (ctx, next) => {
 	if (!isActivityPubReq(ctx)) return await next();
 
-	if (!ObjectID.isValid(ctx.params.note)) {
-		ctx.status = 404;
-		return;
-	}
-
-	const note = await Note.findOne({
-		_id: new ObjectID(ctx.params.note),
-		visibility: { $in: ['public', 'home'] },
-		localOnly: { $ne: true }
+	const note = await Notes.findOne({
+		id: ctx.params.note,
+		visibility: In(['public', 'home']),
+		localOnly: false
 	});
 
-	if (note === null) {
+	if (note == null) {
 		ctx.status = 404;
 		return;
 	}
 
 	// リモートだったらリダイレクト
-	if (note._user.host != null) {
-		if (note.uri == null || isSelfHost(note._user.host)) {
+	if (note.userHost != null) {
+		if (note.uri == null || isSelfHost(note.userHost)) {
 			ctx.status = 500;
 			return;
 		}
@@ -97,19 +91,14 @@ router.get('/notes/:note', async (ctx, next) => {
 
 // note activity
 router.get('/notes/:note/activity', async ctx => {
-	if (!ObjectID.isValid(ctx.params.note)) {
-		ctx.status = 404;
-		return;
-	}
-
-	const note = await Note.findOne({
-		_id: new ObjectID(ctx.params.note),
-		'_user.host': null,
-		visibility: { $in: ['public', 'home'] },
-		localOnly: { $ne: true }
+	const note = await Notes.findOne({
+		id: ctx.params.note,
+		userHost: null,
+		visibility: In(['public', 'home']),
+		localOnly: false
 	});
 
-	if (note === null) {
+	if (note == null) {
 		ctx.status = 404;
 		return;
 	}
@@ -121,32 +110,23 @@ router.get('/notes/:note/activity', async ctx => {
 
 // question
 router.get('/questions/:question', async (ctx, next) => {
-	if (!ObjectID.isValid(ctx.params.question)) {
-		ctx.status = 404;
-		return;
-	}
-
-	const poll = await Note.findOne({
-		_id: new ObjectID(ctx.params.question),
-		'_user.host': null,
-		visibility: { $in: ['public', 'home'] },
-		localOnly: { $ne: true },
-		poll: {
-			$exists: true,
-			$ne: null
-		},
+	const pollNote = await Notes.findOne({
+		id: ctx.params.question,
+		userHost: null,
+		visibility: In(['public', 'home']),
+		localOnly: false,
+		hasPoll: true
 	});
 
-	if (poll === null) {
+	if (pollNote == null) {
 		ctx.status = 404;
 		return;
 	}
 
-	const user = await User.findOne({
-			_id: poll.userId
-	});
+	const user = await Users.findOne(pollNote.userId);
+	const poll = await Polls.findOne({ noteId: pollNote.id });
 
-	ctx.body = renderActivity(await renderQuestion(user as ILocalUser, poll));
+	ctx.body = renderActivity(await renderQuestion(user as ILocalUser, pollNote, poll));
 	setResponseType(ctx);
 });
 
@@ -164,25 +144,24 @@ router.get('/users/:user/collections/featured', Featured);
 
 // publickey
 router.get('/users/:user/publickey', async ctx => {
-	if (!ObjectID.isValid(ctx.params.user)) {
-		ctx.status = 404;
-		return;
-	}
-
-	const userId = new ObjectID(ctx.params.user);
+	const userId = ctx.params.user;
 
-	const user = await User.findOne({
-		_id: userId,
+	const user = await Users.findOne({
+		id: userId,
 		host: null
 	});
 
-	if (user === null) {
+	if (user == null) {
 		ctx.status = 404;
 		return;
 	}
 
-	if (isLocalUser(user)) {
-		ctx.body = renderActivity(renderKey(user));
+	const keypair = await UserKeypairs.findOne({
+		userId: user.id
+	});
+
+	if (Users.isLocalUser(user)) {
+		ctx.body = renderActivity(renderKey(user, keypair));
 		ctx.set('Cache-Control', 'public, max-age=180');
 		setResponseType(ctx);
 	} else {
@@ -191,8 +170,8 @@ router.get('/users/:user/publickey', async ctx => {
 });
 
 // user
-async function userInfo(ctx: Router.IRouterContext, user: IUser) {
-	if (user === null) {
+async function userInfo(ctx: Router.IRouterContext, user: User) {
+	if (user == null) {
 		ctx.status = 404;
 		return;
 	}
@@ -205,15 +184,10 @@ async function userInfo(ctx: Router.IRouterContext, user: IUser) {
 router.get('/users/:user', async (ctx, next) => {
 	if (!isActivityPubReq(ctx)) return await next();
 
-	if (!ObjectID.isValid(ctx.params.user)) {
-		ctx.status = 404;
-		return;
-	}
-
-	const userId = new ObjectID(ctx.params.user);
+	const userId = ctx.params.user;
 
-	const user = await User.findOne({
-		_id: userId,
+	const user = await Users.findOne({
+		id: userId,
 		host: null
 	});
 
@@ -223,7 +197,7 @@ router.get('/users/:user', async (ctx, next) => {
 router.get('/@:user', async (ctx, next) => {
 	if (!isActivityPubReq(ctx)) return await next();
 
-	const user = await User.findOne({
+	const user = await Users.findOne({
 		usernameLower: ctx.params.user.toLowerCase(),
 		host: null
 	});
@@ -234,12 +208,12 @@ router.get('/@:user', async (ctx, next) => {
 
 // emoji
 router.get('/emojis/:emoji', async ctx => {
-	const emoji = await Emoji.findOne({
+	const emoji = await Emojis.findOne({
 		host: null,
 		name: ctx.params.emoji
 	});
 
-	if (emoji === null) {
+	if (emoji == null) {
 		ctx.status = 404;
 		return;
 	}
diff --git a/src/server/activitypub/featured.ts b/src/server/activitypub/featured.ts
index fc6150902bd818ee63b942947474e0caa7232e90..f43312d79a59229ed7a8b5caf6a2e54bc5128900 100644
--- a/src/server/activitypub/featured.ts
+++ b/src/server/activitypub/featured.ts
@@ -1,35 +1,28 @@
-import { ObjectID } from 'mongodb';
 import * as Router from 'koa-router';
 import config from '../../config';
-import User from '../../models/user';
 import { renderActivity } from '../../remote/activitypub/renderer';
 import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
 import { setResponseType } from '../activitypub';
-import Note from '../../models/note';
 import renderNote from '../../remote/activitypub/renderer/note';
+import { Users, Notes, UserNotePinings } from '../../models';
 
 export default async (ctx: Router.IRouterContext) => {
-	if (!ObjectID.isValid(ctx.params.user)) {
-		ctx.status = 404;
-		return;
-	}
-
-	const userId = new ObjectID(ctx.params.user);
+	const userId = ctx.params.user;
 
 	// Verify user
-	const user = await User.findOne({
-		_id: userId,
+	const user = await Users.findOne({
+		id: userId,
 		host: null
 	});
 
-	if (user === null) {
+	if (user == null) {
 		ctx.status = 404;
 		return;
 	}
 
-	const pinnedNoteIds = user.pinnedNoteIds || [];
+	const pinings = await UserNotePinings.find({ userId: user.id });
 
-	const pinnedNotes = await Promise.all(pinnedNoteIds.filter(ObjectID.isValid).map(id => Note.findOne({ _id: id })));
+	const pinnedNotes = await Promise.all(pinings.map(pining => Notes.findOne(pining.noteId)));
 
 	const renderedNotes = await Promise.all(pinnedNotes.map(note => renderNote(note)));
 
diff --git a/src/server/activitypub/followers.ts b/src/server/activitypub/followers.ts
index 002576b2e7a22fef068a935f1950c81dbc788553..62c54399edacc197e77e1dd35f8dc10cd6836696 100644
--- a/src/server/activitypub/followers.ts
+++ b/src/server/activitypub/followers.ts
@@ -1,24 +1,18 @@
-import { ObjectID } from 'mongodb';
 import * as Router from 'koa-router';
 import config from '../../config';
 import $ from 'cafy';
-import ID, { transform } from '../../misc/cafy-id';
-import User from '../../models/user';
-import Following from '../../models/following';
+import { ID } from '../../misc/cafy-id';
 import * as url from '../../prelude/url';
 import { renderActivity } from '../../remote/activitypub/renderer';
 import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
 import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page';
 import renderFollowUser from '../../remote/activitypub/renderer/follow-user';
 import { setResponseType } from '../activitypub';
+import { Users, Followings } from '../../models';
+import { LessThan } from 'typeorm';
 
 export default async (ctx: Router.IRouterContext) => {
-	if (!ObjectID.isValid(ctx.params.user)) {
-		ctx.status = 404;
-		return;
-	}
-
-	const userId = new ObjectID(ctx.params.user);
+	const userId = ctx.params.user;
 
 	// Get 'cursor' parameter
 	const [cursor, cursorErr] = $.optional.type(ID).get(ctx.request.query.cursor);
@@ -34,12 +28,12 @@ export default async (ctx: Router.IRouterContext) => {
 	}
 
 	// Verify user
-	const user = await User.findOne({
-		_id: userId,
+	const user = await Users.findOne({
+		id: userId,
 		host: null
 	});
 
-	if (user === null) {
+	if (user == null) {
 		ctx.status = 404;
 		return;
 	}
@@ -49,22 +43,20 @@ export default async (ctx: Router.IRouterContext) => {
 
 	if (page) {
 		const query = {
-			followeeId: user._id
+			followeeId: user.id
 		} as any;
 
 		// カーソルが指定されている場合
 		if (cursor) {
-			query._id = {
-				$lt: transform(cursor)
-			};
+			query.id = LessThan(cursor);
 		}
 
 		// Get followers
-		const followings = await Following
-			.find(query, {
-				limit: limit + 1,
-				sort: { _id: -1 }
-			});
+		const followings = await Followings.find({
+			where: query,
+			take: limit + 1,
+			order: { id: -1 }
+		});
 
 		// 「次のページ」があるかどうか
 		const inStock = followings.length === limit + 1;
@@ -80,7 +72,7 @@ export default async (ctx: Router.IRouterContext) => {
 			null,
 			inStock ? `${partOf}?${url.query({
 				page: 'true',
-				cursor: followings[followings.length - 1]._id.toHexString()
+				cursor: followings[followings.length - 1].id
 			})}` : null
 		);
 
diff --git a/src/server/activitypub/following.ts b/src/server/activitypub/following.ts
index 0d7486f68ad567c395350aa3c0ba7939747ec02d..4894aac1f8c3acb7d5bb2dccbaf224632690e9de 100644
--- a/src/server/activitypub/following.ts
+++ b/src/server/activitypub/following.ts
@@ -1,24 +1,19 @@
-import { ObjectID } from 'mongodb';
 import * as Router from 'koa-router';
 import config from '../../config';
 import $ from 'cafy';
-import ID, { transform } from '../../misc/cafy-id';
-import User from '../../models/user';
-import Following from '../../models/following';
+import { ID } from '../../misc/cafy-id';
 import * as url from '../../prelude/url';
 import { renderActivity } from '../../remote/activitypub/renderer';
 import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
 import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page';
 import renderFollowUser from '../../remote/activitypub/renderer/follow-user';
 import { setResponseType } from '../activitypub';
+import { Users, Followings } from '../../models';
+import { LessThan, FindConditions } from 'typeorm';
+import { Following } from '../../models/entities/following';
 
 export default async (ctx: Router.IRouterContext) => {
-	if (!ObjectID.isValid(ctx.params.user)) {
-		ctx.status = 404;
-		return;
-	}
-
-	const userId = new ObjectID(ctx.params.user);
+	const userId = ctx.params.user;
 
 	// Get 'cursor' parameter
 	const [cursor, cursorErr] = $.optional.type(ID).get(ctx.request.query.cursor);
@@ -34,12 +29,12 @@ export default async (ctx: Router.IRouterContext) => {
 	}
 
 	// Verify user
-	const user = await User.findOne({
-		_id: userId,
+	const user = await Users.findOne({
+		id: userId,
 		host: null
 	});
 
-	if (user === null) {
+	if (user == null) {
 		ctx.status = 404;
 		return;
 	}
@@ -49,22 +44,20 @@ export default async (ctx: Router.IRouterContext) => {
 
 	if (page) {
 		const query = {
-			followerId: user._id
-		} as any;
+			followerId: user.id
+		} as FindConditions<Following>;
 
 		// カーソルが指定されている場合
 		if (cursor) {
-			query._id = {
-				$lt: transform(cursor)
-			};
+			query.id = LessThan(cursor);
 		}
 
 		// Get followings
-		const followings = await Following
-			.find(query, {
-				limit: limit + 1,
-				sort: { _id: -1 }
-			});
+		const followings = await Followings.find({
+			where: query,
+			take: limit + 1,
+			order: { id: -1 }
+		});
 
 		// 「次のページ」があるかどうか
 		const inStock = followings.length === limit + 1;
@@ -80,7 +73,7 @@ export default async (ctx: Router.IRouterContext) => {
 			null,
 			inStock ? `${partOf}?${url.query({
 				page: 'true',
-				cursor: followings[followings.length - 1]._id.toHexString()
+				cursor: followings[followings.length - 1].id
 			})}` : null
 		);
 
diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts
index ff8f884b190cee9916b951330fe6c1437c2ce949..377f43c98616e0496575fe2619d74eee88d9ca09 100644
--- a/src/server/activitypub/outbox.ts
+++ b/src/server/activitypub/outbox.ts
@@ -1,28 +1,23 @@
-import { ObjectID } from 'mongodb';
 import * as Router from 'koa-router';
 import config from '../../config';
 import $ from 'cafy';
-import ID, { transform } from '../../misc/cafy-id';
-import User from '../../models/user';
+import { ID } from '../../misc/cafy-id';
 import { renderActivity } from '../../remote/activitypub/renderer';
 import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
 import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page';
 import { setResponseType } from '../activitypub';
-
-import Note, { INote } from '../../models/note';
 import renderNote from '../../remote/activitypub/renderer/note';
 import renderCreate from '../../remote/activitypub/renderer/create';
 import renderAnnounce from '../../remote/activitypub/renderer/announce';
 import { countIf } from '../../prelude/array';
 import * as url from '../../prelude/url';
+import { Users, Notes } from '../../models';
+import { makePaginationQuery } from '../api/common/make-pagination-query';
+import { Brackets } from 'typeorm';
+import { Note } from '../../models/entities/note';
 
 export default async (ctx: Router.IRouterContext) => {
-	if (!ObjectID.isValid(ctx.params.user)) {
-		ctx.status = 404;
-		return;
-	}
-
-	const userId = new ObjectID(ctx.params.user);
+	const userId = ctx.params.user;
 
 	// Get 'sinceId' parameter
 	const [sinceId, sinceIdErr] = $.optional.type(ID).get(ctx.request.query.since_id);
@@ -41,12 +36,12 @@ export default async (ctx: Router.IRouterContext) => {
 	}
 
 	// Verify user
-	const user = await User.findOne({
-		_id: userId,
+	const user = await Users.findOne({
+		id: userId,
 		host: null
 	});
 
-	if (user === null) {
+	if (user == null) {
 		ctx.status = 404;
 		return;
 	}
@@ -55,34 +50,15 @@ export default async (ctx: Router.IRouterContext) => {
 	const partOf = `${config.url}/users/${userId}/outbox`;
 
 	if (page) {
-		//#region Construct query
-		const sort = {
-			_id: -1
-		};
-
-		const query = {
-			userId: user._id,
-			visibility: { $in: ['public', 'home'] },
-			localOnly: { $ne: true }
-		} as any;
-
-		if (sinceId) {
-			sort._id = 1;
-			query._id = {
-				$gt: transform(sinceId)
-			};
-		} else if (untilId) {
-			query._id = {
-				$lt: transform(untilId)
-			};
-		}
-		//#endregion
+		const query = makePaginationQuery(Notes.createQueryBuilder('note'), sinceId, untilId)
+			.andWhere('note.userId = :userId', { userId: user.id })
+			.andWhere(new Brackets(qb => { qb
+				.where(`note.visibility = 'public'`)
+				.orWhere(`note.visibility = 'home'`);
+			}))
+			.andWhere('note.localOnly = FALSE');
 
-		const notes = await Note
-			.find(query, {
-				limit: limit,
-				sort: sort
-			});
+		const notes = await query.take(limit).getMany();
 
 		if (sinceId) notes.reverse();
 
@@ -96,11 +72,11 @@ export default async (ctx: Router.IRouterContext) => {
 			user.notesCount, activities, partOf,
 			notes.length ? `${partOf}?${url.query({
 				page: 'true',
-				since_id: notes[0]._id.toHexString()
+				since_id: notes[0].id
 			})}` : null,
 			notes.length ? `${partOf}?${url.query({
 				page: 'true',
-				until_id: notes[notes.length - 1]._id.toHexString()
+				until_id: notes[notes.length - 1].id
 			})}` : null
 		);
 
@@ -123,10 +99,10 @@ export default async (ctx: Router.IRouterContext) => {
  * Pack Create<Note> or Announce Activity
  * @param note Note
  */
-export async function packActivity(note: INote): Promise<object> {
-	if (note.renoteId && note.text == null && note.poll == null && (note.fileIds == null || note.fileIds.length == 0)) {
-		const renote = await Note.findOne(note.renoteId);
-		return renderAnnounce(renote.uri ? renote.uri : `${config.url}/notes/${renote._id}`, note);
+export async function packActivity(note: Note): Promise<object> {
+	if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length == 0)) {
+		const renote = await Notes.findOne(note.renoteId);
+		return renderAnnounce(renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`, note);
 	}
 
 	return renderCreate(await renderNote(note, false), note);
diff --git a/src/server/api/authenticate.ts b/src/server/api/authenticate.ts
index 7781b87c88249c9858b77af933894109e92e6ace..e293e3fed06071013d5d081db37414f3e08f2df6 100644
--- a/src/server/api/authenticate.ts
+++ b/src/server/api/authenticate.ts
@@ -1,37 +1,37 @@
-import App, { IApp } from '../../models/app';
-import { default as User, IUser } from '../../models/user';
-import AccessToken from '../../models/access-token';
 import isNativeToken from './common/is-native-token';
+import { User } from '../../models/entities/user';
+import { App } from '../../models/entities/app';
+import { Users, AccessTokens, Apps } from '../../models';
 
-export default async (token: string): Promise<[IUser, IApp]> => {
+export default async (token: string): Promise<[User, App]> => {
 	if (token == null) {
 		return [null, null];
 	}
 
 	if (isNativeToken(token)) {
 		// Fetch user
-		const user: IUser = await User
+		const user = await Users
 			.findOne({ token });
 
-		if (user === null) {
+		if (user == null) {
 			throw 'user not found';
 		}
 
 		return [user, null];
 	} else {
-		const accessToken = await AccessToken.findOne({
+		const accessToken = await AccessTokens.findOne({
 			hash: token.toLowerCase()
 		});
 
-		if (accessToken === null) {
+		if (accessToken == null) {
 			throw 'invalid signature';
 		}
 
-		const app = await App
-			.findOne({ _id: accessToken.appId });
+		const app = await Apps
+			.findOne(accessToken.appId);
 
-		const user = await User
-			.findOne({ _id: accessToken.userId });
+		const user = await Users
+			.findOne(accessToken.userId);
 
 		return [user, app];
 	}
diff --git a/src/server/api/call.ts b/src/server/api/call.ts
index 89a44b3c65a0ec7c0df30bbd7df8de0b4fc512ea..885c6226676f5258ae3080f01e47134c7127c534 100644
--- a/src/server/api/call.ts
+++ b/src/server/api/call.ts
@@ -1,10 +1,10 @@
 import { performance } from 'perf_hooks';
 import limiter from './limiter';
-import { IUser } from '../../models/user';
-import { IApp } from '../../models/app';
+import { User } from '../../models/entities/user';
 import endpoints from './endpoints';
 import { ApiError } from './error';
 import { apiLogger } from './logger';
+import { App } from '../../models/entities/app';
 
 const accessDenied = {
 	message: 'Access denied.',
@@ -12,7 +12,7 @@ const accessDenied = {
 	id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e'
 };
 
-export default async (endpoint: string, user: IUser, app: IApp, data: any, file?: any) => {
+export default async (endpoint: string, user: User, app: App, data: any, file?: any) => {
 	const isSecure = user != null && app == null;
 
 	const ep = endpoints.find(e => e.name === endpoint);
diff --git a/src/server/api/common/generate-mute-query.ts b/src/server/api/common/generate-mute-query.ts
new file mode 100644
index 0000000000000000000000000000000000000000..090c14eb833f4d383ffb315a5a6b98bd5cf6fe1a
--- /dev/null
+++ b/src/server/api/common/generate-mute-query.ts
@@ -0,0 +1,36 @@
+import { User } from '../../../models/entities/user';
+import { Mutings } from '../../../models';
+import { SelectQueryBuilder, Brackets } from 'typeorm';
+
+export function generateMuteQuery(q: SelectQueryBuilder<any>, me: User) {
+	const mutingQuery = Mutings.createQueryBuilder('muting')
+		.select('muting.muteeId')
+		.where('muting.muterId = :muterId', { muterId: me.id });
+
+	// 投稿の作者をミュートしていない かつ
+	// 投稿の返信先の作者をミュートしていない かつ
+	// 投稿の引用元の作者をミュートしていない
+	q
+		.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`)
+		.andWhere(new Brackets(qb => { qb
+			.where(`note.replyUserId IS NULL`)
+			.orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`);
+		}))
+		.andWhere(new Brackets(qb => { qb
+			.where(`note.renoteUserId IS NULL`)
+			.orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`);
+		}));
+
+	q.setParameters(mutingQuery.getParameters());
+}
+
+export function generateMuteQueryForUsers(q: SelectQueryBuilder<any>, me: User) {
+	const mutingQuery = Mutings.createQueryBuilder('muting')
+		.select('muting.muteeId')
+		.where('muting.muterId = :muterId', { muterId: me.id });
+
+	q
+		.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`);
+
+	q.setParameters(mutingQuery.getParameters());
+}
diff --git a/src/server/api/common/generate-native-user-token.ts b/src/server/api/common/generate-native-user-token.ts
index 2082b89a5a6b1f59806e6cf1acdbcd51fecb55eb..92f8a3a0e8811648fcd0a2414ac174a17141f4b8 100644
--- a/src/server/api/common/generate-native-user-token.ts
+++ b/src/server/api/common/generate-native-user-token.ts
@@ -1,3 +1,3 @@
 import rndstr from 'rndstr';
 
-export default () => `!${rndstr('a-zA-Z0-9', 32)}`;
+export default () => `!${rndstr('a-zA-Z0-9', 31)}`;
diff --git a/src/server/api/common/generate-visibility-query.ts b/src/server/api/common/generate-visibility-query.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2807dc99dcf01b60af042e39a95430823549a40f
--- /dev/null
+++ b/src/server/api/common/generate-visibility-query.ts
@@ -0,0 +1,40 @@
+import { User } from '../../../models/entities/user';
+import { Followings } from '../../../models';
+import { Brackets, SelectQueryBuilder } from 'typeorm';
+
+export function generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: User) {
+	if (me == null) {
+		q.andWhere(new Brackets(qb => { qb
+			.where(`note.visibility = 'public'`)
+			.orWhere(`note.visibility = 'home'`);
+		}));
+	} else {
+		const followingQuery = Followings.createQueryBuilder('following')
+			.select('following.followeeId')
+			.where('following.followerId = :followerId', { followerId: me.id });
+
+		q.andWhere(new Brackets(qb => { qb
+			// 公開投稿である
+			.where(new Brackets(qb => { qb
+				.where(`note.visibility = 'public'`)
+				.orWhere(`note.visibility = 'home'`);
+			}))
+			// または 自分自身
+			.orWhere('note.userId = :userId1', { userId1: me.id })
+			// または 自分宛て
+			.orWhere(':userId2 = ANY(note.visibleUserIds)', { userId2: me.id })
+			.orWhere(new Brackets(qb => { qb
+				// または フォロワー宛ての投稿であり、
+				.where('note.visibility = \'followers\'')
+				.andWhere(new Brackets(qb => { qb
+					// 自分がフォロワーである
+					.where(`note.userId IN (${ followingQuery.getQuery() })`)
+					// または 自分の投稿へのリプライ
+					.orWhere('note.replyUserId = :userId3', { userId3: me.id });
+				}));
+			}));
+		}));
+
+		q.setParameters(followingQuery.getParameters());
+	}
+}
diff --git a/src/server/api/common/get-friends.ts b/src/server/api/common/get-friends.ts
deleted file mode 100644
index 876aa399f746bdffd64aa0aa68c1ae1d04fd302d..0000000000000000000000000000000000000000
--- a/src/server/api/common/get-friends.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import * as mongodb from 'mongodb';
-import Following from '../../../models/following';
-
-export const getFriendIds = async (me: mongodb.ObjectID, includeMe = true) => {
-	// Fetch relation to other users who the I follows
-	// SELECT followee
-	const followings = await Following
-		.find({
-			followerId: me
-		}, {
-			fields: {
-				followeeId: true
-			}
-		});
-
-	// ID list of other users who the I follows
-	const myfollowingIds = followings.map(following => following.followeeId);
-
-	if (includeMe) {
-		myfollowingIds.push(me);
-	}
-
-	return myfollowingIds;
-};
-
-export const getFriends = async (me: mongodb.ObjectID, includeMe = true, remoteOnly = false) => {
-	const q: any = remoteOnly ? {
-		followerId: me,
-		'_followee.host': { $ne: null }
-	} : {
-		followerId: me
-	};
-	// Fetch relation to other users who the I follows
-	const followings = await Following
-		.find(q);
-
-	// ID list of other users who the I follows
-	const myfollowings = followings.map(following => ({
-		id: following.followeeId
-	}));
-
-	if (includeMe) {
-		myfollowings.push({
-			id: me
-		});
-	}
-
-	return myfollowings;
-};
diff --git a/src/server/api/common/get-hide-users.ts b/src/server/api/common/get-hide-users.ts
deleted file mode 100644
index 3cdf80675140be822021f3253b8f50dc5d8c501b..0000000000000000000000000000000000000000
--- a/src/server/api/common/get-hide-users.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import * as mongo from 'mongodb';
-import Mute from '../../../models/mute';
-import User, { IUser } from '../../../models/user';
-import { unique } from '../../../prelude/array';
-
-export async function getHideUserIds(me: IUser) {
-	return await getHideUserIdsById(me ? me._id : null);
-}
-
-export async function getHideUserIdsById(meId?: mongo.ObjectID) {
-	const [suspended, muted] = await Promise.all([
-		User.find({
-			isSuspended: true
-		}, {
-			fields: {
-				_id: true
-			}
-		}),
-		meId ? Mute.find({
-			muterId: meId
-		}) : Promise.resolve([])
-	]);
-
-	return unique(suspended.map(user => user._id).concat(muted.map(mute => mute.muteeId)));
-}
diff --git a/src/server/api/common/getters.ts b/src/server/api/common/getters.ts
index 7a72e6489a42aaf3d1d1c0194cc16525002b8ec2..b720840ebba7fb3c81800f002c9341d40a93f58c 100644
--- a/src/server/api/common/getters.ts
+++ b/src/server/api/common/getters.ts
@@ -1,18 +1,15 @@
-import * as mongo from 'mongodb';
-import Note from '../../../models/note';
-import User, { isRemoteUser, isLocalUser } from '../../../models/user';
 import { IdentifiableError } from '../../../misc/identifiable-error';
+import { User } from '../../../models/entities/user';
+import { Note } from '../../../models/entities/note';
+import { Notes, Users } from '../../../models';
 
 /**
  * Get note for API processing
  */
-export async function getNote(noteId: mongo.ObjectID) {
-	const note = await Note.findOne({
-		_id: noteId,
-		deletedAt: { $exists: false }
-	});
+export async function getNote(noteId: Note['id']) {
+	const note = await Notes.findOne(noteId);
 
-	if (note === null) {
+	if (note == null) {
 		throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');
 	}
 
@@ -22,23 +19,10 @@ export async function getNote(noteId: mongo.ObjectID) {
 /**
  * Get user for API processing
  */
-export async function getUser(userId: mongo.ObjectID) {
-	const user = await User.findOne({
-		_id: userId,
-		$or: [{
-			isDeleted: { $exists: false }
-		}, {
-			isDeleted: false
-		}]
-	}, {
-		fields: {
-			data: false,
-			profile: false,
-			clientSettings: false
-		}
-	});
+export async function getUser(userId: User['id']) {
+	const user = await Users.findOne(userId);
 
-	if (user === null) {
+	if (user == null) {
 		throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', 'No such user.');
 	}
 
@@ -48,10 +32,10 @@ export async function getUser(userId: mongo.ObjectID) {
 /**
  * Get remote user for API processing
  */
-export async function getRemoteUser(userId: mongo.ObjectID) {
+export async function getRemoteUser(userId: User['id']) {
 	const user = await getUser(userId);
 
-	if (!isRemoteUser(user)) {
+	if (!Users.isRemoteUser(user)) {
 		throw 'user is not a remote user';
 	}
 
@@ -61,10 +45,10 @@ export async function getRemoteUser(userId: mongo.ObjectID) {
 /**
  * Get local user for API processing
  */
-export async function getLocalUser(userId: mongo.ObjectID) {
+export async function getLocalUser(userId: User['id']) {
 	const user = await getUser(userId);
 
-	if (!isLocalUser(user)) {
+	if (!Users.isLocalUser(user)) {
 		throw 'user is not a local user';
 	}
 
diff --git a/src/server/api/common/make-pagination-query.ts b/src/server/api/common/make-pagination-query.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0c859a4f8db235baf7bf22b51ebec6b36ea267fb
--- /dev/null
+++ b/src/server/api/common/make-pagination-query.ts
@@ -0,0 +1,28 @@
+import { SelectQueryBuilder } from 'typeorm';
+
+export function makePaginationQuery<T>(q: SelectQueryBuilder<T>, sinceId: string, untilId: string, sinceDate?: number, untilDate?: number) {
+	if (sinceId && untilId) {
+		q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
+		q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
+		q.orderBy(`${q.alias}.id`, 'DESC');
+	} else if (sinceId) {
+		q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
+		q.orderBy(`${q.alias}.id`, 'ASC');
+	} else if (untilId) {
+		q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
+		q.orderBy(`${q.alias}.id`, 'DESC');
+	} else if (sinceDate && untilDate) {
+		q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) });
+		q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) });
+		q.orderBy(`${q.alias}.createdAt`, 'DESC');
+	} else if (sinceDate) {
+		q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) });
+		q.orderBy(`${q.alias}.createdAt`, 'ASC');
+	} else if (untilDate) {
+		q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) });
+		q.orderBy(`${q.alias}.createdAt`, 'DESC');
+	} else {
+		q.orderBy(`${q.alias}.id`, 'DESC');
+	}
+	return q;
+}
diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts
index 9f1e7e6ab4ce810f26a53aa63b7f5ff1cd1f7de8..2cb5a1f87f406845f040ebbb7b6d4d0192ec8f93 100644
--- a/src/server/api/common/read-messaging-message.ts
+++ b/src/server/api/common/read-messaging-message.ts
@@ -1,77 +1,43 @@
-import * as mongo from 'mongodb';
-import isObjectId from '../../../misc/is-objectid';
-import Message from '../../../models/messaging-message';
-import { IMessagingMessage as IMessage } from '../../../models/messaging-message';
 import { publishMainStream } from '../../../services/stream';
 import { publishMessagingStream } from '../../../services/stream';
 import { publishMessagingIndexStream } from '../../../services/stream';
-import User from '../../../models/user';
+import { User } from '../../../models/entities/user';
+import { MessagingMessage } from '../../../models/entities/messaging-message';
+import { MessagingMessages } from '../../../models';
+import { In } from 'typeorm';
 
 /**
  * Mark messages as read
  */
-export default (
-	user: string | mongo.ObjectID,
-	otherparty: string | mongo.ObjectID,
-	message: string | string[] | IMessage | IMessage[] | mongo.ObjectID | mongo.ObjectID[]
-) => new Promise<any>(async (resolve, reject) => {
-
-	const userId = isObjectId(user)
-		? user
-		: new mongo.ObjectID(user);
-
-	const otherpartyId = isObjectId(otherparty)
-		? otherparty
-		: new mongo.ObjectID(otherparty);
-
-	const ids: mongo.ObjectID[] = Array.isArray(message)
-		? isObjectId(message[0])
-			? (message as mongo.ObjectID[])
-			: typeof message[0] === 'string'
-				? (message as string[]).map(m => new mongo.ObjectID(m))
-				: (message as IMessage[]).map(m => m._id)
-		: isObjectId(message)
-			? [(message as mongo.ObjectID)]
-			: typeof message === 'string'
-				? [new mongo.ObjectID(message)]
-				: [(message as IMessage)._id];
+export default async (
+	userId: User['id'],
+	otherpartyId: User['id'],
+	messageIds: MessagingMessage['id'][]
+) => {
+	if (messageIds.length === 0) return;
 
 	// Update documents
-	await Message.update({
-		_id: { $in: ids },
+	await MessagingMessages.update({
+		id: In(messageIds),
 		userId: otherpartyId,
 		recipientId: userId,
 		isRead: false
 	}, {
-			$set: {
-				isRead: true
-			}
-		}, {
-			multi: true
-		});
+		isRead: true
+	});
 
 	// Publish event
-	publishMessagingStream(otherpartyId, userId, 'read', ids.map(id => id.toString()));
-	publishMessagingIndexStream(userId, 'read', ids.map(id => id.toString()));
+	publishMessagingStream(otherpartyId, userId, 'read', messageIds);
+	publishMessagingIndexStream(userId, 'read', messageIds);
 
 	// Calc count of my unread messages
-	const count = await Message
-		.count({
-			recipientId: userId,
-			isRead: false
-		}, {
-				limit: 1
-			});
+	const count = await MessagingMessages.count({
+		recipientId: userId,
+		isRead: false
+	});
 
 	if (count == 0) {
-		// Update flag
-		User.update({ _id: userId }, {
-			$set: {
-				hasUnreadMessagingMessage: false
-			}
-		});
-
 		// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
 		publishMainStream(userId, 'readAllMessagingMessages');
 	}
-});
+};
diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts
index 436130511983871145acb667397b6b256b865229..c8d43ba286266cfe3ee7e2c1d34a60f7989b89f7 100644
--- a/src/server/api/common/read-notification.ts
+++ b/src/server/api/common/read-notification.ts
@@ -1,72 +1,38 @@
-import * as mongo from 'mongodb';
-import isObjectId from '../../../misc/is-objectid';
-import { default as Notification, INotification } from '../../../models/notification';
 import { publishMainStream } from '../../../services/stream';
-import Mute from '../../../models/mute';
-import User from '../../../models/user';
+import { User } from '../../../models/entities/user';
+import { Notification } from '../../../models/entities/notification';
+import { Mutings, Notifications } from '../../../models';
+import { In, Not } from 'typeorm';
 
 /**
  * Mark notifications as read
  */
-export default (
-	user: string | mongo.ObjectID,
-	message: string | string[] | INotification | INotification[] | mongo.ObjectID | mongo.ObjectID[]
-) => new Promise<any>(async (resolve, reject) => {
-
-	const userId = isObjectId(user)
-		? user
-		: new mongo.ObjectID(user);
-
-	const ids: mongo.ObjectID[] = Array.isArray(message)
-		? isObjectId(message[0])
-			? (message as mongo.ObjectID[])
-			: typeof message[0] === 'string'
-				? (message as string[]).map(m => new mongo.ObjectID(m))
-				: (message as INotification[]).map(m => m._id)
-		: isObjectId(message)
-			? [(message as mongo.ObjectID)]
-			: typeof message === 'string'
-				? [new mongo.ObjectID(message)]
-				: [(message as INotification)._id];
-
-	const mute = await Mute.find({
+export async function readNotification(
+	userId: User['id'],
+	notificationIds: Notification['id'][]
+) {
+	const mute = await Mutings.find({
 		muterId: userId
 	});
 	const mutedUserIds = mute.map(m => m.muteeId);
 
 	// Update documents
-	await Notification.update({
-		_id: { $in: ids },
+	await Notifications.update({
+		id: In(notificationIds),
 		isRead: false
 	}, {
-			$set: {
-				isRead: true
-			}
-		}, {
-			multi: true
-		});
+		isRead: true
+	});
 
 	// Calc count of my unread notifications
-	const count = await Notification
-		.count({
-			notifieeId: userId,
-			notifierId: {
-				$nin: mutedUserIds
-			},
-			isRead: false
-		}, {
-				limit: 1
-			});
-
-	if (count == 0) {
-		// Update flag
-		User.update({ _id: userId }, {
-			$set: {
-				hasUnreadNotification: false
-			}
-		});
+	const count = await Notifications.count({
+		notifieeId: userId,
+		...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}),
+		isRead: false
+	});
 
+	if (count === 0) {
 		// 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行
 		publishMainStream(userId, 'readAllNotifications');
 	}
-});
+}
diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts
index 84cad3a9359bf9670b5401b7af777f1968c5835f..0f4ee4ca1135808246ec36535442e9ef0c893226 100644
--- a/src/server/api/common/signin.ts
+++ b/src/server/api/common/signin.ts
@@ -1,7 +1,7 @@
 import * as Koa from 'koa';
 
 import config from '../../../config';
-import { ILocalUser } from '../../../models/user';
+import { ILocalUser } from '../../../models/entities/user';
 
 export default function(ctx: Koa.BaseContext, user: ILocalUser, redirect = false) {
 	if (redirect) {
diff --git a/src/server/api/define.ts b/src/server/api/define.ts
index f2fababc32b631008136842b04b4356eefa98bab..a18419bcf6585f340e4aedf926b03b3b819b1ebb 100644
--- a/src/server/api/define.ts
+++ b/src/server/api/define.ts
@@ -1,8 +1,8 @@
 import * as fs from 'fs';
-import { ILocalUser } from '../../models/user';
-import { IApp } from '../../models/app';
+import { ILocalUser } from '../../models/entities/user';
 import { IEndpointMeta } from './endpoints';
 import { ApiError } from './error';
+import { App } from '../../models/entities/app';
 
 type Params<T extends IEndpointMeta> = {
 	[P in keyof T['params']]: T['params'][P]['transform'] extends Function
@@ -12,8 +12,8 @@ type Params<T extends IEndpointMeta> = {
 
 export type Response = Record<string, any> | void;
 
-export default function <T extends IEndpointMeta>(meta: T, cb: (params: Params<T>, user: ILocalUser, app: IApp, file?: any, cleanup?: Function) => Promise<Response>): (params: any, user: ILocalUser, app: IApp, file?: any) => Promise<any> {
-	return (params: any, user: ILocalUser, app: IApp, file?: any) => {
+export default function <T extends IEndpointMeta>(meta: T, cb: (params: Params<T>, user: ILocalUser, app: App, file?: any, cleanup?: Function) => Promise<Response>): (params: any, user: ILocalUser, app: App, file?: any) => Promise<any> {
+	return (params: any, user: ILocalUser, app: App, file?: any) => {
 		function cleanup() {
 			fs.unlink(file.path, () => {});
 		}
diff --git a/src/server/api/endpoints/admin/abuse-user-reports.ts b/src/server/api/endpoints/admin/abuse-user-reports.ts
index d9fe3429cedd0613c6859193205207d1a41cada1..5c5a734c1d587dff149971fb5c6933fa58b23c27 100644
--- a/src/server/api/endpoints/admin/abuse-user-reports.ts
+++ b/src/server/api/endpoints/admin/abuse-user-reports.ts
@@ -1,7 +1,8 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Report, { packMany } from '../../../../models/abuse-user-report';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
+import { AbuseUserReports } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
 
 export const meta = {
 	tags: ['admin'],
@@ -17,37 +18,18 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 	}
 };
 
 export default define(meta, async (ps) => {
-	const sort = {
-		_id: -1
-	};
-	const query = {} as any;
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		query._id = {
-			$lt: ps.untilId
-		};
-	}
+	const query = makePaginationQuery(AbuseUserReports.createQueryBuilder('report'), ps.sinceId, ps.untilId);
 
-	const reports = await Report
-		.find(query, {
-			limit: ps.limit,
-			sort: sort
-		});
+	const reports = await query.take(ps.limit).getMany();
 
-	return await packMany(reports);
+	return await AbuseUserReports.packMany(reports);
 });
diff --git a/src/server/api/endpoints/admin/drive/files.ts b/src/server/api/endpoints/admin/drive/files.ts
index 8ed417a429c58040ccab2ba99d8266369b541d4a..1ccabc92d91ee1edd5c707858afaabc1aa58a1ca 100644
--- a/src/server/api/endpoints/admin/drive/files.ts
+++ b/src/server/api/endpoints/admin/drive/files.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
-import File, { packMany } from '../../../../../models/drive-file';
 import define from '../../../define';
 import { fallback } from '../../../../../prelude/symbol';
+import { DriveFiles } from '../../../../../models';
 
 export const meta = {
 	tags: ['admin'],
@@ -41,27 +41,25 @@ export const meta = {
 };
 
 const sort: any = { // < https://github.com/Microsoft/TypeScript/issues/1863
-	'+createdAt': { uploadDate: -1 },
-	'-createdAt': { uploadDate: 1 },
-	'+size': { length: -1 },
-	'-size': { length: 1 },
-	[fallback]: { _id: -1 }
+	'+createdAt': { createdAt: -1 },
+	'-createdAt': { createdAt: 1 },
+	'+size': { size: -1 },
+	'-size': { size: 1 },
+	[fallback]: { id: -1 }
 };
 
 export default define(meta, async (ps, me) => {
-	const q = {
-		'metadata.deletedAt': { $exists: false },
-	} as any;
+	const q = {} as any;
 
-	if (ps.origin == 'local') q['metadata._user.host'] = null;
-	if (ps.origin == 'remote') q['metadata._user.host'] = { $ne: null };
+	if (ps.origin == 'local') q['userHost'] = null;
+	if (ps.origin == 'remote') q['userHost'] = { $ne: null };
 
-	const files = await File
-		.find(q, {
-			limit: ps.limit,
-			sort: sort[ps.sort] || sort[fallback],
-			skip: ps.offset
-		});
+	const files = await DriveFiles.find({
+		where: q,
+		take: ps.limit,
+		order: sort[ps.sort] || sort[fallback],
+		skip: ps.offset
+	});
 
-	return await packMany(files, { detail: true, withUser: true, self: true });
+	return await DriveFiles.packMany(files, { detail: true, withUser: true, self: true });
 });
diff --git a/src/server/api/endpoints/admin/drive/show-file.ts b/src/server/api/endpoints/admin/drive/show-file.ts
index 405b6d44ceb18a4b8d8ef8c7b5f313b01bc974e7..a2b6c158f0b3464da83b5e0719da0c9ca9fa2693 100644
--- a/src/server/api/endpoints/admin/drive/show-file.ts
+++ b/src/server/api/endpoints/admin/drive/show-file.ts
@@ -1,8 +1,8 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
-import DriveFile from '../../../../../models/drive-file';
 import { ApiError } from '../../../error';
+import { DriveFiles } from '../../../../../models';
 
 export const meta = {
 	tags: ['admin'],
@@ -13,7 +13,6 @@ export const meta = {
 	params: {
 		fileId: {
 			validator: $.type(ID),
-			transform: transform,
 		},
 	},
 
@@ -27,9 +26,7 @@ export const meta = {
 };
 
 export default define(meta, async (ps, me) => {
-	const file = await DriveFile.findOne({
-		_id: ps.fileId
-	});
+	const file = await DriveFiles.findOne(ps.fileId);
 
 	if (file == null) {
 		throw new ApiError(meta.errors.noSuchFile);
diff --git a/src/server/api/endpoints/admin/emoji/add.ts b/src/server/api/endpoints/admin/emoji/add.ts
index c126c8380f116183e6072a7776fe1ccb3a90ba3a..c26e8dd04d7b4d51a89ee1aa7d49a6b005ff1341 100644
--- a/src/server/api/endpoints/admin/emoji/add.ts
+++ b/src/server/api/endpoints/admin/emoji/add.ts
@@ -1,7 +1,8 @@
 import $ from 'cafy';
-import Emoji from '../../../../../models/emoji';
 import define from '../../../define';
 import { detectUrlMine } from '../../../../../misc/detect-url-mine';
+import { Emojis } from '../../../../../models';
+import { genId } from '../../../../../misc/gen-id';
 
 export const meta = {
 	desc: {
@@ -32,7 +33,8 @@ export const meta = {
 export default define(meta, async (ps) => {
 	const type = await detectUrlMine(ps.url);
 
-	const emoji = await Emoji.insert({
+	const emoji = await Emojis.save({
+		id: genId(),
 		updatedAt: new Date(),
 		name: ps.name,
 		host: null,
@@ -42,6 +44,6 @@ export default define(meta, async (ps) => {
 	});
 
 	return {
-		id: emoji._id
+		id: emoji.id
 	};
 });
diff --git a/src/server/api/endpoints/admin/emoji/list.ts b/src/server/api/endpoints/admin/emoji/list.ts
index 954f8f96c6098476ff531aa39fb8e819bb14697a..07174723b9a1677665a432424e248fd6e41b8174 100644
--- a/src/server/api/endpoints/admin/emoji/list.ts
+++ b/src/server/api/endpoints/admin/emoji/list.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy';
-import Emoji from '../../../../../models/emoji';
 import define from '../../../define';
+import { Emojis } from '../../../../../models';
 
 export const meta = {
 	desc: {
@@ -21,12 +21,12 @@ export const meta = {
 };
 
 export default define(meta, async (ps) => {
-	const emojis = await Emoji.find({
+	const emojis = await Emojis.find({
 		host: ps.host
 	});
 
 	return emojis.map(e => ({
-		id: e._id,
+		id: e.id,
 		name: e.name,
 		aliases: e.aliases,
 		host: e.host,
diff --git a/src/server/api/endpoints/admin/emoji/remove.ts b/src/server/api/endpoints/admin/emoji/remove.ts
index 4c69dffbaee8e809f3abd0076e6997d960cf4f47..316834b884ae74f7a18b23d7a19a1e706571c041 100644
--- a/src/server/api/endpoints/admin/emoji/remove.ts
+++ b/src/server/api/endpoints/admin/emoji/remove.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
-import Emoji from '../../../../../models/emoji';
 import define from '../../../define';
-import ID from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
+import { Emojis } from '../../../../../models';
 
 export const meta = {
 	desc: {
@@ -21,13 +21,9 @@ export const meta = {
 };
 
 export default define(meta, async (ps) => {
-	const emoji = await Emoji.findOne({
-		_id: ps.id
-	});
+	const emoji = await Emojis.findOne(ps.id);
 
 	if (emoji == null) throw new Error('emoji not found');
 
-	await Emoji.remove({ _id: emoji._id });
-
-	return;
+	await Emojis.delete(emoji.id);
 });
diff --git a/src/server/api/endpoints/admin/emoji/update.ts b/src/server/api/endpoints/admin/emoji/update.ts
index 8b1c07be9ea3eb8899c7d82fa92a6e7ee0a5349c..48b4a4ee23cf7f0dc5285b307cbf1f59f36ed1c5 100644
--- a/src/server/api/endpoints/admin/emoji/update.ts
+++ b/src/server/api/endpoints/admin/emoji/update.ts
@@ -1,8 +1,8 @@
 import $ from 'cafy';
-import Emoji from '../../../../../models/emoji';
 import define from '../../../define';
-import ID from '../../../../../misc/cafy-id';
 import { detectUrlMine } from '../../../../../misc/detect-url-mine';
+import { ID } from '../../../../../misc/cafy-id';
+import { Emojis } from '../../../../../models';
 
 export const meta = {
 	desc: {
@@ -34,23 +34,17 @@ export const meta = {
 };
 
 export default define(meta, async (ps) => {
-	const emoji = await Emoji.findOne({
-		_id: ps.id
-	});
+	const emoji = await Emojis.findOne(ps.id);
 
 	if (emoji == null) throw new Error('emoji not found');
 
 	const type = await detectUrlMine(ps.url);
 
-	await Emoji.update({ _id: emoji._id }, {
-		$set: {
-			updatedAt: new Date(),
-			name: ps.name,
-			aliases: ps.aliases,
-			url: ps.url,
-			type,
-		}
+	await Emojis.update(emoji.id, {
+		updatedAt: new Date(),
+		name: ps.name,
+		aliases: ps.aliases,
+		url: ps.url,
+		type,
 	});
-
-	return;
 });
diff --git a/src/server/api/endpoints/admin/federation/remove-all-following.ts b/src/server/api/endpoints/admin/federation/remove-all-following.ts
index 98afdfc2a5d806bc75e7d8fd20ff2eb3f0f6c717..fca76e70864bc66aa6f7850d9a5dca6720fc7256 100644
--- a/src/server/api/endpoints/admin/federation/remove-all-following.ts
+++ b/src/server/api/endpoints/admin/federation/remove-all-following.ts
@@ -1,8 +1,7 @@
 import $ from 'cafy';
 import define from '../../../define';
-import Following from '../../../../../models/following';
-import User from '../../../../../models/user';
 import deleteFollowing from '../../../../../services/following/delete';
+import { Followings, Users } from '../../../../../models';
 
 export const meta = {
 	tags: ['admin'],
@@ -18,13 +17,13 @@ export const meta = {
 };
 
 export default define(meta, async (ps, me) => {
-	const followings = await Following.find({
-		'_follower.host': ps.host
+	const followings = await Followings.find({
+		followerHost: ps.host
 	});
 
 	const pairs = await Promise.all(followings.map(f => Promise.all([
-		User.findOne({ _id: f.followerId }),
-		User.findOne({ _id: f.followeeId })
+		Users.findOne(f.followerId),
+		Users.findOne(f.followeeId)
 	])));
 
 	for (const pair of pairs) {
diff --git a/src/server/api/endpoints/admin/federation/update-instance.ts b/src/server/api/endpoints/admin/federation/update-instance.ts
index 0d127b53b30632d9623845941a470aac94c0277d..d1abe95a5b2b26362d0bd7f93945ee025a2d9d0f 100644
--- a/src/server/api/endpoints/admin/federation/update-instance.ts
+++ b/src/server/api/endpoints/admin/federation/update-instance.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy';
 import define from '../../../define';
-import Instance from '../../../../../models/instance';
+import { Instances } from '../../../../../models';
 
 export const meta = {
 	tags: ['admin'],
@@ -13,10 +13,6 @@ export const meta = {
 			validator: $.str
 		},
 
-		isBlocked: {
-			validator: $.bool
-		},
-
 		isClosed: {
 			validator: $.bool
 		},
@@ -24,18 +20,13 @@ export const meta = {
 };
 
 export default define(meta, async (ps, me) => {
-	const instance = await Instance.findOne({ host: ps.host });
+	const instance = await Instances.findOne({ host: ps.host });
 
 	if (instance == null) {
 		throw new Error('instance not found');
 	}
 
-	Instance.update({ host: ps.host }, {
-		$set: {
-			isBlocked: ps.isBlocked,
-			isMarkedAsClosed: ps.isClosed
-		}
+	Instances.update({ host: ps.host }, {
+		isMarkedAsClosed: ps.isClosed
 	});
-
-	return;
 });
diff --git a/src/server/api/endpoints/admin/invite.ts b/src/server/api/endpoints/admin/invite.ts
index 28aa30195747b1d42ff891570551c7e717b9f406..4e264feef677e3024ef4c076097c99531e3312ed 100644
--- a/src/server/api/endpoints/admin/invite.ts
+++ b/src/server/api/endpoints/admin/invite.ts
@@ -1,6 +1,7 @@
 import rndstr from 'rndstr';
-import RegistrationTicket from '../../../../models/registration-tickets';
 import define from '../../define';
+import { RegistrationTickets } from '../../../../models';
+import { genId } from '../../../../misc/gen-id';
 
 export const meta = {
 	desc: {
@@ -18,7 +19,8 @@ export const meta = {
 export default define(meta, async (ps) => {
 	const code = rndstr({ length: 5, chars: '0-9' });
 
-	await RegistrationTicket.insert({
+	await RegistrationTickets.save({
+		id: genId(),
 		createdAt: new Date(),
 		code: code
 	});
diff --git a/src/server/api/endpoints/admin/logs.ts b/src/server/api/endpoints/admin/logs.ts
index 805a42b9e0a3febed9e5385b90b6fd2d4dce1a3d..eee56a39395a9f546d4f39858362a82459c0a192 100644
--- a/src/server/api/endpoints/admin/logs.ts
+++ b/src/server/api/endpoints/admin/logs.ts
@@ -1,6 +1,7 @@
 import $ from 'cafy';
 import define from '../../define';
-import Log from '../../../../models/log';
+import { Logs } from '../../../../models';
+import { Brackets } from 'typeorm';
 
 export const meta = {
 	tags: ['admin'],
@@ -27,41 +28,44 @@ export const meta = {
 };
 
 export default define(meta, async (ps) => {
-	const sort = {
-		_id: -1
-	};
-	const query = {} as any;
+	const query = Logs.createQueryBuilder('log');
+
+	if (ps.level) query.andWhere('log.level = :level', { level: ps.level });
 
-	if (ps.level) query.level = ps.level;
 	if (ps.domain) {
-		for (const d of ps.domain.split(' ')) {
-			const qs: any[] = [];
-			let i = 0;
-			for (const sd of (d.startsWith('-') ? d.substr(1) : d).split('.')) {
-				qs.push({
-					[`domain.${i}`]: d.startsWith('-') ? { $ne: sd } : sd
-				});
-				i++;
-			}
-			if (d.startsWith('-')) {
-				if (query['$and'] == null) query['$and'] = [];
-				query['$and'].push({
-					$and: qs
-				});
-			} else {
-				if (query['$or'] == null) query['$or'] = [];
-				query['$or'].push({
-					$and: qs
-				});
-			}
+		const whiteDomains = ps.domain.split(' ').filter(x => !x.startsWith('-'));
+		const blackDomains = ps.domain.split(' ').filter(x => x.startsWith('-'));
+
+		if (whiteDomains.length > 0) {
+			query.andWhere(new Brackets(qb => {
+				for (const whiteDomain of whiteDomains) {
+					let i = 0;
+					for (const subDomain of whiteDomain.split('.')) {
+						const p = `whiteSubDomain_${subDomain}_${i}`;
+						// SQL is 1 based, so we need '+ 1'
+						qb.orWhere(`log.domain[${i + 1}] = :${p}`, { [p]: subDomain });
+						i++;
+					}
+				}
+			}));
+		}
+
+		if (blackDomains.length > 0) {
+			query.andWhere(new Brackets(qb => {
+				for (const blackDomain of blackDomains) {
+					let i = 0;
+					for (const subDomain of blackDomain.split('.')) {
+						const p = `blackSubDomain_${subDomain}_${i}`;
+						// SQL is 1 based, so we need '+ 1'
+						qb.andWhere(`log.domain[${i + 1}] != :${p}`, { [p]: subDomain });
+						i++;
+					}
+				}
+			}));
 		}
 	}
 
-	const logs = await Log
-		.find(query, {
-			limit: ps.limit,
-			sort: sort
-		});
+	const logs = await query.take(ps.limit).getMany();
 
 	return logs;
 });
diff --git a/src/server/api/endpoints/admin/moderators/add.ts b/src/server/api/endpoints/admin/moderators/add.ts
index 2271bcd1a919b40dc27b7a81254ec5700f7586c9..a15f0a17a2ea9bcfa3ed3669238a2f5dbcd4c01d 100644
--- a/src/server/api/endpoints/admin/moderators/add.ts
+++ b/src/server/api/endpoints/admin/moderators/add.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
-import User from '../../../../../models/user';
+import { Users } from '../../../../../models';
 
 export const meta = {
 	desc: {
@@ -17,7 +17,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーID',
 				'en-US': 'The user ID'
@@ -27,21 +26,13 @@ export const meta = {
 };
 
 export default define(meta, async (ps) => {
-	const user = await User.findOne({
-		_id: ps.userId
-	});
+	const user = await Users.findOne(ps.userId as string);
 
 	if (user == null) {
 		throw new Error('user not found');
 	}
 
-	await User.update({
-		_id: user._id
-	}, {
-		$set: {
-			isModerator: true
-		}
+	await Users.update(user.id, {
+		isModerator: true
 	});
-
-	return;
 });
diff --git a/src/server/api/endpoints/admin/moderators/remove.ts b/src/server/api/endpoints/admin/moderators/remove.ts
index 84143d3e359fdce0ff8447a7d7a05618fc583c4c..209cf0814f2010804c41e4ebf74c750a8d1a7131 100644
--- a/src/server/api/endpoints/admin/moderators/remove.ts
+++ b/src/server/api/endpoints/admin/moderators/remove.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
-import User from '../../../../../models/user';
+import { Users } from '../../../../../models';
 
 export const meta = {
 	desc: {
@@ -17,7 +17,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーID',
 				'en-US': 'The user ID'
@@ -27,21 +26,13 @@ export const meta = {
 };
 
 export default define(meta, async (ps) => {
-	const user = await User.findOne({
-		_id: ps.userId
-	});
+	const user = await Users.findOne(ps.userId as string);
 
 	if (user == null) {
 		throw new Error('user not found');
 	}
 
-	await User.update({
-		_id: user._id
-	}, {
-		$set: {
-			isModerator: false
-		}
+	await Users.update(user.id, {
+		isModerator: false
 	});
-
-	return;
 });
diff --git a/src/server/api/endpoints/admin/remove-abuse-user-report.ts b/src/server/api/endpoints/admin/remove-abuse-user-report.ts
index fa17e2c9379e8ae8c8ab46136a5ab146e5507485..f293c007183153cbb198bfebcb077880a755ec37 100644
--- a/src/server/api/endpoints/admin/remove-abuse-user-report.ts
+++ b/src/server/api/endpoints/admin/remove-abuse-user-report.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
-import AbuseUserReport from '../../../../models/abuse-user-report';
+import { AbuseUserReports } from '../../../../models';
 
 export const meta = {
 	tags: ['admin'],
@@ -12,23 +12,16 @@ export const meta = {
 	params: {
 		reportId: {
 			validator: $.type(ID),
-			transform: transform
 		},
 	}
 };
 
 export default define(meta, async (ps) => {
-	const report = await AbuseUserReport.findOne({
-		_id: ps.reportId
-	});
+	const report = await AbuseUserReports.findOne(ps.reportId);
 
 	if (report == null) {
 		throw new Error('report not found');
 	}
 
-	await AbuseUserReport.remove({
-		_id: report._id
-	});
-
-	return;
+	await AbuseUserReports.delete(report.id);
 });
diff --git a/src/server/api/endpoints/admin/reset-password.ts b/src/server/api/endpoints/admin/reset-password.ts
index 73901d83584e75623bdfa625691147e3f008bc62..07b8b6d93846088ee1c748b8f6484c790c07d630 100644
--- a/src/server/api/endpoints/admin/reset-password.ts
+++ b/src/server/api/endpoints/admin/reset-password.ts
@@ -1,9 +1,9 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
-import User from '../../../../models/user';
 import * as bcrypt from 'bcryptjs';
 import rndstr from 'rndstr';
+import { Users } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -18,7 +18,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーID',
 				'en-US': 'The user ID which you want to suspend'
@@ -28,9 +27,7 @@ export const meta = {
 };
 
 export default define(meta, async (ps) => {
-	const user = await User.findOne({
-		_id: ps.userId
-	});
+	const user = await Users.findOne(ps.userId as string);
 
 	if (user == null) {
 		throw new Error('user not found');
@@ -45,12 +42,8 @@ export default define(meta, async (ps) => {
 	// Generate hash of password
 	const hash = bcrypt.hashSync(passwd);
 
-	await User.findOneAndUpdate({
-		_id: user._id
-	}, {
-		$set: {
-			password: hash
-		}
+	await Users.update(user.id, {
+		password: hash
 	});
 
 	return {
diff --git a/src/server/api/endpoints/admin/show-user.ts b/src/server/api/endpoints/admin/show-user.ts
index 985f71a8739b264dc540e2d9f866f4e3aa398ba0..452125dea0668e5fa5e36372b041badab671b6f9 100644
--- a/src/server/api/endpoints/admin/show-user.ts
+++ b/src/server/api/endpoints/admin/show-user.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
-import User from '../../../../models/user';
+import { Users } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -16,7 +16,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーID',
 				'en-US': 'The user ID which you want to suspend'
@@ -26,9 +25,7 @@ export const meta = {
 };
 
 export default define(meta, async (ps, me) => {
-	const user = await User.findOne({
-		_id: ps.userId
-	});
+	const user = await Users.findOne(ps.userId as string);
 
 	if (user == null) {
 		throw new Error('user not found');
diff --git a/src/server/api/endpoints/admin/show-users.ts b/src/server/api/endpoints/admin/show-users.ts
index 5feb1b4fd85a10704ed25832e7536c4fb277a4e3..73976b98725fbd278cff0cb0012959f85e413b2c 100644
--- a/src/server/api/endpoints/admin/show-users.ts
+++ b/src/server/api/endpoints/admin/show-users.ts
@@ -1,7 +1,6 @@
 import $ from 'cafy';
-import User, { pack } from '../../../../models/user';
 import define from '../../define';
-import { fallback } from '../../../../prelude/symbol';
+import { Users } from '../../../../models';
 
 export const meta = {
 	tags: ['admin'],
@@ -55,51 +54,38 @@ export const meta = {
 	}
 };
 
-const sort: any = { // < https://github.com/Microsoft/TypeScript/issues/1863
-	'+follower': { followersCount: -1 },
-	'-follower': { followersCount: 1 },
-	'+createdAt': { createdAt: -1 },
-	'-createdAt': { createdAt: 1 },
-	'+updatedAt': { updatedAt: -1 },
-	'-updatedAt': { updatedAt: 1 },
-	[fallback]: { _id: -1 }
-};
-
 export default define(meta, async (ps, me) => {
-	const q = {
-		$and: []
-	} as any;
+	const query = Users.createQueryBuilder('user');
+
+	switch (ps.state) {
+		case 'admin': query.where('user.isAdmin = TRUE'); break;
+		case 'moderator': query.where('user.isModerator = TRUE'); break;
+		case 'adminOrModerator': query.where('user.isAdmin = TRUE OR isModerator = TRUE'); break;
+		case 'verified': query.where('user.isVerified = TRUE'); break;
+		case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break;
+		case 'silenced': query.where('user.isSilenced = TRUE'); break;
+		case 'suspended': query.where('user.isSuspended = TRUE'); break;
+	}
 
-	// state
-	q.$and.push(
-		ps.state == 'admin' ? { isAdmin: true } :
-		ps.state == 'moderator' ? { isModerator: true } :
-		ps.state == 'adminOrModerator' ? {
-			$or: [{
-				isAdmin: true
-			}, {
-				isModerator: true
-			}]
-		} :
-		ps.state == 'verified' ? { isVerified: true } :
-		ps.state == 'silenced' ? { isSilenced: true } :
-		ps.state == 'suspended' ? { isSuspended: true } :
-		{}
-	);
+	switch (ps.origin) {
+		case 'local': query.andWhere('user.host IS NULL'); break;
+		case 'remote': query.andWhere('user.host IS NOT NULL'); break;
+	}
+
+	switch (ps.sort) {
+		case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
+		case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
+		case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break;
+		case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break;
+		case '+updatedAt': query.orderBy('user.updatedAt', 'DESC'); break;
+		case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break;
+		default: query.orderBy('user.id', 'ASC'); break;
+	}
 
-	// origin
-	q.$and.push(
-		ps.origin == 'local' ? { host: null } :
-		ps.origin == 'remote' ? { host: { $ne: null } } :
-		{}
-	);
+	query.take(ps.limit);
+	query.skip(ps.offset);
 
-	const users = await User
-		.find(q, {
-			limit: ps.limit,
-			sort: sort[ps.sort] || sort[fallback],
-			skip: ps.offset
-		});
+	const users = await query.getMany();
 
-	return await Promise.all(users.map(user => pack(user, me, { detail: true })));
+	return await Users.packMany(users, me, { detail: true });
 });
diff --git a/src/server/api/endpoints/admin/silence-user.ts b/src/server/api/endpoints/admin/silence-user.ts
index 2557d8de6a7edbc47b6b5fd7f51f279ae759e623..83aa88012a568eb8984bba177f0e7415a2156e6b 100644
--- a/src/server/api/endpoints/admin/silence-user.ts
+++ b/src/server/api/endpoints/admin/silence-user.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
-import User from '../../../../models/user';
+import { Users } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -17,7 +17,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーID',
 				'en-US': 'The user ID which you want to make silence'
@@ -27,9 +26,7 @@ export const meta = {
 };
 
 export default define(meta, async (ps) => {
-	const user = await User.findOne({
-		_id: ps.userId
-	});
+	const user = await Users.findOne(ps.userId as string);
 
 	if (user == null) {
 		throw new Error('user not found');
@@ -39,13 +36,7 @@ export default define(meta, async (ps) => {
 		throw new Error('cannot silence admin');
 	}
 
-	await User.findOneAndUpdate({
-		_id: user._id
-	}, {
-		$set: {
-			isSilenced: true
-		}
+	await Users.update(user.id, {
+		isSilenced: true
 	});
-
-	return;
 });
diff --git a/src/server/api/endpoints/admin/suspend-user.ts b/src/server/api/endpoints/admin/suspend-user.ts
index 0a2d30953078dc4a1cd4daa844b3b0b7426bd1cf..fa4d378708a07ce7b2576ab3ac549ad2e10f69b4 100644
--- a/src/server/api/endpoints/admin/suspend-user.ts
+++ b/src/server/api/endpoints/admin/suspend-user.ts
@@ -1,9 +1,9 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
-import User, { IUser } from '../../../../models/user';
-import Following from '../../../../models/following';
 import deleteFollowing from '../../../../services/following/delete';
+import { Users, Followings } from '../../../../models';
+import { User } from '../../../../models/entities/user';
 
 export const meta = {
 	desc: {
@@ -19,7 +19,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーID',
 				'en-US': 'The user ID which you want to suspend'
@@ -29,9 +28,7 @@ export const meta = {
 };
 
 export default define(meta, async (ps) => {
-	const user = await User.findOne({
-		_id: ps.userId
-	});
+	const user = await Users.findOne(ps.userId as string);
 
 	if (user == null) {
 		throw new Error('user not found');
@@ -45,27 +42,21 @@ export default define(meta, async (ps) => {
 		throw new Error('cannot suspend moderator');
 	}
 
-	await User.findOneAndUpdate({
-		_id: user._id
-	}, {
-		$set: {
-			isSuspended: true
-		}
+	await Users.update(user.id, {
+		isSuspended: true
 	});
 
 	unFollowAll(user);
-
-	return;
 });
 
-async function unFollowAll(follower: IUser) {
-	const followings = await Following.find({
-		followerId: follower._id
+async function unFollowAll(follower: User) {
+	const followings = await Followings.find({
+		followerId: follower.id
 	});
 
 	for (const following of followings) {
-		const followee = await User.findOne({
-			_id: following.followeeId
+		const followee = await Users.findOne({
+			id: following.followeeId
 		});
 
 		if (followee == null) {
diff --git a/src/server/api/endpoints/admin/unsilence-user.ts b/src/server/api/endpoints/admin/unsilence-user.ts
index 01bf41aaefd2b2308281ddd421bc6a79b80f43d0..f9b173366b52bd3578f54512b2dd3797d2a65f00 100644
--- a/src/server/api/endpoints/admin/unsilence-user.ts
+++ b/src/server/api/endpoints/admin/unsilence-user.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
-import User from '../../../../models/user';
+import { Users } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -17,7 +17,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーID',
 				'en-US': 'The user ID which you want to unsilence'
@@ -27,21 +26,13 @@ export const meta = {
 };
 
 export default define(meta, async (ps) => {
-	const user = await User.findOne({
-		_id: ps.userId
-	});
+	const user = await Users.findOne(ps.userId as string);
 
 	if (user == null) {
 		throw new Error('user not found');
 	}
 
-	await User.findOneAndUpdate({
-		_id: user._id
-	}, {
-		$set: {
-			isSilenced: false
-		}
+	await Users.update(user.id, {
+		isSilenced: false
 	});
-
-	return;
 });
diff --git a/src/server/api/endpoints/admin/unsuspend-user.ts b/src/server/api/endpoints/admin/unsuspend-user.ts
index 5da35f28e644118fe54ea93f3b08e0ddfb147c2c..08dae034d3ab41723807e7079a211527d883317b 100644
--- a/src/server/api/endpoints/admin/unsuspend-user.ts
+++ b/src/server/api/endpoints/admin/unsuspend-user.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
-import User from '../../../../models/user';
+import { Users } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -17,7 +17,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーID',
 				'en-US': 'The user ID which you want to unsuspend'
@@ -27,21 +26,13 @@ export const meta = {
 };
 
 export default define(meta, async (ps) => {
-	const user = await User.findOne({
-		_id: ps.userId
-	});
+	const user = await Users.findOne(ps.userId as string);
 
 	if (user == null) {
 		throw new Error('user not found');
 	}
 
-	await User.findOneAndUpdate({
-		_id: user._id
-	}, {
-		$set: {
-			isSuspended: false
-		}
+	await Users.update(user.id, {
+		isSuspended: false
 	});
-
-	return;
 });
diff --git a/src/server/api/endpoints/admin/unverify-user.ts b/src/server/api/endpoints/admin/unverify-user.ts
index d3ca05cb39e8948a258c9365a85071ace78ac21d..b215dbf10d912cf71d39f7193ac1de55f1abd4d2 100644
--- a/src/server/api/endpoints/admin/unverify-user.ts
+++ b/src/server/api/endpoints/admin/unverify-user.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
-import User from '../../../../models/user';
+import { Users } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -17,7 +17,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーID',
 				'en-US': 'The user ID which you want to unverify'
@@ -27,21 +26,13 @@ export const meta = {
 };
 
 export default define(meta, async (ps) => {
-	const user = await User.findOne({
-		_id: ps.userId
-	});
+	const user = await Users.findOne(ps.userId as string);
 
 	if (user == null) {
 		throw new Error('user not found');
 	}
 
-	await User.findOneAndUpdate({
-		_id: user._id
-	}, {
-		$set: {
-			isVerified: false
-		}
+	await Users.update(user.id, {
+		isVerified: false
 	});
-
-	return;
 });
diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts
index f8f7cb5d9aeab10f2b9f01213952b9e6c342f84b..e242ac71a1c1cd3659e1f95ccffcf1acd3a1c2fa 100644
--- a/src/server/api/endpoints/admin/update-meta.ts
+++ b/src/server/api/endpoints/admin/update-meta.ts
@@ -1,6 +1,7 @@
 import $ from 'cafy';
-import Meta from '../../../../models/meta';
 import define from '../../define';
+import { Metas } from '../../../../models';
+import { Meta } from '../../../../models/entities/meta';
 
 export const meta = {
 	desc: {
@@ -55,7 +56,7 @@ export const meta = {
 			}
 		},
 
-		hidedTags: {
+		hiddenTags: {
 			validator: $.optional.nullable.arr($.str),
 			desc: {
 				'ja-JP': '統計などで無視するハッシュタグ'
@@ -253,27 +254,6 @@ export const meta = {
 			}
 		},
 
-		enableExternalUserRecommendation: {
-			validator: $.optional.bool,
-			desc: {
-				'ja-JP': '外部ユーザーレコメンデーションを有効にする'
-			}
-		},
-
-		externalUserRecommendationEngine: {
-			validator: $.optional.nullable.str,
-			desc: {
-				'ja-JP': '外部ユーザーレコメンデーションのサードパーティエンジン'
-			}
-		},
-
-		externalUserRecommendationTimeout: {
-			validator: $.optional.nullable.num.min(0),
-			desc: {
-				'ja-JP': '外部ユーザーレコメンデーションのタイムアウト (ミリ秒)'
-			}
-		},
-
 		enableEmail: {
 			validator: $.optional.bool,
 			desc: {
@@ -347,7 +327,7 @@ export const meta = {
 };
 
 export default define(meta, async (ps) => {
-	const set = {} as any;
+	const set = {} as Partial<Meta>;
 
 	if (ps.announcements) {
 		set.announcements = ps.announcements;
@@ -373,8 +353,8 @@ export default define(meta, async (ps) => {
 		set.useStarForReactionFallback = ps.useStarForReactionFallback;
 	}
 
-	if (Array.isArray(ps.hidedTags)) {
-		set.hidedTags = ps.hidedTags;
+	if (Array.isArray(ps.hiddenTags)) {
+		set.hiddenTags = ps.hiddenTags;
 	}
 
 	if (ps.mascotImageUrl !== undefined) {
@@ -430,11 +410,11 @@ export default define(meta, async (ps) => {
 	}
 
 	if (ps.maintainerName !== undefined) {
-		set['maintainer.name'] = ps.maintainerName;
+		set.maintainerName = ps.maintainerName;
 	}
 
 	if (ps.maintainerEmail !== undefined) {
-		set['maintainer.email'] = ps.maintainerEmail;
+		set.maintainerEmail = ps.maintainerEmail;
 	}
 
 	if (ps.langs !== undefined) {
@@ -481,18 +461,6 @@ export default define(meta, async (ps) => {
 		set.discordClientSecret = ps.discordClientSecret;
 	}
 
-	if (ps.enableExternalUserRecommendation !== undefined) {
-		set.enableExternalUserRecommendation = ps.enableExternalUserRecommendation;
-	}
-
-	if (ps.externalUserRecommendationEngine !== undefined) {
-		set.externalUserRecommendationEngine = ps.externalUserRecommendationEngine;
-	}
-
-	if (ps.externalUserRecommendationTimeout !== undefined) {
-		set.externalUserRecommendationTimeout = ps.externalUserRecommendationTimeout;
-	}
-
 	if (ps.enableEmail !== undefined) {
 		set.enableEmail = ps.enableEmail;
 	}
@@ -537,9 +505,11 @@ export default define(meta, async (ps) => {
 		set.swPrivateKey = ps.swPrivateKey;
 	}
 
-	await Meta.update({}, {
-		$set: set
-	}, { upsert: true });
+	const meta = await Metas.findOne();
 
-	return;
+	if (meta) {
+		await Metas.update(meta.id, set);
+	} else {
+		await Metas.save(set);
+	}
 });
diff --git a/src/server/api/endpoints/admin/update-remote-user.ts b/src/server/api/endpoints/admin/update-remote-user.ts
index a74685912c771b0286b4dbfc6a708fa8b4e1c7b8..0be9047d5a77e2b921b7cbc7e0871b180740ad0e 100644
--- a/src/server/api/endpoints/admin/update-remote-user.ts
+++ b/src/server/api/endpoints/admin/update-remote-user.ts
@@ -1,6 +1,5 @@
-import * as mongo from 'mongodb';
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
 import { getRemoteUser } from '../../common/getters';
 import { updatePerson } from '../../../../remote/activitypub/models/person';
@@ -19,7 +18,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーID',
 				'en-US': 'The user ID which you want to update'
@@ -29,11 +27,6 @@ export const meta = {
 };
 
 export default define(meta, async (ps) => {
-	await updatePersonById(ps.userId);
-	return;
-});
-
-async function updatePersonById(userId: mongo.ObjectID) {
-	const user = await getRemoteUser(userId);
+	const user = await getRemoteUser(ps.userId);
 	await updatePerson(user.uri);
-}
+});
diff --git a/src/server/api/endpoints/admin/verify-user.ts b/src/server/api/endpoints/admin/verify-user.ts
index f67b6c3bf0fa8a56bc6926a2903ab124d2ff35a9..c1b447a92b500f222e6ab72ae6815f5b4929eca1 100644
--- a/src/server/api/endpoints/admin/verify-user.ts
+++ b/src/server/api/endpoints/admin/verify-user.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
-import User from '../../../../models/user';
+import { Users } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -17,7 +17,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーID',
 				'en-US': 'The user ID which you want to verify'
@@ -27,21 +26,13 @@ export const meta = {
 };
 
 export default define(meta, async (ps) => {
-	const user = await User.findOne({
-		_id: ps.userId
-	});
+	const user = await Users.findOne(ps.userId as string);
 
 	if (user == null) {
 		throw new Error('user not found');
 	}
 
-	await User.findOneAndUpdate({
-		_id: user._id
-	}, {
-		$set: {
-			isVerified: true
-		}
+	await Users.update(user.id, {
+		isVerified: true
 	});
-
-	return;
 });
diff --git a/src/server/api/endpoints/aggregation/hashtags.ts b/src/server/api/endpoints/aggregation/hashtags.ts
deleted file mode 100644
index 978e9f64b7df29b0fedd08bc1526904c29d94c0d..0000000000000000000000000000000000000000
--- a/src/server/api/endpoints/aggregation/hashtags.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import Note from '../../../../models/note';
-import define from '../../define';
-import fetchMeta from '../../../../misc/fetch-meta';
-
-export const meta = {
-	tags: ['hashtags'],
-
-	requireCredential: false,
-};
-
-export default define(meta, async (ps) => {
-	const instance = await fetchMeta();
-	const hidedTags = instance.hidedTags.map(t => t.toLowerCase());
-
-	// 重い
-	//const span = 1000 * 60 * 60 * 24 * 7; // 1週間
-	const span = 1000 * 60 * 60 * 24; // 1æ—¥
-
-	//#region 1. 指定期間の内に投稿されたハッシュタグ(とユーザーのペア)を集計
-	const data = await Note.aggregate([{
-		$match: {
-			createdAt: {
-				$gt: new Date(Date.now() - span)
-			},
-			tagsLower: {
-				$exists: true,
-				$ne: []
-			}
-		}
-	}, {
-		$unwind: '$tagsLower'
-	}, {
-		$group: {
-			_id: { tag: '$tagsLower', userId: '$userId' }
-		}
-	}]) as {
-		_id: {
-			tag: string;
-			userId: any;
-		}
-	}[];
-	//#endregion
-
-	if (data.length == 0) {
-		return [];
-	}
-
-	let tags: {
-		name: string;
-		count: number;
-	}[] = [];
-
-	// カウント
-	for (const x of data.map(x => x._id).filter(x => !hidedTags.includes(x.tag))) {
-		const i = tags.findIndex(tag => tag.name == x.tag);
-		if (i != -1) {
-			tags[i].count++;
-		} else {
-			tags.push({
-				name: x.tag,
-				count: 1
-			});
-		}
-	}
-
-	// タグを人気順に並べ替え
-	tags.sort((a, b) => b.count - a.count);
-
-	tags = tags.slice(0, 30);
-
-	return tags;
-});
diff --git a/src/server/api/endpoints/ap/show.ts b/src/server/api/endpoints/ap/show.ts
index 7f4afa1f6e4f00c5f2b76e78a04741cfd69ab2da..5b2aaeadbb09dea25039cff70839cb4e49a64be2 100644
--- a/src/server/api/endpoints/ap/show.ts
+++ b/src/server/api/endpoints/ap/show.ts
@@ -1,15 +1,15 @@
 import $ from 'cafy';
 import define from '../../define';
 import config from '../../../../config';
-import * as mongo from 'mongodb';
-import User, { pack as packUser, IUser } from '../../../../models/user';
 import { createPerson } from '../../../../remote/activitypub/models/person';
-import Note, { pack as packNote, INote } from '../../../../models/note';
 import { createNote } from '../../../../remote/activitypub/models/note';
 import Resolver from '../../../../remote/activitypub/resolver';
 import { ApiError } from '../../error';
-import Instance from '../../../../models/instance';
 import { extractDbHost } from '../../../../misc/convert-host';
+import { Users, Notes } from '../../../../models';
+import { Note } from '../../../../models/entities/note';
+import { User } from '../../../../models/entities/user';
+import fetchMeta from '../../../../misc/fetch-meta';
 
 export const meta = {
 	tags: ['federation'],
@@ -53,25 +53,40 @@ export default define(meta, async (ps) => {
 async function fetchAny(uri: string) {
 	// URIがこのサーバーを指しているなら、ローカルユーザーIDとしてDBからフェッチ
 	if (uri.startsWith(config.url + '/')) {
-		const id = new mongo.ObjectID(uri.split('/').pop());
-		const [user, note] = await Promise.all([
-			User.findOne({ _id: id }),
-			Note.findOne({ _id: id })
-		]);
-
-		const packed = await mergePack(user, note);
-		if (packed !== null) return packed;
+		const parts = uri.split('/');
+		const id = parts.pop();
+		const type = parts.pop();
+
+		if (type === 'notes') {
+			const note = await Notes.findOne(id);
+
+			if (note) {
+				return {
+					type: 'Note',
+					object: await Notes.pack(note, null, { detail: true })
+				};
+			}
+		} else if (type === 'users') {
+			const user = await Users.findOne(id);
+
+			if (user) {
+				return {
+					type: 'User',
+					object: await Users.pack(user, null, { detail: true })
+				};
+			}
+		}
 	}
 
 	// ブロックしてたら中断
-	const instance = await Instance.findOne({ host: extractDbHost(uri) });
-	if (instance && instance.isBlocked) return null;
+	const meta = await fetchMeta();
+	if (meta.blockedHosts.includes(extractDbHost(uri))) return null;
 
 	// URI(AP Object id)としてDB検索
 	{
 		const [user, note] = await Promise.all([
-			User.findOne({ uri: uri }),
-			Note.findOne({ uri: uri })
+			Users.findOne({ uri: uri }),
+			Notes.findOne({ uri: uri })
 		]);
 
 		const packed = await mergePack(user, note);
@@ -86,8 +101,8 @@ async function fetchAny(uri: string) {
 	// これはDBに存在する可能性があるため再度DB検索
 	if (uri !== object.id) {
 		const [user, note] = await Promise.all([
-			User.findOne({ uri: object.id }),
-			Note.findOne({ uri: object.id })
+			Users.findOne({ uri: object.id }),
+			Notes.findOne({ uri: object.id })
 		]);
 
 		const packed = await mergePack(user, note);
@@ -99,7 +114,7 @@ async function fetchAny(uri: string) {
 		const user = await createPerson(object.id);
 		return {
 			type: 'User',
-			object: await packUser(user, null, { detail: true })
+			object: await Users.pack(user, null, { detail: true })
 		};
 	}
 
@@ -107,25 +122,25 @@ async function fetchAny(uri: string) {
 		const note = await createNote(object.id);
 		return {
 			type: 'Note',
-			object: await packNote(note, null, { detail: true })
+			object: await Notes.pack(note, null, { detail: true })
 		};
 	}
 
 	return null;
 }
 
-async function mergePack(user: IUser, note: INote) {
+async function mergePack(user: User, note: Note) {
 	if (user !== null) {
 		return {
 			type: 'User',
-			object: await packUser(user, null, { detail: true })
+			object: await Users.pack(user, null, { detail: true })
 		};
 	}
 
 	if (note !== null) {
 		return {
 			type: 'Note',
-			object: await packNote(note, null, { detail: true })
+			object: await Notes.pack(note, null, { detail: true })
 		};
 	}
 
diff --git a/src/server/api/endpoints/app/create.ts b/src/server/api/endpoints/app/create.ts
index 67b1b8150a0865ce94f5caad030dac26554d99d2..c7e7e516ad5e2d07758989e1ea0f9d188a8d7773 100644
--- a/src/server/api/endpoints/app/create.ts
+++ b/src/server/api/endpoints/app/create.ts
@@ -1,7 +1,8 @@
 import rndstr from 'rndstr';
 import $ from 'cafy';
-import App, { pack } from '../../../../models/app';
 import define from '../../define';
+import { Apps } from '../../../../models';
+import { genId } from '../../../../misc/gen-id';
 
 export const meta = {
 	tags: ['app'],
@@ -34,9 +35,10 @@ export default define(meta, async (ps, user) => {
 	const secret = rndstr('a-zA-Z0-9', 32);
 
 	// Create account
-	const app = await App.insert({
+	const app = await Apps.save({
+		id: genId(),
 		createdAt: new Date(),
-		userId: user && user._id,
+		userId: user && user.id,
 		name: ps.name,
 		description: ps.description,
 		permission: ps.permission,
@@ -44,7 +46,7 @@ export default define(meta, async (ps, user) => {
 		secret: secret
 	});
 
-	return await pack(app, null, {
+	return await Apps.pack(app, null, {
 		detail: true,
 		includeSecret: true
 	});
diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts
index f3f5b843b370227a4ced39c44c7811a677082cdf..ce9baed2aebc33a5dda8cc5ab0b5a9fcbb0037fe 100644
--- a/src/server/api/endpoints/app/show.ts
+++ b/src/server/api/endpoints/app/show.ts
@@ -1,8 +1,8 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import App, { pack } from '../../../../models/app';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
 import { ApiError } from '../../error';
+import { Apps } from '../../../../models';
 
 export const meta = {
 	tags: ['app'],
@@ -10,7 +10,6 @@ export const meta = {
 	params: {
 		appId: {
 			validator: $.type(ID),
-			transform: transform
 		},
 	},
 
@@ -27,14 +26,14 @@ export default define(meta, async (ps, user, app) => {
 	const isSecure = user != null && app == null;
 
 	// Lookup app
-	const ap = await App.findOne({ _id: ps.appId });
+	const ap = await Apps.findOne(ps.appId);
 
-	if (ap === null) {
+	if (ap == null) {
 		throw new ApiError(meta.errors.noSuchApp);
 	}
 
-	return await pack(ap, user, {
+	return await Apps.pack(ap, user, {
 		detail: true,
-		includeSecret: isSecure && ap.userId.equals(user._id)
+		includeSecret: isSecure && (ap.userId === user.id)
 	});
 });
diff --git a/src/server/api/endpoints/auth/accept.ts b/src/server/api/endpoints/auth/accept.ts
index cedf7821fe069c999e11ff3ff6a8d2e5634b3dbd..21a78011dc0b2b9928782ab4126b44d21f267087 100644
--- a/src/server/api/endpoints/auth/accept.ts
+++ b/src/server/api/endpoints/auth/accept.ts
@@ -1,11 +1,10 @@
 import rndstr from 'rndstr';
 import * as crypto from 'crypto';
 import $ from 'cafy';
-import App from '../../../../models/app';
-import AuthSess from '../../../../models/auth-session';
-import AccessToken from '../../../../models/access-token';
 import define from '../../define';
 import { ApiError } from '../../error';
+import { AuthSessions, AccessTokens, Apps } from '../../../../models';
+import { genId } from '../../../../misc/gen-id';
 
 export const meta = {
 	tags: ['auth'],
@@ -31,10 +30,10 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	// Fetch token
-	const session = await AuthSess
+	const session = await AuthSessions
 		.findOne({ token: ps.token });
 
-	if (session === null) {
+	if (session == null) {
 		throw new ApiError(meta.errors.noSuchSession);
 	}
 
@@ -42,16 +41,14 @@ export default define(meta, async (ps, user) => {
 	const accessToken = rndstr('a-zA-Z0-9', 32);
 
 	// Fetch exist access token
-	const exist = await AccessToken.findOne({
+	const exist = await AccessTokens.findOne({
 		appId: session.appId,
-		userId: user._id,
+		userId: user.id,
 	});
 
-	if (exist === null) {
+	if (exist == null) {
 		// Lookup app
-		const app = await App.findOne({
-			_id: session.appId
-		});
+		const app = await Apps.findOne(session.appId);
 
 		// Generate Hash
 		const sha256 = crypto.createHash('sha256');
@@ -59,20 +56,19 @@ export default define(meta, async (ps, user) => {
 		const hash = sha256.digest('hex');
 
 		// Insert access token doc
-		await AccessToken.insert({
+		await AccessTokens.save({
+			id: genId(),
 			createdAt: new Date(),
 			appId: session.appId,
-			userId: user._id,
+			userId: user.id,
 			token: accessToken,
 			hash: hash
 		});
 	}
 
 	// Update session
-	await AuthSess.update(session._id, {
-		$set: {
-			userId: user._id
-		}
+	await AuthSessions.update(session.id, {
+		userId: user.id
 	});
 
 	return;
diff --git a/src/server/api/endpoints/auth/session/generate.ts b/src/server/api/endpoints/auth/session/generate.ts
index e12bea768114a8826933de3c65e288dc3a7fd8b9..5a9bfe6451cdfe278f9a754daa4b278a087bc77f 100644
--- a/src/server/api/endpoints/auth/session/generate.ts
+++ b/src/server/api/endpoints/auth/session/generate.ts
@@ -1,10 +1,10 @@
 import * as uuid from 'uuid';
 import $ from 'cafy';
-import App from '../../../../../models/app';
-import AuthSess from '../../../../../models/auth-session';
 import config from '../../../../../config';
 import define from '../../../define';
 import { ApiError } from '../../../error';
+import { Apps, AuthSessions } from '../../../../../models';
+import { genId } from '../../../../../misc/gen-id';
 
 export const meta = {
 	tags: ['auth'],
@@ -46,7 +46,7 @@ export const meta = {
 
 export default define(meta, async (ps) => {
 	// Lookup app
-	const app = await App.findOne({
+	const app = await Apps.findOne({
 		secret: ps.appSecret
 	});
 
@@ -58,9 +58,10 @@ export default define(meta, async (ps) => {
 	const token = uuid.v4();
 
 	// Create session token document
-	const doc = await AuthSess.insert({
+	const doc = await AuthSessions.save({
+		id: genId(),
 		createdAt: new Date(),
-		appId: app._id,
+		appId: app.id,
 		token: token
 	});
 
diff --git a/src/server/api/endpoints/auth/session/show.ts b/src/server/api/endpoints/auth/session/show.ts
index 992e0a499e1078cf140674e787ee5ef63721d886..e6ecd8b8390d43d62a717858cd988949a83846bf 100644
--- a/src/server/api/endpoints/auth/session/show.ts
+++ b/src/server/api/endpoints/auth/session/show.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
-import AuthSess, { pack } from '../../../../../models/auth-session';
 import define from '../../../define';
 import { ApiError } from '../../../error';
+import { AuthSessions } from '../../../../../models';
 
 export const meta = {
 	tags: ['auth'],
@@ -29,7 +29,7 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	// Lookup session
-	const session = await AuthSess.findOne({
+	const session = await AuthSessions.findOne({
 		token: ps.token
 	});
 
@@ -37,5 +37,5 @@ export default define(meta, async (ps, user) => {
 		throw new ApiError(meta.errors.noSuchSession);
 	}
 
-	return await pack(session, user);
+	return await AuthSessions.pack(session, user);
 });
diff --git a/src/server/api/endpoints/auth/session/userkey.ts b/src/server/api/endpoints/auth/session/userkey.ts
index e09e16e658e6d9368086564045ec0b2e3551d2b0..8524b96f94d8a5837c16307a1bfb4b9cee324064 100644
--- a/src/server/api/endpoints/auth/session/userkey.ts
+++ b/src/server/api/endpoints/auth/session/userkey.ts
@@ -1,10 +1,7 @@
 import $ from 'cafy';
-import App from '../../../../../models/app';
-import AuthSess from '../../../../../models/auth-session';
-import AccessToken from '../../../../../models/access-token';
-import { pack } from '../../../../../models/user';
 import define from '../../../define';
 import { ApiError } from '../../../error';
+import { Apps, AuthSessions, AccessTokens, Users } from '../../../../../models';
 
 export const meta = {
 	tags: ['auth'],
@@ -67,7 +64,7 @@ export const meta = {
 
 export default define(meta, async (ps) => {
 	// Lookup app
-	const app = await App.findOne({
+	const app = await Apps.findOne({
 		secret: ps.appSecret
 	});
 
@@ -76,13 +73,12 @@ export default define(meta, async (ps) => {
 	}
 
 	// Fetch token
-	const session = await AuthSess
-		.findOne({
-			token: ps.token,
-			appId: app._id
-		});
+	const session = await AuthSessions.findOne({
+		token: ps.token,
+		appId: app.id
+	});
 
-	if (session === null) {
+	if (session == null) {
 		throw new ApiError(meta.errors.noSuchSession);
 	}
 
@@ -91,25 +87,17 @@ export default define(meta, async (ps) => {
 	}
 
 	// Lookup access token
-	const accessToken = await AccessToken.findOne({
-		appId: app._id,
+	const accessToken = await AccessTokens.findOne({
+		appId: app.id,
 		userId: session.userId
 	});
 
 	// Delete session
-
-	/* https://github.com/Automattic/monk/issues/178
-	AuthSess.deleteOne({
-		_id: session._id
-	});
-	*/
-	AuthSess.remove({
-		_id: session._id
-	});
+	AuthSessions.delete(session.id);
 
 	return {
 		accessToken: accessToken.token,
-		user: await pack(session.userId, null, {
+		user: await Users.pack(session.userId, null, {
 			detail: true
 		})
 	};
diff --git a/src/server/api/endpoints/blocking/create.ts b/src/server/api/endpoints/blocking/create.ts
index e723cb03867f86a460263fcd2e64b08847a3a856..0d6626b2d567cf2c27cbdb647184dba95fddbaed 100644
--- a/src/server/api/endpoints/blocking/create.ts
+++ b/src/server/api/endpoints/blocking/create.ts
@@ -1,12 +1,11 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
 import * as ms from 'ms';
-import { pack } from '../../../../models/user';
-import Blocking from '../../../../models/blocking';
 import create from '../../../../services/blocking/create';
 import define from '../../define';
 import { ApiError } from '../../error';
 import { getUser } from '../../common/getters';
+import { Blockings, NoteWatchings } from '../../../../models';
 
 export const meta = {
 	stability: 'stable',
@@ -25,12 +24,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'following-write',
+	kind: 'write:blocks',
 
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -63,7 +61,7 @@ export default define(meta, async (ps, user) => {
 	const blocker = user;
 
 	// 自分自身
-	if (user._id.equals(ps.userId)) {
+	if (user.id === ps.userId) {
 		throw new ApiError(meta.errors.blockeeIsYourself);
 	}
 
@@ -74,19 +72,22 @@ export default define(meta, async (ps, user) => {
 	});
 
 	// Check if already blocking
-	const exist = await Blocking.findOne({
-		blockerId: blocker._id,
-		blockeeId: blockee._id
+	const exist = await Blockings.findOne({
+		blockerId: blocker.id,
+		blockeeId: blockee.id
 	});
 
-	if (exist !== null) {
+	if (exist != null) {
 		throw new ApiError(meta.errors.alreadyBlocking);
 	}
 
 	// Create blocking
 	await create(blocker, blockee);
 
-	return await pack(blockee._id, user, {
-		detail: true
+	NoteWatchings.delete({
+		userId: blocker.id,
+		noteUserId: blockee.id
 	});
+
+	return await Blockings.pack(blockee.id, user);
 });
diff --git a/src/server/api/endpoints/blocking/delete.ts b/src/server/api/endpoints/blocking/delete.ts
index 2a9fdc5e2448665f5b31d354b121e3c963016824..e304dca8110624fdd601afa957fa9d4ae7b8e028 100644
--- a/src/server/api/endpoints/blocking/delete.ts
+++ b/src/server/api/endpoints/blocking/delete.ts
@@ -1,12 +1,11 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
 import * as ms from 'ms';
-import { pack } from '../../../../models/user';
-import Blocking from '../../../../models/blocking';
 import deleteBlocking from '../../../../services/blocking/delete';
 import define from '../../define';
 import { ApiError } from '../../error';
 import { getUser } from '../../common/getters';
+import { Blockings } from '../../../../models';
 
 export const meta = {
 	stability: 'stable',
@@ -25,12 +24,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'following-write',
+	kind: 'write:blocks',
 
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -63,7 +61,7 @@ export default define(meta, async (ps, user) => {
 	const blocker = user;
 
 	// Check if the blockee is yourself
-	if (user._id.equals(ps.userId)) {
+	if (user.id === ps.userId) {
 		throw new ApiError(meta.errors.blockeeIsYourself);
 	}
 
@@ -74,19 +72,17 @@ export default define(meta, async (ps, user) => {
 	});
 
 	// Check not blocking
-	const exist = await Blocking.findOne({
-		blockerId: blocker._id,
-		blockeeId: blockee._id
+	const exist = await Blockings.findOne({
+		blockerId: blocker.id,
+		blockeeId: blockee.id
 	});
 
-	if (exist === null) {
+	if (exist == null) {
 		throw new ApiError(meta.errors.notBlocking);
 	}
 
 	// Delete blocking
 	await deleteBlocking(blocker, blockee);
 
-	return await pack(blockee._id, user, {
-		detail: true
-	});
+	return await Blockings.pack(blockee.id, user);
 });
diff --git a/src/server/api/endpoints/blocking/list.ts b/src/server/api/endpoints/blocking/list.ts
index b9ad6e8a3f7996d3340ddda7dc2b36c9f33872f2..a078891ab0912e62e06ca95ed28d77462c9bbc52 100644
--- a/src/server/api/endpoints/blocking/list.ts
+++ b/src/server/api/endpoints/blocking/list.ts
@@ -1,7 +1,8 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Blocking, { packMany } from '../../../../models/blocking';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
+import { Blockings } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
 
 export const meta = {
 	desc: {
@@ -13,7 +14,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'following-read',
+	kind: 'read:blocks',
 
 	params: {
 		limit: {
@@ -23,12 +24,10 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 	},
 
@@ -41,30 +40,12 @@ export const meta = {
 };
 
 export default define(meta, async (ps, me) => {
-	const query = {
-		blockerId: me._id
-	} as any;
+	const query = makePaginationQuery(Blockings.createQueryBuilder('blocking'), ps.sinceId, ps.untilId)
+		.andWhere(`blocking.blockerId = :meId`, { meId: me.id });
 
-	const sort = {
-		_id: -1
-	};
+	const blockings = await query
+		.take(ps.limit)
+		.getMany();
 
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		query._id = {
-			$lt: ps.untilId
-		};
-	}
-
-	const blockings = await Blocking
-		.find(query, {
-			limit: ps.limit,
-			sort: sort
-		});
-
-	return await packMany(blockings, me);
+	return await Blockings.packMany(blockings, me);
 });
diff --git a/src/server/api/endpoints/charts/active-users.ts b/src/server/api/endpoints/charts/active-users.ts
index 9dad942e06d24150f92c540eacac296db2523e50..60fa72c5c766c8881425ece01c3f66f7635453b0 100644
--- a/src/server/api/endpoints/charts/active-users.ts
+++ b/src/server/api/endpoints/charts/active-users.ts
@@ -1,6 +1,7 @@
 import $ from 'cafy';
 import define from '../../define';
-import activeUsersChart from '../../../../services/chart/active-users';
+import { convertLog } from '../../../../services/chart/core';
+import { activeUsersChart } from '../../../../services/chart';
 
 export const meta = {
 	stability: 'stable',
@@ -28,12 +29,7 @@ export const meta = {
 		},
 	},
 
-	res: {
-		type: 'array',
-		items: {
-			type: 'object',
-		},
-	},
+	res: convertLog(activeUsersChart.schema),
 };
 
 export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/drive.ts b/src/server/api/endpoints/charts/drive.ts
index 6bbb266f96eb0a9285ae388a645837162aec0ac3..a9676e15864075320af9a62d256f7ed94d580f60 100644
--- a/src/server/api/endpoints/charts/drive.ts
+++ b/src/server/api/endpoints/charts/drive.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
 import define from '../../define';
-import driveChart, { driveLogSchema } from '../../../../services/chart/drive';
-import { convertLog } from '../../../../services/chart';
+import { convertLog } from '../../../../services/chart/core';
+import { driveChart } from '../../../../services/chart';
 
 export const meta = {
 	stability: 'stable',
@@ -29,7 +29,7 @@ export const meta = {
 		},
 	},
 
-	res: convertLog(driveLogSchema),
+	res: convertLog(driveChart.schema),
 };
 
 export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/federation.ts b/src/server/api/endpoints/charts/federation.ts
index c7b34f10158a5495d1685ac3ba25904f6d644df6..95f16c76a71827e444b2698f4656056d6ac4abaa 100644
--- a/src/server/api/endpoints/charts/federation.ts
+++ b/src/server/api/endpoints/charts/federation.ts
@@ -1,6 +1,7 @@
 import $ from 'cafy';
 import define from '../../define';
-import federationChart from '../../../../services/chart/federation';
+import { convertLog } from '../../../../services/chart/core';
+import { federationChart } from '../../../../services/chart';
 
 export const meta = {
 	stability: 'stable',
@@ -28,12 +29,7 @@ export const meta = {
 		},
 	},
 
-	res: {
-		type: 'array',
-		items: {
-			type: 'object',
-		},
-	},
+	res: convertLog(federationChart.schema),
 };
 
 export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/hashtag.ts b/src/server/api/endpoints/charts/hashtag.ts
index 4db6e624080de26f965a9d2b66422339157fa419..a7ec12707e3a95372a8704ebb3f0acb5e054b796 100644
--- a/src/server/api/endpoints/charts/hashtag.ts
+++ b/src/server/api/endpoints/charts/hashtag.ts
@@ -1,6 +1,7 @@
 import $ from 'cafy';
 import define from '../../define';
-import hashtagChart from '../../../../services/chart/hashtag';
+import { convertLog } from '../../../../services/chart/core';
+import { hashtagChart } from '../../../../services/chart';
 
 export const meta = {
 	stability: 'stable',
@@ -35,12 +36,7 @@ export const meta = {
 		},
 	},
 
-	res: {
-		type: 'array',
-		items: {
-			type: 'object',
-		},
-	},
+	res: convertLog(hashtagChart.schema),
 };
 
 export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/instance.ts b/src/server/api/endpoints/charts/instance.ts
index 3fe85f086a7d439529f6548fe1123811af45c4e7..cf3094f7e1249daa631d3f858f9a21a6c61dc6b6 100644
--- a/src/server/api/endpoints/charts/instance.ts
+++ b/src/server/api/endpoints/charts/instance.ts
@@ -1,6 +1,7 @@
 import $ from 'cafy';
 import define from '../../define';
-import instanceChart from '../../../../services/chart/instance';
+import { convertLog } from '../../../../services/chart/core';
+import { instanceChart } from '../../../../services/chart';
 
 export const meta = {
 	stability: 'stable',
@@ -36,12 +37,7 @@ export const meta = {
 		}
 	},
 
-	res: {
-		type: 'array',
-		items: {
-			type: 'object',
-		},
-	},
+	res: convertLog(instanceChart.schema),
 };
 
 export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/network.ts b/src/server/api/endpoints/charts/network.ts
index 48b1d0f66f7c7f53ba3f4c3ee0da6d26a0aa85a8..c0fcd95fe9be1ce40bf2117eb0efcda4d47ba5b5 100644
--- a/src/server/api/endpoints/charts/network.ts
+++ b/src/server/api/endpoints/charts/network.ts
@@ -1,6 +1,7 @@
 import $ from 'cafy';
 import define from '../../define';
-import networkChart from '../../../../services/chart/network';
+import { convertLog } from '../../../../services/chart/core';
+import { networkChart } from '../../../../services/chart';
 
 export const meta = {
 	stability: 'stable',
@@ -28,12 +29,7 @@ export const meta = {
 		},
 	},
 
-	res: {
-		type: 'array',
-		items: {
-			type: 'object',
-		},
-	},
+	res: convertLog(networkChart.schema),
 };
 
 export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/notes.ts b/src/server/api/endpoints/charts/notes.ts
index cc0ca8bef7a4ad63e557355705daec071def4bcb..86f30e4b898a045904dd68d44d9779d1cc1ee819 100644
--- a/src/server/api/endpoints/charts/notes.ts
+++ b/src/server/api/endpoints/charts/notes.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
 import define from '../../define';
-import notesChart, { notesLogSchema } from '../../../../services/chart/notes';
-import { convertLog } from '../../../../services/chart';
+import { convertLog } from '../../../../services/chart/core';
+import { notesChart } from '../../../../services/chart';
 
 export const meta = {
 	stability: 'stable',
@@ -29,7 +29,7 @@ export const meta = {
 		},
 	},
 
-	res: convertLog(notesLogSchema),
+	res: convertLog(notesChart.schema),
 };
 
 export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/user/drive.ts b/src/server/api/endpoints/charts/user/drive.ts
index 064c7c7b721ca955f4b27428f21a63bbad71dd2f..e3696dfda1adb2b78d6bf0cf9216183b5d419ccc 100644
--- a/src/server/api/endpoints/charts/user/drive.ts
+++ b/src/server/api/endpoints/charts/user/drive.ts
@@ -1,8 +1,8 @@
 import $ from 'cafy';
 import define from '../../../define';
-import perUserDriveChart, { perUserDriveLogSchema } from '../../../../../services/chart/per-user-drive';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import { convertLog } from '../../../../../services/chart';
+import { ID } from '../../../../../misc/cafy-id';
+import { convertLog } from '../../../../../services/chart/core';
+import { perUserDriveChart } from '../../../../../services/chart';
 
 export const meta = {
 	stability: 'stable',
@@ -31,7 +31,6 @@ export const meta = {
 
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -39,7 +38,7 @@ export const meta = {
 		}
 	},
 
-	res: convertLog(perUserDriveLogSchema),
+	res: convertLog(perUserDriveChart.schema),
 };
 
 export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/user/following.ts b/src/server/api/endpoints/charts/user/following.ts
index f5b1355038518f1f5c9d8c4aed9a7e800c54404a..8feba0bd169a16103ee94803fe7fa4220e498f7b 100644
--- a/src/server/api/endpoints/charts/user/following.ts
+++ b/src/server/api/endpoints/charts/user/following.ts
@@ -1,8 +1,8 @@
 import $ from 'cafy';
 import define from '../../../define';
-import perUserFollowingChart, { perUserFollowingLogSchema } from '../../../../../services/chart/per-user-following';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import { convertLog } from '../../../../../services/chart';
+import { ID } from '../../../../../misc/cafy-id';
+import { convertLog } from '../../../../../services/chart/core';
+import { perUserFollowingChart } from '../../../../../services/chart';
 
 export const meta = {
 	stability: 'stable',
@@ -31,7 +31,6 @@ export const meta = {
 
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -39,9 +38,9 @@ export const meta = {
 		}
 	},
 
-	res: convertLog(perUserFollowingLogSchema),
+	res: convertLog(perUserFollowingChart.schema),
 };
 
 export default define(meta, async (ps) => {
-	return  await perUserFollowingChart.getChart(ps.span as any, ps.limit, ps.userId);
+	return await perUserFollowingChart.getChart(ps.span as any, ps.limit, ps.userId);
 });
diff --git a/src/server/api/endpoints/charts/user/notes.ts b/src/server/api/endpoints/charts/user/notes.ts
index 7e31978bf3e741e9b7df443d9483c2fa46f4bd77..8c1db54f76843c4ee4a4554907b4cee7811a3086 100644
--- a/src/server/api/endpoints/charts/user/notes.ts
+++ b/src/server/api/endpoints/charts/user/notes.ts
@@ -1,8 +1,8 @@
 import $ from 'cafy';
 import define from '../../../define';
-import perUserNotesChart, { perUserNotesLogSchema } from '../../../../../services/chart/per-user-notes';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import { convertLog } from '../../../../../services/chart';
+import { ID } from '../../../../../misc/cafy-id';
+import { convertLog } from '../../../../../services/chart/core';
+import { perUserNotesChart } from '../../../../../services/chart';
 
 export const meta = {
 	stability: 'stable',
@@ -31,7 +31,6 @@ export const meta = {
 
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -39,7 +38,7 @@ export const meta = {
 		}
 	},
 
-	res: convertLog(perUserNotesLogSchema),
+	res: convertLog(perUserNotesChart.schema),
 };
 
 export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/user/reactions.ts b/src/server/api/endpoints/charts/user/reactions.ts
index 51ff83f20ebd3c39a031b7372e02ee67b67779a7..7c9b2508ae1ad45607e62ecb81e73607c0bc94e5 100644
--- a/src/server/api/endpoints/charts/user/reactions.ts
+++ b/src/server/api/endpoints/charts/user/reactions.ts
@@ -1,7 +1,8 @@
 import $ from 'cafy';
 import define from '../../../define';
-import perUserReactionsChart from '../../../../../services/chart/per-user-reactions';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
+import { convertLog } from '../../../../../services/chart/core';
+import { perUserReactionsChart } from '../../../../../services/chart';
 
 export const meta = {
 	stability: 'stable',
@@ -30,7 +31,6 @@ export const meta = {
 
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -38,12 +38,7 @@ export const meta = {
 		}
 	},
 
-	res: {
-		type: 'array',
-		items: {
-			type: 'object',
-		},
-	},
+	res: convertLog(perUserReactionsChart.schema),
 };
 
 export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/charts/users.ts b/src/server/api/endpoints/charts/users.ts
index 9de54a630e63887ec38e7658a7d1b96e61673b7f..3ed5e093493907dae4ac9b89ab54b0e783476539 100644
--- a/src/server/api/endpoints/charts/users.ts
+++ b/src/server/api/endpoints/charts/users.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
 import define from '../../define';
-import usersChart, { usersLogSchema } from '../../../../services/chart/users';
-import { convertLog } from '../../../../services/chart';
+import { convertLog } from '../../../../services/chart/core';
+import { usersChart } from '../../../../services/chart';
 
 export const meta = {
 	stability: 'stable',
@@ -29,7 +29,7 @@ export const meta = {
 		},
 	},
 
-	res: convertLog(usersLogSchema),
+	res: convertLog(usersChart.schema),
 };
 
 export default define(meta, async (ps) => {
diff --git a/src/server/api/endpoints/drive.ts b/src/server/api/endpoints/drive.ts
index 138adffad2dc2ca19d632136009e12dc63299709..adf780301bc89e82a5e6b170f2c162bb4c5b19ae 100644
--- a/src/server/api/endpoints/drive.ts
+++ b/src/server/api/endpoints/drive.ts
@@ -1,6 +1,6 @@
-import DriveFile from '../../../models/drive-file';
 import define from '../define';
 import fetchMeta from '../../../misc/fetch-meta';
+import { DriveFiles } from '../../../models';
 
 export const meta = {
 	desc: {
@@ -12,7 +12,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'drive-read',
+	kind: 'read:drive',
 
 	res: {
 		type: 'object',
@@ -31,27 +31,7 @@ export default define(meta, async (ps, user) => {
 	const instance = await fetchMeta();
 
 	// Calculate drive usage
-	const usage = await DriveFile.aggregate([{
-		$match: {
-			'metadata.userId': user._id,
-			'metadata.deletedAt': { $exists: false }
-		}
-	}, {
-		$project: {
-			length: true
-		}
-	}, {
-		$group: {
-			_id: null,
-			usage: { $sum: '$length' }
-		}
-	}])
-	.then((aggregates: any[]) => {
-		if (aggregates.length > 0) {
-			return aggregates[0].usage;
-		}
-		return 0;
-	});
+	const usage = await DriveFiles.clacDriveUsageOf(user);
 
 	return {
 		capacity: 1024 * 1024 * instance.localDriveCapacityMb,
diff --git a/src/server/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts
index f108e820e7def81b4caed6de3835268d29b71e17..400b73d3b73edf9b79b49504c19cffc060660986 100644
--- a/src/server/api/endpoints/drive/files.ts
+++ b/src/server/api/endpoints/drive/files.ts
@@ -1,7 +1,8 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import DriveFile, { packMany } from '../../../../models/drive-file';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
+import { DriveFiles } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
 
 export const meta = {
 	desc: {
@@ -13,7 +14,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'drive-read',
+	kind: 'read:drive',
 
 	params: {
 		limit: {
@@ -23,18 +24,15 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		folderId: {
 			validator: $.optional.nullable.type(ID),
 			default: null as any,
-			transform: transform,
 		},
 
 		type: {
@@ -51,36 +49,24 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const sort = {
-		_id: -1
-	};
+	const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId)
+		.andWhere('file.userId = :userId', { userId: user.id });
 
-	const query = {
-		'metadata.userId': user._id,
-		'metadata.folderId': ps.folderId,
-		'metadata.deletedAt': { $exists: false }
-	} as any;
-
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		query._id = {
-			$lt: ps.untilId
-		};
+	if (ps.folderId) {
+		query.andWhere('file.folderId = :folderId', { folderId: ps.folderId });
+	} else {
+		query.andWhere('file.folderId IS NULL');
 	}
 
 	if (ps.type) {
-		query.contentType = new RegExp(`^${ps.type.replace(/\*/g, '.+?')}$`);
+		if (ps.type.endsWith('/*')) {
+			query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' });
+		} else {
+			query.andWhere('file.type = :type', { type: ps.type });
+		}
 	}
 
-	const files = await DriveFile
-		.find(query, {
-			limit: ps.limit,
-			sort: sort
-		});
+	const files = await query.take(ps.limit).getMany();
 
-	return await packMany(files, { detail: false, self: true });
+	return await DriveFiles.packMany(files, { detail: false, self: true });
 });
diff --git a/src/server/api/endpoints/drive/files/attached-notes.ts b/src/server/api/endpoints/drive/files/attached-notes.ts
index c9eeab58c597443f9eeab522e03a49f2e98565d4..7214463dde1bbd85682d3b3b2bb04c30e5fb4053 100644
--- a/src/server/api/endpoints/drive/files/attached-notes.ts
+++ b/src/server/api/endpoints/drive/files/attached-notes.ts
@@ -1,9 +1,8 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFile from '../../../../../models/drive-file';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
-import { packMany } from '../../../../../models/note';
 import { ApiError } from '../../../error';
+import { DriveFiles } from '../../../../../models';
 
 export const meta = {
 	stability: 'stable',
@@ -17,12 +16,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'drive-read',
+	kind: 'read:drive',
 
 	params: {
 		fileId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のファイルID',
 				'en-US': 'Target file ID'
@@ -48,18 +46,17 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	// Fetch file
-	const file = await DriveFile
-		.findOne({
-			_id: ps.fileId,
-			'metadata.userId': user._id,
-			'metadata.deletedAt': { $exists: false }
-		});
+	const file = await DriveFiles.findOne({
+		id: ps.fileId,
+		userId: user.id,
+	});
 
-	if (file === null) {
+	if (file == null) {
 		throw new ApiError(meta.errors.noSuchFile);
 	}
 
+	/* v11 TODO
 	return await packMany(file.metadata.attachedNoteIds || [], user, {
 		detail: true
-	});
+	});*/
 });
diff --git a/src/server/api/endpoints/drive/files/check-existence.ts b/src/server/api/endpoints/drive/files/check-existence.ts
index 926411c83accbfb6dba18950c74b1c82c79dba46..3a87a9497fa1d723ef71df06801d6480113c06f7 100644
--- a/src/server/api/endpoints/drive/files/check-existence.ts
+++ b/src/server/api/endpoints/drive/files/check-existence.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy';
-import DriveFile, { pack } from '../../../../../models/drive-file';
 import define from '../../../define';
+import { DriveFiles } from '../../../../../models';
 
 export const meta = {
 	desc: {
@@ -12,7 +12,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'drive-read',
+	kind: 'read:drive',
 
 	params: {
 		md5: {
@@ -29,11 +29,12 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const file = await DriveFile.findOne({
+	const file = await DriveFiles.findOne({
 		md5: ps.md5,
-		'metadata.userId': user._id,
-		'metadata.deletedAt': { $exists: false }
+		userId: user.id,
 	});
 
-	return { file: file ? await pack(file, { self: true }) : null };
+	return {
+		file: file ? await DriveFiles.pack(file, { self: true }) : null
+	};
 });
diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts
index b2979c4888b86201cab637381cd90917c000eb5d..5702c70fc0e89318c8f70fc9e493b2760ba2fee5 100644
--- a/src/server/api/endpoints/drive/files/create.ts
+++ b/src/server/api/endpoints/drive/files/create.ts
@@ -1,11 +1,11 @@
 import * as ms from 'ms';
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import { validateFileName, pack } from '../../../../../models/drive-file';
+import { ID } from '../../../../../misc/cafy-id';
 import create from '../../../../../services/drive/add-file';
 import define from '../../../define';
 import { apiLogger } from '../../../logger';
 import { ApiError } from '../../../error';
+import { DriveFiles } from '../../../../../models';
 
 export const meta = {
 	desc: {
@@ -24,12 +24,11 @@ export const meta = {
 
 	requireFile: true,
 
-	kind: 'drive-write',
+	kind: 'write:drive',
 
 	params: {
 		folderId: {
 			validator: $.optional.nullable.type(ID),
-			transform: transform,
 			default: null as any,
 			desc: {
 				'ja-JP': 'フォルダID'
@@ -78,7 +77,7 @@ export default define(meta, async (ps, user, app, file, cleanup) => {
 			name = null;
 		} else if (name === 'blob') {
 			name = null;
-		} else if (!validateFileName(name)) {
+		} else if (!DriveFiles.validateFileName(name)) {
 			throw new ApiError(meta.errors.invalidFileName);
 		}
 	} else {
@@ -88,7 +87,7 @@ export default define(meta, async (ps, user, app, file, cleanup) => {
 	try {
 		// Create file
 		const driveFile = await create(user, file.path, name, null, ps.folderId, ps.force, false, null, null, ps.isSensitive);
-		return pack(driveFile, { self: true });
+		return DriveFiles.pack(driveFile, { self: true });
 	} catch (e) {
 		apiLogger.error(e);
 		throw new ApiError();
diff --git a/src/server/api/endpoints/drive/files/delete.ts b/src/server/api/endpoints/drive/files/delete.ts
index dd4e187fcd7544edfb01737a9505627fe7b0d1a3..d8cc5ec0a19aea50f1b70d44b45cad047550517e 100644
--- a/src/server/api/endpoints/drive/files/delete.ts
+++ b/src/server/api/endpoints/drive/files/delete.ts
@@ -1,10 +1,10 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFile from '../../../../../models/drive-file';
+import { ID } from '../../../../../misc/cafy-id';
 import del from '../../../../../services/drive/delete-file';
 import { publishDriveStream } from '../../../../../services/stream';
 import define from '../../../define';
 import { ApiError } from '../../../error';
+import { DriveFiles } from '../../../../../models';
 
 export const meta = {
 	stability: 'stable',
@@ -18,12 +18,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'drive-write',
+	kind: 'write:drive',
 
 	params: {
 		fileId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のファイルID',
 				'en-US': 'Target file ID'
@@ -47,17 +46,13 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	// Fetch file
-	const file = await DriveFile
-		.findOne({
-			_id: ps.fileId
-		});
+	const file = await DriveFiles.findOne(ps.fileId);
 
-	if (file === null) {
+	if (file == null) {
 		throw new ApiError(meta.errors.noSuchFile);
 	}
 
-	if (!user.isAdmin && !user.isModerator && !file.metadata.userId.equals(user._id)) {
+	if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) {
 		throw new ApiError(meta.errors.accessDenied);
 	}
 
@@ -65,7 +60,5 @@ export default define(meta, async (ps, user) => {
 	await del(file);
 
 	// Publish fileDeleted event
-	publishDriveStream(user._id, 'fileDeleted', file._id);
-
-	return;
+	publishDriveStream(user.id, 'fileDeleted', file.id);
 });
diff --git a/src/server/api/endpoints/drive/files/find.ts b/src/server/api/endpoints/drive/files/find.ts
index 0d4102a48fc87e33196c0b8de1e163d1e83669f6..265850f84c82f912552c200ee7654d8c80dffd08 100644
--- a/src/server/api/endpoints/drive/files/find.ts
+++ b/src/server/api/endpoints/drive/files/find.ts
@@ -1,14 +1,14 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFile, { pack } from '../../../../../models/drive-file';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
+import { DriveFiles } from '../../../../../models';
 
 export const meta = {
 	requireCredential: true,
 
 	tags: ['drive'],
 
-	kind: 'drive-read',
+	kind: 'read:drive',
 
 	params: {
 		name: {
@@ -17,7 +17,6 @@ export const meta = {
 
 		folderId: {
 			validator: $.optional.nullable.type(ID),
-			transform: transform,
 			default: null as any,
 			desc: {
 				'ja-JP': 'フォルダID'
@@ -27,12 +26,11 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const files = await DriveFile
-		.find({
-			filename: ps.name,
-			'metadata.userId': user._id,
-			'metadata.folderId': ps.folderId
-		});
+	const files = await DriveFiles.find({
+		name: ps.name,
+		userId: user.id,
+		folderId: ps.folderId
+	});
 
-	return await Promise.all(files.map(file => pack(file, { self: true })));
+	return await Promise.all(files.map(file => DriveFiles.pack(file, { self: true })));
 });
diff --git a/src/server/api/endpoints/drive/files/show.ts b/src/server/api/endpoints/drive/files/show.ts
index 6d63a8605c5e059074bddde69b8ac74062bd1dee..b516ec2df67a5d7660f3c0aa4cbcc05f607d2bbf 100644
--- a/src/server/api/endpoints/drive/files/show.ts
+++ b/src/server/api/endpoints/drive/files/show.ts
@@ -1,10 +1,9 @@
 import $ from 'cafy';
-import * as mongo from 'mongodb';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFile, { pack, IDriveFile } from '../../../../../models/drive-file';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
-import config from '../../../../../config';
 import { ApiError } from '../../../error';
+import { DriveFile } from '../../../../../models/entities/drive-file';
+import { DriveFiles } from '../../../../../models';
 
 export const meta = {
 	stability: 'stable',
@@ -18,12 +17,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'drive-read',
+	kind: 'read:drive',
 
 	params: {
 		fileId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のファイルID',
 				'en-US': 'Target file ID'
@@ -65,49 +63,33 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	let file: IDriveFile;
+	let file: DriveFile;
 
 	if (ps.fileId) {
-		file = await DriveFile.findOne({
-			_id: ps.fileId,
-			'metadata.deletedAt': { $exists: false }
-		});
+		file = await DriveFiles.findOne(ps.fileId);
 	} else if (ps.url) {
-		const isInternalStorageUrl = ps.url.startsWith(config.driveUrl);
-		if (isInternalStorageUrl) {
-			// Extract file ID from url
-			// e.g.
-			// http://misskey.local/files/foo?original=bar --> foo
-			const fileId = new mongo.ObjectID(ps.url.replace(config.driveUrl, '').replace(/\?(.*)$/, '').replace(/\//g, ''));
-			file = await DriveFile.findOne({
-				_id: fileId,
-				'metadata.deletedAt': { $exists: false }
-			});
-		} else {
-			file = await DriveFile.findOne({
-				$or: [{
-					'metadata.url': ps.url
-				}, {
-					'metadata.webpublicUrl': ps.url
-				}, {
-					'metadata.thumbnailUrl': ps.url
-				}],
-				'metadata.deletedAt': { $exists: false }
-			});
-		}
+		file = await DriveFiles.findOne({
+			where: [{
+				url: ps.url
+			}, {
+				webpublicUrl: ps.url
+			}, {
+				thumbnailUrl: ps.url
+			}],
+		});
 	} else {
 		throw new ApiError(meta.errors.fileIdOrUrlRequired);
 	}
 
-	if (!user.isAdmin && !user.isModerator && !file.metadata.userId.equals(user._id)) {
+	if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) {
 		throw new ApiError(meta.errors.accessDenied);
 	}
 
-	if (file === null) {
+	if (file == null) {
 		throw new ApiError(meta.errors.noSuchFile);
 	}
 
-	return await pack(file, {
+	return await DriveFiles.pack(file, {
 		detail: true,
 		self: true
 	});
diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts
index c8803bec3a1a265e0ce32efda50129dc434b69c5..81e86a27345c235bb4121a90207e9271f1614e0b 100644
--- a/src/server/api/endpoints/drive/files/update.ts
+++ b/src/server/api/endpoints/drive/files/update.ts
@@ -1,11 +1,9 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFolder from '../../../../../models/drive-folder';
-import DriveFile, { validateFileName, pack } from '../../../../../models/drive-file';
+import { ID } from '../../../../../misc/cafy-id';
 import { publishDriveStream } from '../../../../../services/stream';
 import define from '../../../define';
-import Note from '../../../../../models/note';
 import { ApiError } from '../../../error';
+import { DriveFiles, DriveFolders } from '../../../../../models';
 
 export const meta = {
 	desc: {
@@ -17,12 +15,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'drive-write',
+	kind: 'write:drive',
 
 	params: {
 		fileId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のファイルID'
 			}
@@ -30,7 +27,6 @@ export const meta = {
 
 		folderId: {
 			validator: $.optional.nullable.type(ID),
-			transform: transform,
 			default: undefined as any,
 			desc: {
 				'ja-JP': 'フォルダID'
@@ -38,7 +34,7 @@ export const meta = {
 		},
 
 		name: {
-			validator: $.optional.str.pipe(validateFileName),
+			validator: $.optional.str.pipe(DriveFiles.validateFileName),
 			default: undefined as any,
 			desc: {
 				'ja-JP': 'ファイル名',
@@ -78,69 +74,47 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	// Fetch file
-	const file = await DriveFile
-		.findOne({
-			_id: ps.fileId
-		});
+	const file = await DriveFiles.findOne(ps.fileId);
 
-	if (file === null) {
+	if (file == null) {
 		throw new ApiError(meta.errors.noSuchFile);
 	}
 
-	if (!user.isAdmin && !user.isModerator && !file.metadata.userId.equals(user._id)) {
+	if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) {
 		throw new ApiError(meta.errors.accessDenied);
 	}
 
-	if (ps.name) file.filename = ps.name;
+	if (ps.name) file.name = ps.name;
 
-	if (ps.isSensitive !== undefined) file.metadata.isSensitive = ps.isSensitive;
+	if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive;
 
 	if (ps.folderId !== undefined) {
 		if (ps.folderId === null) {
-			file.metadata.folderId = null;
+			file.folderId = null;
 		} else {
-			// Fetch folder
-			const folder = await DriveFolder
-				.findOne({
-					_id: ps.folderId,
-					userId: user._id
-				});
-
-			if (folder === null) {
+			const folder = await DriveFolders.findOne({
+				id: ps.folderId,
+				userId: user.id
+			});
+
+			if (folder == null) {
 				throw new ApiError(meta.errors.noSuchFolder);
 			}
 
-			file.metadata.folderId = folder._id;
+			file.folderId = folder.id;
 		}
 	}
 
-	await DriveFile.update(file._id, {
-		$set: {
-			filename: file.filename,
-			'metadata.folderId': file.metadata.folderId,
-			'metadata.isSensitive': file.metadata.isSensitive
-		}
-	});
-
-	// ドライブのファイルが非正規化されているドキュメントも更新
-	Note.find({
-		'_files._id': file._id
-	}).then(notes => {
-		for (const note of notes) {
-			note._files[note._files.findIndex(f => f._id.equals(file._id))] = file;
-			Note.update({ _id: note._id }, {
-				$set: {
-					_files: note._files
-				}
-			});
-		}
+	await DriveFiles.update(file.id, {
+		name: file.name,
+		folderId: file.folderId,
+		isSensitive: file.isSensitive
 	});
 
-	const fileObj = await pack(file, { self: true });
+	const fileObj = await DriveFiles.pack(file, { self: true });
 
 	// Publish fileUpdated event
-	publishDriveStream(user._id, 'fileUpdated', fileObj);
+	publishDriveStream(user.id, 'fileUpdated', fileObj);
 
 	return fileObj;
 });
diff --git a/src/server/api/endpoints/drive/files/upload-from-url.ts b/src/server/api/endpoints/drive/files/upload-from-url.ts
index 93a9fa62fae8b5c9284b644042f63016397650a7..034ab10f19e5eb3e23b9de087dfdf5f15f4f93aa 100644
--- a/src/server/api/endpoints/drive/files/upload-from-url.ts
+++ b/src/server/api/endpoints/drive/files/upload-from-url.ts
@@ -1,9 +1,9 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
 import * as ms from 'ms';
-import { pack } from '../../../../../models/drive-file';
 import uploadFromUrl from '../../../../../services/drive/upload-from-url';
 import define from '../../../define';
+import { DriveFiles } from '../../../../../models';
 
 export const meta = {
 	desc: {
@@ -19,7 +19,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'drive-write',
+	kind: 'write:drive',
 
 	params: {
 		url: {
@@ -30,7 +30,6 @@ export const meta = {
 		folderId: {
 			validator: $.optional.nullable.type(ID),
 			default: null as any,
-			transform: transform
 		},
 
 		isSensitive: {
@@ -53,5 +52,5 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	return await pack(await uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force), { self: true });
+	return await DriveFiles.pack(await uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force), { self: true });
 });
diff --git a/src/server/api/endpoints/drive/folders.ts b/src/server/api/endpoints/drive/folders.ts
index 73c179f7be2f805190514f99d51e63ce5dd866d4..f5c38164073681b4add40726bd81174a49f439d1 100644
--- a/src/server/api/endpoints/drive/folders.ts
+++ b/src/server/api/endpoints/drive/folders.ts
@@ -1,7 +1,8 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import DriveFolder, { pack } from '../../../../models/drive-folder';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
+import { DriveFolders } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
 
 export const meta = {
 	desc: {
@@ -13,7 +14,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'drive-read',
+	kind: 'read:drive',
 
 	params: {
 		limit: {
@@ -23,18 +24,15 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		folderId: {
 			validator: $.optional.nullable.type(ID),
 			default: null as any,
-			transform: transform,
 		}
 	},
 
@@ -47,29 +45,16 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const sort = {
-		_id: -1
-	};
-	const query = {
-		userId: user._id,
-		parentId: ps.folderId
-	} as any;
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		query._id = {
-			$lt: ps.untilId
-		};
+	const query = makePaginationQuery(DriveFolders.createQueryBuilder('folder'), ps.sinceId, ps.untilId)
+		.andWhere('folder.userId = :userId', { userId: user.id });
+
+	if (ps.folderId) {
+		query.andWhere('folder.parentId = :parentId', { parentId: ps.folderId });
+	} else {
+		query.andWhere('folder.parentId IS NULL');
 	}
 
-	const folders = await DriveFolder
-		.find(query, {
-			limit: ps.limit,
-			sort: sort
-		});
+	const folders = await query.take(ps.limit).getMany();
 
-	return await Promise.all(folders.map(folder => pack(folder)));
+	return await Promise.all(folders.map(folder => DriveFolders.pack(folder)));
 });
diff --git a/src/server/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts
index 5fab0b91a1295f9ca6034eb3709a4da5e3f8f181..5530abf9dc310fae5002a06602f5817da206746a 100644
--- a/src/server/api/endpoints/drive/folders/create.ts
+++ b/src/server/api/endpoints/drive/folders/create.ts
@@ -1,9 +1,10 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder';
+import { ID } from '../../../../../misc/cafy-id';
 import { publishDriveStream } from '../../../../../services/stream';
 import define from '../../../define';
 import { ApiError } from '../../../error';
+import { DriveFolders } from '../../../../../models';
+import { genId } from '../../../../../misc/gen-id';
 
 export const meta = {
 	stability: 'stable',
@@ -17,11 +18,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'drive-write',
+	kind: 'write:drive',
 
 	params: {
 		name: {
-			validator: $.optional.str.pipe(isValidFolderName),
+			validator: $.optional.str.pipe(DriveFolders.validateFolderName),
 			default: 'Untitled',
 			desc: {
 				'ja-JP': 'フォルダ名',
@@ -31,7 +32,6 @@ export const meta = {
 
 		parentId: {
 			validator: $.optional.nullable.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '親フォルダID',
 				'en-US': 'Parent folder ID'
@@ -53,29 +53,29 @@ export default define(meta, async (ps, user) => {
 	let parent = null;
 	if (ps.parentId) {
 		// Fetch parent folder
-		parent = await DriveFolder
-			.findOne({
-				_id: ps.parentId,
-				userId: user._id
-			});
+		parent = await DriveFolders.findOne({
+			id: ps.parentId,
+			userId: user.id
+		});
 
-		if (parent === null) {
+		if (parent == null) {
 			throw new ApiError(meta.errors.noSuchFolder);
 		}
 	}
 
 	// Create folder
-	const folder = await DriveFolder.insert({
+	const folder = await DriveFolders.save({
+		id: genId(),
 		createdAt: new Date(),
 		name: ps.name,
-		parentId: parent !== null ? parent._id : null,
-		userId: user._id
+		parentId: parent !== null ? parent.id : null,
+		userId: user.id
 	});
 
-	const folderObj = await pack(folder);
+	const folderObj = await DriveFolders.pack(folder);
 
 	// Publish folderCreated event
-	publishDriveStream(user._id, 'folderCreated', folderObj);
+	publishDriveStream(user.id, 'folderCreated', folderObj);
 
 	return folderObj;
 });
diff --git a/src/server/api/endpoints/drive/folders/delete.ts b/src/server/api/endpoints/drive/folders/delete.ts
index 9f22bf9ea7e9f9fe6b73d1d1733fadb722f5806a..fe6c05ad07d65543154af96c3e007d1cb88720eb 100644
--- a/src/server/api/endpoints/drive/folders/delete.ts
+++ b/src/server/api/endpoints/drive/folders/delete.ts
@@ -1,10 +1,9 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFolder from '../../../../../models/drive-folder';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
 import { publishDriveStream } from '../../../../../services/stream';
-import DriveFile from '../../../../../models/drive-file';
 import { ApiError } from '../../../error';
+import { DriveFolders, DriveFiles } from '../../../../../models';
 
 export const meta = {
 	stability: 'stable',
@@ -18,12 +17,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'drive-write',
+	kind: 'write:drive',
 
 	params: {
 		folderId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のフォルダID',
 				'en-US': 'Target folder ID'
@@ -48,29 +46,26 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	// Get folder
-	const folder = await DriveFolder
-		.findOne({
-			_id: ps.folderId,
-			userId: user._id
-		});
+	const folder = await DriveFolders.findOne({
+		id: ps.folderId,
+		userId: user.id
+	});
 
-	if (folder === null) {
+	if (folder == null) {
 		throw new ApiError(meta.errors.noSuchFolder);
 	}
 
 	const [childFoldersCount, childFilesCount] = await Promise.all([
-		DriveFolder.count({ parentId: folder._id }),
-		DriveFile.count({ 'metadata.folderId': folder._id })
+		DriveFolders.count({ parentId: folder.id }),
+		DriveFiles.count({ folderId: folder.id })
 	]);
 
 	if (childFoldersCount !== 0 || childFilesCount !== 0) {
 		throw new ApiError(meta.errors.hasChildFilesOrFolders);
 	}
 
-	await DriveFolder.remove({ _id: folder._id });
+	await DriveFolders.delete(folder.id);
 
 	// Publish folderCreated event
-	publishDriveStream(user._id, 'folderDeleted', folder._id);
-
-	return;
+	publishDriveStream(user.id, 'folderDeleted', folder.id);
 });
diff --git a/src/server/api/endpoints/drive/folders/find.ts b/src/server/api/endpoints/drive/folders/find.ts
index 16b6c10633a3e753a51d5fc87cd41f9d1a18672a..f0989ec5ae474574233b79f61d643bc6292ad2e3 100644
--- a/src/server/api/endpoints/drive/folders/find.ts
+++ b/src/server/api/endpoints/drive/folders/find.ts
@@ -1,14 +1,14 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFolder, { pack } from '../../../../../models/drive-folder';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
+import { DriveFolders } from '../../../../../models';
 
 export const meta = {
 	tags: ['drive'],
 
 	requireCredential: true,
 
-	kind: 'drive-read',
+	kind: 'read:drive',
 
 	params: {
 		name: {
@@ -17,7 +17,6 @@ export const meta = {
 
 		parentId: {
 			validator: $.optional.nullable.type(ID),
-			transform: transform,
 			default: null as any,
 			desc: {
 				'ja-JP': 'フォルダID'
@@ -34,12 +33,11 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const folders = await DriveFolder
-		.find({
-			name: ps.name,
-			userId: user._id,
-			parentId: ps.parentId
-		});
-
-	return await Promise.all(folders.map(folder => pack(folder)));
+	const folders = await DriveFolders.find({
+		name: ps.name,
+		userId: user.id,
+		parentId: ps.parentId
+	});
+
+	return await Promise.all(folders.map(folder => DriveFolders.pack(folder)));
 });
diff --git a/src/server/api/endpoints/drive/folders/show.ts b/src/server/api/endpoints/drive/folders/show.ts
index bbfcbed51f995183caebecbe616e3fec39db047b..60507e7d7f15aacb9e7806dbae9d11ed2595f70d 100644
--- a/src/server/api/endpoints/drive/folders/show.ts
+++ b/src/server/api/endpoints/drive/folders/show.ts
@@ -1,8 +1,8 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFolder, { pack } from '../../../../../models/drive-folder';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
 import { ApiError } from '../../../error';
+import { DriveFolders } from '../../../../../models';
 
 export const meta = {
 	stability: 'stable',
@@ -16,12 +16,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'drive-read',
+	kind: 'read:drive',
 
 	params: {
 		folderId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のフォルダID',
 				'en-US': 'Target folder ID'
@@ -44,17 +43,16 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	// Get folder
-	const folder = await DriveFolder
-		.findOne({
-			_id: ps.folderId,
-			userId: user._id
-		});
+	const folder = await DriveFolders.findOne({
+		id: ps.folderId,
+		userId: user.id
+	});
 
-	if (folder === null) {
+	if (folder == null) {
 		throw new ApiError(meta.errors.noSuchFolder);
 	}
 
-	return await pack(folder, {
+	return await DriveFolders.pack(folder, {
 		detail: true
 	});
 });
diff --git a/src/server/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts
index a1ee2669f013527c5fa3dfbcdd4c033ed46d2ceb..90129bed631c6a27c03c684ce0727930261b55d3 100644
--- a/src/server/api/endpoints/drive/folders/update.ts
+++ b/src/server/api/endpoints/drive/folders/update.ts
@@ -1,9 +1,9 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder';
+import { ID } from '../../../../../misc/cafy-id';
 import { publishDriveStream } from '../../../../../services/stream';
 import define from '../../../define';
 import { ApiError } from '../../../error';
+import { DriveFolders } from '../../../../../models';
 
 export const meta = {
 	stability: 'stable',
@@ -17,12 +17,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'drive-write',
+	kind: 'write:drive',
 
 	params: {
 		folderId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のフォルダID',
 				'en-US': 'Target folder ID'
@@ -30,7 +29,7 @@ export const meta = {
 		},
 
 		name: {
-			validator: $.optional.str.pipe(isValidFolderName),
+			validator: $.optional.str.pipe(DriveFolders.validateFolderName),
 			desc: {
 				'ja-JP': 'フォルダ名',
 				'en-US': 'Folder name'
@@ -39,7 +38,6 @@ export const meta = {
 
 		parentId: {
 			validator: $.optional.nullable.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '親フォルダID',
 				'en-US': 'Parent folder ID'
@@ -70,46 +68,41 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	// Fetch folder
-	const folder = await DriveFolder
-		.findOne({
-			_id: ps.folderId,
-			userId: user._id
-		});
+	const folder = await DriveFolders.findOne({
+		id: ps.folderId,
+		userId: user.id
+	});
 
-	if (folder === null) {
+	if (folder == null) {
 		throw new ApiError(meta.errors.noSuchFolder);
 	}
 
 	if (ps.name) folder.name = ps.name;
 
 	if (ps.parentId !== undefined) {
-		if (ps.parentId.equals(folder._id)) {
+		if (ps.parentId === folder.id) {
 			throw new ApiError(meta.errors.recursiveNesting);
 		} else if (ps.parentId === null) {
 			folder.parentId = null;
 		} else {
 			// Get parent folder
-			const parent = await DriveFolder
-				.findOne({
-					_id: ps.parentId,
-					userId: user._id
-				});
+			const parent = await DriveFolders.findOne({
+				id: ps.parentId,
+				userId: user.id
+			});
 
-			if (parent === null) {
+			if (parent == null) {
 				throw new ApiError(meta.errors.noSuchParentFolder);
 			}
 
 			// Check if the circular reference will occur
 			async function checkCircle(folderId: any): Promise<boolean> {
 				// Fetch folder
-				const folder2 = await DriveFolder.findOne({
-					_id: folderId
-				}, {
-					_id: true,
-					parentId: true
+				const folder2 = await DriveFolders.findOne({
+					id: folderId
 				});
 
-				if (folder2._id.equals(folder._id)) {
+				if (folder2.id === folder.id) {
 					return true;
 				} else if (folder2.parentId) {
 					return await checkCircle(folder2.parentId);
@@ -124,22 +117,20 @@ export default define(meta, async (ps, user) => {
 				}
 			}
 
-			folder.parentId = parent._id;
+			folder.parentId = parent.id;
 		}
 	}
 
 	// Update
-	DriveFolder.update(folder._id, {
-		$set: {
-			name: folder.name,
-			parentId: folder.parentId
-		}
+	DriveFolders.update(folder.id, {
+		name: folder.name,
+		parentId: folder.parentId
 	});
 
-	const folderObj = await pack(folder);
+	const folderObj = await DriveFolders.pack(folder);
 
 	// Publish folderUpdated event
-	publishDriveStream(user._id, 'folderUpdated', folderObj);
+	publishDriveStream(user.id, 'folderUpdated', folderObj);
 
 	return folderObj;
 });
diff --git a/src/server/api/endpoints/drive/stream.ts b/src/server/api/endpoints/drive/stream.ts
index 916482be4d76d6665001d4b0bfd6555ee1fd1905..9a84627767cb91fefdc6a3933877f5b887aaac94 100644
--- a/src/server/api/endpoints/drive/stream.ts
+++ b/src/server/api/endpoints/drive/stream.ts
@@ -1,14 +1,15 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import DriveFile, { packMany } from '../../../../models/drive-file';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
+import { DriveFiles } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
 
 export const meta = {
 	tags: ['drive'],
 
 	requireCredential: true,
 
-	kind: 'drive-read',
+	kind: 'read:drive',
 
 	params: {
 		limit: {
@@ -18,12 +19,10 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		type: {
@@ -40,35 +39,18 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const sort = {
-		_id: -1
-	};
-
-	const query = {
-		'metadata.userId': user._id,
-		'metadata.deletedAt': { $exists: false }
-	} as any;
-
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		query._id = {
-			$lt: ps.untilId
-		};
-	}
+	const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId)
+		.andWhere('file.userId = :userId', { userId: user.id });
 
 	if (ps.type) {
-		query.contentType = new RegExp(`^${ps.type.replace(/\*/g, '.+?')}$`);
+		if (ps.type.endsWith('/*')) {
+			query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' });
+		} else {
+			query.andWhere('file.type = :type', { type: ps.type });
+		}
 	}
 
-	const files = await DriveFile
-		.find(query, {
-			limit: ps.limit,
-			sort: sort
-		});
+	const files = await query.take(ps.limit).getMany();
 
-		return await packMany(files, { self: true });
+	return await DriveFiles.packMany(files, { detail: false, self: true });
 });
diff --git a/src/server/api/endpoints/federation/instances.ts b/src/server/api/endpoints/federation/instances.ts
index f81f81822e9b0835fa1a2ec66831628c55e9137a..1946d26dece6fb1944c126a2fecd34aa8daa81cc 100644
--- a/src/server/api/endpoints/federation/instances.ts
+++ b/src/server/api/endpoints/federation/instances.ts
@@ -1,6 +1,7 @@
 import $ from 'cafy';
 import define from '../../define';
-import Instance from '../../../../models/instance';
+import { Instances } from '../../../../models';
+import fetchMeta from '../../../../misc/fetch-meta';
 
 export const meta = {
 	tags: ['federation'],
@@ -37,92 +38,55 @@ export const meta = {
 };
 
 export default define(meta, async (ps, me) => {
-	let sort;
-
-	if (ps.sort) {
-		if (ps.sort == '+notes') {
-			sort = {
-				notesCount: -1
-			};
-		} else if (ps.sort == '-notes') {
-			sort = {
-				notesCount: 1
-			};
-		} else if (ps.sort == '+users') {
-			sort = {
-				usersCount: -1
-			};
-		} else if (ps.sort == '-users') {
-			sort = {
-				usersCount: 1
-			};
-		} else if (ps.sort == '+following') {
-			sort = {
-				followingCount: -1
-			};
-		} else if (ps.sort == '-following') {
-			sort = {
-				followingCount: 1
-			};
-		} else if (ps.sort == '+followers') {
-			sort = {
-				followersCount: -1
-			};
-		} else if (ps.sort == '-followers') {
-			sort = {
-				followersCount: 1
-			};
-		} else if (ps.sort == '+caughtAt') {
-			sort = {
-				caughtAt: -1
-			};
-		} else if (ps.sort == '-caughtAt') {
-			sort = {
-				caughtAt: 1
-			};
-		} else if (ps.sort == '+lastCommunicatedAt') {
-			sort = {
-				lastCommunicatedAt: -1
-			};
-		} else if (ps.sort == '-lastCommunicatedAt') {
-			sort = {
-				lastCommunicatedAt: 1
-			};
-		} else if (ps.sort == '+driveUsage') {
-			sort = {
-				driveUsage: -1
-			};
-		} else if (ps.sort == '-driveUsage') {
-			sort = {
-				driveUsage: 1
-			};
-		} else if (ps.sort == '+driveFiles') {
-			sort = {
-				driveFiles: -1
-			};
-		} else if (ps.sort == '-driveFiles') {
-			sort = {
-				driveFiles: 1
-			};
+	const query = Instances.createQueryBuilder('instance');
+
+	switch (ps.sort) {
+		case '+notes': query.orderBy('instance.notesCount', 'DESC'); break;
+		case '-notes': query.orderBy('instance.notesCount', 'ASC'); break;
+		case '+usersCount': query.orderBy('instance.usersCount', 'DESC'); break;
+		case '-usersCount': query.orderBy('instance.usersCount', 'ASC'); break;
+		case '+followingCount': query.orderBy('instance.followingCount', 'DESC'); break;
+		case '-followingCount': query.orderBy('instance.followingCount', 'ASC'); break;
+		case '+followersCount': query.orderBy('instance.followersCount', 'DESC'); break;
+		case '-followersCount': query.orderBy('instance.followersCount', 'ASC'); break;
+		case '+caughtAt': query.orderBy('instance.caughtAt', 'DESC'); break;
+		case '-caughtAt': query.orderBy('instance.caughtAt', 'ASC'); break;
+		case '+lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'DESC'); break;
+		case '-lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'ASC'); break;
+		case '+driveUsage': query.orderBy('instance.driveUsage', 'DESC'); break;
+		case '-driveUsage': query.orderBy('instance.driveUsage', 'ASC'); break;
+		case '+driveFiles': query.orderBy('instance.driveFiles', 'DESC'); break;
+		case '-driveFiles': query.orderBy('instance.driveFiles', 'ASC'); break;
+
+		default: query.orderBy('instance.id', 'DESC'); break;
+	}
+
+	if (typeof ps.blocked === 'boolean') {
+		const meta = await fetchMeta();
+		if (ps.blocked) {
+			query.andWhere('instance.host IN (:...blocks)', { blocks: meta.blockedHosts });
+		} else {
+			query.andWhere('instance.host NOT IN (:...blocks)', { blocks: meta.blockedHosts });
 		}
-	} else {
-		sort = {
-			_id: -1
-		};
 	}
 
-	const q = {} as any;
+	if (typeof ps.notResponding === 'boolean') {
+		if (ps.notResponding) {
+			query.andWhere('instance.isNotResponding = TRUE');
+		} else {
+			query.andWhere('instance.isNotResponding = FALSE');
+		}
+	}
 
-	if (typeof ps.blocked === 'boolean') q.isBlocked = ps.blocked;
-	if (typeof ps.notResponding === 'boolean') q.isNotResponding = ps.notResponding;
-	if (typeof ps.markedAsClosed === 'boolean') q.isMarkedAsClosed = ps.markedAsClosed;
+	if (typeof ps.markedAsClosed === 'boolean') {
+		if (ps.markedAsClosed) {
+			query.andWhere('instance.isMarkedAsClosed = TRUE');
+		} else {
+			query.andWhere('instance.isMarkedAsClosed = FALSE');
+		}
+	}
 
-	const instances = await Instance
-		.find(q, {
-			limit: ps.limit,
-			sort: sort,
-			skip: ps.offset
-		});
+	const instances = await query.take(ps.limit).skip(ps.offset).getMany();
 
 	return instances;
 });
diff --git a/src/server/api/endpoints/federation/show-instance.ts b/src/server/api/endpoints/federation/show-instance.ts
index e7f68620af642ef5446600d8400fc654ea8a9a48..875afa05b2f7f1f4c6ca2c51a745cad0d8bcfbf3 100644
--- a/src/server/api/endpoints/federation/show-instance.ts
+++ b/src/server/api/endpoints/federation/show-instance.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy';
 import define from '../../define';
-import Instance from '../../../../models/instance';
+import { Instances } from '../../../../models';
 
 export const meta = {
 	tags: ['federation'],
@@ -15,7 +15,7 @@ export const meta = {
 };
 
 export default define(meta, async (ps, me) => {
-	const instance = await Instance
+	const instance = await Instances
 		.findOne({ host: ps.host });
 
 	return instance;
diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
index 81b239955140d07d521b3eedbac0b8a3c8200361..5b43815a5e0f663f69885dcbc971df7f618a4cfc 100644
--- a/src/server/api/endpoints/following/create.ts
+++ b/src/server/api/endpoints/following/create.ts
@@ -1,12 +1,11 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
 import * as ms from 'ms';
-import { pack } from '../../../../models/user';
-import Following from '../../../../models/following';
 import create from '../../../../services/following/create';
 import define from '../../define';
 import { ApiError } from '../../error';
 import { getUser } from '../../common/getters';
+import { Followings, Users } from '../../../../models';
 
 export const meta = {
 	stability: 'stable',
@@ -25,12 +24,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'following-write',
+	kind: 'write:following',
 
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -75,7 +73,7 @@ export default define(meta, async (ps, user) => {
 	const follower = user;
 
 	// 自分自身
-	if (user._id.equals(ps.userId)) {
+	if (user.id === ps.userId) {
 		throw new ApiError(meta.errors.followeeIsYourself);
 	}
 
@@ -86,12 +84,12 @@ export default define(meta, async (ps, user) => {
 	});
 
 	// Check if already following
-	const exist = await Following.findOne({
-		followerId: follower._id,
-		followeeId: followee._id
+	const exist = await Followings.findOne({
+		followerId: follower.id,
+		followeeId: followee.id
 	});
 
-	if (exist !== null) {
+	if (exist != null) {
 		throw new ApiError(meta.errors.alreadyFollowing);
 	}
 
@@ -103,5 +101,5 @@ export default define(meta, async (ps, user) => {
 		throw e;
 	}
 
-	return await pack(followee._id, user);
+	return await Users.pack(followee.id, user);
 });
diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts
index 8f8249b1e849558ac2d68d7e28e2d99a9a7e81b7..240a037c9e30dc6eaeeab21f82923a92de2eb5a7 100644
--- a/src/server/api/endpoints/following/delete.ts
+++ b/src/server/api/endpoints/following/delete.ts
@@ -1,12 +1,11 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
 import * as ms from 'ms';
-import { pack } from '../../../../models/user';
-import Following from '../../../../models/following';
 import deleteFollowing from '../../../../services/following/delete';
 import define from '../../define';
 import { ApiError } from '../../error';
 import { getUser } from '../../common/getters';
+import { Followings, Users } from '../../../../models';
 
 export const meta = {
 	stability: 'stable',
@@ -25,12 +24,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'following-write',
+	kind: 'write:following',
 
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -63,7 +61,7 @@ export default define(meta, async (ps, user) => {
 	const follower = user;
 
 	// Check if the followee is yourself
-	if (user._id.equals(ps.userId)) {
+	if (user.id === ps.userId) {
 		throw new ApiError(meta.errors.followeeIsYourself);
 	}
 
@@ -74,16 +72,16 @@ export default define(meta, async (ps, user) => {
 	});
 
 	// Check not following
-	const exist = await Following.findOne({
-		followerId: follower._id,
-		followeeId: followee._id
+	const exist = await Followings.findOne({
+		followerId: follower.id,
+		followeeId: followee.id
 	});
 
-	if (exist === null) {
+	if (exist == null) {
 		throw new ApiError(meta.errors.notFollowing);
 	}
 
 	await deleteFollowing(follower, followee);
 
-	return await pack(followee._id, user);
+	return await Users.pack(followee.id, user);
 });
diff --git a/src/server/api/endpoints/following/requests/accept.ts b/src/server/api/endpoints/following/requests/accept.ts
index 0975990c0224c0471dcfc2745a7f5f51cc31996d..65c24f7be90c99c3867d32dbd1aebfd84292c36a 100644
--- a/src/server/api/endpoints/following/requests/accept.ts
+++ b/src/server/api/endpoints/following/requests/accept.ts
@@ -1,5 +1,5 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
 import acceptFollowRequest from '../../../../../services/following/requests/accept';
 import define from '../../../define';
 import { ApiError } from '../../../error';
@@ -15,12 +15,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'following-write',
+	kind: 'write:following',
 
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
diff --git a/src/server/api/endpoints/following/requests/cancel.ts b/src/server/api/endpoints/following/requests/cancel.ts
index 371f9f0ed38452290ab3ee99dc8ac3d853f57c9f..79cdb776f28c3a69768b5b67c73ec51990304434 100644
--- a/src/server/api/endpoints/following/requests/cancel.ts
+++ b/src/server/api/endpoints/following/requests/cancel.ts
@@ -1,10 +1,10 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
 import cancelFollowRequest from '../../../../../services/following/requests/cancel';
-import { pack } from '../../../../../models/user';
 import define from '../../../define';
 import { ApiError } from '../../../error';
 import { getUser } from '../../../common/getters';
+import { Users } from '../../../../../models';
 
 export const meta = {
 	desc: {
@@ -16,12 +16,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'following-write',
+	kind: 'write:following',
 
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -58,5 +57,5 @@ export default define(meta, async (ps, user) => {
 		throw e;
 	}
 
-	return await pack(followee._id, user);
+	return await Users.pack(followee.id, user);
 });
diff --git a/src/server/api/endpoints/following/requests/list.ts b/src/server/api/endpoints/following/requests/list.ts
index c9bcedf9292f2e5f39fe07ba86b0bcdd6454fc8c..13e4a39388ec3537d03105800ef906f099d800e7 100644
--- a/src/server/api/endpoints/following/requests/list.ts
+++ b/src/server/api/endpoints/following/requests/list.ts
@@ -1,5 +1,5 @@
-import FollowRequest, { pack } from '../../../../../models/follow-request';
 import define from '../../../define';
+import { FollowRequests } from '../../../../../models';
 
 export const meta = {
 	desc: {
@@ -11,13 +11,13 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'following-read'
+	kind: 'read:following'
 };
 
 export default define(meta, async (ps, user) => {
-	const reqs = await FollowRequest.find({
-		followeeId: user._id
+	const reqs = await FollowRequests.find({
+		followeeId: user.id
 	});
 
-	return await Promise.all(reqs.map(req => pack(req)));
+	return await Promise.all(reqs.map(req => FollowRequests.pack(req)));
 });
diff --git a/src/server/api/endpoints/following/requests/reject.ts b/src/server/api/endpoints/following/requests/reject.ts
index 5e59d4bc97cc30694493646bf25c32b1c7642cbb..cccb60b24317d47d6823492f856468b437a42b13 100644
--- a/src/server/api/endpoints/following/requests/reject.ts
+++ b/src/server/api/endpoints/following/requests/reject.ts
@@ -1,5 +1,5 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
 import rejectFollowRequest from '../../../../../services/following/requests/reject';
 import define from '../../../define';
 import { ApiError } from '../../../error';
@@ -15,12 +15,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'following-write',
+	kind: 'write:following',
 
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
diff --git a/src/server/api/endpoints/games/reversi/games.ts b/src/server/api/endpoints/games/reversi/games.ts
index e3c22c7611b4dbcf9bf5722947fbebddc765dbef..07736e04245d09c713ab322481d4d9a5d377fcb2 100644
--- a/src/server/api/endpoints/games/reversi/games.ts
+++ b/src/server/api/endpoints/games/reversi/games.ts
@@ -1,7 +1,9 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import ReversiGame, { pack } from '../../../../../models/games/reversi/game';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
+import { ReversiGames } from '../../../../../models';
+import { makePaginationQuery } from '../../../common/make-pagination-query';
+import { Brackets } from 'typeorm';
 
 export const meta = {
 	tags: ['games'],
@@ -14,12 +16,10 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		my: {
@@ -30,39 +30,20 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const q: any = ps.my ? {
-		isStarted: true,
-		$or: [{
-			user1Id: user._id
-		}, {
-			user2Id: user._id
-		}]
-	} : {
-		isStarted: true
-	};
-
-	const sort = {
-		_id: -1
-	};
-
-	if (ps.sinceId) {
-		sort._id = 1;
-		q._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		q._id = {
-			$lt: ps.untilId
-		};
+	const query = makePaginationQuery(ReversiGames.createQueryBuilder('game'), ps.sinceId, ps.untilId)
+		.andWhere('game.isStarted = TRUE');
+
+	if (ps.my) {
+		query.andWhere(new Brackets(qb => { qb
+			.where('game.user1Id = :userId', { userId: user.id })
+			.orWhere('game.user2Id = :userId', { userId: user.id });
+		}));
 	}
 
 	// Fetch games
-	const games = await ReversiGame.find(q, {
-		sort: sort,
-		limit: ps.limit
-	});
+	const games = await query.take(ps.limit).getMany();
 
-	return await Promise.all(games.map((g) => pack(g, user, {
+	return await Promise.all(games.map((g) => ReversiGames.pack(g, user, {
 		detail: false
 	})));
 });
diff --git a/src/server/api/endpoints/games/reversi/games/show.ts b/src/server/api/endpoints/games/reversi/games/show.ts
index 766ca90119d58d0649d2568b140294fe23d58281..ea2776b16f5573a1fb30d3c5927a93714b8d608f 100644
--- a/src/server/api/endpoints/games/reversi/games/show.ts
+++ b/src/server/api/endpoints/games/reversi/games/show.ts
@@ -1,9 +1,9 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../../misc/cafy-id';
-import ReversiGame, { pack } from '../../../../../../models/games/reversi/game';
+import { ID } from '../../../../../../misc/cafy-id';
 import Reversi from '../../../../../../games/reversi/core';
 import define from '../../../../define';
 import { ApiError } from '../../../../error';
+import { ReversiGames } from '../../../../../../models';
 
 export const meta = {
 	tags: ['games'],
@@ -11,7 +11,6 @@ export const meta = {
 	params: {
 		gameId: {
 			validator: $.type(ID),
-			transform: transform,
 		},
 	},
 
@@ -25,22 +24,23 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const game = await ReversiGame.findOne({ _id: ps.gameId });
+	const game = await ReversiGames.findOne(ps.gameId);
 
 	if (game == null) {
 		throw new ApiError(meta.errors.noSuchGame);
 	}
 
-	const o = new Reversi(game.settings.map, {
-		isLlotheo: game.settings.isLlotheo,
-		canPutEverywhere: game.settings.canPutEverywhere,
-		loopedBoard: game.settings.loopedBoard
+	const o = new Reversi(game.map, {
+		isLlotheo: game.isLlotheo,
+		canPutEverywhere: game.canPutEverywhere,
+		loopedBoard: game.loopedBoard
 	});
 
-	for (const log of game.logs)
+	for (const log of game.logs) {
 		o.put(log.color, log.pos);
+	}
 
-	const packed = await pack(game, user);
+	const packed = await ReversiGames.pack(game, user);
 
 	return Object.assign({
 		board: o.board,
diff --git a/src/server/api/endpoints/games/reversi/games/surrender.ts b/src/server/api/endpoints/games/reversi/games/surrender.ts
index 446210894d093f4f60be8ffe609ffc02f9037ae8..56d66fb20535a37fc4e07db37d1cfa5295e40438 100644
--- a/src/server/api/endpoints/games/reversi/games/surrender.ts
+++ b/src/server/api/endpoints/games/reversi/games/surrender.ts
@@ -1,9 +1,9 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../../misc/cafy-id';
-import ReversiGame, { pack } from '../../../../../../models/games/reversi/game';
+import { ID } from '../../../../../../misc/cafy-id';
 import { publishReversiGameStream } from '../../../../../../services/stream';
 import define from '../../../../define';
 import { ApiError } from '../../../../error';
+import { ReversiGames } from '../../../../../../models';
 
 export const meta = {
 	tags: ['games'],
@@ -17,7 +17,6 @@ export const meta = {
 	params: {
 		gameId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '投了したい対局'
 			}
@@ -46,7 +45,7 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const game = await ReversiGame.findOne({ _id: ps.gameId });
+	const game = await ReversiGames.findOne(ps.gameId);
 
 	if (game == null) {
 		throw new ApiError(meta.errors.noSuchGame);
@@ -56,26 +55,20 @@ export default define(meta, async (ps, user) => {
 		throw new ApiError(meta.errors.alreadyEnded);
 	}
 
-	if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) {
+	if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) {
 		throw new ApiError(meta.errors.accessDenied);
 	}
 
-	const winnerId = game.user1Id.equals(user._id) ? game.user2Id : game.user1Id;
+	const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id;
 
-	await ReversiGame.update({
-		_id: game._id
-	}, {
-		$set: {
-			surrendered: user._id,
-			isEnded: true,
-			winnerId: winnerId
-		}
+	await ReversiGames.update(game.id, {
+		surrendered: user.id,
+		isEnded: true,
+		winnerId: winnerId
 	});
 
-	publishReversiGameStream(game._id, 'ended', {
+	publishReversiGameStream(game.id, 'ended', {
 		winnerId: winnerId,
-		game: await pack(game._id, user)
+		game: await ReversiGames.pack(game.id, user)
 	});
-
-	return;
 });
diff --git a/src/server/api/endpoints/games/reversi/invitations.ts b/src/server/api/endpoints/games/reversi/invitations.ts
index c20477057878ab0eefa4e2610bbf348160461728..71f5aca1d1f337026032e72ea4d58f961a9002a3 100644
--- a/src/server/api/endpoints/games/reversi/invitations.ts
+++ b/src/server/api/endpoints/games/reversi/invitations.ts
@@ -1,5 +1,5 @@
-import Matching, { pack as packMatching } from '../../../../../models/games/reversi/matching';
 import define from '../../../define';
+import { ReversiMatchings } from '../../../../../models';
 
 export const meta = {
 	tags: ['games'],
@@ -9,13 +9,9 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	// Find session
-	const invitations = await Matching.find({
-		childId: user._id
-	}, {
-		sort: {
-			_id: -1
-		}
+	const invitations = await ReversiMatchings.find({
+		childId: user.id
 	});
 
-	return await Promise.all(invitations.map((i) => packMatching(i, user)));
+	return await Promise.all(invitations.map((i) => ReversiMatchings.pack(i, user)));
 });
diff --git a/src/server/api/endpoints/games/reversi/match.ts b/src/server/api/endpoints/games/reversi/match.ts
index e66765944d30edfeb8c525cf66af94a756103423..e34d3c67f4e9344b847ffde88b21bdf9ad423f15 100644
--- a/src/server/api/endpoints/games/reversi/match.ts
+++ b/src/server/api/endpoints/games/reversi/match.ts
@@ -1,12 +1,14 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import Matching, { pack as packMatching } from '../../../../../models/games/reversi/matching';
-import ReversiGame, { pack as packGame } from '../../../../../models/games/reversi/game';
+import { ID } from '../../../../../misc/cafy-id';
 import { publishMainStream, publishReversiStream } from '../../../../../services/stream';
 import { eighteight } from '../../../../../games/reversi/maps';
 import define from '../../../define';
 import { ApiError } from '../../../error';
 import { getUser } from '../../../common/getters';
+import { genId } from '../../../../../misc/gen-id';
+import { ReversiMatchings, ReversiGames } from '../../../../../models';
+import { ReversiGame } from '../../../../../models/entities/games/reversi/game';
+import { ReversiMatching } from '../../../../../models/entities/games/reversi/matching';
 
 export const meta = {
 	tags: ['games'],
@@ -16,7 +18,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -41,50 +42,47 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	// Myself
-	if (ps.userId.equals(user._id)) {
+	if (ps.userId === user.id) {
 		throw new ApiError(meta.errors.isYourself);
 	}
 
 	// Find session
-	const exist = await Matching.findOne({
+	const exist = await ReversiMatchings.findOne({
 		parentId: ps.userId,
-		childId: user._id
+		childId: user.id
 	});
 
 	if (exist) {
 		// Destroy session
-		Matching.remove({
-			_id: exist._id
-		});
+		ReversiMatchings.delete(exist.id);
 
 		// Create game
-		const game = await ReversiGame.insert({
+		const game = await ReversiGames.save({
+			id: genId(),
 			createdAt: new Date(),
 			user1Id: exist.parentId,
-			user2Id: user._id,
+			user2Id: user.id,
 			user1Accepted: false,
 			user2Accepted: false,
 			isStarted: false,
 			isEnded: false,
 			logs: [],
-			settings: {
-				map: eighteight.data,
-				bw: 'random',
-				isLlotheo: false
-			}
-		});
+			map: eighteight.data,
+			bw: 'random',
+			isLlotheo: false
+		} as ReversiGame);
 
-		publishReversiStream(exist.parentId, 'matched', await packGame(game, exist.parentId));
+		publishReversiStream(exist.parentId, 'matched', await ReversiGames.pack(game, exist.parentId));
 
-		const other = await Matching.count({
-			childId: user._id
+		const other = await ReversiMatchings.count({
+			childId: user.id
 		});
 
 		if (other == 0) {
-			publishMainStream(user._id, 'reversiNoInvites');
+			publishMainStream(user.id, 'reversiNoInvites');
 		}
 
-		return await packGame(game, user);
+		return await ReversiGames.pack(game, user);
 	} else {
 		// Fetch child
 		const child = await getUser(ps.userId).catch(e => {
@@ -93,21 +91,22 @@ export default define(meta, async (ps, user) => {
 		});
 
 		// 以前のセッションはすべて削除しておく
-		await Matching.remove({
-			parentId: user._id
+		await ReversiMatchings.delete({
+			parentId: user.id
 		});
 
 		// セッションを作成
-		const matching = await Matching.insert({
+		const matching = await ReversiMatchings.save({
+			id: genId(),
 			createdAt: new Date(),
-			parentId: user._id,
-			childId: child._id
-		});
+			parentId: user.id,
+			childId: child.id
+		} as ReversiMatching);
 
-		const packed = await packMatching(matching, child);
-		publishReversiStream(child._id, 'invited', packed);
-		publishMainStream(child._id, 'reversiInvited', packed);
+		const packed = await ReversiMatchings.pack(matching, child);
+		publishReversiStream(child.id, 'invited', packed);
+		publishMainStream(child.id, 'reversiInvited', packed);
 
-		return;
+		return null;
 	}
 });
diff --git a/src/server/api/endpoints/games/reversi/match/cancel.ts b/src/server/api/endpoints/games/reversi/match/cancel.ts
index fb230032d8a4d98e0401fd3d317886f4b5a2c72f..71aaae5ee1c52628c93aaec9bc8539aad41fb34c 100644
--- a/src/server/api/endpoints/games/reversi/match/cancel.ts
+++ b/src/server/api/endpoints/games/reversi/match/cancel.ts
@@ -1,5 +1,5 @@
-import Matching from '../../../../../../models/games/reversi/matching';
 import define from '../../../../define';
+import { ReversiMatchings } from '../../../../../../models';
 
 export const meta = {
 	tags: ['games'],
@@ -8,9 +8,7 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	await Matching.remove({
-		parentId: user._id
+	await ReversiMatchings.delete({
+		parentId: user.id
 	});
-
-	return;
 });
diff --git a/src/server/api/endpoints/hashtags/list.ts b/src/server/api/endpoints/hashtags/list.ts
index f454d47fedf71a45b0993f80f99bfdc666b5e1d2..7996c816694933b0f75fa39a6e80cf293bd5698c 100644
--- a/src/server/api/endpoints/hashtags/list.ts
+++ b/src/server/api/endpoints/hashtags/list.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy';
 import define from '../../define';
-import Hashtag from '../../../../models/hashtag';
+import { Hashtags } from '../../../../models';
 
 export const meta = {
 	tags: ['hashtags'],
@@ -54,40 +54,39 @@ export const meta = {
 	},
 };
 
-const sort: any = {
-	'+mentionedUsers': { mentionedUsersCount: -1 },
-	'-mentionedUsers': { mentionedUsersCount: 1 },
-	'+mentionedLocalUsers': { mentionedLocalUsersCount: -1 },
-	'-mentionedLocalUsers': { mentionedLocalUsersCount: 1 },
-	'+mentionedRemoteUsers': { mentionedRemoteUsersCount: -1 },
-	'-mentionedRemoteUsers': { mentionedRemoteUsersCount: 1 },
-	'+attachedUsers': { attachedUsersCount: -1 },
-	'-attachedUsers': { attachedUsersCount: 1 },
-	'+attachedLocalUsers': { attachedLocalUsersCount: -1 },
-	'-attachedLocalUsers': { attachedLocalUsersCount: 1 },
-	'+attachedRemoteUsers': { attachedRemoteUsersCount: -1 },
-	'-attachedRemoteUsers': { attachedRemoteUsersCount: 1 },
-};
-
 export default define(meta, async (ps, me) => {
-	const q = {} as any;
-	if (ps.attachedToUserOnly) q.attachedUsersCount = { $ne: 0 };
-	if (ps.attachedToLocalUserOnly) q.attachedLocalUsersCount = { $ne: 0 };
-	if (ps.attachedToRemoteUserOnly) q.attachedRemoteUsersCount = { $ne: 0 };
-	const tags = await Hashtag
-		.find(q, {
-			limit: ps.limit,
-			sort: sort[ps.sort],
-			fields: {
-				tag: true,
-				mentionedUsersCount: true,
-				mentionedLocalUsersCount: true,
-				mentionedRemoteUsersCount: true,
-				attachedUsersCount: true,
-				attachedLocalUsersCount: true,
-				attachedRemoteUsersCount: true
-			}
-		});
+	const query = Hashtags.createQueryBuilder('tag');
+
+	if (ps.attachedToUserOnly) query.andWhere('tag.attachedUsersCount != 0');
+	if (ps.attachedToLocalUserOnly) query.andWhere('tag.attachedLocalUsersCount != 0');
+	if (ps.attachedToRemoteUserOnly) query.andWhere('tag.attachedRemoteUsersCount != 0');
+
+	switch (ps.sort) {
+		case '+mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'DESC'); break;
+		case '-mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'ASC'); break;
+		case '+mentionedLocalUsers': query.orderBy('tag.mentionedLocalUsersCount', 'DESC'); break;
+		case '-mentionedLocalUsers': query.orderBy('tag.mentionedLocalUsersCount', 'ASC'); break;
+		case '+mentionedRemoteUsers': query.orderBy('tag.mentionedRemoteUsersCount', 'DESC'); break;
+		case '-mentionedRemoteUsers': query.orderBy('tag.mentionedRemoteUsersCount', 'ASC'); break;
+		case '+attachedUsers': query.orderBy('tag.attachedUsersCount', 'DESC'); break;
+		case '-attachedUsers': query.orderBy('tag.attachedUsersCount', 'ASC'); break;
+		case '+attachedLocalUsers': query.orderBy('tag.attachedLocalUsersCount', 'DESC'); break;
+		case '-attachedLocalUsers': query.orderBy('tag.attachedLocalUsersCount', 'ASC'); break;
+		case '+attachedRemoteUsers': query.orderBy('tag.attachedRemoteUsersCount', 'DESC'); break;
+		case '-attachedRemoteUsers': query.orderBy('tag.attachedRemoteUsersCount', 'ASC'); break;
+	}
+
+	query.select([
+		'tag.name',
+		'tag.mentionedUsersCount',
+		'tag.mentionedLocalUsersCount',
+		'tag.mentionedRemoteUsersCount',
+		'tag.attachedUsersCount',
+		'tag.attachedLocalUsersCount',
+		'tag.attachedRemoteUsersCount',
+	]);
+
+	const tags = await query.take(ps.limit).getMany();
 
 	return tags;
 });
diff --git a/src/server/api/endpoints/hashtags/search.ts b/src/server/api/endpoints/hashtags/search.ts
index 19b2975adff5d7fabd1b5dab55ff86c12da88685..fd91a2ebcc4cdd9e74fcf0feddfc423449c6cd79 100644
--- a/src/server/api/endpoints/hashtags/search.ts
+++ b/src/server/api/endpoints/hashtags/search.ts
@@ -1,7 +1,6 @@
 import $ from 'cafy';
-import Hashtag from '../../../../models/hashtag';
 import define from '../../define';
-import * as escapeRegexp from 'escape-regexp';
+import { Hashtags } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -46,16 +45,12 @@ export const meta = {
 };
 
 export default define(meta, async (ps) => {
-	const hashtags = await Hashtag
-		.find({
-			tag: new RegExp('^' + escapeRegexp(ps.query.toLowerCase()))
-		}, {
-			sort: {
-				count: -1
-			},
-			limit: ps.limit,
-			skip: ps.offset
-		});
-
-	return hashtags.map(tag => tag.tag);
+	const hashtags = await Hashtags.createQueryBuilder('tag')
+		.where('tag.name like :q', { q: ps.query.toLowerCase() + '%' })
+		.orderBy('tag.count', 'DESC')
+		.take(ps.limit)
+		.skip(ps.offset)
+		.getMany();
+
+	return hashtags.map(tag => tag.name);
 });
diff --git a/src/server/api/endpoints/hashtags/trend.ts b/src/server/api/endpoints/hashtags/trend.ts
index 8b8dd70245ab6c86f28af547e810e4b459e8befd..c750e72a1534fa8b0a52538aac13a36137f9c334 100644
--- a/src/server/api/endpoints/hashtags/trend.ts
+++ b/src/server/api/endpoints/hashtags/trend.ts
@@ -1,17 +1,19 @@
-import Note from '../../../../models/note';
-import { erase } from '../../../../prelude/array';
 import define from '../../define';
 import fetchMeta from '../../../../misc/fetch-meta';
+import { Notes } from '../../../../models';
+import { Note } from '../../../../models/entities/note';
 
 /*
 トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要
 ユニーク投稿数とはそのハッシュタグと投稿ユーザーのペアのカウントで、例えば同じユーザーが複数回同じハッシュタグを投稿してもそのハッシュタグのユニーク投稿数は1とカウントされる
+
+..が理想だけどPostgreSQLでどうするのか分からないので単に「直近Aの内に投稿されたユニーク投稿数が多いハッシュタグ」で妥協する
 */
 
 const rangeA = 1000 * 60 * 30; // 30分
-const rangeB = 1000 * 60 * 120; // 2時間
-const coefficient = 1.25; // 「n倍」の部分
-const requiredUsers = 3; // 最低何人がそのタグを投稿している必要があるか
+//const rangeB = 1000 * 60 * 120; // 2時間
+//const coefficient = 1.25; // 「n倍」の部分
+//const requiredUsers = 3; // 最低何人がそのタグを投稿している必要があるか
 
 const max = 5;
 
@@ -23,92 +25,47 @@ export const meta = {
 
 export default define(meta, async () => {
 	const instance = await fetchMeta();
-	const hidedTags = instance.hidedTags.map(t => t.toLowerCase());
-
-	//#region 1. 直近Aの内に投稿されたハッシュタグ(とユーザーのペア)を集計
-	const data = await Note.aggregate([{
-		$match: {
-			createdAt: {
-				$gt: new Date(Date.now() - rangeA)
-			},
-			tagsLower: {
-				$exists: true,
-				$ne: []
-			}
-		}
-	}, {
-		$unwind: '$tagsLower'
-	}, {
-		$group: {
-			_id: { tag: '$tagsLower', userId: '$userId' }
-		}
-	}]) as {
-		_id: {
-			tag: string;
-			userId: any;
-		}
-	}[];
-	//#endregion
+	const hiddenTags = instance.hiddenTags.map(t => t.toLowerCase());
+
+	const tagNotes = await Notes.createQueryBuilder('note')
+		.where(`note.createdAt > :date`, { date: new Date(Date.now() - rangeA) })
+		.andWhere(`note.tags != '{}'`)
+		.select(['note.tags', 'note.userId'])
+		.getMany();
 
-	if (data.length == 0) {
+	if (tagNotes.length === 0) {
 		return [];
 	}
 
 	const tags: {
 		name: string;
-		count: number;
+		users: Note['userId'][];
 	}[] = [];
 
-	// カウント
-	for (const x of data.map(x => x._id).filter(x => !hidedTags.includes(x.tag))) {
-		const i = tags.findIndex(tag => tag.name == x.tag);
-		if (i != -1) {
-			tags[i].count++;
-		} else {
-			tags.push({
-				name: x.tag,
-				count: 1
-			});
-		}
-	}
-
-	// 最低要求投稿者数を下回るならカットする
-	const limitedTags = tags.filter(tag => tag.count >= requiredUsers);
-
-	//#region 2. 1で取得したそれぞれのタグについて、「直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上」かどうかを判定する
-	const hotsPromises = limitedTags.map(async tag => {
-		const passedCount = (await Note.distinct('userId', {
-			tagsLower: tag.name,
-			createdAt: {
-				$lt: new Date(Date.now() - rangeA),
-				$gt: new Date(Date.now() - rangeB)
+	for (const note of tagNotes) {
+		for (const tag of note.tags) {
+			if (hiddenTags.includes(tag)) continue;
+
+			const x = tags.find(x => x.name === tag);
+			if (x) {
+				if (!x.users.includes(note.userId)) {
+					x.users.push(note.userId);
+				}
+			} else {
+				tags.push({
+					name: tag,
+					users: [note.userId]
+				});
 			}
-		}) as any).length;
-
-		if (tag.count >= (passedCount * coefficient)) {
-			return tag;
-		} else {
-			return null;
 		}
-	});
-	//#endregion
+	}
 
 	// タグを人気順に並べ替え
-	let hots = erase(null, await Promise.all(hotsPromises))
-		.sort((a, b) => b.count - a.count)
+	const hots = tags
+		.sort((a, b) => b.users.length - a.users.length)
 		.map(tag => tag.name)
 		.slice(0, max);
 
-	//#region 3. もし上記の方法でのトレンド抽出の結果、求められているタグ数に達しなければ「ただ単に現在投稿数が多いハッシュタグ」に切り替える
-	if (hots.length < max) {
-		hots = hots.concat(tags
-			.filter(tag => hots.indexOf(tag.name) == -1)
-			.sort((a, b) => b.count - a.count)
-			.map(tag => tag.name)
-			.slice(0, max - hots.length));
-	}
-	//#endregion
-
 	//#region 2(または3)で話題と判定されたタグそれぞれについて過去の投稿数グラフを取得する
 	const countPromises: Promise<any[]>[] = [];
 
@@ -118,23 +75,25 @@ export default define(meta, async () => {
 	const interval = 1000 * 60 * 10;
 
 	for (let i = 0; i < range; i++) {
-		countPromises.push(Promise.all(hots.map(tag => Note.distinct('userId', {
-			tagsLower: tag,
-			createdAt: {
-				$lt: new Date(Date.now() - (interval * i)),
-				$gt: new Date(Date.now() - (interval * (i + 1)))
-			}
-		}))));
+		countPromises.push(Promise.all(hots.map(tag => Notes.createQueryBuilder('note')
+			.select('count(distinct note.userId)')
+			.where(':tag = ANY(note.tags)', { tag: tag })
+			.andWhere('note.createdAt < :lt', { lt: new Date(Date.now() - (interval * i)) })
+			.andWhere('note.createdAt > :gt', { gt: new Date(Date.now() - (interval * (i + 1))) })
+			.getRawOne()
+			.then(x => parseInt(x.count, 10))
+		)));
 	}
 
 	const countsLog = await Promise.all(countPromises);
 
-	const totalCounts: any = await Promise.all(hots.map(tag => Note.distinct('userId', {
-		tagsLower: tag,
-		createdAt: {
-			$gt: new Date(Date.now() - (interval * range))
-		}
-	})));
+	const totalCounts: any = await Promise.all(hots.map(tag => Notes.createQueryBuilder('note')
+		.select('count(distinct note.userId)')
+		.where(':tag = ANY(note.tags)', { tag: tag })
+		.andWhere('note.createdAt > :gt', { gt: new Date(Date.now() - (interval * range)) })
+		.getRawOne()
+		.then(x => parseInt(x.count, 10))
+	));
 	//#endregion
 
 	const stats = hots.map((tag, i) => ({
diff --git a/src/server/api/endpoints/hashtags/users.ts b/src/server/api/endpoints/hashtags/users.ts
index 4b047aee95891e06c2bf24bc3874057c849251be..20cba96d0e1fa2c78656c1b20ad1699f7cfeaced 100644
--- a/src/server/api/endpoints/hashtags/users.ts
+++ b/src/server/api/endpoints/hashtags/users.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy';
-import User, { pack } from '../../../../models/user';
 import define from '../../define';
+import { Users } from '../../../../models';
 
 export const meta = {
 	requireCredential: false,
@@ -54,39 +54,32 @@ export const meta = {
 	},
 };
 
-const sort: any = {
-	'+follower': { followersCount: -1 },
-	'-follower': { followersCount: 1 },
-	'+createdAt': { createdAt: -1 },
-	'-createdAt': { createdAt: 1 },
-	'+updatedAt': { updatedAt: -1 },
-	'-updatedAt': { updatedAt: 1 },
-};
-
 export default define(meta, async (ps, me) => {
-	const q = {
-		tags: ps.tag,
-		$and: []
-	} as any;
+	const query = Users.createQueryBuilder('user')
+		.where(':tag = ANY(user.tags)', { tag: ps.tag });
+
+	const recent = new Date(Date.now() - (1000 * 60 * 60 * 24 * 5));
+
+	if (ps.state === 'alive') {
+		query.andWhere('user.updatedAt > :date', { date: recent });
+	}
 
-	// state
-	q.$and.push(
-		ps.state == 'alive' ? { updatedAt: { $gt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 5)) } } :
-		{}
-	);
+	if (ps.origin === 'local') {
+		query.andWhere('user.host IS NULL');
+	} else if (ps.origin === 'remote') {
+		query.andWhere('user.host IS NOT NULL');
+	}
 
-	// origin
-	q.$and.push(
-		ps.origin == 'local' ? { host: null } :
-		ps.origin == 'remote' ? { host: { $ne: null } } :
-		{}
-	);
+	switch (ps.sort) {
+		case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
+		case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
+		case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break;
+		case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break;
+		case '+updatedAt': query.orderBy('user.updatedAt', 'DESC'); break;
+		case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break;
+	}
 
-	const users = await User
-		.find(q, {
-			limit: ps.limit,
-			sort: sort[ps.sort],
-		});
+	const users = await query.take(ps.limit).getMany();
 
-	return await Promise.all(users.map(user => pack(user, me, { detail: true })));
+	return await Users.packMany(users, me, { detail: true });
 });
diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts
index 7b50cc76c2600bd4de9ce447ec0ec1d261b5d0a6..afad38c4694e3d0560ae8a764961002dac026d4a 100644
--- a/src/server/api/endpoints/i.ts
+++ b/src/server/api/endpoints/i.ts
@@ -1,5 +1,5 @@
-import { pack } from '../../../models/user';
 import define from '../define';
+import { Users } from '../../../models';
 
 export const meta = {
 	stability: 'stable',
@@ -22,7 +22,7 @@ export const meta = {
 export default define(meta, async (ps, user, app) => {
 	const isSecure = user != null && app == null;
 
-	return await pack(user, user, {
+	return await Users.pack(user, user, {
 		detail: true,
 		includeHasUnreadNotes: true,
 		includeSecrets: isSecure
diff --git a/src/server/api/endpoints/i/2fa/done.ts b/src/server/api/endpoints/i/2fa/done.ts
index 556354c386f331fcd4b203275f0e70750f046870..8ccb09b8b7c20a07e92676b9fa4ccbd261d5aa6a 100644
--- a/src/server/api/endpoints/i/2fa/done.ts
+++ b/src/server/api/endpoints/i/2fa/done.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
 import * as speakeasy from 'speakeasy';
-import User from '../../../../../models/user';
 import define from '../../../define';
+import { Users } from '../../../../../models';
 
 export const meta = {
 	requireCredential: true,
@@ -32,12 +32,8 @@ export default define(meta, async (ps, user) => {
 		throw new Error('not verified');
 	}
 
-	await User.update(user._id, {
-		$set: {
-			'twoFactorSecret': user.twoFactorTempSecret,
-			'twoFactorEnabled': true
-		}
+	await Users.update(user.id, {
+		twoFactorSecret: user.twoFactorTempSecret,
+		twoFactorEnabled: true
 	});
-
-	return;
 });
diff --git a/src/server/api/endpoints/i/2fa/register.ts b/src/server/api/endpoints/i/2fa/register.ts
index 302b51ec0be7b6fdee364a9f8119a13734b126cf..5efe77900af22b84e890da706e37e063bcaf850b 100644
--- a/src/server/api/endpoints/i/2fa/register.ts
+++ b/src/server/api/endpoints/i/2fa/register.ts
@@ -2,9 +2,9 @@ import $ from 'cafy';
 import * as bcrypt from 'bcryptjs';
 import * as speakeasy from 'speakeasy';
 import * as QRCode from 'qrcode';
-import User from '../../../../../models/user';
 import config from '../../../../../config';
 import define from '../../../define';
+import { Users } from '../../../../../models';
 
 export const meta = {
 	requireCredential: true,
@@ -31,10 +31,8 @@ export default define(meta, async (ps, user) => {
 		length: 32
 	});
 
-	await User.update(user._id, {
-		$set: {
-			twoFactorTempSecret: secret.base32
-		}
+	await Users.update(user.id, {
+		twoFactorTempSecret: secret.base32
 	});
 
 	// Get the data URL of the authenticator URL
diff --git a/src/server/api/endpoints/i/2fa/unregister.ts b/src/server/api/endpoints/i/2fa/unregister.ts
index 37b26391985ff333fd0f54527994884fd519660c..fb3ecd40435dc48eca38961454a65e1b4a87486d 100644
--- a/src/server/api/endpoints/i/2fa/unregister.ts
+++ b/src/server/api/endpoints/i/2fa/unregister.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
 import * as bcrypt from 'bcryptjs';
-import User from '../../../../../models/user';
 import define from '../../../define';
+import { Users } from '../../../../../models';
 
 export const meta = {
 	requireCredential: true,
@@ -23,11 +23,9 @@ export default define(meta, async (ps, user) => {
 		throw new Error('incorrect password');
 	}
 
-	await User.update(user._id, {
-		$set: {
-			'twoFactorSecret': null,
-			'twoFactorEnabled': false
-		}
+	await Users.update(user.id, {
+		twoFactorSecret: null,
+		twoFactorEnabled: false
 	});
 
 	return;
diff --git a/src/server/api/endpoints/i/authorized-apps.ts b/src/server/api/endpoints/i/authorized-apps.ts
index cb8be9ed97c0538ebc62d0911d0f214cbc39d670..ebf04fcb5872fc55ae726adffa7cc1ef39fe0a40 100644
--- a/src/server/api/endpoints/i/authorized-apps.ts
+++ b/src/server/api/endpoints/i/authorized-apps.ts
@@ -1,7 +1,6 @@
 import $ from 'cafy';
-import AccessToken from '../../../../models/access-token';
-import { pack } from '../../../../models/app';
 import define from '../../define';
+import { AccessTokens, Apps } from '../../../../models';
 
 export const meta = {
 	requireCredential: true,
@@ -28,18 +27,18 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	// Get tokens
-	const tokens = await AccessToken
-		.find({
-			userId: user._id
-		}, {
-			limit: ps.limit,
-			skip: ps.offset,
-			sort: {
-				_id: ps.sort == 'asc' ? 1 : -1
-			}
-		});
+	const tokens = await AccessTokens.find({
+		where: {
+			userId: user.id
+		},
+		take: ps.limit,
+		skip: ps.offset,
+		order: {
+			id: ps.sort == 'asc' ? 1 : -1
+		}
+	});
 
-	return await Promise.all(tokens.map(token => pack(token.appId, user, {
+	return await Promise.all(tokens.map(token => Apps.pack(token.appId, user, {
 		detail: true
 	})));
 });
diff --git a/src/server/api/endpoints/i/change-password.ts b/src/server/api/endpoints/i/change-password.ts
index 8ab286b4bffeebc49fb862512c682fe50e8103c6..f8f977200f512da8e8a12d58bf5ee6c3fd9fff2f 100644
--- a/src/server/api/endpoints/i/change-password.ts
+++ b/src/server/api/endpoints/i/change-password.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
 import * as bcrypt from 'bcryptjs';
-import User from '../../../../models/user';
 import define from '../../define';
+import { Users } from '../../../../models';
 
 export const meta = {
 	requireCredential: true,
@@ -31,11 +31,7 @@ export default define(meta, async (ps, user) => {
 	const salt = await bcrypt.genSalt(8);
 	const hash = await bcrypt.hash(ps.newPassword, salt);
 
-	await User.update(user._id, {
-		$set: {
-			'password': hash
-		}
+	await Users.update(user.id, {
+		password: hash
 	});
-
-	return;
 });
diff --git a/src/server/api/endpoints/i/clear-follow-request-notification.ts b/src/server/api/endpoints/i/clear-follow-request-notification.ts
deleted file mode 100644
index 38c6ec1cefe4cbc81b6893fde0c4470b77c67a59..0000000000000000000000000000000000000000
--- a/src/server/api/endpoints/i/clear-follow-request-notification.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import User from '../../../../models/user';
-import define from '../../define';
-
-export const meta = {
-	tags: ['account', 'following'],
-
-	requireCredential: true,
-
-	kind: 'account-write',
-
-	params: {
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	await User.update({ _id: user._id }, {
-		$set: {
-			pendingReceivedFollowRequestsCount: 0
-		}
-	});
-
-	return;
-});
diff --git a/src/server/api/endpoints/i/delete-account.ts b/src/server/api/endpoints/i/delete-account.ts
index fed38eab5a266585a175a0e05f6e76dfd939e2fc..5aff74e0cc631bec232a30546b2e3aac6b81d9b6 100644
--- a/src/server/api/endpoints/i/delete-account.ts
+++ b/src/server/api/endpoints/i/delete-account.ts
@@ -1,10 +1,7 @@
 import $ from 'cafy';
 import * as bcrypt from 'bcryptjs';
-import User from '../../../../models/user';
 import define from '../../define';
-import { createDeleteNotesJob, createDeleteDriveFilesJob } from '../../../../queue';
-import Message from '../../../../models/messaging-message';
-import Signin from '../../../../models/signin';
+import { Users } from '../../../../models';
 
 export const meta = {
 	requireCredential: true,
@@ -26,27 +23,5 @@ export default define(meta, async (ps, user) => {
 		throw new Error('incorrect password');
 	}
 
-	await User.update({ _id: user._id }, {
-		$set: {
-			isDeleted: true,
-			name: null,
-			description: null,
-			pinnedNoteIds: [],
-			password: null,
-			email: null,
-			twitter: null,
-			github: null,
-			discord: null,
-			profile: {},
-			fields: [],
-			clientSettings: {},
-		}
-	});
-
-	Message.remove({ userId: user._id });
-	Signin.remove({ userId: user._id });
-	createDeleteNotesJob(user);
-	createDeleteDriveFilesJob(user);
-
-	return;
+	await Users.delete(user.id);
 });
diff --git a/src/server/api/endpoints/i/export-blocking.ts b/src/server/api/endpoints/i/export-blocking.ts
index 346b29c79dc16ee05ec732ee9bda08aab649e5a2..14d49487e8417f753ec3f166e6c54d2e039a4f3c 100644
--- a/src/server/api/endpoints/i/export-blocking.ts
+++ b/src/server/api/endpoints/i/export-blocking.ts
@@ -13,6 +13,4 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	createExportBlockingJob(user);
-
-	return;
 });
diff --git a/src/server/api/endpoints/i/export-following.ts b/src/server/api/endpoints/i/export-following.ts
index 5977b0310513c51dec2eb67a4b88b5b640f0080a..50dd28837fcb48faa5ed449777604c4c7138cd4d 100644
--- a/src/server/api/endpoints/i/export-following.ts
+++ b/src/server/api/endpoints/i/export-following.ts
@@ -13,6 +13,4 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	createExportFollowingJob(user);
-
-	return;
 });
diff --git a/src/server/api/endpoints/i/export-mute.ts b/src/server/api/endpoints/i/export-mute.ts
index 22ceff363111111fc61c2e0a10006eedbc9b9eb6..1eb51cd77ecc8bca3a77f7fbdd8e022a8a047a01 100644
--- a/src/server/api/endpoints/i/export-mute.ts
+++ b/src/server/api/endpoints/i/export-mute.ts
@@ -13,6 +13,4 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	createExportMuteJob(user);
-
-	return;
 });
diff --git a/src/server/api/endpoints/i/export-notes.ts b/src/server/api/endpoints/i/export-notes.ts
index 2881aa26976f375403d67923aa9b5d50769c8bdd..dd32c18d11cab4f2d8197f6a253a551afc67515a 100644
--- a/src/server/api/endpoints/i/export-notes.ts
+++ b/src/server/api/endpoints/i/export-notes.ts
@@ -13,6 +13,4 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	createExportNotesJob(user);
-
-	return;
 });
diff --git a/src/server/api/endpoints/i/export-user-lists.ts b/src/server/api/endpoints/i/export-user-lists.ts
index 9d7424ad89e2737677787aef86dbefcf22af3163..7650ca721081ec738a57c290b820b556df2b9c2c 100644
--- a/src/server/api/endpoints/i/export-user-lists.ts
+++ b/src/server/api/endpoints/i/export-user-lists.ts
@@ -13,6 +13,4 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	createExportUserListsJob(user);
-
-	return;
 });
diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts
index 7ea6f7b966a1f54a0890ef3ff8378163e5a3d0f8..d2d149b2d1983b3294e2d5a15d1ffc042b64e95f 100644
--- a/src/server/api/endpoints/i/favorites.ts
+++ b/src/server/api/endpoints/i/favorites.ts
@@ -1,7 +1,8 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Favorite, { packMany } from '../../../../models/favorite';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
+import { NoteFavorites } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
 
 export const meta = {
 	desc: {
@@ -23,42 +24,22 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
-		}
+		},
 	}
 };
 
 export default define(meta, async (ps, user) => {
-	const query = {
-		userId: user._id
-	} as any;
-
-	const sort = {
-		_id: -1
-	};
-
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		query._id = {
-			$lt: ps.untilId
-		};
-	}
+	const query = makePaginationQuery(NoteFavorites.createQueryBuilder('favorite'), ps.sinceId, ps.untilId)
+		.andWhere(`favorite.userId = :meId`, { meId: user.id })
+		.leftJoinAndSelect('favorite.note', 'note');
 
-	// Get favorites
-	const favorites = await Favorite
-		.find(query, {
-			limit: ps.limit,
-			sort: sort
-		});
+	const favorites = await query
+		.take(ps.limit)
+		.getMany();
 
-	return await packMany(favorites, user);
+	return await NoteFavorites.packMany(favorites, user);
 });
diff --git a/src/server/api/endpoints/i/import-following.ts b/src/server/api/endpoints/i/import-following.ts
index f188291bc2d14671de1c986dd721a75775ec6869..deafec18ecd9a549fef23491219bae1f948a8494 100644
--- a/src/server/api/endpoints/i/import-following.ts
+++ b/src/server/api/endpoints/i/import-following.ts
@@ -1,10 +1,10 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
 import { createImportFollowingJob } from '../../../../queue';
 import ms = require('ms');
-import DriveFile from '../../../../models/drive-file';
 import { ApiError } from '../../error';
+import { DriveFiles } from '../../../../models';
 
 export const meta = {
 	secure: true,
@@ -17,7 +17,6 @@ export const meta = {
 	params: {
 		fileId: {
 			validator: $.type(ID),
-			transform: transform,
 		}
 	},
 
@@ -49,16 +48,12 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const file = await DriveFile.findOne({
-		_id: ps.fileId
-	});
+	const file = await DriveFiles.findOne(ps.fileId);
 
 	if (file == null) throw new ApiError(meta.errors.noSuchFile);
-	//if (!file.contentType.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
-	if (file.length > 50000) throw new ApiError(meta.errors.tooBigFile);
-	if (file.length === 0) throw new ApiError(meta.errors.emptyFile);
+	//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
+	if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
+	if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
 
-	createImportFollowingJob(user, file._id);
-
-	return;
+	createImportFollowingJob(user, file.id);
 });
diff --git a/src/server/api/endpoints/i/import-user-lists.ts b/src/server/api/endpoints/i/import-user-lists.ts
index ed3085e5f8b2e38d95d7ef50b393392954590d7d..b7d9d029b7ab6d2f0df94e62d67c40040c1ef602 100644
--- a/src/server/api/endpoints/i/import-user-lists.ts
+++ b/src/server/api/endpoints/i/import-user-lists.ts
@@ -1,10 +1,10 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
 import { createImportUserListsJob } from '../../../../queue';
 import ms = require('ms');
-import DriveFile from '../../../../models/drive-file';
 import { ApiError } from '../../error';
+import { DriveFiles } from '../../../../models';
 
 export const meta = {
 	secure: true,
@@ -17,7 +17,6 @@ export const meta = {
 	params: {
 		fileId: {
 			validator: $.type(ID),
-			transform: transform,
 		}
 	},
 
@@ -49,16 +48,12 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const file = await DriveFile.findOne({
-		_id: ps.fileId
-	});
+	const file = await DriveFiles.findOne(ps.fileId);
 
 	if (file == null) throw new ApiError(meta.errors.noSuchFile);
-	//if (!file.contentType.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
-	if (file.length > 30000) throw new ApiError(meta.errors.tooBigFile);
-	if (file.length === 0) throw new ApiError(meta.errors.emptyFile);
+	//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
+	if (file.size > 30000) throw new ApiError(meta.errors.tooBigFile);
+	if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
 
-	createImportUserListsJob(user, file._id);
-
-	return;
+	createImportUserListsJob(user, file.id);
 });
diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts
index d3e3064abd3ae5f81b8f580588c07cb20ae47e71..9b016e0a2daf65ac67e69a3131beda03809c6fef 100644
--- a/src/server/api/endpoints/i/notifications.ts
+++ b/src/server/api/endpoints/i/notifications.ts
@@ -1,11 +1,9 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Notification from '../../../../models/notification';
-import { packMany } from '../../../../models/notification';
-import { getFriendIds } from '../../common/get-friends';
-import read from '../../common/read-notification';
+import { ID } from '../../../../misc/cafy-id';
+import { readNotification } from '../../common/read-notification';
 import define from '../../define';
-import { getHideUserIds } from '../../common/get-hide-users';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Notifications, Followings, Mutings } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -17,7 +15,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account-read',
+	kind: 'read:notifications',
 
 	params: {
 		limit: {
@@ -27,12 +25,10 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		following: {
@@ -46,12 +42,12 @@ export const meta = {
 		},
 
 		includeTypes: {
-			validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'poll_vote', 'receiveFollowRequest'])),
+			validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'])),
 			default: [] as string[]
 		},
 
 		excludeTypes: {
-			validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'poll_vote', 'receiveFollowRequest'])),
+			validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'])),
 			default: [] as string[]
 		}
 	},
@@ -65,63 +61,38 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const hideUserIds = await getHideUserIds(user);
+	const followingQuery = Followings.createQueryBuilder('following')
+		.select('following.followeeId')
+		.where('following.followerId = :followerId', { followerId: user.id });
 
-	const query = {
-		notifieeId: user._id,
-		$and: [{
-			notifierId: {
-				$nin: hideUserIds
-			}
-		}]
-	} as any;
+	const mutingQuery = Mutings.createQueryBuilder('muting')
+		.select('muting.muteeId')
+		.where('muting.muterId = :muterId', { muterId: user.id });
 
-	const sort = {
-		_id: -1
-	};
+	const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId)
+		.andWhere(`notification.notifieeId = :meId`, { meId: user.id })
+		.leftJoinAndSelect('notification.notifier', 'notifier');
 
-	if (ps.following) {
-		// ID list of the user itself and other users who the user follows
-		const followingIds = await getFriendIds(user._id);
-
-		query.$and.push({
-			notifierId: {
-				$in: followingIds
-			}
-		});
-	}
+	query.andWhere(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`);
+	query.setParameters(mutingQuery.getParameters());
 
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		query._id = {
-			$lt: ps.untilId
-		};
+	if (ps.following) {
+		query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: user.id });
+		query.setParameters(followingQuery.getParameters());
 	}
 
 	if (ps.includeTypes.length > 0) {
-		query.type = {
-			$in: ps.includeTypes
-		};
+		query.andWhere(`notification.type IN (:...includeTypes)`, { includeTypes: ps.includeTypes });
 	} else if (ps.excludeTypes.length > 0) {
-		query.type = {
-			$nin: ps.excludeTypes
-		};
+		query.andWhere(`notification.type NOT IN (:...excludeTypes)`, { excludeTypes: ps.excludeTypes });
 	}
 
-	const notifications = await Notification
-		.find(query, {
-			limit: ps.limit,
-			sort: sort
-		});
+	const notifications = await query.take(ps.limit).getMany();
 
 	// Mark all as read
 	if (notifications.length > 0 && ps.markAsRead) {
-		read(user._id, notifications);
+		readNotification(user.id, notifications.map(x => x.id));
 	}
 
-	return await packMany(notifications);
+	return await Notifications.packMany(notifications);
 });
diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts
index 8d853d45c879a06eef5d14dab43cf0086839f3a4..ac104b19f9b945a7b7feb86306bd0a843b8be281 100644
--- a/src/server/api/endpoints/i/pin.ts
+++ b/src/server/api/endpoints/i/pin.ts
@@ -1,9 +1,9 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import { pack } from '../../../../models/user';
+import { ID } from '../../../../misc/cafy-id';
 import { addPinned } from '../../../../services/i/pin';
 import define from '../../define';
 import { ApiError } from '../../error';
+import { Users } from '../../../../models';
 
 export const meta = {
 	stability: 'stable',
@@ -16,12 +16,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account-write',
+	kind: 'write:account',
 
 	params: {
 		noteId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID'
@@ -58,7 +57,7 @@ export default define(meta, async (ps, user) => {
 		throw e;
 	});
 
-	return await pack(user, user, {
+	return await Users.pack(user, user, {
 		detail: true
 	});
 });
diff --git a/src/server/api/endpoints/i/read-all-messaging-messages.ts b/src/server/api/endpoints/i/read-all-messaging-messages.ts
index bbbfa0d7b3d4fc3126f86a99cc9005c129b3e199..e8ada277e976b8fa477450957abe9ae2fa9a0447 100644
--- a/src/server/api/endpoints/i/read-all-messaging-messages.ts
+++ b/src/server/api/endpoints/i/read-all-messaging-messages.ts
@@ -1,7 +1,6 @@
-import User from '../../../../models/user';
 import { publishMainStream } from '../../../../services/stream';
-import Message from '../../../../models/messaging-message';
 import define from '../../define';
+import { MessagingMessages } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -13,7 +12,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account-write',
+	kind: 'write:account',
 
 	params: {
 	}
@@ -21,24 +20,12 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	// Update documents
-	await Message.update({
-		recipientId: user._id,
+	await MessagingMessages.update({
+		recipientId: user.id,
 		isRead: false
 	}, {
-		$set: {
-			isRead: true
-		}
-	}, {
-		multi: true
-	});
-
-	User.update({ _id: user._id }, {
-		$set: {
-			hasUnreadMessagingMessage: false
-		}
+		isRead: true
 	});
 
-	publishMainStream(user._id, 'readAllMessagingMessages');
-
-	return;
+	publishMainStream(user.id, 'readAllMessagingMessages');
 });
diff --git a/src/server/api/endpoints/i/read-all-unread-notes.ts b/src/server/api/endpoints/i/read-all-unread-notes.ts
index 742c2d99087c4d740be21255da3829fa5f6a9923..cc8ebf58ec5b3a245e0dfb85b27f9baf9f2a97af 100644
--- a/src/server/api/endpoints/i/read-all-unread-notes.ts
+++ b/src/server/api/endpoints/i/read-all-unread-notes.ts
@@ -1,7 +1,6 @@
-import User from '../../../../models/user';
 import { publishMainStream } from '../../../../services/stream';
-import NoteUnread from '../../../../models/note-unread';
 import define from '../../define';
+import { NoteUnreads } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -13,7 +12,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account-write',
+	kind: 'write:account',
 
 	params: {
 	}
@@ -21,20 +20,11 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	// Remove documents
-	await NoteUnread.remove({
-		userId: user._id
-	});
-
-	User.update({ _id: user._id }, {
-		$set: {
-			hasUnreadMentions: false,
-			hasUnreadSpecifiedNotes: false
-		}
+	await NoteUnreads.delete({
+		userId: user.id
 	});
 
 	// 全て既読になったイベントを発行
-	publishMainStream(user._id, 'readAllUnreadMentions');
-	publishMainStream(user._id, 'readAllUnreadSpecifiedNotes');
-
-	return;
+	publishMainStream(user.id, 'readAllUnreadMentions');
+	publishMainStream(user.id, 'readAllUnreadSpecifiedNotes');
 });
diff --git a/src/server/api/endpoints/i/regenerate-token.ts b/src/server/api/endpoints/i/regenerate-token.ts
index ad10b99b369bcd47ac3df5ecc3385493042a97dd..729c1a300a4b3ee675131cc4f5ad0b4c6c37af24 100644
--- a/src/server/api/endpoints/i/regenerate-token.ts
+++ b/src/server/api/endpoints/i/regenerate-token.ts
@@ -1,9 +1,9 @@
 import $ from 'cafy';
 import * as bcrypt from 'bcryptjs';
-import User from '../../../../models/user';
 import { publishMainStream } from '../../../../services/stream';
 import generateUserToken from '../../common/generate-native-user-token';
 import define from '../../define';
+import { Users } from '../../../../models';
 
 export const meta = {
 	requireCredential: true,
@@ -28,14 +28,10 @@ export default define(meta, async (ps, user) => {
 	// Generate secret
 	const secret = generateUserToken();
 
-	await User.update(user._id, {
-		$set: {
-			'token': secret
-		}
+	await Users.update(user.id, {
+		token: secret
 	});
 
 	// Publish event
-	publishMainStream(user._id, 'myTokenRegenerated');
-
-	return;
+	publishMainStream(user.id, 'myTokenRegenerated');
 });
diff --git a/src/server/api/endpoints/i/signin-history.ts b/src/server/api/endpoints/i/signin-history.ts
index 87160a9f912b3c2b6f764511e24de526262a6ca9..e9ae19d734901762e1b3e030fee87d7c06373933 100644
--- a/src/server/api/endpoints/i/signin-history.ts
+++ b/src/server/api/endpoints/i/signin-history.ts
@@ -1,7 +1,8 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Signin, { pack } from '../../../../models/signin';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
+import { Signins } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
 
 export const meta = {
 	requireCredential: true,
@@ -16,41 +17,19 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		}
 	}
 };
 
 export default define(meta, async (ps, user) => {
-	const query = {
-		userId: user._id
-	} as any;
-
-	const sort = {
-		_id: -1
-	};
-
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		query._id = {
-			$lt: ps.untilId
-		};
-	}
+	const query = makePaginationQuery(Signins.createQueryBuilder('signin'), ps.sinceId, ps.untilId)
+		.andWhere(`signin.userId = :meId`, { meId: user.id });
 
-	const history = await Signin
-		.find(query, {
-			limit: ps.limit,
-			sort: sort
-		});
+	const history = await query.take(ps.limit).getMany();
 
-	return await Promise.all(history.map(record => pack(record)));
+	return await Promise.all(history.map(record => Signins.pack(record)));
 });
diff --git a/src/server/api/endpoints/i/unpin.ts b/src/server/api/endpoints/i/unpin.ts
index 184d46f2c361f5891f9ff8fce3cbbf33ec982f4d..4688533578d5a9613bdb2dce30c71a01143fda61 100644
--- a/src/server/api/endpoints/i/unpin.ts
+++ b/src/server/api/endpoints/i/unpin.ts
@@ -1,9 +1,9 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import { pack } from '../../../../models/user';
+import { ID } from '../../../../misc/cafy-id';
 import { removePinned } from '../../../../services/i/pin';
 import define from '../../define';
 import { ApiError } from '../../error';
+import { Users } from '../../../../models';
 
 export const meta = {
 	stability: 'stable',
@@ -16,12 +16,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account-write',
+	kind: 'write:account',
 
 	params: {
 		noteId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID'
@@ -44,7 +43,7 @@ export default define(meta, async (ps, user) => {
 		throw e;
 	});
 
-	return await pack(user, user, {
+	return await Users.pack(user, user, {
 		detail: true
 	});
 });
diff --git a/src/server/api/endpoints/i/update-client-setting.ts b/src/server/api/endpoints/i/update-client-setting.ts
index 79cd04e169840f87a6df46685256fe00cda0e20d..edbfe28f35c416f504473f45354f415ae75a2052 100644
--- a/src/server/api/endpoints/i/update-client-setting.ts
+++ b/src/server/api/endpoints/i/update-client-setting.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
-import User from '../../../../models/user';
 import { publishMainStream } from '../../../../services/stream';
 import define from '../../define';
+import { Users } from '../../../../models';
 
 export const meta = {
 	requireCredential: true,
@@ -10,7 +10,7 @@ export const meta = {
 
 	params: {
 		name: {
-			validator: $.str
+			validator: $.str.match(/^[a-zA-Z]+$/)
 		},
 
 		value: {
@@ -20,18 +20,18 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const x: any = {};
-	x[`clientSettings.${ps.name}`] = ps.value;
-
-	await User.update(user._id, {
-		$set: x
-	});
+	await Users.createQueryBuilder().update()
+		.set({
+			clientData: {
+				[ps.name]: ps.value
+			},
+		})
+		.where('id = :id', { id: user.id })
+		.execute();
 
 	// Publish event
-	publishMainStream(user._id, 'clientSettingUpdated', {
+	publishMainStream(user.id, 'clientSettingUpdated', {
 		key: ps.name,
 		value: ps.value
 	});
-
-	return;
 });
diff --git a/src/server/api/endpoints/i/update-email.ts b/src/server/api/endpoints/i/update-email.ts
index c90462d85041495b068bf3dd99e9c76df87dff50..253017535ff9a5e9cd643d904cad42064861978f 100644
--- a/src/server/api/endpoints/i/update-email.ts
+++ b/src/server/api/endpoints/i/update-email.ts
@@ -1,5 +1,4 @@
 import $ from 'cafy';
-import User, { pack } from '../../../../models/user';
 import { publishMainStream } from '../../../../services/stream';
 import define from '../../define';
 import * as nodemailer from 'nodemailer';
@@ -9,6 +8,7 @@ import config from '../../../../config';
 import * as ms from 'ms';
 import * as bcrypt from 'bcryptjs';
 import { apiLogger } from '../../logger';
+import { Users } from '../../../../models';
 
 export const meta = {
 	requireCredential: true,
@@ -39,29 +39,25 @@ export default define(meta, async (ps, user) => {
 		throw new Error('incorrect password');
 	}
 
-	await User.update(user._id, {
-		$set: {
-			email: ps.email,
-			emailVerified: false,
-			emailVerifyCode: null
-		}
+	await Users.update(user.id, {
+		email: ps.email,
+		emailVerified: false,
+		emailVerifyCode: null
 	});
 
-	const iObj = await pack(user._id, user, {
+	const iObj = await Users.pack(user.id, user, {
 		detail: true,
 		includeSecrets: true
 	});
 
 	// Publish meUpdated event
-	publishMainStream(user._id, 'meUpdated', iObj);
+	publishMainStream(user.id, 'meUpdated', iObj);
 
 	if (ps.email != null) {
 		const code = rndstr('a-z0-9', 16);
 
-		await User.update(user._id, {
-			$set: {
-				emailVerifyCode: code
-			}
+		await Users.update(user.id, {
+			emailVerifyCode: code
 		});
 
 		const meta = await fetchMeta();
@@ -84,7 +80,7 @@ export default define(meta, async (ps, user) => {
 		transporter.sendMail({
 			from: meta.email,
 			to: ps.email,
-			subject: meta.name,
+			subject: meta.name || 'Misskey',
 			text: `To verify email, please click this link: ${link}`
 		}, (error, info) => {
 			if (error) {
diff --git a/src/server/api/endpoints/i/update-home.ts b/src/server/api/endpoints/i/update-home.ts
deleted file mode 100644
index e2c319887f628d436c17232fed1b477e6bbf0e43..0000000000000000000000000000000000000000
--- a/src/server/api/endpoints/i/update-home.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import $ from 'cafy';
-import User from '../../../../models/user';
-import { publishMainStream } from '../../../../services/stream';
-import define from '../../define';
-
-export const meta = {
-	requireCredential: true,
-
-	secure: true,
-
-	params: {
-		home: {
-			validator: $.arr($.obj({
-				name: $.str,
-				id: $.str,
-				place: $.str,
-				data: $.obj()
-			}).strict())
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	await User.update(user._id, {
-		$set: {
-			'clientSettings.home': ps.home
-		}
-	});
-
-	publishMainStream(user._id, 'homeUpdated', ps.home);
-
-	return;
-});
diff --git a/src/server/api/endpoints/i/update-mobile-home.ts b/src/server/api/endpoints/i/update-mobile-home.ts
deleted file mode 100644
index 642e2b3e09f38c2a60e62039e710c037a12921db..0000000000000000000000000000000000000000
--- a/src/server/api/endpoints/i/update-mobile-home.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import $ from 'cafy';
-import User from '../../../../models/user';
-import { publishMainStream } from '../../../../services/stream';
-import define from '../../define';
-
-export const meta = {
-	requireCredential: true,
-
-	secure: true,
-
-	params: {
-		home: {
-			validator: $.arr($.obj({
-				name: $.str,
-				id: $.str,
-				data: $.obj()
-			}).strict())
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	await User.update(user._id, {
-		$set: {
-			'clientSettings.mobileHome': ps.home
-		}
-	});
-
-	publishMainStream(user._id, 'mobileHomeUpdated', ps.home);
-
-	return;
-});
diff --git a/src/server/api/endpoints/i/update-widget.ts b/src/server/api/endpoints/i/update-widget.ts
deleted file mode 100644
index 67d342278d7928182b2a3d49d55e16f39cf0063e..0000000000000000000000000000000000000000
--- a/src/server/api/endpoints/i/update-widget.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import $ from 'cafy';
-import User from '../../../../models/user';
-import { publishMainStream } from '../../../../services/stream';
-import define from '../../define';
-
-export const meta = {
-	requireCredential: true,
-
-	secure: true,
-
-	params: {
-		id: {
-			validator: $.str
-		},
-
-		data: {
-			validator: $.obj()
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	if (ps.id == null && ps.data == null) throw new Error('you need to set id and data params if home param unset');
-
-	let widget;
-
-	//#region Desktop home
-	if (widget == null && user.clientSettings.home) {
-		const desktopHome = user.clientSettings.home;
-		widget = desktopHome.find((w: any) => w.id == ps.id);
-		if (widget) {
-				widget.data = ps.data;
-
-			await User.update(user._id, {
-				$set: {
-					'clientSettings.home': desktopHome
-				}
-			});
-		}
-	}
-	//#endregion
-
-	//#region Mobile home
-	if (widget == null && user.clientSettings.mobileHome) {
-		const mobileHome = user.clientSettings.mobileHome;
-		widget = mobileHome.find((w: any) => w.id == ps.id);
-		if (widget) {
-				widget.data = ps.data;
-
-			await User.update(user._id, {
-				$set: {
-					'clientSettings.mobileHome': mobileHome
-				}
-			});
-		}
-	}
-	//#endregion
-
-	//#region Deck
-	if (widget == null && user.clientSettings.deck && user.clientSettings.deck.columns) {
-		const deck = user.clientSettings.deck;
-		for (const c of deck.columns.filter((c: any) => c.type == 'widgets')) {
-			for (const w of c.widgets.filter((w: any) => w.id == ps.id)) {
-				widget = w;
-			}
-		}
-		if (widget) {
-				widget.data = ps.data;
-
-			await User.update(user._id, {
-				$set: {
-					'clientSettings.deck': deck
-				}
-			});
-		}
-	}
-	//#endregion
-
-	if (widget) {
-		publishMainStream(user._id, 'widgetUpdated', {
-			id: ps.id, data: ps.data
-		});
-
-		return;
-	} else {
-		throw new Error('widget not found');
-	}
-});
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index 099ef3399087881f5c214d1737e4a10de3561100..f3e5d41021313528ba892ab837584b2acc694ecf 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -1,18 +1,16 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user';
+import { ID } from '../../../../misc/cafy-id';
 import { publishMainStream } from '../../../../services/stream';
-import DriveFile from '../../../../models/drive-file';
 import acceptAllFollowRequests from '../../../../services/following/requests/accept-all';
 import { publishToFollowers } from '../../../../services/i/update';
 import define from '../../define';
-import getDriveFileUrl from '../../../../misc/get-drive-file-url';
 import { parse, parsePlain } from '../../../../mfm/parse';
 import extractEmojis from '../../../../misc/extract-emojis';
 import extractHashtags from '../../../../misc/extract-hashtags';
 import * as langmap from 'langmap';
 import { updateHashtag } from '../../../../services/update-hashtag';
 import { ApiError } from '../../error';
+import { Users, DriveFiles } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -24,18 +22,18 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account-write',
+	kind: 'write:account',
 
 	params: {
 		name: {
-			validator: $.optional.nullable.str.pipe(isValidName),
+			validator: $.optional.nullable.str.pipe(Users.isValidName),
 			desc: {
 				'ja-JP': '名前(ハンドルネームやニックネーム)'
 			}
 		},
 
 		description: {
-			validator: $.optional.nullable.str.pipe(isValidDescription),
+			validator: $.optional.nullable.str.pipe(Users.isValidDescription),
 			desc: {
 				'ja-JP': 'アカウントの説明や自己紹介'
 			}
@@ -49,14 +47,14 @@ export const meta = {
 		},
 
 		location: {
-			validator: $.optional.nullable.str.pipe(isValidLocation),
+			validator: $.optional.nullable.str.pipe(Users.isValidLocation),
 			desc: {
 				'ja-JP': '住んでいる地域、所在'
 			}
 		},
 
 		birthday: {
-			validator: $.optional.nullable.str.pipe(isValidBirthday),
+			validator: $.optional.nullable.str.pipe(Users.isValidBirthday),
 			desc: {
 				'ja-JP': '誕生日 (YYYY-MM-DD形式)'
 			}
@@ -64,7 +62,6 @@ export const meta = {
 
 		avatarId: {
 			validator: $.optional.nullable.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': 'アイコンに設定する画像のドライブファイルID'
 			}
@@ -72,20 +69,11 @@ export const meta = {
 
 		bannerId: {
 			validator: $.optional.nullable.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': 'バナーに設定する画像のドライブファイルID'
 			}
 		},
 
-		wallpaperId: {
-			validator: $.optional.nullable.type(ID),
-			transform: transform,
-			desc: {
-				'ja-JP': '壁紙に設定する画像のドライブファイルID'
-			}
-		},
-
 		isLocked: {
 			validator: $.optional.bool,
 			desc: {
@@ -171,116 +159,76 @@ export default define(meta, async (ps, user, app) => {
 	if (ps.name !== undefined) updates.name = ps.name;
 	if (ps.description !== undefined) updates.description = ps.description;
 	if (ps.lang !== undefined) updates.lang = ps.lang;
-	if (ps.location !== undefined) updates['profile.location'] = ps.location;
-	if (ps.birthday !== undefined) updates['profile.birthday'] = ps.birthday;
+	if (ps.location !== undefined) updates.location = ps.location;
+	if (ps.birthday !== undefined) updates.birthday = ps.birthday;
 	if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
 	if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId;
-	if (ps.wallpaperId !== undefined) updates.wallpaperId = ps.wallpaperId;
 	if (typeof ps.isLocked == 'boolean') updates.isLocked = ps.isLocked;
 	if (typeof ps.isBot == 'boolean') updates.isBot = ps.isBot;
 	if (typeof ps.carefulBot == 'boolean') updates.carefulBot = ps.carefulBot;
 	if (typeof ps.autoAcceptFollowed == 'boolean') updates.autoAcceptFollowed = ps.autoAcceptFollowed;
 	if (typeof ps.isCat == 'boolean') updates.isCat = ps.isCat;
-	if (typeof ps.autoWatch == 'boolean') updates['settings.autoWatch'] = ps.autoWatch;
-	if (typeof ps.alwaysMarkNsfw == 'boolean') updates['settings.alwaysMarkNsfw'] = ps.alwaysMarkNsfw;
+	if (typeof ps.autoWatch == 'boolean') updates.autoWatch = ps.autoWatch;
+	if (typeof ps.alwaysMarkNsfw == 'boolean') updates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
 
 	if (ps.avatarId) {
-		const avatar = await DriveFile.findOne({
-			_id: ps.avatarId
-		});
+		const avatar = await DriveFiles.findOne(ps.avatarId);
 
-		if (avatar == null) throw new ApiError(meta.errors.noSuchAvatar);
-		if (!avatar.contentType.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage);
+		if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
+		if (!avatar.type.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage);
 
-		if (avatar.metadata.deletedAt) {
-			updates.avatarUrl = null;
-		} else {
-			updates.avatarUrl = getDriveFileUrl(avatar, true);
+		updates.avatarUrl = avatar.thumbnailUrl;
 
-			if (avatar.metadata.properties.avgColor) {
-				updates.avatarColor = avatar.metadata.properties.avgColor;
-			}
+		if (avatar.properties.avgColor) {
+			updates.avatarColor = avatar.properties.avgColor;
 		}
 	}
 
 	if (ps.bannerId) {
-		const banner = await DriveFile.findOne({
-			_id: ps.bannerId
-		});
+		const banner = await DriveFiles.findOne(ps.bannerId);
 
-		if (banner == null) throw new ApiError(meta.errors.noSuchBanner);
-		if (!banner.contentType.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage);
+		if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
+		if (!banner.type.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage);
 
-		if (banner.metadata.deletedAt) {
-			updates.bannerUrl = null;
-		} else {
-			updates.bannerUrl = getDriveFileUrl(banner, false);
+		updates.bannerUrl = banner.webpublicUrl;
 
-			if (banner.metadata.properties.avgColor) {
-				updates.bannerColor = banner.metadata.properties.avgColor;
-			}
-		}
-	}
-
-	if (ps.wallpaperId !== undefined) {
-		if (ps.wallpaperId === null) {
-			updates.wallpaperUrl = null;
-			updates.wallpaperColor = null;
-		} else {
-			const wallpaper = await DriveFile.findOne({
-				_id: ps.wallpaperId
-			});
-
-			if (wallpaper == null) throw new Error('wallpaper not found');
-
-			if (wallpaper.metadata.deletedAt) {
-				updates.wallpaperUrl = null;
-			} else {
-				updates.wallpaperUrl = getDriveFileUrl(wallpaper);
-
-				if (wallpaper.metadata.properties.avgColor) {
-					updates.wallpaperColor = wallpaper.metadata.properties.avgColor;
-				}
-			}
+		if (banner.properties.avgColor) {
+			updates.bannerColor = banner.properties.avgColor;
 		}
 	}
 
 	//#region emojis/tags
-	if (updates.name != null || updates.description != null) {
-		let emojis = [] as string[];
-		let tags = [] as string[];
+	let emojis = [] as string[];
+	let tags = [] as string[];
 
-		if (updates.name != null) {
-			const tokens = parsePlain(updates.name);
-			emojis = emojis.concat(extractEmojis(tokens));
-		}
+	if (updates.name != null) {
+		const tokens = parsePlain(updates.name);
+		emojis = emojis.concat(extractEmojis(tokens));
+	}
 
-		if (updates.description != null) {
-			const tokens = parse(updates.description);
-			emojis = emojis.concat(extractEmojis(tokens));
-			tags = extractHashtags(tokens).map(tag => tag.toLowerCase());
-		}
+	if (updates.description != null) {
+		const tokens = parse(updates.description);
+		emojis = emojis.concat(extractEmojis(tokens));
+		tags = extractHashtags(tokens).map(tag => tag.toLowerCase());
+	}
 
-		updates.emojis = emojis;
-		updates.tags = tags;
+	updates.emojis = emojis;
+	updates.tags = tags;
 
-		// ハッシュタグ更新
-		for (const tag of tags) updateHashtag(user, tag, true, true);
-		for (const tag of (user.tags || []).filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false);
-	}
+	// ハッシュタグ更新
+	for (const tag of tags) updateHashtag(user, tag, true, true);
+	for (const tag of user.tags.filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false);
 	//#endregion
 
-	await User.update(user._id, {
-		$set: updates
-	});
+	await Users.update(user.id, updates);
 
-	const iObj = await pack(user._id, user, {
+	const iObj = await Users.pack(user.id, user, {
 		detail: true,
 		includeSecrets: isSecure
 	});
 
 	// Publish meUpdated event
-	publishMainStream(user._id, 'meUpdated', iObj);
+	publishMainStream(user.id, 'meUpdated', iObj);
 
 	// 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
 	if (user.isLocked && ps.isLocked === false) {
@@ -288,7 +236,7 @@ export default define(meta, async (ps, user, app) => {
 	}
 
 	// フォロワーにUpdateを配信
-	publishToFollowers(user._id);
+	publishToFollowers(user.id);
 
 	return iObj;
 });
diff --git a/src/server/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts
index 699dc7c253bb78835a0745fb210f8d605aefbaf7..c0aec61212d0474c8f97b295c2cf1f13ff057303 100644
--- a/src/server/api/endpoints/messaging/history.ts
+++ b/src/server/api/endpoints/messaging/history.ts
@@ -1,7 +1,8 @@
 import $ from 'cafy';
-import Mute from '../../../../models/mute';
-import Message, { pack, IMessagingMessage } from '../../../../models/messaging-message';
 import define from '../../define';
+import { MessagingMessage } from '../../../../models/entities/messaging-message';
+import { MessagingMessages, Mutings } from '../../../../models';
+import { Brackets } from 'typeorm';
 
 export const meta = {
 	desc: {
@@ -31,34 +32,33 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const mute = await Mute.find({
-		muterId: user._id,
-		deletedAt: { $exists: false }
+	const mute = await Mutings.find({
+		muterId: user.id,
 	});
 
-	const history: IMessagingMessage[] = [];
+	const history: MessagingMessage[] = [];
 
 	for (let i = 0; i < ps.limit; i++) {
-		const found = history.map(m => m.userId.equals(user._id) ? m.recipientId : m.userId);
+		const found = history.map(m => (m.userId === user.id) ? m.recipientId : m.userId);
 
-		const message = await Message.findOne({
-			$or: [{
-				userId: user._id
-			}, {
-				recipientId: user._id
-			}],
-			$and: [{
-				userId: { $nin: found },
-				recipientId: { $nin: found }
-			}, {
-				userId: { $nin: mute.map(m => m.muteeId) },
-				recipientId: { $nin: mute.map(m => m.muteeId) }
-			}]
-		}, {
-			sort: {
-				createdAt: -1
-			}
-		});
+		const query = MessagingMessages.createQueryBuilder('message')
+			.where(new Brackets(qb => { qb
+				.where(`message.userId = :userId`, { userId: user.id })
+				.orWhere(`message.recipientId = :userId`, { userId: user.id });
+			}))
+			.orderBy('message.createdAt', 'DESC');
+
+		if (found.length > 0) {
+			query.andWhere(`message.userId NOT IN (:...found)`, { found: found });
+			query.andWhere(`message.recipientId NOT IN (:...found)`, { found: found });
+		}
+
+		if (mute.length > 0) {
+			query.andWhere(`message.userId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) });
+			query.andWhere(`message.recipientId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) });
+		}
+
+		const message = await query.getOne();
 
 		if (message) {
 			history.push(message);
@@ -67,5 +67,5 @@ export default define(meta, async (ps, user) => {
 		}
 	}
 
-	return await Promise.all(history.map(h => pack(h._id, user)));
+	return await Promise.all(history.map(h => MessagingMessages.pack(h.id, user)));
 });
diff --git a/src/server/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts
index c19db45f1f1fef0d6dc813d4e71d26f82c691974..02c57b8d03b5f0567a65d4a831266db15a5a61ed 100644
--- a/src/server/api/endpoints/messaging/messages.ts
+++ b/src/server/api/endpoints/messaging/messages.ts
@@ -1,11 +1,11 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Message from '../../../../models/messaging-message';
-import { pack } from '../../../../models/messaging-message';
+import { ID } from '../../../../misc/cafy-id';
 import read from '../../common/read-messaging-message';
 import define from '../../define';
 import { ApiError } from '../../error';
 import { getUser } from '../../common/getters';
+import { MessagingMessages } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
 
 export const meta = {
 	desc: {
@@ -22,7 +22,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -36,12 +35,10 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		markAsRead: {
@@ -73,43 +70,17 @@ export default define(meta, async (ps, user) => {
 		throw e;
 	});
 
-	const query = {
-		$or: [{
-			userId: user._id,
-			recipientId: recipient._id
-		}, {
-			userId: recipient._id,
-			recipientId: user._id
-		}]
-	} as any;
-
-	const sort = {
-		_id: -1
-	};
-
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		query._id = {
-			$lt: ps.untilId
-		};
-	}
+	const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId)
+		.andWhere(`(message.userId = :meId AND message.recipientId = :recipientId) OR (message.userId = :recipientId AND message.recipientId = :meId)`, { meId: user.id, recipientId: recipient.id });
 
-	const messages = await Message
-		.find(query, {
-			limit: ps.limit,
-			sort: sort
-		});
+	const messages = await query.getMany();
 
 	// Mark all as read
 	if (ps.markAsRead) {
-		read(user._id, recipient._id, messages);
+		read(user.id, recipient.id, messages.map(x => x.id));
 	}
 
-	return await Promise.all(messages.map(message => pack(message, user, {
+	return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, {
 		populateRecipient: false
 	})));
 });
diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts
index fc048e6edd8d93462ebd8e66d42c12568f22c9d1..2c7e5ad2d958f015bb1065c267827a06f616f536 100644
--- a/src/server/api/endpoints/messaging/messages/create.ts
+++ b/src/server/api/endpoints/messaging/messages/create.ts
@@ -1,17 +1,14 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import Message from '../../../../../models/messaging-message';
-import { isValidText } from '../../../../../models/messaging-message';
-import User from '../../../../../models/user';
-import Mute from '../../../../../models/mute';
-import DriveFile from '../../../../../models/drive-file';
-import { pack } from '../../../../../models/messaging-message';
+import { ID } from '../../../../../misc/cafy-id';
 import { publishMainStream } from '../../../../../services/stream';
 import { publishMessagingStream, publishMessagingIndexStream } from '../../../../../services/stream';
 import pushSw from '../../../../../services/push-notification';
 import define from '../../../define';
 import { ApiError } from '../../../error';
 import { getUser } from '../../../common/getters';
+import { MessagingMessages, DriveFiles, Mutings } from '../../../../../models';
+import { MessagingMessage } from '../../../../../models/entities/messaging-message';
+import { genId } from '../../../../../misc/gen-id';
 
 export const meta = {
 	desc: {
@@ -28,7 +25,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -36,12 +32,11 @@ export const meta = {
 		},
 
 		text: {
-			validator: $.optional.str.pipe(isValidText)
+			validator: $.optional.str.pipe(MessagingMessages.isValidText)
 		},
 
 		fileId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		}
 	},
 
@@ -78,7 +73,7 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	// Myself
-	if (ps.userId.equals(user._id)) {
+	if (ps.userId === user.id) {
 		throw new ApiError(meta.errors.recipientIsYourself);
 	}
 
@@ -90,12 +85,12 @@ export default define(meta, async (ps, user) => {
 
 	let file = null;
 	if (ps.fileId != null) {
-		file = await DriveFile.findOne({
-			_id: ps.fileId,
-			'metadata.userId': user._id
+		file = await DriveFiles.findOne({
+			id: ps.fileId,
+			userId: user.id
 		});
 
-		if (file === null) {
+		if (file == null) {
 			throw new ApiError(meta.errors.noSuchFile);
 		}
 	}
@@ -105,16 +100,17 @@ export default define(meta, async (ps, user) => {
 		throw new ApiError(meta.errors.contentRequired);
 	}
 
-	const message = await Message.insert({
+	const message = await MessagingMessages.save({
+		id: genId(),
 		createdAt: new Date(),
-		fileId: file ? file._id : undefined,
-		recipientId: recipient._id,
-		text: ps.text ? ps.text.trim() : undefined,
-		userId: user._id,
+		fileId: file ? file.id : null,
+		recipientId: recipient.id,
+		text: ps.text ? ps.text.trim() : null,
+		userId: user.id,
 		isRead: false
-	});
+	} as MessagingMessage);
 
-	const messageObj = await pack(message);
+	const messageObj = await MessagingMessages.pack(message);
 
 	// 自分のストリーム
 	publishMessagingStream(message.userId, message.recipientId, 'message', messageObj);
@@ -126,25 +122,17 @@ export default define(meta, async (ps, user) => {
 	publishMessagingIndexStream(message.recipientId, 'message', messageObj);
 	publishMainStream(message.recipientId, 'messagingMessage', messageObj);
 
-	// Update flag
-	User.update({ _id: recipient._id }, {
-		$set: {
-			hasUnreadMessagingMessage: true
-		}
-	});
-
 	// 2秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する
 	setTimeout(async () => {
-		const freshMessage = await Message.findOne({ _id: message._id }, { isRead: true });
+		const freshMessage = await MessagingMessages.findOne({ id: message.id });
 		if (freshMessage == null) return; // メッセージが削除されている場合もある
 		if (!freshMessage.isRead) {
 			//#region ただしミュートされているなら発行しない
-			const mute = await Mute.find({
-				muterId: recipient._id,
-				deletedAt: { $exists: false }
+			const mute = await Mutings.find({
+				muterId: recipient.id,
 			});
 			const mutedUserIds = mute.map(m => m.muteeId.toString());
-			if (mutedUserIds.indexOf(user._id.toString()) != -1) {
+			if (mutedUserIds.indexOf(user.id) != -1) {
 				return;
 			}
 			//#endregion
diff --git a/src/server/api/endpoints/messaging/messages/delete.ts b/src/server/api/endpoints/messaging/messages/delete.ts
index 0ca12846c118262e50efac3d5f7e4ab98af2ac12..9f55caba624be903671c0f3f2ec5068baf12d585 100644
--- a/src/server/api/endpoints/messaging/messages/delete.ts
+++ b/src/server/api/endpoints/messaging/messages/delete.ts
@@ -1,10 +1,10 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import Message from '../../../../../models/messaging-message';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
 import { publishMessagingStream } from '../../../../../services/stream';
 import * as ms from 'ms';
 import { ApiError } from '../../../error';
+import { MessagingMessages } from '../../../../../models';
 
 export const meta = {
 	stability: 'stable',
@@ -29,7 +29,6 @@ export const meta = {
 	params: {
 		messageId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のメッセージのID',
 				'en-US': 'Target message ID.'
@@ -47,19 +46,17 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const message = await Message.findOne({
-		_id: ps.messageId,
-		userId: user._id
+	const message = await MessagingMessages.findOne({
+		id: ps.messageId,
+		userId: user.id
 	});
 
-	if (message === null) {
+	if (message == null) {
 		throw new ApiError(meta.errors.noSuchMessage);
 	}
 
-	await Message.remove({ _id: message._id });
+	await MessagingMessages.delete(message.id);
 
-	publishMessagingStream(message.userId, message.recipientId, 'deleted', message._id);
-	publishMessagingStream(message.recipientId, message.userId, 'deleted', message._id);
-
-	return;
+	publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id);
+	publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id);
 });
diff --git a/src/server/api/endpoints/messaging/messages/read.ts b/src/server/api/endpoints/messaging/messages/read.ts
index aa8ecdc4ff3ce3440448c2e8c889264051250c38..24a28285bf228d03537bdc89e8445b8ba7847fdc 100644
--- a/src/server/api/endpoints/messaging/messages/read.ts
+++ b/src/server/api/endpoints/messaging/messages/read.ts
@@ -1,9 +1,9 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import Message from '../../../../../models/messaging-message';
+import { ID } from '../../../../../misc/cafy-id';
 import read from '../../../common/read-messaging-message';
 import define from '../../../define';
 import { ApiError } from '../../../error';
+import { MessagingMessages } from '../../../../../models';
 
 export const meta = {
 	desc: {
@@ -20,7 +20,6 @@ export const meta = {
 	params: {
 		messageId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '既読にするメッセージのID',
 				'en-US': 'The ID of a message that you want to mark as read'
@@ -38,16 +37,14 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const message = await Message.findOne({
-		_id: ps.messageId,
-		recipientId: user._id
+	const message = await MessagingMessages.findOne({
+		id: ps.messageId,
+		recipientId: user.id
 	});
 
 	if (message == null) {
 		throw new ApiError(meta.errors.noSuchMessage);
 	}
 
-	read(user._id, message.userId, message);
-
-	return;
+	read(user.id, message.userId, [message.id]);
 });
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index a297f47e0ef1df48d8f219d5fa08595bf168e0d7..785f21f22bac726f68228cf42fd7032da2af1508 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -1,10 +1,10 @@
 import $ from 'cafy';
 import * as os from 'os';
 import config from '../../../config';
-import Emoji from '../../../models/emoji';
 import define from '../define';
 import fetchMeta from '../../../misc/fetch-meta';
 import * as pkg from '../../../../package.json';
+import { Emojis } from '../../../models';
 
 export const meta = {
 	stability: 'stable',
@@ -81,14 +81,11 @@ export const meta = {
 export default define(meta, async (ps, me) => {
 	const instance = await fetchMeta();
 
-	const emojis = await Emoji.find({ host: null }, {
-		fields: {
-			_id: false
-		}
-	});
+	const emojis = await Emojis.find({ host: null });
 
 	const response: any = {
-		maintainer: instance.maintainer,
+		maintainerName: instance.maintainerName,
+		maintainerEmail: instance.maintainerEmail,
 
 		version: pkg.version,
 
@@ -145,17 +142,12 @@ export default define(meta, async (ps, me) => {
 			github: instance.enableGithubIntegration,
 			discord: instance.enableDiscordIntegration,
 			serviceWorker: instance.enableServiceWorker,
-			userRecommendation: {
-				external: instance.enableExternalUserRecommendation,
-				engine: instance.externalUserRecommendationEngine,
-				timeout: instance.externalUserRecommendationTimeout
-			}
 		};
 	}
 
 	if (me && (me.isAdmin || me.isModerator)) {
 		response.useStarForReactionFallback = instance.useStarForReactionFallback;
-		response.hidedTags = instance.hidedTags;
+		response.hiddenTags = instance.hiddenTags;
 		response.recaptchaSecretKey = instance.recaptchaSecretKey;
 		response.proxyAccount = instance.proxyAccount;
 		response.twitterConsumerKey = instance.twitterConsumerKey;
@@ -164,9 +156,6 @@ export default define(meta, async (ps, me) => {
 		response.githubClientSecret = instance.githubClientSecret;
 		response.discordClientId = instance.discordClientId;
 		response.discordClientSecret = instance.discordClientSecret;
-		response.enableExternalUserRecommendation = instance.enableExternalUserRecommendation;
-		response.externalUserRecommendationEngine = instance.externalUserRecommendationEngine;
-		response.externalUserRecommendationTimeout = instance.externalUserRecommendationTimeout;
 		response.summalyProxy = instance.summalyProxy;
 		response.email = instance.email;
 		response.smtpSecure = instance.smtpSecure;
diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts
index 7eaee90a05b28f8ea640e8d3e330af7a1b5ab579..d13c546fdc6f82e9845e9087befb91a94852c4f1 100644
--- a/src/server/api/endpoints/mute/create.ts
+++ b/src/server/api/endpoints/mute/create.ts
@@ -1,9 +1,11 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Mute from '../../../../models/mute';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
 import { ApiError } from '../../error';
 import { getUser } from '../../common/getters';
+import { genId } from '../../../../misc/gen-id';
+import { Mutings, NoteWatchings } from '../../../../models';
+import { Muting } from '../../../../models/entities/muting';
 
 export const meta = {
 	desc: {
@@ -15,12 +17,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account/write',
+	kind: 'write:mutes',
 
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -53,7 +54,7 @@ export default define(meta, async (ps, user) => {
 	const muter = user;
 
 	// 自分自身
-	if (user._id.equals(ps.userId)) {
+	if (user.id === ps.userId) {
 		throw new ApiError(meta.errors.muteeIsYourself);
 	}
 
@@ -64,21 +65,25 @@ export default define(meta, async (ps, user) => {
 	});
 
 	// Check if already muting
-	const exist = await Mute.findOne({
-		muterId: muter._id,
-		muteeId: mutee._id
+	const exist = await Mutings.findOne({
+		muterId: muter.id,
+		muteeId: mutee.id
 	});
 
-	if (exist !== null) {
+	if (exist != null) {
 		throw new ApiError(meta.errors.alreadyMuting);
 	}
 
 	// Create mute
-	await Mute.insert({
+	await Mutings.save({
+		id: genId(),
 		createdAt: new Date(),
-		muterId: muter._id,
-		muteeId: mutee._id,
-	});
+		muterId: muter.id,
+		muteeId: mutee.id,
+	} as Muting);
 
-	return;
+	NoteWatchings.delete({
+		userId: muter.id,
+		noteUserId: mutee.id
+	});
 });
diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts
index 1a03f6371b81a7a06504ac0013a15979d8864000..1aae15af910804d6053b76865a4815561a93e17c 100644
--- a/src/server/api/endpoints/mute/delete.ts
+++ b/src/server/api/endpoints/mute/delete.ts
@@ -1,9 +1,9 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Mute from '../../../../models/mute';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
 import { ApiError } from '../../error';
 import { getUser } from '../../common/getters';
+import { Mutings } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -15,12 +15,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account/write',
+	kind: 'write:mutes',
 
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -53,7 +52,7 @@ export default define(meta, async (ps, user) => {
 	const muter = user;
 
 	// Check if the mutee is yourself
-	if (user._id.equals(ps.userId)) {
+	if (user.id === ps.userId) {
 		throw new ApiError(meta.errors.muteeIsYourself);
 	}
 
@@ -64,19 +63,17 @@ export default define(meta, async (ps, user) => {
 	});
 
 	// Check not muting
-	const exist = await Mute.findOne({
-		muterId: muter._id,
-		muteeId: mutee._id
+	const exist = await Mutings.findOne({
+		muterId: muter.id,
+		muteeId: mutee.id
 	});
 
-	if (exist === null) {
+	if (exist == null) {
 		throw new ApiError(meta.errors.notMuting);
 	}
 
 	// Delete mute
-	await Mute.remove({
-		_id: exist._id
+	await Mutings.delete({
+		id: exist.id
 	});
-
-	return;
 });
diff --git a/src/server/api/endpoints/mute/list.ts b/src/server/api/endpoints/mute/list.ts
index 1b8f7594968aa51bd6ef0fe417bf72bf04398452..5f2d1844724cfe2c5918b50845f17294cd560efb 100644
--- a/src/server/api/endpoints/mute/list.ts
+++ b/src/server/api/endpoints/mute/list.ts
@@ -1,7 +1,8 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Mute, { packMany } from '../../../../models/mute';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Mutings } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -13,7 +14,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account/read',
+	kind: 'read:mutes',
 
 	params: {
 		limit: {
@@ -23,12 +24,10 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 	},
 
@@ -41,30 +40,12 @@ export const meta = {
 };
 
 export default define(meta, async (ps, me) => {
-	const query = {
-		muterId: me._id
-	} as any;
+	const query = makePaginationQuery(Mutings.createQueryBuilder('muting'), ps.sinceId, ps.untilId)
+		.andWhere(`muting.muterId = :meId`, { meId: me.id });
 
-	const sort = {
-		_id: -1
-	};
+	const mutings = await query
+		.take(ps.limit)
+		.getMany();
 
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		query._id = {
-			$lt: ps.untilId
-		};
-	}
-
-	const mutes = await Mute
-		.find(query, {
-			limit: ps.limit,
-			sort: sort
-		});
-
-	return await packMany(mutes, me);
+	return await Mutings.packMany(mutings, me);
 });
diff --git a/src/server/api/endpoints/my/apps.ts b/src/server/api/endpoints/my/apps.ts
index 1a936c918b14c01babc31efb94b90b794b0163fb..d205d1674c0057e46b6ed62a00342702b97feb6b 100644
--- a/src/server/api/endpoints/my/apps.ts
+++ b/src/server/api/endpoints/my/apps.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy';
-import App, { pack } from '../../../../models/app';
 import define from '../../define';
+import { Apps } from '../../../../models';
 
 export const meta = {
 	tags: ['account', 'app'],
@@ -27,19 +27,16 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	const query = {
-		userId: user._id
+		userId: user.id
 	};
 
-	const apps = await App
-		.find(query, {
-			limit: ps.limit,
-			skip: ps.offset,
-			sort: {
-				_id: -1
-			}
-		});
+	const apps = await Apps.find({
+		where: query,
+		take: ps.limit,
+		skip: ps.offset,
+	});
 
-	return await Promise.all(apps.map(app => pack(app, user, {
+	return await Promise.all(apps.map(app => Apps.pack(app, user, {
 		detail: true
 	})));
 });
diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts
index 835c515cfe67bf4cb9d017e93be7bb1960622d65..10f6e398450ec4847010c938f5e153439526e0ad 100644
--- a/src/server/api/endpoints/notes.ts
+++ b/src/server/api/endpoints/notes.ts
@@ -1,7 +1,8 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../misc/cafy-id';
-import Note, { packMany } from '../../../models/note';
+import { ID } from '../../../misc/cafy-id';
 import define from '../define';
+import { makePaginationQuery } from '../common/make-pagination-query';
+import { Notes } from '../../../models';
 
 export const meta = {
 	desc: {
@@ -39,14 +40,6 @@ export const meta = {
 			}
 		},
 
-		media: {
-			validator: $.optional.bool,
-			deprecated: true,
-			desc: {
-				'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
-			}
-		},
-
 		poll: {
 			validator: $.optional.bool,
 			desc: {
@@ -61,12 +54,10 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 	},
 
@@ -79,43 +70,29 @@ export const meta = {
 };
 
 export default define(meta, async (ps) => {
-	const sort = {
-		_id: -1
-	};
-	const query = {
-		deletedAt: null,
-		visibility: 'public',
-		localOnly: { $ne: true },
-	} as any;
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		query._id = {
-			$lt: ps.untilId
-		};
-	}
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+		.andWhere(`note.visibility = 'public'`)
+		.andWhere(`note.localOnly = FALSE`)
+		.leftJoinAndSelect('note.user', 'user');
 
 	if (ps.local) {
-		query['_user.host'] = null;
+		query.andWhere('note.userHost IS NULL');
 	}
 
 	if (ps.reply != undefined) {
-		query.replyId = ps.reply ? { $exists: true, $ne: null } : null;
+		query.andWhere(ps.reply ? 'note.replyId IS NOT NULL' : 'note.replyId IS NULL');
 	}
 
 	if (ps.renote != undefined) {
-		query.renoteId = ps.renote ? { $exists: true, $ne: null } : null;
+		query.andWhere(ps.renote ? 'note.renoteId IS NOT NULL' : 'note.renoteId IS NULL');
 	}
 
-	const withFiles = ps.withFiles != undefined ? ps.withFiles : ps.media;
-
-	if (withFiles) query.fileIds = { $exists: true, $ne: null };
+	if (ps.withFiles != undefined) {
+		query.andWhere(ps.withFiles ? `note.fileIds != '{}'` : `note.fileIds = '{}'`);
+	}
 
 	if (ps.poll != undefined) {
-		query.poll = ps.poll ? { $exists: true, $ne: null } : null;
+		query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE');
 	}
 
 	// TODO
@@ -123,10 +100,7 @@ export default define(meta, async (ps) => {
 	//	query.isBot = bot;
 	//}
 
-	const notes = await Note.find(query, {
-		limit: ps.limit,
-		sort: sort
-	});
+	const notes = await query.take(ps.limit).getMany();
 
-	return await packMany(notes);
+	return await Notes.packMany(notes);
 });
diff --git a/src/server/api/endpoints/notes/children.ts b/src/server/api/endpoints/notes/children.ts
index 3738459b7109e12ec95eefd47001000ebfc3bced..72f2c39d6ab25920387150673c6bde941a8e7970 100644
--- a/src/server/api/endpoints/notes/children.ts
+++ b/src/server/api/endpoints/notes/children.ts
@@ -1,9 +1,11 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note, { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
-import { getFriends } from '../../common/get-friends';
-import { getHideUserIds } from '../../common/get-hide-users';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { Brackets } from 'typeorm';
+import { Notes } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -18,7 +20,6 @@ export const meta = {
 	params: {
 		noteId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID'
@@ -32,12 +33,10 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 	},
 
@@ -50,83 +49,24 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const [followings, hideUserIds] = await Promise.all([
-		// フォローを取得
-		// Fetch following
-		user ? getFriends(user._id) : [],
-
-		// 隠すユーザーを取得
-		getHideUserIds(user)
-	]);
-
-	const visibleQuery = user == null ? [{
-		visibility: { $in: [ 'public', 'home' ] }
-	}] : [{
-		visibility: { $in: [ 'public', 'home' ] }
-	}, {
-		// myself (for followers/specified/private)
-		userId: user._id
-	}, {
-		// to me (for specified)
-		visibleUserIds: { $in: [ user._id ] }
-	}, {
-		visibility: 'followers',
-		$or: [{
-			// フォロワーの投稿
-			userId: { $in: followings.map(f => f.id) },
-		}, {
-			// 自分の投稿へのリプライ
-			'_reply.userId': user._id,
-		}, {
-			// 自分へのメンションが含まれている
-			mentions: { $in: [ user._id ] }
-		}]
-	}];
-
-	const q = {
-		$and: [{
-			$or: [{
-				replyId: ps.noteId,
-			}, {
-				renoteId: ps.noteId,
-				$or: [{
-					text: { $ne: null }
-				}, {
-					fileIds: { $ne: [] }
-				}, {
-					poll: { $ne: null }
-				}]
-			}]
-		}, {
-			$or: visibleQuery
-		}]
-	} as any;
-
-	if (hideUserIds && hideUserIds.length > 0) {
-		q['userId'] = {
-			$nin: hideUserIds
-		};
-	}
-
-	const sort = {
-		_id: -1
-	};
-
-	if (ps.sinceId) {
-		sort._id = 1;
-		q._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		q._id = {
-			$lt: ps.untilId
-		};
-	}
-
-	const notes = await Note.find(q, {
-		limit: ps.limit,
-		sort: sort
-	});
-
-	return await packMany(notes, user);
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+		.andWhere(new Brackets(qb => { qb
+			.where(`note.replyId = :noteId`, { noteId: ps.noteId })
+			.orWhere(new Brackets(qb => { qb
+				.where(`note.renoteId = :noteId`, { noteId: ps.noteId })
+				.andWhere(new Brackets(qb => { qb
+					.where(`note.text IS NOT NULL`)
+					.orWhere(`note.fileIds != '{}'`)
+					.orWhere(`note.hasPoll = TRUE`);
+				}));
+			}));
+		}))
+		.leftJoinAndSelect('note.user', 'user');
+
+	if (user) generateVisibilityQuery(query, user);
+	if (user) generateMuteQuery(query, user);
+
+	const notes = await query.take(ps.limit).getMany();
+
+	return await Notes.packMany(notes, user);
 });
diff --git a/src/server/api/endpoints/notes/conversation.ts b/src/server/api/endpoints/notes/conversation.ts
index 702d8dc430a558db19cc289f48b7ffd1b02d5f6c..6defd790423506f5a868392f1218ef47b69634aa 100644
--- a/src/server/api/endpoints/notes/conversation.ts
+++ b/src/server/api/endpoints/notes/conversation.ts
@@ -1,9 +1,10 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note, { packMany, INote } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
 import { ApiError } from '../../error';
 import { getNote } from '../../common/getters';
+import { Note } from '../../../../models/entities/note';
+import { Notes } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -18,7 +19,6 @@ export const meta = {
 	params: {
 		noteId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID'
@@ -58,12 +58,12 @@ export default define(meta, async (ps, user) => {
 		throw e;
 	});
 
-	const conversation: INote[] = [];
+	const conversation: Note[] = [];
 	let i = 0;
 
 	async function get(id: any) {
 		i++;
-		const p = await Note.findOne({ _id: id });
+		const p = await Notes.findOne(id);
 
 		if (i > ps.offset) {
 			conversation.push(p);
@@ -82,5 +82,5 @@ export default define(meta, async (ps, user) => {
 		await get(note.replyId);
 	}
 
-	return await packMany(conversation, user);
+	return await Notes.packMany(conversation, user);
 });
diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts
index 8cc5e4b81502cdaa64fb3df8300ada3a3932b693..138f05fb3b9cf472914e0b9342b3503bba847a5f 100644
--- a/src/server/api/endpoints/notes/create.ts
+++ b/src/server/api/endpoints/notes/create.ts
@@ -1,14 +1,15 @@
 import $ from 'cafy';
-import ID, { transform, transformMany } from '../../../../misc/cafy-id';
 import * as ms from 'ms';
 import { length } from 'stringz';
-import Note, { INote, isValidCw, pack } from '../../../../models/note';
-import User, { IUser } from '../../../../models/user';
-import DriveFile, { IDriveFile } from '../../../../models/drive-file';
 import create from '../../../../services/note/create';
 import define from '../../define';
 import fetchMeta from '../../../../misc/fetch-meta';
 import { ApiError } from '../../error';
+import { ID } from '../../../../misc/cafy-id';
+import { User } from '../../../../models/entities/user';
+import { Users, DriveFiles, Notes } from '../../../../models';
+import { DriveFile } from '../../../../models/entities/drive-file';
+import { Note } from '../../../../models/entities/note';
 
 let maxNoteTextLength = 1000;
 
@@ -34,7 +35,7 @@ export const meta = {
 		max: 300
 	},
 
-	kind: 'note-write',
+	kind: 'write:notes',
 
 	params: {
 		visibility: {
@@ -47,7 +48,6 @@ export const meta = {
 
 		visibleUserIds: {
 			validator: $.optional.arr($.type(ID)).unique().min(0),
-			transform: transformMany,
 			desc: {
 				'ja-JP': '(投稿の公開範囲が specified の場合)投稿を閲覧できるユーザー'
 			}
@@ -64,7 +64,7 @@ export const meta = {
 		},
 
 		cw: {
-			validator: $.optional.nullable.str.pipe(isValidCw),
+			validator: $.optional.nullable.str.pipe(Notes.validateCw),
 			desc: {
 				'ja-JP': 'コンテンツの警告。このパラメータを指定すると設定したテキストで投稿のコンテンツを隠す事が出来ます。'
 			}
@@ -129,7 +129,6 @@ export const meta = {
 
 		fileIds: {
 			validator: $.optional.arr($.type(ID)).unique().range(1, 4),
-			transform: transformMany,
 			desc: {
 				'ja-JP': '添付するファイル'
 			}
@@ -137,7 +136,6 @@ export const meta = {
 
 		mediaIds: {
 			validator: $.optional.arr($.type(ID)).unique().range(1, 4),
-			transform: transformMany,
 			deprecated: true,
 			desc: {
 				'ja-JP': '添付するファイル (このパラメータは廃止予定です。代わりに fileIds を使ってください。)'
@@ -146,7 +144,6 @@ export const meta = {
 
 		replyId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '返信対象'
 			}
@@ -154,7 +151,6 @@ export const meta = {
 
 		renoteId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': 'Renote対象'
 			}
@@ -227,32 +223,28 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user, app) => {
-	let visibleUsers: IUser[] = [];
+	let visibleUsers: User[] = [];
 	if (ps.visibleUserIds) {
-		visibleUsers = await Promise.all(ps.visibleUserIds.map(id => User.findOne({
-			_id: id
-		})));
+		visibleUsers = await Promise.all(ps.visibleUserIds.map(id => Users.findOne(id)));
 	}
 
-	let files: IDriveFile[] = [];
+	let files: DriveFile[] = [];
 	const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null;
 	if (fileIds != null) {
 		files = await Promise.all(fileIds.map(fileId => {
-			return DriveFile.findOne({
-				_id: fileId,
-				'metadata.userId': user._id
+			return DriveFiles.findOne({
+				id: fileId,
+				userId: user.id
 			});
 		}));
 
 		files = files.filter(file => file != null);
 	}
 
-	let renote: INote = null;
+	let renote: Note = null;
 	if (ps.renoteId != null) {
 		// Fetch renote to note
-		renote = await Note.findOne({
-			_id: ps.renoteId
-		});
+		renote = await Notes.findOne(ps.renoteId);
 
 		if (renote == null) {
 			throw new ApiError(meta.errors.noSuchRenoteTarget);
@@ -261,14 +253,12 @@ export default define(meta, async (ps, user, app) => {
 		}
 	}
 
-	let reply: INote = null;
+	let reply: Note = null;
 	if (ps.replyId != null) {
 		// Fetch reply
-		reply = await Note.findOne({
-			_id: ps.replyId
-		});
+		reply = await Notes.findOne(ps.replyId);
 
-		if (reply === null) {
+		if (reply == null) {
 			throw new ApiError(meta.errors.noSuchReplyTarget);
 		}
 
@@ -279,12 +269,6 @@ export default define(meta, async (ps, user, app) => {
 	}
 
 	if (ps.poll) {
-		(ps.poll as any).choices = (ps.poll as any).choices.map((choice: string, i: number) => ({
-			id: i, // IDを付与
-			text: choice.trim(),
-			votes: 0
-		}));
-
 		if (typeof ps.poll.expiresAt === 'number') {
 			if (ps.poll.expiresAt < Date.now())
 				throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
@@ -298,11 +282,6 @@ export default define(meta, async (ps, user, app) => {
 		throw new ApiError(meta.errors.contentRequired);
 	}
 
-	// 後方互換性のため
-	if (ps.visibility == 'private') {
-		ps.visibility = 'specified';
-	}
-
 	// 投稿を作成
 	const note = await create(user, {
 		createdAt: new Date(),
@@ -311,7 +290,7 @@ export default define(meta, async (ps, user, app) => {
 			choices: ps.poll.choices,
 			multiple: ps.poll.multiple || false,
 			expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null
-		} : undefined,
+		} : null,
 		text: ps.text,
 		reply,
 		renote,
@@ -321,13 +300,13 @@ export default define(meta, async (ps, user, app) => {
 		localOnly: ps.localOnly,
 		visibility: ps.visibility,
 		visibleUsers,
-		apMentions: ps.noExtractMentions ? [] : undefined,
-		apHashtags: ps.noExtractHashtags ? [] : undefined,
-		apEmojis: ps.noExtractEmojis ? [] : undefined,
+		apMentions: ps.noExtractMentions ? [] : null,
+		apHashtags: ps.noExtractHashtags ? [] : null,
+		apEmojis: ps.noExtractEmojis ? [] : null,
 		geo: ps.geo
 	});
 
 	return {
-		createdNote: await pack(note, user)
+		createdNote: await Notes.pack(note, user)
 	};
 });
diff --git a/src/server/api/endpoints/notes/delete.ts b/src/server/api/endpoints/notes/delete.ts
index 399f9288d640f555e831edfc4e94cd88018783ac..dbaf91bca37555dc77a065e9ee3357df6b27edf3 100644
--- a/src/server/api/endpoints/notes/delete.ts
+++ b/src/server/api/endpoints/notes/delete.ts
@@ -1,11 +1,11 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
 import deleteNote from '../../../../services/note/delete';
-import User from '../../../../models/user';
 import define from '../../define';
 import * as ms from 'ms';
 import { getNote } from '../../common/getters';
 import { ApiError } from '../../error';
+import { Users } from '../../../../models';
 
 export const meta = {
 	stability: 'stable',
@@ -19,7 +19,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'note-write',
+	kind: 'write:notes',
 
 	limit: {
 		duration: ms('1hour'),
@@ -30,7 +30,6 @@ export const meta = {
 	params: {
 		noteId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID.'
@@ -59,9 +58,10 @@ export default define(meta, async (ps, user) => {
 		throw e;
 	});
 
-	if (!user.isAdmin && !user.isModerator && !note.userId.equals(user._id)) {
+	if (!user.isAdmin && !user.isModerator && (note.userId !== user.id)) {
 		throw new ApiError(meta.errors.accessDenied);
 	}
 
-	await deleteNote(await User.findOne({ _id: note.userId }), note);
+	// この操作を行うのが投稿者とは限らない(例えばモデレーター)ため
+	await deleteNote(await Users.findOne(note.userId), note);
 });
diff --git a/src/server/api/endpoints/notes/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts
index 9cde1a7dcf7cd73cc553fe0da33ba346efea48d8..7e046377588930bb26bdf2072147ff78cec1ed6e 100644
--- a/src/server/api/endpoints/notes/favorites/create.ts
+++ b/src/server/api/endpoints/notes/favorites/create.ts
@@ -1,9 +1,10 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import Favorite from '../../../../../models/favorite';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
 import { ApiError } from '../../../error';
 import { getNote } from '../../../common/getters';
+import { NoteFavorites } from '../../../../../models';
+import { genId } from '../../../../../misc/gen-id';
 
 export const meta = {
 	stability: 'stable',
@@ -22,7 +23,6 @@ export const meta = {
 	params: {
 		noteId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID.'
@@ -53,21 +53,20 @@ export default define(meta, async (ps, user) => {
 	});
 
 	// if already favorited
-	const exist = await Favorite.findOne({
-		noteId: note._id,
-		userId: user._id
+	const exist = await NoteFavorites.findOne({
+		noteId: note.id,
+		userId: user.id
 	});
 
-	if (exist !== null) {
+	if (exist != null) {
 		throw new ApiError(meta.errors.alreadyFavorited);
 	}
 
 	// Create favorite
-	await Favorite.insert({
+	await NoteFavorites.save({
+		id: genId(),
 		createdAt: new Date(),
-		noteId: note._id,
-		userId: user._id
+		noteId: note.id,
+		userId: user.id
 	});
-
-	return;
 });
diff --git a/src/server/api/endpoints/notes/favorites/delete.ts b/src/server/api/endpoints/notes/favorites/delete.ts
index e2c787f3b5aa0409a6e3ca91fc65496aa2e456f4..a889c84d4d8fd8f89e847acb564c273ed72d7442 100644
--- a/src/server/api/endpoints/notes/favorites/delete.ts
+++ b/src/server/api/endpoints/notes/favorites/delete.ts
@@ -1,9 +1,9 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import Favorite from '../../../../../models/favorite';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
 import { ApiError } from '../../../error';
 import { getNote } from '../../../common/getters';
+import { NoteFavorites } from '../../../../../models';
 
 export const meta = {
 	stability: 'stable',
@@ -22,7 +22,6 @@ export const meta = {
 	params: {
 		noteId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID.'
@@ -53,19 +52,15 @@ export default define(meta, async (ps, user) => {
 	});
 
 	// if already favorited
-	const exist = await Favorite.findOne({
-		noteId: note._id,
-		userId: user._id
+	const exist = await NoteFavorites.findOne({
+		noteId: note.id,
+		userId: user.id
 	});
 
-	if (exist === null) {
+	if (exist == null) {
 		throw new ApiError(meta.errors.notFavorited);
 	}
 
 	// Delete favorite
-	await Favorite.remove({
-		_id: exist._id
-	});
-
-	return;
+	await NoteFavorites.delete(exist.id);
 });
diff --git a/src/server/api/endpoints/notes/featured.ts b/src/server/api/endpoints/notes/featured.ts
index 3648b307d73d0c66c5f431b27ee24d8db8982faa..c44a5275bbe9d629009299f3b262615492474aa9 100644
--- a/src/server/api/endpoints/notes/featured.ts
+++ b/src/server/api/endpoints/notes/featured.ts
@@ -1,8 +1,7 @@
 import $ from 'cafy';
-import Note from '../../../../models/note';
-import { packMany } from '../../../../models/note';
 import define from '../../define';
-import { getHideUserIds } from '../../common/get-hide-users';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { Notes } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -35,25 +34,14 @@ export const meta = {
 export default define(meta, async (ps, user) => {
 	const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで
 
-	const hideUserIds = await getHideUserIds(user);
+	const query = Notes.createQueryBuilder('note')
+		.andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) })
+		.andWhere(`note.visibility = 'public'`)
+		.leftJoinAndSelect('note.user', 'user');
 
-	const notes = await Note.find({
-		createdAt: {
-			$gt: new Date(Date.now() - day)
-		},
-		deletedAt: null,
-		visibility: 'public',
-		'_user.host': null,
-		...(hideUserIds && hideUserIds.length > 0 ? { userId: { $nin: hideUserIds } } : {})
-	}, {
-		limit: ps.limit,
-		sort: {
-			score: -1
-		},
-		hint: {
-			score: -1
-		}
-	});
+	if (user) generateMuteQuery(query, user);
+
+	const notes = await query.orderBy('note.score', 'DESC').take(ps.limit).getMany();
 
-	return await packMany(notes, user);
+	return await Notes.packMany(notes, user);
 });
diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts
index 0eb761cdb6930aa513a1bce8c919519af2604853..7bf62f366bfe29ab37746a22b60d8d03ffa41574 100644
--- a/src/server/api/endpoints/notes/global-timeline.ts
+++ b/src/server/api/endpoints/notes/global-timeline.ts
@@ -1,11 +1,12 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note from '../../../../models/note';
-import { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
 import fetchMeta from '../../../../misc/fetch-meta';
-import { getHideUserIds } from '../../common/get-hide-users';
 import { ApiError } from '../../error';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Notes } from '../../../../models';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { activeUsersChart } from '../../../../services/chart';
 
 export const meta = {
 	desc: {
@@ -22,14 +23,6 @@ export const meta = {
 			}
 		},
 
-		mediaOnly: {
-			validator: $.optional.bool,
-			deprecated: true,
-			desc: {
-				'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
-			}
-		},
-
 		limit: {
 			validator: $.optional.num.range(1, 100),
 			default: 10
@@ -37,12 +30,10 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		sinceDate: {
@@ -71,6 +62,7 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
+	// TODO どっかにキャッシュ
 	const m = await fetchMeta();
 	if (m.disableGlobalTimeline) {
 		if (user == null || (!user.isAdmin && !user.isModerator)) {
@@ -78,68 +70,25 @@ export default define(meta, async (ps, user) => {
 		}
 	}
 
-	// 隠すユーザーを取得
-	const hideUserIds = await getHideUserIds(user);
-
 	//#region Construct query
-	const sort = {
-		_id: -1
-	};
-
-	const query = {
-		deletedAt: null,
-
-		// public only
-		visibility: 'public',
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'),
+			ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+		.andWhere('note.visibility = \'public\'')
+		.andWhere('note.replyId IS NULL')
+		.leftJoinAndSelect('note.user', 'user');
 
-		replyId: null
-	} as any;
+	if (user) generateMuteQuery(query, user);
 
-	if (hideUserIds && hideUserIds.length > 0) {
-		query.userId = {
-			$nin: hideUserIds
-		};
-
-		query['_reply.userId'] = {
-			$nin: hideUserIds
-		};
-
-		query['_renote.userId'] = {
-			$nin: hideUserIds
-		};
+	if (ps.withFiles) {
+		query.andWhere('note.fileIds != \'{}\'');
 	}
+	//#endregion
 
-	const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
+	const timeline = await query.take(ps.limit).getMany();
 
-	if (withFiles) {
-		query.fileIds = { $exists: true, $ne: [] };
+	if (user) {
+		activeUsersChart.update(user);
 	}
 
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		query._id = {
-			$lt: ps.untilId
-		};
-	} else if (ps.sinceDate) {
-		sort._id = 1;
-		query.createdAt = {
-			$gt: new Date(ps.sinceDate)
-		};
-	} else if (ps.untilDate) {
-		query.createdAt = {
-			$lt: new Date(ps.untilDate)
-		};
-	}
-	//#endregion
-
-	const timeline = await Note.find(query, {
-		limit: ps.limit,
-		sort: sort
-	});
-
-	return await packMany(timeline, user);
+	return await Notes.packMany(timeline, user);
 });
diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts
index 57ef4c3e15fcc45986dbe9a653b6ce3f43a41192..cd07341342b3f85afd03479334a49f17b2ec2388 100644
--- a/src/server/api/endpoints/notes/local-timeline.ts
+++ b/src/server/api/endpoints/notes/local-timeline.ts
@@ -1,12 +1,14 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note from '../../../../models/note';
-import { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
 import fetchMeta from '../../../../misc/fetch-meta';
-import activeUsersChart from '../../../../services/chart/active-users';
-import { getHideUserIds } from '../../common/get-hide-users';
 import { ApiError } from '../../error';
+import { Notes } from '../../../../models';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { activeUsersChart } from '../../../../services/chart';
+import { Brackets } from 'typeorm';
 
 export const meta = {
 	desc: {
@@ -23,14 +25,6 @@ export const meta = {
 			}
 		},
 
-		mediaOnly: {
-			validator: $.optional.bool,
-			deprecated: true,
-			desc: {
-				'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
-			}
-		},
-
 		fileType: {
 			validator: $.optional.arr($.str),
 			desc: {
@@ -53,12 +47,10 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		sinceDate: {
@@ -87,6 +79,7 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
+	// TODO どっかにキャッシュ
 	const m = await fetchMeta();
 	if (m.disableLocalTimeline) {
 		if (user == null || (!user.isAdmin && !user.isModerator)) {
@@ -94,90 +87,44 @@ export default define(meta, async (ps, user) => {
 		}
 	}
 
-	// 隠すユーザーを取得
-	const hideUserIds = await getHideUserIds(user);
-
 	//#region Construct query
-	const sort = {
-		_id: -1
-	};
-
-	const query = {
-		deletedAt: null,
-
-		// public only
-		visibility: 'public',
-
-		// リプライでない
-		//replyId: null,
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'),
+			ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+		.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
+		.leftJoinAndSelect('note.user', 'user');
 
-		// local
-		'_user.host': null
-	} as any;
+	if (user) generateVisibilityQuery(query, user);
+	if (user) generateMuteQuery(query, user);
 
-	if (hideUserIds && hideUserIds.length > 0) {
-		query.userId = {
-			$nin: hideUserIds
-		};
-
-		query['_reply.userId'] = {
-			$nin: hideUserIds
-		};
-
-		query['_renote.userId'] = {
-			$nin: hideUserIds
-		};
-	}
-
-	const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
-
-	if (withFiles) {
-		query.fileIds = { $exists: true, $ne: [] };
+	if (ps.withFiles) {
+		query.andWhere('note.fileIds != \'{}\'');
 	}
 
 	if (ps.fileType) {
-		query.fileIds = { $exists: true, $ne: [] };
-
-		query['_files.contentType'] = {
-			$in: ps.fileType
-		};
+		query.andWhere('note.fileIds != \'{}\'');
+		query.andWhere(new Brackets(qb => {
+			for (const type of ps.fileType) {
+				const i = ps.fileType.indexOf(type);
+				qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
+			}
+		}));
 
 		if (ps.excludeNsfw) {
-			query['_files.metadata.isSensitive'] = {
+			// v11 TODO
+			/*
+			query['_files.isSensitive'] = {
 				$ne: true
 			};
+			*/
 		}
 	}
-
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		query._id = {
-			$lt: ps.untilId
-		};
-	} else if (ps.sinceDate) {
-		sort._id = 1;
-		query.createdAt = {
-			$gt: new Date(ps.sinceDate)
-		};
-	} else if (ps.untilDate) {
-		query.createdAt = {
-			$lt: new Date(ps.untilDate)
-		};
-	}
 	//#endregion
 
-	const timeline = await Note.find(query, {
-		limit: ps.limit,
-		sort: sort
-	});
+	const timeline = await query.take(ps.limit).getMany();
 
 	if (user) {
 		activeUsersChart.update(user);
 	}
 
-	return await packMany(timeline, user);
+	return await Notes.packMany(timeline, user);
 });
diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts
index 91333174edeb4edc2000a951bcd49c119230b771..0bbe7d3327016bc61b68d3ebe82f20e1d73f1b2c 100644
--- a/src/server/api/endpoints/notes/mentions.ts
+++ b/src/server/api/endpoints/notes/mentions.ts
@@ -1,11 +1,12 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note from '../../../../models/note';
-import { getFriendIds, getFriends } from '../../common/get-friends';
-import { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
 import read from '../../../../services/note/read';
-import { getHideUserIds } from '../../common/get-hide-users';
+import { Notes, Followings } from '../../../../models';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Brackets } from 'typeorm';
 
 export const meta = {
 	desc: {
@@ -30,12 +31,10 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		visibility: {
@@ -52,97 +51,34 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	// フォローを取得
-	const followings = await getFriends(user._id);
-
-	const visibleQuery = [{
-		visibility: { $in: [ 'public', 'home' ] }
-	}, {
-		// myself (for followers/specified/private)
-		userId: user._id
-	}, {
-		// to me (for specified)
-		visibleUserIds: { $in: [ user._id ] }
-	}, {
-		visibility: 'followers',
-		$or: [{
-			// フォロワーの投稿
-			userId: { $in: followings.map(f => f.id) },
-		}, {
-			// 自分の投稿へのリプライ
-			'_reply.userId': user._id,
-		}, {
-			// 自分へのメンションが含まれている
-			mentions: { $in: [ user._id ] }
-		}]
-	}];
-
-	const query = {
-		$and: [{
-			deletedAt: null,
-		}, {
-			$or: visibleQuery,
-		}],
-
-		$or: [{
-			mentions: user._id
-		}, {
-			visibleUserIds: user._id
-		}]
-	} as any;
-
-	// 隠すユーザーを取得
-	const hideUserIds = await getHideUserIds(user);
-
-	if (hideUserIds && hideUserIds.length > 0) {
-		query.userId = {
-			$nin: hideUserIds
-		};
-
-		query['_reply.userId'] = {
-			$nin: hideUserIds
-		};
-
-		query['_renote.userId'] = {
-			$nin: hideUserIds
-		};
-	}
+	const followingQuery = Followings.createQueryBuilder('following')
+		.select('following.followeeId')
+		.where('following.followerId = :followerId', { followerId: user.id });
+
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+		.andWhere(new Brackets(qb => { qb
+			.where(`:meId = ANY(note.mentions)`, { meId: user.id })
+			.orWhere(`:meId = ANY(note.visibleUserIds)`, { meId: user.id });
+		}))
+		.leftJoinAndSelect('note.user', 'user');
 
-	const sort = {
-		_id: -1
-	};
+	generateVisibilityQuery(query, user);
+	generateMuteQuery(query, user);
 
 	if (ps.visibility) {
-		query.visibility = ps.visibility;
+		query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });
 	}
 
 	if (ps.following) {
-		const followingIds = await getFriendIds(user._id);
-
-		query.userId = {
-			$in: followingIds
-		};
-	}
-
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		query._id = {
-			$lt: ps.untilId
-		};
+		query.andWhere(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id });
+		query.setParameters(followingQuery.getParameters());
 	}
 
-	const mentions = await Note.find(query, {
-		limit: ps.limit,
-		sort: sort
-	});
+	const mentions = await query.take(ps.limit).getMany();
 
 	for (const note of mentions) {
-		read(user._id, note._id);
+		read(user.id, note.id);
 	}
 
-	return await packMany(mentions, user);
+	return await Notes.packMany(mentions, user);
 });
diff --git a/src/server/api/endpoints/notes/polls/recommendation.ts b/src/server/api/endpoints/notes/polls/recommendation.ts
index 9adabdf0e917875a84e2a5470591f6c4ead9a8bf..ff838d4f4fddff0718e1f9fb849daed977be1a62 100644
--- a/src/server/api/endpoints/notes/polls/recommendation.ts
+++ b/src/server/api/endpoints/notes/polls/recommendation.ts
@@ -1,8 +1,7 @@
 import $ from 'cafy';
-import Vote from '../../../../../models/poll-vote';
-import Note, { pack } from '../../../../../models/note';
 import define from '../../../define';
-import { getHideUserIds } from '../../../common/get-hide-users';
+import { Polls, Mutings, Notes, PollVotes } from '../../../../../models';
+import { Brackets, In } from 'typeorm';
 
 export const meta = {
 	desc: {
@@ -28,51 +27,46 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	// Get votes
-	const votes = await Vote.find({
-		userId: user._id
-	}, {
-		fields: {
-			_id: false,
-			noteId: true
-		}
-	});
+	const query = Polls.createQueryBuilder('poll')
+		.where('poll.userHost IS NULL')
+		.andWhere(`poll.userId != :meId`, { meId: user.id })
+		.andWhere(`poll.noteVisibility = 'public'`)
+		.andWhere(new Brackets(qb => { qb
+			.where('poll.expiresAt IS NULL')
+			.orWhere('poll.expiresAt > :now', { now: new Date() });
+		}));
 
-	const nin = votes && votes.length != 0 ? votes.map(v => v.noteId) : [];
+	//#region exclude arleady voted polls
+	const votedQuery = PollVotes.createQueryBuilder('vote')
+		.select('vote.noteId')
+		.where('vote.userId = :meId', { meId: user.id });
 
-	// 隠すユーザーを取得
-	const hideUserIds = await getHideUserIds(user);
+	query
+		.andWhere(`poll.noteId NOT IN (${ votedQuery.getQuery() })`);
 
-	const notes = await Note.find({
-		'_user.host': null,
-		_id: {
-			$nin: nin
-		},
-		userId: {
-			$ne: user._id,
-			$nin: hideUserIds
-		},
-		visibility: 'public',
-		poll: {
-			$exists: true,
-			$ne: null
-		},
-		$or: [{
-			'poll.expiresAt': null
-		}, {
-			'poll.expiresAt': {
-				$gt: new Date()
-			}
-		}],
-	}, {
-		limit: ps.limit,
-		skip: ps.offset,
-		sort: {
-			_id: -1
-		}
+	query.setParameters(votedQuery.getParameters());
+	//#endregion
+
+	//#region mute
+	const mutingQuery = Mutings.createQueryBuilder('muting')
+		.select('muting.muteeId')
+		.where('muting.muterId = :muterId', { muterId: user.id });
+
+	query
+		.andWhere(`poll.userId NOT IN (${ mutingQuery.getQuery() })`);
+
+	query.setParameters(mutingQuery.getParameters());
+	//#endregion
+
+	const polls = await query.take(ps.limit).skip(ps.offset).getMany();
+
+	if (polls.length === 0) return [];
+
+	const notes = await Notes.find({
+		id: In(polls.map(poll => poll.noteId))
 	});
 
-	return await Promise.all(notes.map(note => pack(note, user, {
+	return await Notes.packMany(notes, user, {
 		detail: true
-	})));
+	});
 });
diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts
index ed20e0221f420db4e39c83876e73c22a8f2385a1..7d0ed6e4f9cd6652ab690dd55880ae00e2c7567a 100644
--- a/src/server/api/endpoints/notes/polls/vote.ts
+++ b/src/server/api/endpoints/notes/polls/vote.ts
@@ -1,19 +1,19 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import Vote from '../../../../../models/poll-vote';
-import Note from '../../../../../models/note';
-import Watching from '../../../../../models/note-watching';
+import { ID } from '../../../../../misc/cafy-id';
 import watch from '../../../../../services/note/watch';
 import { publishNoteStream } from '../../../../../services/stream';
-import notify from '../../../../../services/create-notification';
+import { createNotification } from '../../../../../services/create-notification';
 import define from '../../../define';
-import User, { IRemoteUser } from '../../../../../models/user';
 import { ApiError } from '../../../error';
 import { getNote } from '../../../common/getters';
 import { deliver } from '../../../../../queue';
 import { renderActivity } from '../../../../../remote/activitypub/renderer';
 import renderVote from '../../../../../remote/activitypub/renderer/vote';
 import { deliverQuestionUpdate } from '../../../../../services/note/polls/update';
+import { PollVotes, NoteWatchings, Users, Polls } from '../../../../../models';
+import { Not } from 'typeorm';
+import { IRemoteUser } from '../../../../../models/entities/user';
+import { genId } from '../../../../../misc/gen-id';
 
 export const meta = {
 	desc: {
@@ -30,7 +30,6 @@ export const meta = {
 	params: {
 		noteId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID'
@@ -84,26 +83,28 @@ export default define(meta, async (ps, user) => {
 		throw e;
 	});
 
-	if (note.poll == null) {
+	if (!note.hasPoll) {
 		throw new ApiError(meta.errors.noPoll);
 	}
 
-	if (note.poll.expiresAt && note.poll.expiresAt < createdAt) {
+	const poll = await Polls.findOne({ noteId: note.id });
+
+	if (poll.expiresAt && poll.expiresAt < createdAt) {
 		throw new ApiError(meta.errors.alreadyExpired);
 	}
 
-	if (!note.poll.choices.some(x => x.id == ps.choice)) {
+	if (poll.choices[ps.choice] == null) {
 		throw new ApiError(meta.errors.invalidChoice);
 	}
 
 	// if already voted
-	const exist = await Vote.find({
-		noteId: note._id,
-		userId: user._id
+	const exist = await PollVotes.find({
+		noteId: note.id,
+		userId: user.id
 	});
 
 	if (exist.length) {
-		if (note.poll.multiple) {
+		if (poll.multiple) {
 			if (exist.some(x => x.choice == ps.choice))
 				throw new ApiError(meta.errors.alreadyVoted);
 		} else {
@@ -112,69 +113,54 @@ export default define(meta, async (ps, user) => {
 	}
 
 	// Create vote
-	const vote = await Vote.insert({
+	const vote = await PollVotes.save({
+		id: genId(),
 		createdAt,
-		noteId: note._id,
-		userId: user._id,
+		noteId: note.id,
+		userId: user.id,
 		choice: ps.choice
 	});
 
-	const inc: any = {};
-	inc[`poll.choices.${note.poll.choices.findIndex(c => c.id == ps.choice)}.votes`] = 1;
-
 	// Increment votes count
-	await Note.update({ _id: note._id }, {
-		$inc: inc
-	});
+	const index = ps.choice + 1; // In SQL, array index is 1 based
+	await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE id = '${poll.id}'`);
 
-	publishNoteStream(note._id, 'pollVoted', {
+	publishNoteStream(note.id, 'pollVoted', {
 		choice: ps.choice,
-		userId: user._id.toHexString()
+		userId: user.id
 	});
 
 	// Notify
-	notify(note.userId, user._id, 'poll_vote', {
-		noteId: note._id,
+	createNotification(note.userId, user.id, 'pollVote', {
+		noteId: note.id,
 		choice: ps.choice
 	});
 
 	// Fetch watchers
-	Watching
-		.find({
-			noteId: note._id,
-			userId: { $ne: user._id },
-			// 削除されたドキュメントは除く
-			deletedAt: { $exists: false }
-		}, {
-			fields: {
-				userId: true
-			}
-		})
-		.then(watchers => {
-			for (const watcher of watchers) {
-				notify(watcher.userId, user._id, 'poll_vote', {
-					noteId: note._id,
-					choice: ps.choice
-				});
-			}
-		});
+	NoteWatchings.find({
+		noteId: note.id,
+		userId: Not(user.id),
+	}).then(watchers => {
+		for (const watcher of watchers) {
+			createNotification(watcher.userId, user.id, 'pollVote', {
+				noteId: note.id,
+				choice: ps.choice
+			});
+		}
+	});
 
 	// この投稿をWatchする
-	if (user.settings.autoWatch !== false) {
-		watch(user._id, note);
+	if (user.autoWatch !== false) {
+		watch(user.id, note);
 	}
 
 	// リモート投票の場合リプライ送信
-	if (note._user.host != null) {
-		const pollOwner: IRemoteUser = await User.findOne({
-			_id: note.userId
-		});
+	if (note.userHost != null) {
+		const pollOwner: IRemoteUser = await Users.findOne(note.userId);
 
-		deliver(user, renderActivity(await renderVote(user, vote, note, pollOwner)), pollOwner.inbox);
+		deliver(user, renderActivity(await renderVote(user, vote, note, poll, pollOwner)), pollOwner.inbox);
 	}
 
 	// リモートフォロワーにUpdate配信
-	deliverQuestionUpdate(note._id);
-
-	return;
+	deliverQuestionUpdate(note.id);
 });
diff --git a/src/server/api/endpoints/notes/reactions.ts b/src/server/api/endpoints/notes/reactions.ts
index 7d977154f2b7bb8a38444302921424ba28a9c365..b1b5ca9d339f4b90ba08cabf7674d5116620c266 100644
--- a/src/server/api/endpoints/notes/reactions.ts
+++ b/src/server/api/endpoints/notes/reactions.ts
@@ -1,9 +1,9 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import NoteReaction, { pack } from '../../../../models/note-reaction';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
 import { getNote } from '../../common/getters';
 import { ApiError } from '../../error';
+import { NoteReactions } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -18,7 +18,6 @@ export const meta = {
 	params: {
 		noteId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'The ID of the target note'
@@ -37,12 +36,10 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 	},
 
@@ -69,29 +66,17 @@ export default define(meta, async (ps, user) => {
 	});
 
 	const query = {
-		noteId: note._id
-	} as any;
-
-	const sort = {
-		_id: -1
+		noteId: note.id
 	};
 
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		query._id = {
-			$lt: ps.untilId
-		};
-	}
-
-	const reactions = await NoteReaction.find(query, {
-		limit: ps.limit,
+	const reactions = await NoteReactions.find({
+		where: query,
+		take: ps.limit,
 		skip: ps.offset,
-		sort: sort
+		order: {
+			id: -1
+		}
 	});
 
-	return await Promise.all(reactions.map(reaction => pack(reaction, user)));
+	return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, user)));
 });
diff --git a/src/server/api/endpoints/notes/reactions/create.ts b/src/server/api/endpoints/notes/reactions/create.ts
index 299ed302783a3d5a8583f61571af326ad165ca86..b6aa4c58f3a9d21938943d2d196fb44356604ada 100644
--- a/src/server/api/endpoints/notes/reactions/create.ts
+++ b/src/server/api/endpoints/notes/reactions/create.ts
@@ -1,5 +1,5 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
 import createReaction from '../../../../../services/note/reaction/create';
 import define from '../../../define';
 import { getNote } from '../../../common/getters';
@@ -17,12 +17,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'reaction-write',
+	kind: 'write:reactions',
 
 	params: {
 		noteId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿'
 			}
diff --git a/src/server/api/endpoints/notes/reactions/delete.ts b/src/server/api/endpoints/notes/reactions/delete.ts
index 08442226c5dfaa3d37fb06ae6aa82f39c48103fd..0bdea58027926d9865767a29ab38934968c13a77 100644
--- a/src/server/api/endpoints/notes/reactions/delete.ts
+++ b/src/server/api/endpoints/notes/reactions/delete.ts
@@ -1,5 +1,5 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
 import * as ms from 'ms';
 import deleteReaction from '../../../../../services/note/reaction/delete';
@@ -16,7 +16,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'reaction-write',
+	kind: 'write:reactions',
 
 	limit: {
 		duration: ms('1hour'),
@@ -27,7 +27,6 @@ export const meta = {
 	params: {
 		noteId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID'
diff --git a/src/server/api/endpoints/notes/renotes.ts b/src/server/api/endpoints/notes/renotes.ts
index 15dcf55dcee245be79fc16deb4b29323f14ec506..81b899836de8fc7d539fb049d804babc911b11b9 100644
--- a/src/server/api/endpoints/notes/renotes.ts
+++ b/src/server/api/endpoints/notes/renotes.ts
@@ -1,9 +1,12 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note, { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
 import { getNote } from '../../common/getters';
 import { ApiError } from '../../error';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Notes } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -18,7 +21,6 @@ export const meta = {
 	params: {
 		noteId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID'
@@ -32,12 +34,10 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 		}
 	},
 
@@ -63,29 +63,14 @@ export default define(meta, async (ps, user) => {
 		throw e;
 	});
 
-	const sort = {
-		_id: -1
-	};
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+		.andWhere(`note.renoteId = :renoteId`, { renoteId: note.id })
+		.leftJoinAndSelect('note.user', 'user');
 
-	const query = {
-		renoteId: note._id
-	} as any;
+	if (user) generateVisibilityQuery(query, user);
+	if (user) generateMuteQuery(query, user);
 
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		query._id = {
-			$lt: ps.untilId
-		};
-	}
-
-	const renotes = await Note.find(query, {
-		limit: ps.limit,
-		sort: sort
-	});
+	const renotes = await query.take(ps.limit).getMany();
 
-	return await packMany(renotes, user);
+	return await Notes.packMany(renotes, user);
 });
diff --git a/src/server/api/endpoints/notes/replies.ts b/src/server/api/endpoints/notes/replies.ts
index c80fd732056d5c131ab0f684483ee126be4051b5..09b0f17164de6ad5345df92589854bb5afbd5c97 100644
--- a/src/server/api/endpoints/notes/replies.ts
+++ b/src/server/api/endpoints/notes/replies.ts
@@ -1,9 +1,10 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note, { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
-import { getFriends } from '../../common/get-friends';
-import { getHideUserIds } from '../../common/get-hide-users';
+import { Notes } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMuteQuery } from '../../common/generate-mute-query';
 
 export const meta = {
 	desc: {
@@ -18,22 +19,30 @@ export const meta = {
 	params: {
 		noteId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID'
 			}
 		},
 
+		sinceId: {
+			validator: $.optional.type(ID),
+			desc: {
+				'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します'
+			}
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+			desc: {
+				'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します'
+			}
+		},
+
 		limit: {
 			validator: $.optional.num.range(1, 100),
 			default: 10
 		},
-
-		offset: {
-			validator: $.optional.num.min(0),
-			default: 0
-		},
 	},
 
 	res: {
@@ -45,54 +54,14 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const [followings, hideUserIds] = await Promise.all([
-		// フォローを取得
-		// Fetch following
-		user ? getFriends(user._id) : [],
-
-		// 隠すユーザーを取得
-		getHideUserIds(user)
-	]);
-
-	const visibleQuery = user == null ? [{
-		visibility: { $in: [ 'public', 'home' ] }
-	}] : [{
-		visibility: { $in: [ 'public', 'home' ] }
-	}, {
-		// myself (for followers/specified/private)
-		userId: user._id
-	}, {
-		// to me (for specified)
-		visibleUserIds: { $in: [ user._id ] }
-	}, {
-		visibility: 'followers',
-		$or: [{
-			// フォロワーの投稿
-			userId: { $in: followings.map(f => f.id) },
-		}, {
-			// 自分の投稿へのリプライ
-			'_reply.userId': user._id,
-		}, {
-			// 自分へのメンションが含まれている
-			mentions: { $in: [ user._id ] }
-		}]
-	}];
-
-	const q = {
-		replyId: ps.noteId,
-		$or: visibleQuery
-	} as any;
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+		.andWhere('note.replyId = :replyId', { replyId: ps.noteId })
+		.leftJoinAndSelect('note.user', 'user');
 
-	if (hideUserIds && hideUserIds.length > 0) {
-		q['userId'] = {
-			$nin: hideUserIds
-		};
-	}
+	if (user) generateVisibilityQuery(query, user);
+	if (user) generateMuteQuery(query, user);
 
-	const notes = await Note.find(q, {
-		limit: ps.limit,
-		skip: ps.offset
-	});
+	const timeline = await query.take(ps.limit).getMany();
 
-	return await packMany(notes, user);
+	return await Notes.packMany(timeline, user);
 });
diff --git a/src/server/api/endpoints/notes/search-by-tag.ts b/src/server/api/endpoints/notes/search-by-tag.ts
index b33c884049032f43da4a3b8991d4735887077a74..48de88d36e9e489176b57f500a6c3b6551db4e36 100644
--- a/src/server/api/endpoints/notes/search-by-tag.ts
+++ b/src/server/api/endpoints/notes/search-by-tag.ts
@@ -1,10 +1,11 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note from '../../../../models/note';
-import { getFriendIds } from '../../common/get-friends';
-import { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
-import { getHideUserIds } from '../../common/get-hide-users';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Notes } from '../../../../models';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { Brackets } from 'typeorm';
 
 export const meta = {
 	desc: {
@@ -28,16 +29,6 @@ export const meta = {
 			}
 		},
 
-		following: {
-			validator: $.optional.nullable.bool,
-			default: null as any
-		},
-
-		mute: {
-			validator: $.optional.str,
-			default: 'mute_all'
-		},
-
 		reply: {
 			validator: $.optional.nullable.bool,
 			default: null as any,
@@ -61,44 +52,28 @@ export const meta = {
 			}
 		},
 
-		media: {
+		poll: {
 			validator: $.optional.nullable.bool,
 			default: null as any,
-			deprecated: true,
 			desc: {
-				'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
+				'ja-JP': 'アンケートが添付された投稿に限定するか否か'
 			}
 		},
 
-		poll: {
-			validator: $.optional.nullable.bool,
-			default: null as any,
+		sinceId: {
+			validator: $.optional.type(ID),
 			desc: {
-				'ja-JP': 'アンケートが添付された投稿に限定するか否か'
+				'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します'
 			}
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 			desc: {
-				'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
+				'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します'
 			}
 		},
 
-		sinceDate: {
-			validator: $.optional.num,
-		},
-
-		untilDate: {
-			validator: $.optional.num,
-		},
-
-		offset: {
-			validator: $.optional.num.min(0),
-			default: 0
-		},
-
 		limit: {
 			validator: $.optional.num.range(1, 30),
 			default: 10
@@ -114,226 +89,58 @@ export const meta = {
 };
 
 export default define(meta, async (ps, me) => {
-	const visibleQuery = me == null ? [{
-		visibility: { $in: [ 'public', 'home' ] }
-	}] : [{
-		visibility: { $in: [ 'public', 'home' ] }
-	}, {
-		// myself (for specified/private)
-		userId: me._id
-	}, {
-		// to me (for specified)
-		visibleUserIds: { $in: [ me._id ] }
-	}];
-
-	const q: any = {
-		$and: [ps.tag ? {
-			tagsLower: ps.tag.toLowerCase()
-		} : {
-			$or: ps.query.map(tags => ({
-				$and: tags.map(t => ({
-					tagsLower: t.toLowerCase()
-				}))
-			}))
-		}],
-		deletedAt: { $exists: false },
-		$or: visibleQuery
-	};
-
-	const push = (x: any) => q.$and.push(x);
-
-	if (ps.following != null && me != null) {
-		const ids = await getFriendIds(me._id, false);
-		push({
-			userId: ps.following ? {
-				$in: ids
-			} : {
-				$nin: ids.concat(me._id)
-			}
-		});
-	}
-
-	if (me != null) {
-		const hideUserIds = await getHideUserIds(me);
-
-		switch (ps.mute) {
-			case 'mute_all':
-				push({
-					userId: {
-						$nin: hideUserIds
-					},
-					'_reply.userId': {
-						$nin: hideUserIds
-					},
-					'_renote.userId': {
-						$nin: hideUserIds
-					}
-				});
-				break;
-			case 'mute_related':
-				push({
-					'_reply.userId': {
-						$nin: hideUserIds
-					},
-					'_renote.userId': {
-						$nin: hideUserIds
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+		.leftJoinAndSelect('note.user', 'user');
+
+	if (me) generateVisibilityQuery(query, me);
+	if (me) generateMuteQuery(query, me);
+
+	if (ps.tag) {
+		query.andWhere(':tag = ANY(note.tags)', { tag: ps.tag });
+	} else {
+		let i = 0;
+		query.andWhere(new Brackets(qb => {
+			for (const tags of ps.query) {
+				qb.orWhere(new Brackets(qb => {
+					for (const tag of tags) {
+						qb.andWhere(`:tag${i} = ANY(note.tags)`, { [`tag${i}`]: tag });
+						i++;
 					}
-				});
-				break;
-			case 'mute_direct':
-				push({
-					userId: {
-						$nin: hideUserIds
-					}
-				});
-				break;
-			case 'direct_only':
-				push({
-					userId: {
-						$in: hideUserIds
-					}
-				});
-				break;
-			case 'related_only':
-				push({
-					$or: [{
-						'_reply.userId': {
-							$in: hideUserIds
-						}
-					}, {
-						'_renote.userId': {
-							$in: hideUserIds
-						}
-					}]
-				});
-				break;
-			case 'all_only':
-				push({
-					$or: [{
-						userId: {
-							$in: hideUserIds
-						}
-					}, {
-						'_reply.userId': {
-							$in: hideUserIds
-						}
-					}, {
-						'_renote.userId': {
-							$in: hideUserIds
-						}
-					}]
-				});
-				break;
-		}
+				}));
+			}
+		}));
 	}
 
 	if (ps.reply != null) {
 		if (ps.reply) {
-			push({
-				replyId: {
-					$exists: true,
-					$ne: null
-				}
-			});
+			query.andWhere('note.replyId IS NOT NULL');
 		} else {
-			push({
-				$or: [{
-					replyId: {
-						$exists: false
-					}
-				}, {
-					replyId: null
-				}]
-			});
+			query.andWhere('note.replyId IS NULL');
 		}
 	}
 
 	if (ps.renote != null) {
 		if (ps.renote) {
-			push({
-				renoteId: {
-					$exists: true,
-					$ne: null
-				}
-			});
+			query.andWhere('note.renoteId IS NOT NULL');
 		} else {
-			push({
-				$or: [{
-					renoteId: {
-						$exists: false
-					}
-				}, {
-					renoteId: null
-				}]
-			});
+			query.andWhere('note.renoteId IS NULL');
 		}
 	}
 
-	const withFiles = ps.withFiles != null ? ps.withFiles : ps.media;
-
-	if (withFiles) {
-		push({
-			fileIds: { $exists: true, $ne: [] }
-		});
+	if (ps.withFiles) {
+		query.andWhere('note.fileIds != \'{}\'');
 	}
 
 	if (ps.poll != null) {
 		if (ps.poll) {
-			push({
-				poll: {
-					$exists: true,
-					$ne: null
-				}
-			});
+			query.andWhere('note.hasPoll = TRUE');
 		} else {
-			push({
-				$or: [{
-					poll: {
-						$exists: false
-					}
-				}, {
-					poll: null
-				}]
-			});
+			query.andWhere('note.hasPoll = FALSE');
 		}
 	}
 
-	if (ps.untilId) {
-		push({
-			_id: {
-				$lt: ps.untilId
-			}
-		});
-	}
-
-	if (ps.sinceDate) {
-		push({
-			createdAt: {
-				$gt: new Date(ps.sinceDate)
-			}
-		});
-	}
-
-	if (ps.untilDate) {
-		push({
-			createdAt: {
-				$lt: new Date(ps.untilDate)
-			}
-		});
-	}
-
-	if (q.$and.length == 0) {
-		delete q.$and;
-	}
-
 	// Search notes
-	const notes = await Note.find(q, {
-		sort: {
-			_id: -1
-		},
-		limit: ps.limit,
-		skip: ps.offset
-	});
+	const notes = await query.take(ps.limit).getMany();
 
-	return await packMany(notes, me);
+	return await Notes.packMany(notes, me);
 });
diff --git a/src/server/api/endpoints/notes/search.ts b/src/server/api/endpoints/notes/search.ts
index edc8a1456041c975b081254fbf9e015bb6419430..cc88fb9380ac7cc154b3c6fd7c3e47ab38f35cc1 100644
--- a/src/server/api/endpoints/notes/search.ts
+++ b/src/server/api/endpoints/notes/search.ts
@@ -1,10 +1,9 @@
 import $ from 'cafy';
-import * as mongo from 'mongodb';
-import Note from '../../../../models/note';
-import { packMany } from '../../../../models/note';
 import es from '../../../../db/elasticsearch';
 import define from '../../define';
 import { ApiError } from '../../error';
+import { Notes } from '../../../../models';
+import { In } from 'typeorm';
 
 export const meta = {
 	desc: {
@@ -74,18 +73,19 @@ export default define(meta, async (ps, me) => {
 		return [];
 	}
 
-	const hits = response.hits.hits.map(hit => new mongo.ObjectID(hit._id));
+	const hits = response.hits.hits.map((hit: any) => hit.id);
+
+	if (hits.length === 0) return [];
 
 	// Fetch found notes
-	const notes = await Note.find({
-		_id: {
-			$in: hits
-		}
-	}, {
-		sort: {
-			_id: -1
+	const notes = await Notes.find({
+		where: {
+			id: In(hits)
+		},
+		order: {
+			id: -1
 		}
 	});
 
-	return await packMany(notes, me);
+	return await Notes.packMany(notes, me);
 });
diff --git a/src/server/api/endpoints/notes/show.ts b/src/server/api/endpoints/notes/show.ts
index 6d8dc73ff297955eed425b290584cc705f8ccabc..d41dc20c5498fe3e0194bf72f311f948bf05a685 100644
--- a/src/server/api/endpoints/notes/show.ts
+++ b/src/server/api/endpoints/notes/show.ts
@@ -1,9 +1,9 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import { pack } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
 import { getNote } from '../../common/getters';
 import { ApiError } from '../../error';
+import { Notes } from '../../../../models';
 
 export const meta = {
 	stability: 'stable',
@@ -20,7 +20,6 @@ export const meta = {
 	params: {
 		noteId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID.'
@@ -47,7 +46,7 @@ export default define(meta, async (ps, user) => {
 		throw e;
 	});
 
-	return await pack(note, user, {
+	return await Notes.pack(note, user, {
 		detail: true
 	});
 });
diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/social-timeline.ts
similarity index 50%
rename from src/server/api/endpoints/notes/hybrid-timeline.ts
rename to src/server/api/endpoints/notes/social-timeline.ts
index 9695547f0466b1002299f3558ef072af601a919d..10e215d6c4793c74654f86b285a5dc3f7d19fc53 100644
--- a/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/src/server/api/endpoints/notes/social-timeline.ts
@@ -1,17 +1,18 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note from '../../../../models/note';
-import { getFriends } from '../../common/get-friends';
-import { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
 import fetchMeta from '../../../../misc/fetch-meta';
-import activeUsersChart from '../../../../services/chart/active-users';
-import { getHideUserIds } from '../../common/get-hide-users';
 import { ApiError } from '../../error';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Followings, Notes } from '../../../../models';
+import { Brackets } from 'typeorm';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { activeUsersChart } from '../../../../services/chart';
 
 export const meta = {
 	desc: {
-		'ja-JP': 'ハイブリッドタイムラインを取得します。'
+		'ja-JP': 'ソーシャルタイムラインを取得します。'
 	},
 
 	tags: ['notes'],
@@ -27,17 +28,15 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 			desc: {
-				'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します'
+				'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します'
 			}
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 			desc: {
-				'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
+				'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します'
 			}
 		},
 
@@ -85,14 +84,6 @@ export const meta = {
 				'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
 			}
 		},
-
-		mediaOnly: {
-			validator: $.optional.bool,
-			deprecated: true,
-			desc: {
-				'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
-			}
-		},
 	},
 
 	res: {
@@ -112,94 +103,30 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
+	// TODO どっかにキャッシュ
 	const m = await fetchMeta();
 	if (m.disableLocalTimeline && !user.isAdmin && !user.isModerator) {
 		throw new ApiError(meta.errors.stlDisabled);
 	}
 
-	const [followings, hideUserIds] = await Promise.all([
-		// フォローを取得
-		// Fetch following
-		getFriends(user._id, true, false),
-
-		// 隠すユーザーを取得
-		getHideUserIds(user)
-	]);
-
 	//#region Construct query
-	const sort = {
-		_id: -1
-	};
-
-	const followQuery = followings.map(f => ({
-		userId: f.id,
-
-		/*// リプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める)
-		$or: [{
-			// リプライでない
-			replyId: null
-		}, { // または
-			// リプライだが返信先が投稿者自身の投稿
-			$expr: {
-				$eq: ['$_reply.userId', '$userId']
-			}
-		}, { // または
-			// リプライだが返信先が自分(フォロワー)の投稿
-			'_reply.userId': user._id
-		}, { // または
-			// 自分(フォロワー)が送信したリプライ
-			userId: user._id
-		}]*/
-	}));
-
-	const visibleQuery = user == null ? [{
-		visibility: { $in: ['public', 'home'] }
-	}] : [{
-		visibility: { $in: ['public', 'home', 'followers'] }
-	}, {
-		// myself (for specified/private)
-		userId: user._id
-	}, {
-		// to me (for specified)
-		visibleUserIds: { $in: [ user._id ] }
-	}];
-
-	const query = {
-		$and: [{
-			deletedAt: null,
-
-			$or: [{
-				$and: [{
-					// フォローしている人の投稿
-					$or: followQuery
-				}, {
-					// visible for me
-					$or: visibleQuery
-				}]
-			}, {
-				// public only
-				visibility: 'public',
-
-				// リプライでない
-				//replyId: null,
-
-				// local
-				'_user.host': null
-			}],
-
-			// hide
-			userId: {
-				$nin: hideUserIds
-			},
-			'_reply.userId': {
-				$nin: hideUserIds
-			},
-			'_renote.userId': {
-				$nin: hideUserIds
-			},
-		}]
-	} as any;
-
+	const followingQuery = Followings.createQueryBuilder('following')
+		.select('following.followeeId')
+		.where('following.followerId = :followerId', { followerId: user.id });
+
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'),
+			ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+		.andWhere(new Brackets(qb => {
+			qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id })
+				.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
+		}))
+		.leftJoinAndSelect('note.user', 'user')
+		.setParameters(followingQuery.getParameters());
+
+	generateVisibilityQuery(query, user);
+	generateMuteQuery(query, user);
+
+	/* TODO
 	// MongoDBではトップレベルで否定ができないため、De Morganの法則を利用してクエリします。
 	// つまり、「『自分の投稿かつRenote』ではない」を「『自分の投稿ではない』または『Renoteではない』」と表現します。
 	// for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws
@@ -207,7 +134,7 @@ export default define(meta, async (ps, user) => {
 	if (ps.includeMyRenotes === false) {
 		query.$and.push({
 			$or: [{
-				userId: { $ne: user._id }
+				userId: { $ne: user.id }
 			}, {
 				renoteId: null
 			}, {
@@ -223,7 +150,7 @@ export default define(meta, async (ps, user) => {
 	if (ps.includeRenotedMyNotes === false) {
 		query.$and.push({
 			$or: [{
-				'_renote.userId': { $ne: user._id }
+				'_renote.userId': { $ne: user.id }
 			}, {
 				renoteId: null
 			}, {
@@ -251,40 +178,18 @@ export default define(meta, async (ps, user) => {
 			}]
 		});
 	}
+	*/
 
-	if (ps.withFiles || ps.mediaOnly) {
-		query.$and.push({
-			fileIds: { $exists: true, $ne: [] }
-		});
-	}
-
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		query._id = {
-			$lt: ps.untilId
-		};
-	} else if (ps.sinceDate) {
-		sort._id = 1;
-		query.createdAt = {
-			$gt: new Date(ps.sinceDate)
-		};
-	} else if (ps.untilDate) {
-		query.createdAt = {
-			$lt: new Date(ps.untilDate)
-		};
+	if (ps.withFiles) {
+		query.andWhere('note.fileIds != \'{}\'');
 	}
 	//#endregion
 
-	const timeline = await Note.find(query, {
-		limit: ps.limit,
-		sort: sort
-	});
+	const timeline = await query.take(ps.limit).getMany();
 
-	activeUsersChart.update(user);
+	if (user) {
+		activeUsersChart.update(user);
+	}
 
-	return await packMany(timeline, user);
+	return await Notes.packMany(timeline, user);
 });
diff --git a/src/server/api/endpoints/notes/state.ts b/src/server/api/endpoints/notes/state.ts
index 4944802849c0b5cbac04bf69edb5448991f84b3a..df1d9d9fb0d42c136763dc040d1d0bf9e1a069f3 100644
--- a/src/server/api/endpoints/notes/state.ts
+++ b/src/server/api/endpoints/notes/state.ts
@@ -1,8 +1,7 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
-import Favorite from '../../../../models/favorite';
-import NoteWatching from '../../../../models/note-watching';
+import { NoteFavorites, NoteWatchings } from '../../../../models';
 
 export const meta = {
 	stability: 'stable',
@@ -19,7 +18,6 @@ export const meta = {
 	params: {
 		noteId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID.'
@@ -30,17 +28,19 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	const [favorite, watching] = await Promise.all([
-		Favorite.count({
-			userId: user._id,
+		NoteFavorites.count({
+			where: {
+			userId: user.id,
 			noteId: ps.noteId
-		}, {
-			limit: 1
+			},
+			take: 1
 		}),
-		NoteWatching.count({
-			userId: user._id,
+		NoteWatchings.count({
+			where: {
+			userId: user.id,
 			noteId: ps.noteId
-		}, {
-			limit: 1
+			},
+			take: 1
 		})
 	]);
 
diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts
index 6ff7690c74303e7dcdd9bea15b114d276a35bf1e..e22db4d1b0ee889c042129349c4f8f872c3a005d 100644
--- a/src/server/api/endpoints/notes/timeline.ts
+++ b/src/server/api/endpoints/notes/timeline.ts
@@ -1,11 +1,12 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note from '../../../../models/note';
-import { getFriends } from '../../common/get-friends';
-import { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
-import activeUsersChart from '../../../../services/chart/active-users';
-import { getHideUserIds } from '../../common/get-hide-users';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Notes, Followings } from '../../../../models';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { activeUsersChart } from '../../../../services/chart';
+import { Brackets } from 'typeorm';
 
 export const meta = {
 	desc: {
@@ -28,17 +29,15 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 			desc: {
-				'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します'
+				'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します'
 			}
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 			desc: {
-				'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
+				'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します'
 			}
 		},
 
@@ -86,14 +85,6 @@ export const meta = {
 				'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
 			}
 		},
-
-		mediaOnly: {
-			validator: $.optional.bool,
-			deprecated: true,
-			desc: {
-				'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
-			}
-		},
 	},
 
 	res: {
@@ -105,78 +96,24 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const [followings, hideUserIds] = await Promise.all([
-		// フォローを取得
-		// Fetch following
-		getFriends(user._id),
-
-		// 隠すユーザーを取得
-		getHideUserIds(user)
-	]);
-
 	//#region Construct query
-	const sort = {
-		_id: -1
-	};
-
-	const followQuery = followings.map(f => ({
-		userId: f.id,
-
-		/*// リプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める)
-		$or: [{
-			// リプライでない
-			replyId: null
-		}, { // または
-			// リプライだが返信先が投稿者自身の投稿
-			$expr: {
-				$eq: ['$_reply.userId', '$userId']
-			}
-		}, { // または
-			// リプライだが返信先が自分(フォロワー)の投稿
-			'_reply.userId': user._id
-		}, { // または
-			// 自分(フォロワー)が送信したリプライ
-			userId: user._id
-		}]*/
-	}));
-
-	const visibleQuery = user == null ? [{
-		visibility: { $in: [ 'public', 'home' ] }
-	}] : [{
-		visibility: { $in: [ 'public', 'home', 'followers' ] }
-	}, {
-		// myself (for specified/private)
-		userId: user._id
-	}, {
-		// to me (for specified)
-		visibleUserIds: { $in: [ user._id ] }
-	}];
-
-	const query = {
-		$and: [{
-			deletedAt: null,
-
-			$and: [{
-				// フォローしている人の投稿
-				$or: followQuery
-			}, {
-				// visible for me
-				$or: visibleQuery
-			}],
-
-			// mute
-			userId: {
-				$nin: hideUserIds
-			},
-			'_reply.userId': {
-				$nin: hideUserIds
-			},
-			'_renote.userId': {
-				$nin: hideUserIds
-			},
-		}]
-	} as any;
-
+	const followingQuery = Followings.createQueryBuilder('following')
+		.select('following.followeeId')
+		.where('following.followerId = :followerId', { followerId: user.id });
+
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'),
+			ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+		.andWhere(new Brackets(qb => { qb
+			.where(`note.userId IN (${ followingQuery.getQuery() })`)
+			.orWhere('note.userId = :meId', { meId: user.id });
+		}))
+		.leftJoinAndSelect('note.user', 'user')
+		.setParameters(followingQuery.getParameters());
+
+	generateVisibilityQuery(query, user);
+	generateMuteQuery(query, user);
+
+	/* v11 TODO
 	// MongoDBではトップレベルで否定ができないため、De Morganの法則を利用してクエリします。
 	// つまり、「『自分の投稿かつRenote』ではない」を「『自分の投稿ではない』または『Renoteではない』」と表現します。
 	// for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws
@@ -184,7 +121,7 @@ export default define(meta, async (ps, user) => {
 	if (ps.includeMyRenotes === false) {
 		query.$and.push({
 			$or: [{
-				userId: { $ne: user._id }
+				userId: { $ne: user.id }
 			}, {
 				renoteId: null
 			}, {
@@ -200,7 +137,7 @@ export default define(meta, async (ps, user) => {
 	if (ps.includeRenotedMyNotes === false) {
 		query.$and.push({
 			$or: [{
-				'_renote.userId': { $ne: user._id }
+				'_renote.userId': { $ne: user.id }
 			}, {
 				renoteId: null
 			}, {
@@ -227,43 +164,16 @@ export default define(meta, async (ps, user) => {
 				poll: { $ne: null }
 			}]
 		});
-	}
-
-	const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
-
-	if (withFiles) {
-		query.$and.push({
-			fileIds: { $exists: true, $ne: [] }
-		});
-	}
+	}*/
 
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		query._id = {
-			$lt: ps.untilId
-		};
-	} else if (ps.sinceDate) {
-		sort._id = 1;
-		query.createdAt = {
-			$gt: new Date(ps.sinceDate)
-		};
-	} else if (ps.untilDate) {
-		query.createdAt = {
-			$lt: new Date(ps.untilDate)
-		};
+	if (ps.withFiles) {
+		query.andWhere('note.fileIds != \'{}\'');
 	}
 	//#endregion
 
-	const timeline = await Note.find(query, {
-		limit: ps.limit,
-		sort: sort
-	});
+	const timeline = await query.take(ps.limit).getMany();
 
 	activeUsersChart.update(user);
 
-	return await packMany(timeline, user);
+	return await Notes.packMany(timeline, user);
 });
diff --git a/src/server/api/endpoints/notes/user-list-timeline.ts b/src/server/api/endpoints/notes/user-list-timeline.ts
index 17c24ab11969b2131a609642a3d642ce5bc99fe7..deda04acb468db53f15289ecd1eaae4c9b0387df 100644
--- a/src/server/api/endpoints/notes/user-list-timeline.ts
+++ b/src/server/api/endpoints/notes/user-list-timeline.ts
@@ -1,12 +1,11 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note from '../../../../models/note';
-import { packMany } from '../../../../models/note';
-import UserList from '../../../../models/user-list';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
-import { getFriends } from '../../common/get-friends';
-import { getHideUserIds } from '../../common/get-hide-users';
 import { ApiError } from '../../error';
+import { UserLists, UserListJoinings, Notes } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { activeUsersChart } from '../../../../services/chart';
 
 export const meta = {
 	desc: {
@@ -21,7 +20,6 @@ export const meta = {
 	params: {
 		listId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': 'リストのID'
 			}
@@ -37,17 +35,15 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 			desc: {
-				'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します'
+				'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します'
 			}
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 			desc: {
-				'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
+				'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します'
 			}
 		},
 
@@ -95,14 +91,6 @@ export const meta = {
 				'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
 			}
 		},
-
-		mediaOnly: {
-			validator: $.optional.bool,
-			deprecated: true,
-			desc: {
-				'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
-			}
-		},
 	},
 
 	res: {
@@ -122,94 +110,28 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const [list, followings, hideUserIds] = await Promise.all([
-		// リストを取得
-		// Fetch the list
-		UserList.findOne({
-			_id: ps.listId,
-			userId: user._id
-		}),
-
-		// フォローを取得
-		// Fetch following
-		getFriends(user._id, true, false),
-
-		// 隠すユーザーを取得
-		getHideUserIds(user)
-	]);
+	const list = await UserLists.findOne({
+		id: ps.listId,
+		userId: user.id
+	});
 
 	if (list == null) {
 		throw new ApiError(meta.errors.noSuchList);
 	}
 
-	if (list.userIds.length == 0) {
-		return [];
-	}
-
 	//#region Construct query
-	const sort = {
-		_id: -1
-	};
-
-	const listQuery = list.userIds.map(u => ({
-		userId: u,
-
-		/*// リプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める)
-		$or: [{
-			// リプライでない
-			replyId: null
-		}, { // または
-			// リプライだが返信先が投稿者自身の投稿
-			$expr: {
-				$eq: ['$_reply.userId', '$userId']
-			}
-		}, { // または
-			// リプライだが返信先が自分(フォロワー)の投稿
-			'_reply.userId': user._id
-		}, { // または
-			// 自分(フォロワー)が送信したリプライ
-			userId: user._id
-		}]*/
-	}));
-
-	const visibleQuery = [{
-		visibility: { $in: ['public', 'home'] }
-	}, {
-		// myself (for specified/private)
-		userId: user._id
-	}, {
-		// to me (for specified)
-		visibleUserIds: { $in: [user._id] }
-	}, {
-		visibility: 'followers',
-		userId: { $in: followings.map(f => f.id) }
-	}];
-
-	const query = {
-		$and: [{
-			deletedAt: null,
+	const listQuery = UserListJoinings.createQueryBuilder('joining')
+		.select('joining.userId')
+		.where('joining.userListId = :userListId', { userListId: list.id });
 
-			$and: [{
-				// リストに入っている人のタイムラインへの投稿
-				$or: listQuery
-			}, {
-				// visible for me
-				$or: visibleQuery
-			}],
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+		.andWhere(`note.userId IN (${ listQuery.getQuery() })`)
+		.leftJoinAndSelect('note.user', 'user')
+		.setParameters(listQuery.getParameters());
 
-			// mute
-			userId: {
-				$nin: hideUserIds
-			},
-			'_reply.userId': {
-				$nin: hideUserIds
-			},
-			'_renote.userId': {
-				$nin: hideUserIds
-			},
-		}]
-	} as any;
+	generateVisibilityQuery(query, user);
 
+	/* TODO
 	// MongoDBではトップレベルで否定ができないため、De Morganの法則を利用してクエリします。
 	// つまり、「『自分の投稿かつRenote』ではない」を「『自分の投稿ではない』または『Renoteではない』」と表現します。
 	// for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws
@@ -217,7 +139,7 @@ export default define(meta, async (ps, user) => {
 	if (ps.includeMyRenotes === false) {
 		query.$and.push({
 			$or: [{
-				userId: { $ne: user._id }
+				userId: { $ne: user.id }
 			}, {
 				renoteId: null
 			}, {
@@ -233,7 +155,7 @@ export default define(meta, async (ps, user) => {
 	if (ps.includeRenotedMyNotes === false) {
 		query.$and.push({
 			$or: [{
-				'_renote.userId': { $ne: user._id }
+				'_renote.userId': { $ne: user.id }
 			}, {
 				renoteId: null
 			}, {
@@ -260,41 +182,16 @@ export default define(meta, async (ps, user) => {
 				poll: { $ne: null }
 			}]
 		});
-	}
-
-	const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
-
-	if (withFiles) {
-		query.$and.push({
-			fileIds: { $exists: true, $ne: [] }
-		});
-	}
+	}*/
 
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		query._id = {
-			$lt: ps.untilId
-		};
-	} else if (ps.sinceDate) {
-		sort._id = 1;
-		query.createdAt = {
-			$gt: new Date(ps.sinceDate)
-		};
-	} else if (ps.untilDate) {
-		query.createdAt = {
-			$lt: new Date(ps.untilDate)
-		};
+	if (ps.withFiles) {
+		query.andWhere('note.fileIds != \'{}\'');
 	}
 	//#endregion
 
-	const timeline = await Note.find(query, {
-		limit: ps.limit,
-		sort: sort
-	});
+	const timeline = await query.take(ps.limit).getMany();
+
+	activeUsersChart.update(user);
 
-	return await packMany(timeline, user);
+	return await Notes.packMany(timeline, user);
 });
diff --git a/src/server/api/endpoints/notes/watching/create.ts b/src/server/api/endpoints/notes/watching/create.ts
index 2b2de1bd3b76439e7338afb085469d0a31943ca1..b4045fe93c07ccf86161bc34a4b3b5539578f087 100644
--- a/src/server/api/endpoints/notes/watching/create.ts
+++ b/src/server/api/endpoints/notes/watching/create.ts
@@ -1,5 +1,5 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
 import watch from '../../../../../services/note/watch';
 import { getNote } from '../../../common/getters';
@@ -17,12 +17,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account-write',
+	kind: 'write:account',
 
 	params: {
 		noteId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID.'
@@ -45,5 +44,5 @@ export default define(meta, async (ps, user) => {
 		throw e;
 	});
 
-	await watch(user._id, note);
+	await watch(user.id, note);
 });
diff --git a/src/server/api/endpoints/notes/watching/delete.ts b/src/server/api/endpoints/notes/watching/delete.ts
index 512db793ea85250939fdc93dc4077d565f05e102..a272ecc37d29e8d54ccd217b952187b914a383c8 100644
--- a/src/server/api/endpoints/notes/watching/delete.ts
+++ b/src/server/api/endpoints/notes/watching/delete.ts
@@ -1,5 +1,5 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
 import unwatch from '../../../../../services/note/unwatch';
 import { getNote } from '../../../common/getters';
@@ -17,12 +17,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account-write',
+	kind: 'write:account',
 
 	params: {
 		noteId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象の投稿のID',
 				'en-US': 'Target note ID.'
@@ -45,5 +44,5 @@ export default define(meta, async (ps, user) => {
 		throw e;
 	});
 
-	await unwatch(user._id, note);
+	await unwatch(user.id, note);
 });
diff --git a/src/server/api/endpoints/notifications/mark-all-as-read.ts b/src/server/api/endpoints/notifications/mark-all-as-read.ts
index e5df648285006cd9ec04d0a47aa9e376caedbfb6..9f34a32e80acc9c7a900d6eb99257e933328a75b 100644
--- a/src/server/api/endpoints/notifications/mark-all-as-read.ts
+++ b/src/server/api/endpoints/notifications/mark-all-as-read.ts
@@ -1,7 +1,6 @@
-import Notification from '../../../../models/notification';
 import { publishMainStream } from '../../../../services/stream';
-import User from '../../../../models/user';
 import define from '../../define';
+import { Notifications } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -13,29 +12,18 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'notification-write'
+	kind: 'write:notifications'
 };
 
 export default define(meta, async (ps, user) => {
 	// Update documents
-	await Notification.update({
-		notifieeId: user._id,
-		isRead: false
+	await Notifications.update({
+		notifieeId: user.id,
+		isRead: false,
 	}, {
-		$set: {
-			isRead: true
-		}
-	}, {
-		multi: true
-	});
-
-	// Update flag
-	User.update({ _id: user._id }, {
-		$set: {
-			hasUnreadNotification: false
-		}
+		isRead: true
 	});
 
 	// 全ての通知を読みましたよというイベントを発行
-	publishMainStream(user._id, 'readAllNotifications');
+	publishMainStream(user.id, 'readAllNotifications');
 });
diff --git a/src/server/api/endpoints/stats.ts b/src/server/api/endpoints/stats.ts
index 30c49cdd8601780c1c3510ec7c04fc94bbd5d7aa..f3ebaa16ad88cf8874c8cf65ed8727708f3be96e 100644
--- a/src/server/api/endpoints/stats.ts
+++ b/src/server/api/endpoints/stats.ts
@@ -1,7 +1,6 @@
 import define from '../define';
-import driveChart from '../../../services/chart/drive';
-import federationChart from '../../../services/chart/federation';
-import fetchMeta from '../../../misc/fetch-meta';
+import { Notes, Users } from '../../../models';
+import { federationChart, driveChart } from '../../../services/chart';
 
 export const meta = {
 	requireCredential: false,
@@ -43,16 +42,17 @@ export const meta = {
 };
 
 export default define(meta, async () => {
-	const instance = await fetchMeta();
-
-	const stats: any = instance.stats;
-
-	const driveStats = await driveChart.getChart('hour', 1);
-	stats.driveUsageLocal = driveStats.local.totalSize[0];
-	stats.driveUsageRemote = driveStats.remote.totalSize[0];
-
-	const federationStats = await federationChart.getChart('hour', 1);
-	stats.instances = federationStats.instance.total[0];
-
-	return stats;
+	const [notesCount, originalNotesCount, usersCount, originalUsersCount, instances, driveUsageLocal, driveUsageRemote] = await Promise.all([
+		Notes.count(),
+		Notes.count({ userHost: null }),
+		Users.count(),
+		Users.count({ host: null }),
+		federationChart.getChart('hour', 1).then(chart => chart.instance.total[0]),
+		driveChart.getChart('hour', 1).then(chart => chart.local.totalSize[0]),
+		driveChart.getChart('hour', 1).then(chart => chart.remote.totalSize[0]),
+	]);
+
+	return {
+		notesCount, originalNotesCount, usersCount, originalUsersCount, instances, driveUsageLocal, driveUsageRemote
+	};
 });
diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts
index 0b81b06abe4496de94fa3f2e5e091711c3dc726b..cb0572aa902b60002f2d9bd918de494f1bbce5c8 100644
--- a/src/server/api/endpoints/sw/register.ts
+++ b/src/server/api/endpoints/sw/register.ts
@@ -1,7 +1,8 @@
 import $ from 'cafy';
-import Subscription from '../../../../models/sw-subscription';
 import define from '../../define';
 import fetchMeta from '../../../../misc/fetch-meta';
+import { genId } from '../../../../misc/gen-id';
+import { SwSubscriptions } from '../../../../models';
 
 export const meta = {
 	tags: ['account'],
@@ -25,12 +26,11 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	// if already subscribed
-	const exist = await Subscription.findOne({
-		userId: user._id,
+	const exist = await SwSubscriptions.findOne({
+		userId: user.id,
 		endpoint: ps.endpoint,
 		auth: ps.auth,
 		publickey: ps.publickey,
-		deletedAt: { $exists: false }
 	});
 
 	const instance = await fetchMeta();
@@ -42,8 +42,9 @@ export default define(meta, async (ps, user) => {
 		};
 	}
 
-	await Subscription.insert({
-		userId: user._id,
+	await SwSubscriptions.save({
+		id: genId(),
+		userId: user.id,
 		endpoint: ps.endpoint,
 		auth: ps.auth,
 		publickey: ps.publickey
diff --git a/src/server/api/endpoints/username/available.ts b/src/server/api/endpoints/username/available.ts
index 1d098eb399ca9e3777650e66768a732f0fcf6e36..42ab1766524b8d4b8b170bd18e783457441f138c 100644
--- a/src/server/api/endpoints/username/available.ts
+++ b/src/server/api/endpoints/username/available.ts
@@ -1,7 +1,6 @@
 import $ from 'cafy';
-import User from '../../../../models/user';
-import { validateUsername } from '../../../../models/user';
 import define from '../../define';
+import { Users } from '../../../../models';
 
 export const meta = {
 	tags: ['users'],
@@ -10,18 +9,16 @@ export const meta = {
 
 	params: {
 		username: {
-			validator: $.str.pipe(validateUsername)
+			validator: $.str.pipe(Users.validateUsername)
 		}
 	}
 };
 
 export default define(meta, async (ps) => {
 	// Get exist
-	const exist = await User.count({
+	const exist = await Users.count({
 		host: null,
 		usernameLower: ps.username.toLowerCase()
-	}, {
-		limit: 1
 	});
 
 	return {
diff --git a/src/server/api/endpoints/users.ts b/src/server/api/endpoints/users.ts
index be83dcd9cc0b65c8ae11d625f3986d54d8123c7e..f99165f3d58dd1d0dd94afa637cec398e9b7f327 100644
--- a/src/server/api/endpoints/users.ts
+++ b/src/server/api/endpoints/users.ts
@@ -1,10 +1,7 @@
 import $ from 'cafy';
-import User, { pack } from '../../../models/user';
 import define from '../define';
-import { fallback } from '../../../prelude/symbol';
-import { getHideUserIds } from '../common/get-hide-users';
-
-const nonnull = { $ne: null as any };
+import { Users } from '../../../models';
+import { generateMuteQueryForUsers } from '../common/generate-mute-query';
 
 export const meta = {
 	tags: ['users'],
@@ -63,53 +60,38 @@ export const meta = {
 	},
 };
 
-const state: any = { // < https://github.com/Microsoft/TypeScript/issues/1863
-	'admin': { isAdmin: true },
-	'moderator': { isModerator: true },
-	'adminOrModerator': {
-		$or: [
-			{ isAdmin: true },
-			{ isModerator: true }
-		]
-	},
-	'verified': { isVerified: true },
-	'alive': {
-		updatedAt: { $gt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }
-	},
-	[fallback]: {}
-};
+export default define(meta, async (ps, me) => {
+	const query = Users.createQueryBuilder('user');
 
-const origin: any = { // < https://github.com/Microsoft/TypeScript/issues/1863
-	'local': { host: null },
-	'remote': { host: nonnull },
-	[fallback]: {}
-};
+	switch (ps.state) {
+		case 'admin': query.where('user.isAdmin = TRUE'); break;
+		case 'moderator': query.where('user.isModerator = TRUE'); break;
+		case 'adminOrModerator': query.where('user.isAdmin = TRUE OR isModerator = TRUE'); break;
+		case 'verified': query.where('user.isVerified = TRUE'); break;
+		case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break;
+	}
 
-const sort: any = { // < https://github.com/Microsoft/TypeScript/issues/1863
-	'+follower': { followersCount: -1 },
-	'-follower': { followersCount: 1 },
-	'+createdAt': { createdAt: -1 },
-	'-createdAt': { createdAt: 1 },
-	'+updatedAt': { updatedAt: -1 },
-	'-updatedAt': { updatedAt: 1 },
-	[fallback]: { _id: -1 }
-};
+	switch (ps.origin) {
+		case 'local': query.andWhere('user.host IS NULL'); break;
+		case 'remote': query.andWhere('user.host IS NOT NULL'); break;
+	}
 
-export default define(meta, async (ps, me) => {
-	const hideUserIds = await getHideUserIds(me);
-
-	const users = await User
-		.find({
-			$and: [
-				state[ps.state] || state[fallback],
-				origin[ps.origin] || origin[fallback]
-			],
-			...(hideUserIds && hideUserIds.length > 0 ? { _id: { $nin: hideUserIds } } : {})
-		}, {
-			limit: ps.limit,
-			sort: sort[ps.sort] || sort[fallback],
-			skip: ps.offset
-		});
-
-	return await Promise.all(users.map(user => pack(user, me, { detail: true })));
+	switch (ps.sort) {
+		case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
+		case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
+		case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break;
+		case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break;
+		case '+updatedAt': query.orderBy('user.updatedAt', 'DESC'); break;
+		case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break;
+		default: query.orderBy('user.id', 'ASC'); break;
+	}
+
+	if (me) generateMuteQueryForUsers(query, me);
+
+	query.take(ps.limit);
+	query.skip(ps.offset);
+
+	const users = await query.getMany();
+
+	return await Users.packMany(users, me, { detail: true });
 });
diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts
index 3c8290a8b15c9d6d4757416f80d9181cb4550cb1..51b007ddaa3f16a0be19f2be206ca14b5b3c1eb3 100644
--- a/src/server/api/endpoints/users/followers.ts
+++ b/src/server/api/endpoints/users/followers.ts
@@ -1,11 +1,9 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import User from '../../../../models/user';
-import Following from '../../../../models/following';
-import { pack } from '../../../../models/user';
-import { getFriendIds } from '../../common/get-friends';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
 import { ApiError } from '../../error';
+import { Users, Followings } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
 
 export const meta = {
 	desc: {
@@ -20,7 +18,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -35,38 +32,25 @@ export const meta = {
 			validator: $.optional.nullable.str
 		},
 
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
+		sinceId: {
+			validator: $.optional.type(ID),
 		},
 
-		cursor: {
+		untilId: {
 			validator: $.optional.type(ID),
-			default: null as any,
-			transform: transform,
 		},
 
-		iknow: {
-			validator: $.optional.bool,
-			default: false,
-		}
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
 	},
 
 	res: {
-		type: 'object',
-		properties: {
-			users: {
-				type: 'array',
-				items: {
-					type: 'User',
-				}
-			},
-			next: {
-				type: 'string',
-				format: 'id',
-				nullable: true
-			}
-		}
+		type: 'array',
+		items: {
+			type: 'Following',
+		},
 	},
 
 	errors: {
@@ -79,54 +63,20 @@ export const meta = {
 };
 
 export default define(meta, async (ps, me) => {
-	const q: any = ps.userId != null
-		? { _id: ps.userId }
-		: { usernameLower: ps.username.toLowerCase(), host: ps.host };
-
-	const user = await User.findOne(q);
+	const user = await Users.findOne(ps.userId != null
+		? { id: ps.userId }
+		: { usernameLower: ps.username.toLowerCase(), host: ps.host });
 
-	if (user === null) {
+	if (user == null) {
 		throw new ApiError(meta.errors.noSuchUser);
 	}
 
-	const query = {
-		followeeId: user._id
-	} as any;
-
-	// ログインしていてかつ iknow フラグがあるとき
-	if (me && ps.iknow) {
-		// Get my friends
-		const myFriends = await getFriendIds(me._id);
-
-		query.followerId = {
-			$in: myFriends
-		};
-	}
-
-	// カーソルが指定されている場合
-	if (ps.cursor) {
-		query._id = {
-			$lt: ps.cursor
-		};
-	}
-
-	// Get followers
-	const following = await Following
-		.find(query, {
-			limit: ps.limit + 1,
-			sort: { _id: -1 }
-		});
-
-	// 「次のページ」があるかどうか
-	const inStock = following.length === ps.limit + 1;
-	if (inStock) {
-		following.pop();
-	}
+	const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
+		.andWhere(`following.followeeId = :userId`, { userId: user.id });
 
-	const users = await Promise.all(following.map(f => pack(f.followerId, me, { detail: true })));
+	const followings = await query
+		.take(ps.limit)
+		.getMany();
 
-	return {
-		users: users,
-		next: inStock ? following[following.length - 1]._id : null,
-	};
+	return await Followings.packMany(followings, me, { populateFollower: true });
 });
diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts
index 4bc740cad984a15a8b9470525c9596e825f7254f..46550f0f77a5dd28d612cb39a2e98607c8de1424 100644
--- a/src/server/api/endpoints/users/following.ts
+++ b/src/server/api/endpoints/users/following.ts
@@ -1,11 +1,9 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import User from '../../../../models/user';
-import Following from '../../../../models/following';
-import { pack } from '../../../../models/user';
-import { getFriendIds } from '../../common/get-friends';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
 import { ApiError } from '../../error';
+import { Users, Followings } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
 
 export const meta = {
 	desc: {
@@ -20,7 +18,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -35,38 +32,25 @@ export const meta = {
 			validator: $.optional.nullable.str
 		},
 
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
+		sinceId: {
+			validator: $.optional.type(ID),
 		},
 
-		cursor: {
+		untilId: {
 			validator: $.optional.type(ID),
-			default: null as any,
-			transform: transform,
 		},
 
-		iknow: {
-			validator: $.optional.bool,
-			default: false,
-		}
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
 	},
 
 	res: {
-		type: 'object',
-		properties: {
-			users: {
-				type: 'array',
-				items: {
-					type: 'User',
-				}
-			},
-			next: {
-				type: 'string',
-				format: 'id',
-				nullable: true
-			}
-		}
+		type: 'array',
+		items: {
+			type: 'Following',
+		},
 	},
 
 	errors: {
@@ -79,54 +63,20 @@ export const meta = {
 };
 
 export default define(meta, async (ps, me) => {
-	const q: any = ps.userId != null
-		? { _id: ps.userId }
-		: { usernameLower: ps.username.toLowerCase(), host: ps.host };
-
-	const user = await User.findOne(q);
+	const user = await Users.findOne(ps.userId != null
+		? { id: ps.userId }
+		: { usernameLower: ps.username.toLowerCase(), host: ps.host });
 
-	if (user === null) {
+	if (user == null) {
 		throw new ApiError(meta.errors.noSuchUser);
 	}
 
-	const query = {
-		followerId: user._id
-	} as any;
-
-	// ログインしていてかつ iknow フラグがあるとき
-	if (me && ps.iknow) {
-		// Get my friends
-		const myFriends = await getFriendIds(me._id);
-
-		query.followeeId = {
-			$in: myFriends
-		};
-	}
-
-	// カーソルが指定されている場合
-	if (ps.cursor) {
-		query._id = {
-			$lt: ps.cursor
-		};
-	}
-
-	// Get followers
-	const following = await Following
-		.find(query, {
-			limit: ps.limit + 1,
-			sort: { _id: -1 }
-		});
-
-	// 「次のページ」があるかどうか
-	const inStock = following.length === ps.limit + 1;
-	if (inStock) {
-		following.pop();
-	}
+	const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
+		.andWhere(`following.followerId = :userId`, { userId: user.id });
 
-	const users = await Promise.all(following.map(f => pack(f.followeeId, me, { detail: true })));
+	const followings = await query
+		.take(ps.limit)
+		.getMany();
 
-	return {
-		users: users,
-		next: inStock ? following[following.length - 1]._id : null,
-	};
+	return await Followings.packMany(followings, me, { populateFollowee: true });
 });
diff --git a/src/server/api/endpoints/users/get-frequently-replied-users.ts b/src/server/api/endpoints/users/get-frequently-replied-users.ts
index 46c7fba2f61f8325b6ba499d0d59220df39b79f3..f82f437629b8d89c59b73cbd91c895a4656bcbff 100644
--- a/src/server/api/endpoints/users/get-frequently-replied-users.ts
+++ b/src/server/api/endpoints/users/get-frequently-replied-users.ts
@@ -1,12 +1,11 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note from '../../../../models/note';
-import { pack } from '../../../../models/user';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
 import { maximum } from '../../../../prelude/array';
-import { getHideUserIds } from '../../common/get-hide-users';
 import { ApiError } from '../../error';
 import { getUser } from '../../common/getters';
+import { Not, In } from 'typeorm';
+import { Notes, Users } from '../../../../models';
 
 export const meta = {
 	tags: ['users'],
@@ -16,7 +15,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -53,21 +51,16 @@ export default define(meta, async (ps, me) => {
 	});
 
 	// Fetch recent notes
-	const recentNotes = await Note.find({
-		userId: user._id,
-		replyId: {
-			$exists: true,
-			$ne: null
-		}
-	}, {
-		sort: {
-			_id: -1
+	const recentNotes = await Notes.find({
+		where: {
+			userId: user.id,
+			replyId: Not(null)
 		},
-		limit: 1000,
-		fields: {
-			_id: false,
-			replyId: true
-		}
+		order: {
+			id: -1
+		},
+		take: 1000,
+		select: ['replyId']
 	});
 
 	// 投稿が少なかったら中断
@@ -75,21 +68,12 @@ export default define(meta, async (ps, me) => {
 		return [];
 	}
 
-	const hideUserIds = await getHideUserIds(me);
-	hideUserIds.push(user._id);
-
-	const replyTargetNotes = await Note.find({
-		_id: {
-			$in: recentNotes.map(p => p.replyId)
+	// TODO ミュートを考慮
+	const replyTargetNotes = await Notes.find({
+		where: {
+			id: In(recentNotes.map(p => p.replyId)),
 		},
-		userId: {
-			$nin: hideUserIds
-		}
-	}, {
-		fields: {
-			_id: false,
-			userId: true
-		}
+		select: ['userId']
 	});
 
 	const repliedUsers: any = {};
@@ -114,7 +98,7 @@ export default define(meta, async (ps, me) => {
 
 	// Make replies object (includes weights)
 	const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
-		user: await pack(user, me, { detail: true }),
+		user: await Users.pack(user, me, { detail: true }),
 		weight: repliedUsers[user] / peak
 	})));
 
diff --git a/src/server/api/endpoints/users/lists/create.ts b/src/server/api/endpoints/users/lists/create.ts
index 00d2538c9f67fb3d26ac186dc1e1dcac7960e53a..21dc6d331df11fe63a79e12265e5e1571ff899c0 100644
--- a/src/server/api/endpoints/users/lists/create.ts
+++ b/src/server/api/endpoints/users/lists/create.ts
@@ -1,6 +1,8 @@
 import $ from 'cafy';
-import UserList, { pack } from '../../../../../models/user-list';
 import define from '../../../define';
+import { UserLists } from '../../../../../models';
+import { genId } from '../../../../../misc/gen-id';
+import { UserList } from '../../../../../models/entities/user-list';
 
 export const meta = {
 	desc: {
@@ -12,7 +14,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account-write',
+	kind: 'write:account',
 
 	params: {
 		title: {
@@ -22,12 +24,12 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const userList = await UserList.insert({
+	const userList = await UserLists.save({
+		id: genId(),
 		createdAt: new Date(),
-		userId: user._id,
-		title: ps.title,
-		userIds: []
-	});
+		userId: user.id,
+		name: ps.title,
+	} as UserList);
 
-	return await pack(userList);
+	return await UserLists.pack(userList);
 });
diff --git a/src/server/api/endpoints/users/lists/delete.ts b/src/server/api/endpoints/users/lists/delete.ts
index d8faaa928cf483cd4d7b382bef0c0cfa061ad739..0634bca4e3b04a4345bd535f4ec8a97cbda9907f 100644
--- a/src/server/api/endpoints/users/lists/delete.ts
+++ b/src/server/api/endpoints/users/lists/delete.ts
@@ -1,8 +1,8 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import UserList from '../../../../../models/user-list';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
 import { ApiError } from '../../../error';
+import { UserLists } from '../../../../../models';
 
 export const meta = {
 	desc: {
@@ -14,12 +14,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account-write',
+	kind: 'write:account',
 
 	params: {
 		listId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象となるユーザーリストのID',
 				'en-US': 'ID of target user list'
@@ -37,16 +36,14 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
-	const userList = await UserList.findOne({
-		_id: ps.listId,
-		userId: user._id
+	const userList = await UserLists.findOne({
+		id: ps.listId,
+		userId: user.id
 	});
 
 	if (userList == null) {
 		throw new ApiError(meta.errors.noSuchList);
 	}
 
-	await UserList.remove({
-		_id: userList._id
-	});
+	await UserLists.delete(userList.id);
 });
diff --git a/src/server/api/endpoints/users/lists/list.ts b/src/server/api/endpoints/users/lists/list.ts
index ece2af5603c096f05a713e1be4a3c8a08766b30f..b05fc45527f9bcc6fd6183e522c2b0179b2f8830 100644
--- a/src/server/api/endpoints/users/lists/list.ts
+++ b/src/server/api/endpoints/users/lists/list.ts
@@ -1,5 +1,5 @@
-import UserList, { pack } from '../../../../../models/user-list';
 import define from '../../../define';
+import { UserLists } from '../../../../../models';
 
 export const meta = {
 	desc: {
@@ -10,7 +10,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account-read',
+	kind: 'read:account',
 
 	res: {
 		type: 'array',
@@ -21,9 +21,9 @@ export const meta = {
 };
 
 export default define(meta, async (ps, me) => {
-	const userLists = await UserList.find({
-		userId: me._id,
+	const userLists = await UserLists.find({
+		userId: me.id,
 	});
 
-	return await Promise.all(userLists.map(x => pack(x)));
+	return await Promise.all(userLists.map(x => UserLists.pack(x)));
 });
diff --git a/src/server/api/endpoints/users/lists/pull.ts b/src/server/api/endpoints/users/lists/pull.ts
index 0eee1975db44c7c5db8fd5bb983465a688e99bdb..524670b341703f5d3595339b656c16800c87adc6 100644
--- a/src/server/api/endpoints/users/lists/pull.ts
+++ b/src/server/api/endpoints/users/lists/pull.ts
@@ -1,11 +1,10 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import UserList from '../../../../../models/user-list';
-import { pack as packUser } from '../../../../../models/user';
+import { ID } from '../../../../../misc/cafy-id';
 import { publishUserListStream } from '../../../../../services/stream';
 import define from '../../../define';
 import { ApiError } from '../../../error';
 import { getUser } from '../../../common/getters';
+import { UserLists, UserListJoinings, Users } from '../../../../../models';
 
 export const meta = {
 	desc: {
@@ -17,17 +16,15 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account-write',
+	kind: 'write:account',
 
 	params: {
 		listId: {
 			validator: $.type(ID),
-			transform: transform,
 		},
 
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -52,9 +49,9 @@ export const meta = {
 
 export default define(meta, async (ps, me) => {
 	// Fetch the list
-	const userList = await UserList.findOne({
-		_id: ps.listId,
-		userId: me._id,
+	const userList = await UserLists.findOne({
+		id: ps.listId,
+		userId: me.id,
 	});
 
 	if (userList == null) {
@@ -68,11 +65,7 @@ export default define(meta, async (ps, me) => {
 	});
 
 	// Pull the user
-	await UserList.update({ _id: userList._id }, {
-		$pull: {
-			userIds: user._id
-		}
-	});
+	await UserListJoinings.delete({ userId: user.id });
 
-	publishUserListStream(userList._id, 'userRemoved', await packUser(user));
+	publishUserListStream(userList.id, 'userRemoved', await Users.pack(user));
 });
diff --git a/src/server/api/endpoints/users/lists/push.ts b/src/server/api/endpoints/users/lists/push.ts
index eea2f39a8cf7e8f54205318280acaa59200b765a..2763b3a19c62b648412f3498b6236066c2c36adc 100644
--- a/src/server/api/endpoints/users/lists/push.ts
+++ b/src/server/api/endpoints/users/lists/push.ts
@@ -1,10 +1,10 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import UserList from '../../../../../models/user-list';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
 import { ApiError } from '../../../error';
 import { getUser } from '../../../common/getters';
 import { pushUserToUserList } from '../../../../../services/user-list/push';
+import { UserLists, UserListJoinings } from '../../../../../models';
 
 export const meta = {
 	desc: {
@@ -16,17 +16,15 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account-write',
+	kind: 'write:account',
 
 	params: {
 		listId: {
 			validator: $.type(ID),
-			transform: transform,
 		},
 
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -57,9 +55,9 @@ export const meta = {
 
 export default define(meta, async (ps, me) => {
 	// Fetch the list
-	const userList = await UserList.findOne({
-		_id: ps.listId,
-		userId: me._id,
+	const userList = await UserLists.findOne({
+		id: ps.listId,
+		userId: me.id,
 	});
 
 	if (userList == null) {
@@ -72,7 +70,12 @@ export default define(meta, async (ps, me) => {
 		throw e;
 	});
 
-	if (userList.userIds.map(id => id.toHexString()).includes(user._id.toHexString())) {
+	const exist = await UserListJoinings.findOne({
+		userListId: userList.id,
+		userId: user.id
+	});
+
+	if (exist) {
 		throw new ApiError(meta.errors.alreadyAdded);
 	}
 
diff --git a/src/server/api/endpoints/users/lists/show.ts b/src/server/api/endpoints/users/lists/show.ts
index 0fab2fa4998700a756fb9a64acce3754b740b9c8..1a997ec7c5a52d8c9c27ba40c3c0b9319eb260c2 100644
--- a/src/server/api/endpoints/users/lists/show.ts
+++ b/src/server/api/endpoints/users/lists/show.ts
@@ -1,8 +1,8 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import UserList, { pack } from '../../../../../models/user-list';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
 import { ApiError } from '../../../error';
+import { UserLists } from '../../../../../models';
 
 export const meta = {
 	desc: {
@@ -14,12 +14,11 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account-read',
+	kind: 'read:account',
 
 	params: {
 		listId: {
 			validator: $.type(ID),
-			transform: transform,
 		},
 	},
 
@@ -38,14 +37,14 @@ export const meta = {
 
 export default define(meta, async (ps, me) => {
 	// Fetch the list
-	const userList = await UserList.findOne({
-		_id: ps.listId,
-		userId: me._id,
+	const userList = await UserLists.findOne({
+		id: ps.listId,
+		userId: me.id,
 	});
 
 	if (userList == null) {
 		throw new ApiError(meta.errors.noSuchList);
 	}
 
-	return await pack(userList);
+	return await UserLists.pack(userList);
 });
diff --git a/src/server/api/endpoints/users/lists/update.ts b/src/server/api/endpoints/users/lists/update.ts
index 58976931442dfc69cf22851260d6f25a9f8fc2cd..dc08d59f6a4a128777f89d6970e7be01cc17d1d9 100644
--- a/src/server/api/endpoints/users/lists/update.ts
+++ b/src/server/api/endpoints/users/lists/update.ts
@@ -1,8 +1,8 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../../misc/cafy-id';
-import UserList, { pack } from '../../../../../models/user-list';
+import { ID } from '../../../../../misc/cafy-id';
 import define from '../../../define';
 import { ApiError } from '../../../error';
+import { UserLists } from '../../../../../models';
 
 export const meta = {
 	desc: {
@@ -14,19 +14,18 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account-write',
+	kind: 'write:account',
 
 	params: {
 		listId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象となるユーザーリストのID',
 				'en-US': 'ID of target user list'
 			}
 		},
 
-		title: {
+		name: {
 			validator: $.str.range(1, 100),
 			desc: {
 				'ja-JP': 'このユーザーリストの名前',
@@ -46,20 +45,18 @@ export const meta = {
 
 export default define(meta, async (ps, user) => {
 	// Fetch the list
-	const userList = await UserList.findOne({
-		_id: ps.listId,
-		userId: user._id
+	const userList = await UserLists.findOne({
+		id: ps.listId,
+		userId: user.id
 	});
 
 	if (userList == null) {
 		throw new ApiError(meta.errors.noSuchList);
 	}
 
-	await UserList.update({ _id: userList._id }, {
-		$set: {
-			title: ps.title
-		}
+	await UserLists.update(userList.id, {
+		name: ps.name
 	});
 
-	return await pack(userList._id);
+	return await UserLists.pack(userList.id);
 });
diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts
index 10d2f37fc2ea11c2facaff496e2427b4f5401d70..6df394cbb11ece42e5acbc2b5aaf17cb9be4dcc5 100644
--- a/src/server/api/endpoints/users/notes.ts
+++ b/src/server/api/endpoints/users/notes.ts
@@ -1,10 +1,13 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
-import Note, { packMany } from '../../../../models/note';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
-import Following from '../../../../models/following';
 import { ApiError } from '../../error';
 import { getUser } from '../../common/getters';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { Notes } from '../../../../models';
+import { generateMuteQuery } from '../../common/generate-mute-query';
+import { Brackets } from 'typeorm';
 
 export const meta = {
 	desc: {
@@ -16,7 +19,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -42,17 +44,15 @@ export const meta = {
 
 		sinceId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 			desc: {
-				'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します'
+				'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します'
 			}
 		},
 
 		untilId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 			desc: {
-				'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
+				'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します'
 			}
 		},
 
@@ -102,15 +102,6 @@ export const meta = {
 			}
 		},
 
-		mediaOnly: {
-			validator: $.optional.bool,
-			default: false,
-			deprecated: true,
-			desc: {
-				'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
-			}
-		},
-
 		fileType: {
 			validator: $.optional.arr($.str),
 			desc: {
@@ -150,67 +141,44 @@ export default define(meta, async (ps, me) => {
 		throw e;
 	});
 
-	const isFollowing = me == null ? false : ((await Following.findOne({
-		followerId: me._id,
-		followeeId: user._id
-	})) != null);
-
 	//#region Construct query
-	const sort = { } as any;
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+		.andWhere('note.userId = :userId', { userId: user.id })
+		.leftJoinAndSelect('note.user', 'user');
 
-	const visibleQuery = me == null ? [{
-		visibility: { $in: ['public', 'home'] }
-	}] : [{
-		visibility: {
-			$in: isFollowing ? ['public', 'home', 'followers'] : ['public', 'home']
-		}
-	}, {
-		// myself (for specified/private)
-		userId: me._id
-	}, {
-		// to me (for specified)
-		visibleUserIds: { $in: [ me._id ] }
-	}];
+	if (me) generateVisibilityQuery(query, me);
+	if (me) generateMuteQuery(query, me);
+
+	if (ps.withFiles) {
+		query.andWhere('note.fileIds != \'{}\'');
+	}
 
-	const query = {
-		$and: [ {} ],
-		deletedAt: null,
-		userId: user._id,
-		$or: visibleQuery
-	} as any;
+	if (ps.fileType) {
+		query.andWhere('note.fileIds != \'{}\'');
+		query.andWhere(new Brackets(qb => {
+			for (const type of ps.fileType) {
+				const i = ps.fileType.indexOf(type);
+				qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
+			}
+		}));
 
-	if (ps.sinceId) {
-		sort._id = 1;
-		query._id = {
-			$gt: ps.sinceId
-		};
-	} else if (ps.untilId) {
-		sort._id = -1;
-		query._id = {
-			$lt: ps.untilId
-		};
-	} else if (ps.sinceDate) {
-		sort.createdAt = 1;
-		query.createdAt = {
-			$gt: new Date(ps.sinceDate)
-		};
-	} else if (ps.untilDate) {
-		sort.createdAt = -1;
-		query.createdAt = {
-			$lt: new Date(ps.untilDate)
-		};
-	} else {
-		sort._id = -1;
+		if (ps.excludeNsfw) {
+			// v11 TODO
+			/*query['_files.isSensitive'] = {
+				$ne: true
+			};*/
+		}
 	}
 
 	if (!ps.includeReplies) {
-		query.replyId = null;
+		query.andWhere('note.replyId IS NULL');
 	}
 
+	/* TODO
 	if (ps.includeMyRenotes === false) {
 		query.$and.push({
 			$or: [{
-				userId: { $ne: user._id }
+				userId: { $ne: user.id }
 			}, {
 				renoteId: null
 			}, {
@@ -222,35 +190,11 @@ export default define(meta, async (ps, me) => {
 			}]
 		});
 	}
+	*/
 
-	const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
-
-	if (withFiles) {
-		query.fileIds = {
-			$exists: true,
-			$ne: []
-		};
-	}
-
-	if (ps.fileType) {
-		query.fileIds = { $exists: true, $ne: [] };
-
-		query['_files.contentType'] = {
-			$in: ps.fileType
-		};
-
-		if (ps.excludeNsfw) {
-			query['_files.metadata.isSensitive'] = {
-				$ne: true
-			};
-		}
-	}
 	//#endregion
 
-	const notes = await Note.find(query, {
-		limit: ps.limit,
-		sort: sort
-	});
+	const timeline = await query.take(ps.limit).getMany();
 
-	return await packMany(notes, me);
+	return await Notes.packMany(timeline, user);
 });
diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts
index 60710fffca0e939d39c28d936263f06cbbbd6213..2c82d6613e915b584b2b0dd06c6da085b2e255e1 100644
--- a/src/server/api/endpoints/users/recommendation.ts
+++ b/src/server/api/endpoints/users/recommendation.ts
@@ -1,14 +1,8 @@
 import * as ms from 'ms';
 import $ from 'cafy';
-import User, { pack, ILocalUser } from '../../../../models/user';
-import { getFriendIds } from '../../common/get-friends';
-import * as request from 'request-promise-native';
-import config from '../../../../config';
 import define from '../../define';
-import fetchMeta from '../../../../misc/fetch-meta';
-import resolveUser from '../../../../remote/resolve-user';
-import { getHideUserIds } from '../../common/get-hide-users';
-import { apiLogger } from '../../logger';
+import { Users, Followings } from '../../../../models';
+import { generateMuteQueryForUsers } from '../../common/generate-mute-query';
 
 export const meta = {
 	desc: {
@@ -19,7 +13,7 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account-read',
+	kind: 'read:account',
 
 	params: {
 		limit: {
@@ -42,83 +36,24 @@ export const meta = {
 };
 
 export default define(meta, async (ps, me) => {
-	const instance = await fetchMeta();
+	const query = Users.createQueryBuilder('user')
+		.where('user.isLocked = FALSE')
+		.where('user.host IS NULL')
+		.where('user.updatedAt >= :date', { date: new Date(Date.now() - ms('7days')) })
+		.orderBy('user.followersCount', 'DESC');
 
-	if (instance.enableExternalUserRecommendation) {
-		const userName = me.username;
-		const hostName = config.hostname;
-		const limit = ps.limit;
-		const offset = ps.offset;
-		const timeout = instance.externalUserRecommendationTimeout;
-		const engine = instance.externalUserRecommendationEngine;
-		const url = engine
-			.replace('{{host}}', hostName)
-			.replace('{{user}}', userName)
-			.replace('{{limit}}', limit.toString())
-			.replace('{{offset}}', offset.toString());
+	generateMuteQueryForUsers(query, me);
 
-		const users = await request({
-			url: url,
-			proxy: config.proxy,
-			timeout: timeout,
-			json: true,
-			followRedirect: true,
-			followAllRedirects: true
-		})
-		.then(body => convertUsers(body, me));
+	const followingQuery = Followings.createQueryBuilder('following')
+		.select('following.followeeId')
+		.where('following.followerId = :followerId', { followerId: me.id });
 
-		return users;
-	} else {
-		// ID list of the user itself and other users who the user follows
-		const followingIds = await getFriendIds(me._id);
+	query
+		.andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`);
 
-		// 隠すユーザーを取得
-		const hideUserIds = await getHideUserIds(me);
+	query.setParameters(followingQuery.getParameters());
 
-		const users = await User.find({
-			_id: {
-				$nin: followingIds.concat(hideUserIds)
-			},
-			isLocked: { $ne: true },
-			updatedAt: {
-				$gte: new Date(Date.now() - ms('7days'))
-			},
-			host: null
-		}, {
-			limit: ps.limit,
-			skip: ps.offset,
-			sort: {
-				followersCount: -1
-			}
-		});
+	const users = await query.take(ps.limit).skip(ps.offset).getMany();
 
-		return await Promise.all(users.map(user => pack(user, me, { detail: true })));
-	}
+	return await Users.packMany(users, me, { detail: true });
 });
-
-type IRecommendUser = {
-	name: string;
-	username: string;
-	host: string;
-	description: string;
-	avatarUrl: string;
-};
-
-/**
- * Resolve/Pack dummy users
- */
-async function convertUsers(src: IRecommendUser[], me: ILocalUser) {
-	const packed = await Promise.all(src.map(async x => {
-		const user = await resolveUser(x.username, x.host)
-			.catch(() => {
-				apiLogger.warn(`Can't resolve ${x.username}@${x.host}`);
-				return null;
-			});
-
-		if (user == null) return x;
-
-		return await pack(user, me, { detail: true });
-	}));
-
-	return packed;
-}
diff --git a/src/server/api/endpoints/users/relation.ts b/src/server/api/endpoints/users/relation.ts
index f4121aa0d0b46dee99aad510ba3ca68116e2c1d3..4971738d32038b5b69f5a3c8c25ed31b61141f27 100644
--- a/src/server/api/endpoints/users/relation.ts
+++ b/src/server/api/endpoints/users/relation.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
-import ID, { transform, ObjectId } from '../../../../misc/cafy-id';
-import { getRelation } from '../../../../models/user';
 import define from '../../define';
+import { ID } from '../../../../misc/cafy-id';
+import { Users } from '../../../../models';
 
 export const meta = {
 	desc: {
@@ -15,7 +15,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.either($.type(ID), $.arr($.type(ID)).unique()),
-			transform: (v: any): ObjectId | ObjectId[] => Array.isArray(v) ? v.map(x => transform(x)) : transform(v),
 			desc: {
 				'ja-JP': 'ユーザーID (配列でも可)'
 			}
@@ -26,7 +25,7 @@ export const meta = {
 export default define(meta, async (ps, me) => {
 	const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId];
 
-	const relations = await Promise.all(ids.map(id => getRelation(me._id, id)));
+	const relations = await Promise.all(ids.map(id => Users.getRelation(me.id, id)));
 
 	return Array.isArray(ps.userId) ? relations : relations[0];
 });
diff --git a/src/server/api/endpoints/users/report-abuse.ts b/src/server/api/endpoints/users/report-abuse.ts
index 0f23f8f0c3886ba65ba3b0c74f2e1cfa188bb8d6..2ee28c90023e4dcc1f0cc6fe3166b3b1dedaea3b 100644
--- a/src/server/api/endpoints/users/report-abuse.ts
+++ b/src/server/api/endpoints/users/report-abuse.ts
@@ -1,11 +1,11 @@
 import $ from 'cafy';
-import ID, { transform } from '../../../../misc/cafy-id';
+import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
-import User from '../../../../models/user';
-import AbuseUserReport from '../../../../models/abuse-user-report';
 import { publishAdminStream } from '../../../../services/stream';
 import { ApiError } from '../../error';
 import { getUser } from '../../common/getters';
+import { AbuseUserReports, Users } from '../../../../models';
+import { genId } from '../../../../misc/gen-id';
 
 export const meta = {
 	desc: {
@@ -19,7 +19,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -62,7 +61,7 @@ export default define(meta, async (ps, me) => {
 		throw e;
 	});
 
-	if (user._id.equals(me._id)) {
+	if (user.id === me.id) {
 		throw new ApiError(meta.errors.cannotReportYourself);
 	}
 
@@ -70,17 +69,18 @@ export default define(meta, async (ps, me) => {
 		throw new ApiError(meta.errors.cannotReportAdmin);
 	}
 
-	const report = await AbuseUserReport.insert({
+	const report = await AbuseUserReports.save({
+		id: genId(),
 		createdAt: new Date(),
-		userId: user._id,
-		reporterId: me._id,
+		userId: user.id,
+		reporterId: me.id,
 		comment: ps.comment
 	});
 
 	// Publish event to moderators
 	setTimeout(async () => {
-		const moderators = await User.find({
-			$or: [{
+		const moderators = await Users.find({
+			where: [{
 				isAdmin: true
 			}, {
 				isModerator: true
@@ -88,8 +88,8 @@ export default define(meta, async (ps, me) => {
 		});
 
 		for (const moderator of moderators) {
-			publishAdminStream(moderator._id, 'newAbuseUserReport', {
-				id: report._id,
+			publishAdminStream(moderator.id, 'newAbuseUserReport', {
+				id: report.id,
 				userId: report.userId,
 				reporterId: report.reporterId,
 				comment: report.comment
diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts
index a95f6df6de195b0802e6a091c556392e382fb2d6..2e76546ade6297da32257e4abf25883fc10fe4d8 100644
--- a/src/server/api/endpoints/users/search.ts
+++ b/src/server/api/endpoints/users/search.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
-import * as escapeRegexp from 'escape-regexp';
-import User, { pack, validateUsername, IUser } from '../../../../models/user';
 import define from '../../define';
+import { Users } from '../../../../models';
+import { User } from '../../../../models/entities/user';
 
 export const meta = {
 	desc: {
@@ -62,34 +62,30 @@ export const meta = {
 };
 
 export default define(meta, async (ps, me) => {
-	const isUsername = validateUsername(ps.query.replace('@', ''), !ps.localOnly);
+	const isUsername = Users.validateUsername(ps.query.replace('@', ''), !ps.localOnly);
 
-	let users: IUser[] = [];
+	let users: User[] = [];
 
 	if (isUsername) {
-		users = await User
-			.find({
-				host: null,
-				usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase())),
-				isSuspended: { $ne: true }
-			}, {
-				limit: ps.limit,
-				skip: ps.offset
-			});
+		users = await Users.createQueryBuilder('user')
+			.where('user.host IS NULL')
+			.where('user.isSuspended = FALSE')
+			.where('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' })
+			.take(ps.limit)
+			.skip(ps.offset)
+			.getMany();
 
 		if (users.length < ps.limit && !ps.localOnly) {
-			const otherUsers = await User
-				.find({
-					host: { $ne: null },
-					usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase())),
-					isSuspended: { $ne: true }
-				}, {
-					limit: ps.limit - users.length
-				});
+			const otherUsers = await Users.createQueryBuilder('user')
+				.where('user.host IS NOT NULL')
+				.where('user.isSuspended = FALSE')
+				.where('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' })
+				.take(ps.limit - users.length)
+				.getMany();
 
 			users = users.concat(otherUsers);
 		}
 	}
 
-	return await Promise.all(users.map(user => pack(user, me, { detail: ps.detail })));
+	return await Users.packMany(users, me, { detail: ps.detail });
 });
diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts
index 4e59945eba19bf1d72108b1e0994dfadae6a6a71..a605eaf30aa35eb27e1ead99131d27effe5ca771 100644
--- a/src/server/api/endpoints/users/show.ts
+++ b/src/server/api/endpoints/users/show.ts
@@ -1,12 +1,11 @@
 import $ from 'cafy';
-import ID, { transform, transformMany } from '../../../../misc/cafy-id';
-import User, { pack, isRemoteUser } from '../../../../models/user';
 import resolveRemoteUser from '../../../../remote/resolve-user';
 import define from '../../define';
 import { apiLogger } from '../../logger';
 import { ApiError } from '../../error';
-
-const cursorOption = { fields: { data: false } };
+import { ID } from '../../../../misc/cafy-id';
+import { Users } from '../../../../models';
+import { In } from 'typeorm';
 
 export const meta = {
 	desc: {
@@ -20,7 +19,6 @@ export const meta = {
 	params: {
 		userId: {
 			validator: $.optional.type(ID),
-			transform: transform,
 			desc: {
 				'ja-JP': '対象のユーザーのID',
 				'en-US': 'Target user ID'
@@ -29,7 +27,6 @@ export const meta = {
 
 		userIds: {
 			validator: $.optional.arr($.type(ID)).unique(),
-			transform: transformMany,
 			desc: {
 				'ja-JP': 'ユーザーID (配列)'
 			}
@@ -68,42 +65,40 @@ export default define(meta, async (ps, me) => {
 	let user;
 
 	if (ps.userIds) {
-		const users = await User.find({
-			_id: {
-				$in: ps.userIds
-			}
+		const users = await Users.find({
+			id: In(ps.userIds)
 		});
 
-		return await Promise.all(users.map(u => pack(u, me, {
+		return await Promise.all(users.map(u => Users.pack(u, me, {
 			detail: true
 		})));
 	} else {
 		// Lookup user
 		if (typeof ps.host === 'string') {
-			user = await resolveRemoteUser(ps.username, ps.host, cursorOption).catch(e => {
+			user = await resolveRemoteUser(ps.username, ps.host).catch(e => {
 				apiLogger.warn(`failed to resolve remote user: ${e}`);
 				throw new ApiError(meta.errors.failedToResolveRemoteUser);
 			});
 		} else {
 			const q: any = ps.userId != null
-				? { _id: ps.userId }
+				? { id: ps.userId }
 				: { usernameLower: ps.username.toLowerCase(), host: null };
 
-			user = await User.findOne(q, cursorOption);
+			user = await Users.findOne(q);
 		}
 
-		if (user === null) {
+		if (user == null) {
 			throw new ApiError(meta.errors.noSuchUser);
 		}
 
 		// ユーザー情報更新
-		if (isRemoteUser(user)) {
+		if (Users.isRemoteUser(user)) {
 			if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
 				resolveRemoteUser(ps.username, ps.host, { }, true);
 			}
 		}
 
-		return await pack(user, me, {
+		return await Users.pack(user, me, {
 			detail: true
 		});
 	}
diff --git a/src/server/api/index.ts b/src/server/api/index.ts
index fac57ca06ee9c59ece703ae548b18ea4b2bd3545..7858efd92793a7cde498f78330f1086e658636b4 100644
--- a/src/server/api/index.ts
+++ b/src/server/api/index.ts
@@ -15,8 +15,8 @@ import signin from './private/signin';
 import discord from './service/discord';
 import github from './service/github';
 import twitter from './service/twitter';
-import Instance from '../../models/instance';
 import { toASCII } from 'punycode';
+import { Instances } from '../../models';
 
 // Init app
 const app = new Koa();
@@ -67,10 +67,9 @@ router.use(github.routes());
 router.use(twitter.routes());
 
 router.get('/v1/instance/peers', async ctx => {
-	const instances = await Instance.find({
-		}, {
-			host: 1
-		});
+	const instances = await Instances.find({
+		select: ['host']
+	});
 
 	const punyCodes = instances.map(instance => toASCII(instance.host));
 
diff --git a/src/server/api/limiter.ts b/src/server/api/limiter.ts
index 3d66172fd8bc316772ac1878226117b812d73232..e29c061337dcb9daea0512a603a47d2c879c7124 100644
--- a/src/server/api/limiter.ts
+++ b/src/server/api/limiter.ts
@@ -2,12 +2,12 @@ import * as Limiter from 'ratelimiter';
 import limiterDB from '../../db/redis';
 import { IEndpoint } from './endpoints';
 import getAcct from '../../misc/acct/render';
-import { IUser } from '../../models/user';
+import { User } from '../../models/entities/user';
 import Logger from '../../services/logger';
 
 const logger = new Logger('limiter');
 
-export default (endpoint: IEndpoint, user: IUser) => new Promise((ok, reject) => {
+export default (endpoint: IEndpoint, user: User) => new Promise((ok, reject) => {
 	// Redisがインストールされてない場合は常に許可
 	if (limiterDB == null) {
 		ok();
@@ -38,7 +38,7 @@ export default (endpoint: IEndpoint, user: IUser) => new Promise((ok, reject) =>
 	// Short-term limit
 	function min() {
 		const minIntervalLimiter = new Limiter({
-			id: `${user._id}:${key}:min`,
+			id: `${user.id}:${key}:min`,
 			duration: limitation.minInterval,
 			max: 1,
 			db: limiterDB
@@ -66,7 +66,7 @@ export default (endpoint: IEndpoint, user: IUser) => new Promise((ok, reject) =>
 	// Long term limit
 	function max() {
 		const limiter = new Limiter({
-			id: `${user._id}:${key}`,
+			id: `${user.id}:${key}`,
 			duration: limitation.duration,
 			max: limitation.max,
 			db: limiterDB
diff --git a/src/server/api/openapi/schemas.ts b/src/server/api/openapi/schemas.ts
index 70a0d6faf008d590e0c71bf52262ee0eb1b1c780..5992fee835968a4ed732e1d6b355ec8782e914a6 100644
--- a/src/server/api/openapi/schemas.ts
+++ b/src/server/api/openapi/schemas.ts
@@ -221,7 +221,7 @@ export const schemas = {
 			},
 			type: {
 				type: 'string',
-				enum: ['follow', 'receiveFollowRequest', 'mention', 'reply', 'renote', 'quote', 'reaction', 'poll_vote'],
+				enum: ['follow', 'receiveFollowRequest', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote'],
 				description: 'The type of the notification.'
 			},
 		},
@@ -258,7 +258,7 @@ export const schemas = {
 				description: 'The MD5 hash of this Drive file.',
 				example: '15eca7fba0480996e2245f5185bf39f2'
 			},
-			datasize: {
+			size: {
 				type: 'number',
 				description: 'The size of this Drive file. (bytes)',
 				example: 51469
@@ -275,7 +275,7 @@ export const schemas = {
 				description: 'Whether this Drive file is sensitive.',
 			},
 		},
-		required: ['id', 'createdAt', 'name', 'type', 'datasize', 'md5']
+		required: ['id', 'createdAt', 'name', 'type', 'size', 'md5']
 	},
 
 	DriveFolder: {
@@ -318,6 +318,40 @@ export const schemas = {
 		required: ['id', 'createdAt', 'name']
 	},
 
+	Following: {
+		type: 'object',
+		properties: {
+			id: {
+				type: 'string',
+				format: 'id',
+				description: 'The unique identifier for this following.',
+				example: 'xxxxxxxxxxxxxxxxxxxxxxxx',
+			},
+			createdAt: {
+				type: 'string',
+				format: 'date-time',
+				description: 'The date that the following was created.'
+			},
+			followeeId: {
+				type: 'string',
+				format: 'id',
+			},
+			followee: {
+				$ref: '#/components/schemas/User',
+				description: 'The followee.'
+			},
+			followerId: {
+				type: 'string',
+				format: 'id',
+			},
+			follower: {
+				$ref: '#/components/schemas/User',
+				description: 'The follower.'
+			},
+		},
+		required: ['id', 'createdAt', 'followeeId', 'followerId']
+	},
+
 	Muting: {
 		type: 'object',
 		properties: {
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index 40bcd2c5d6653dbf8b5bb4b90e67c5c4410812a1..c1fd908d8a402ad7af729ec09d89b5a8e7303051 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -1,11 +1,12 @@
 import * as Koa from 'koa';
 import * as bcrypt from 'bcryptjs';
 import * as speakeasy from 'speakeasy';
-import User, { ILocalUser } from '../../../models/user';
-import Signin, { pack } from '../../../models/signin';
 import { publishMainStream } from '../../../services/stream';
 import signin from '../common/signin';
 import config from '../../../config';
+import { Users, Signins } from '../../../models';
+import { ILocalUser } from '../../../models/entities/user';
+import { genId } from '../../../misc/gen-id';
 
 export default async (ctx: Koa.BaseContext) => {
 	ctx.set('Access-Control-Allow-Origin', config.url);
@@ -32,17 +33,12 @@ export default async (ctx: Koa.BaseContext) => {
 	}
 
 	// Fetch user
-	const user = await User.findOne({
+	const user = await Users.findOne({
 		usernameLower: username.toLowerCase(),
 		host: null
-	}, {
-			fields: {
-				data: false,
-				profile: false
-			}
-		}) as ILocalUser;
+	}) as ILocalUser;
 
-	if (user === null) {
+	if (user == null) {
 		ctx.throw(404, {
 			error: 'user not found'
 		});
@@ -77,14 +73,15 @@ export default async (ctx: Koa.BaseContext) => {
 	}
 
 	// Append signin history
-	const record = await Signin.insert({
+	const record = await Signins.save({
+		id: genId(),
 		createdAt: new Date(),
-		userId: user._id,
+		userId: user.id,
 		ip: ctx.ip,
 		headers: ctx.headers,
 		success: same
 	});
 
 	// Publish signin event
-	publishMainStream(user._id, 'signin', await pack(record));
+	publishMainStream(user.id, 'signin', await Signins.pack(record));
 };
diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index 89b7b330d21512fee4bd4e3f75a7c18502a02db7..1d304b8e11c996a3fe055f07bf9cf2c9d005a0d6 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -1,14 +1,15 @@
 import * as Koa from 'koa';
 import * as bcrypt from 'bcryptjs';
-import { generate as generateKeypair } from '../../../crypto_key';
-import User, { IUser, validateUsername, validatePassword, pack } from '../../../models/user';
+import { generateKeyPair } from 'crypto';
 import generateUserToken from '../common/generate-native-user-token';
 import config from '../../../config';
-import Meta from '../../../models/meta';
-import RegistrationTicket from '../../../models/registration-tickets';
-import usersChart from '../../../services/chart/users';
 import fetchMeta from '../../../misc/fetch-meta';
 import * as recaptcha from 'recaptcha-promise';
+import { Users, RegistrationTickets, UserServiceLinkings, UserKeypairs } from '../../../models';
+import { genId } from '../../../misc/gen-id';
+import { usersChart } from '../../../services/chart';
+import { UserServiceLinking } from '../../../models/entities/user-service-linking';
+import { User } from '../../../models/entities/user';
 
 export default async (ctx: Koa.BaseContext) => {
 	const body = ctx.request.body as any;
@@ -32,6 +33,7 @@ export default async (ctx: Koa.BaseContext) => {
 
 	const username = body['username'];
 	const password = body['password'];
+	const host = process.env.NODE_ENV === 'test' ? (body['host'] || null) : null;
 	const invitationCode = body['invitationCode'];
 
 	if (instance && instance.disableRegistration) {
@@ -40,7 +42,7 @@ export default async (ctx: Koa.BaseContext) => {
 			return;
 		}
 
-		const ticket = await RegistrationTicket.findOne({
+		const ticket = await RegistrationTickets.findOne({
 			code: invitationCode
 		});
 
@@ -49,39 +51,22 @@ export default async (ctx: Koa.BaseContext) => {
 			return;
 		}
 
-		RegistrationTicket.remove({
-			_id: ticket._id
-		});
+		RegistrationTickets.delete(ticket.id);
 	}
 
 	// Validate username
-	if (!validateUsername(username)) {
+	if (!Users.validateUsername(username)) {
 		ctx.status = 400;
 		return;
 	}
 
 	// Validate password
-	if (!validatePassword(password)) {
+	if (!Users.validatePassword(password)) {
 		ctx.status = 400;
 		return;
 	}
 
-	const usersCount = await User.count({});
-
-	// Fetch exist user that same username
-	const usernameExist = await User
-		.count({
-			usernameLower: username.toLowerCase(),
-			host: null
-		}, {
-			limit: 1
-		});
-
-	// Check username already used
-	if (usernameExist !== 0) {
-		ctx.status = 400;
-		return;
-	}
+	const usersCount = await Users.count({});
 
 	// Generate hash of password
 	const salt = await bcrypt.genSalt(8);
@@ -90,46 +75,50 @@ export default async (ctx: Koa.BaseContext) => {
 	// Generate secret
 	const secret = generateUserToken();
 
-	// Create account
-	const account: IUser = await User.insert({
-		avatarId: null,
-		bannerId: null,
+	if (await Users.findOne({ usernameLower: username.toLowerCase(), host: null })) {
+		ctx.status = 400;
+		return;
+	}
+
+	const account = await Users.save({
+		id: genId(),
 		createdAt: new Date(),
-		description: null,
-		followersCount: 0,
-		followingCount: 0,
-		name: null,
-		notesCount: 0,
 		username: username,
 		usernameLower: username.toLowerCase(),
-		host: null,
-		keypair: generateKeypair(),
+		host: host,
 		token: secret,
 		password: hash,
 		isAdmin: config.autoAdmin && usersCount === 0,
 		autoAcceptFollowed: true,
-		profile: {
-			bio: null,
-			birthday: null,
-			location: null
-		},
-		settings: {
-			autoWatch: false
-		}
+		autoWatch: false
+	} as User);
+
+	await UserKeypairs.save({
+		id: genId(),
+		keyPem: await new Promise<string>((s, j) => generateKeyPair('rsa', {
+			modulusLength: 4096,
+			publicKeyEncoding: {
+				type: 'pkcs1',
+				format: 'pem'
+			},
+			privateKeyEncoding: {
+				type: 'pkcs1',
+				format: 'pem',
+				cipher: undefined,
+				passphrase: undefined
+			}
+		}, (e, _, x) => e ? j(e) : s(x))),
+		userId: account.id
 	});
 
-	//#region Increment users count
-	Meta.update({}, {
-		$inc: {
-			'stats.usersCount': 1,
-			'stats.originalUsersCount': 1
-		}
-	}, { upsert: true });
-	//#endregion
+	await UserServiceLinkings.save({
+		id: genId(),
+		userId: account.id
+	} as UserServiceLinking);
 
 	usersChart.update(account, true);
 
-	const res = await pack(account, account, {
+	const res = await Users.pack(account, account, {
 		detail: true,
 		includeSecrets: true
 	});
diff --git a/src/server/api/service/discord.ts b/src/server/api/service/discord.ts
index 92f5bbf72d9e994be6378f2a493c41ad7f0547ea..4290e1ff9d9a3d60e8df705a1e59e9137c226c58 100644
--- a/src/server/api/service/discord.ts
+++ b/src/server/api/service/discord.ts
@@ -2,13 +2,14 @@ import * as Koa from 'koa';
 import * as Router from 'koa-router';
 import * as request from 'request';
 import { OAuth2 } from 'oauth';
-import User, { pack, ILocalUser } from '../../../models/user';
 import config from '../../../config';
 import { publishMainStream } from '../../../services/stream';
 import redis from '../../../db/redis';
 import * as uuid from 'uuid';
 import signin from '../common/signin';
 import fetchMeta from '../../../misc/fetch-meta';
+import { Users, UserServiceLinkings } from '../../../models';
+import { ILocalUser } from '../../../models/entities/user';
 
 function getUserToken(ctx: Koa.BaseContext) {
 	return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1];
@@ -39,19 +40,27 @@ router.get('/disconnect/discord', async ctx => {
 		return;
 	}
 
-	const user = await User.findOneAndUpdate({
+	const user = await Users.findOne({
 		host: null,
-		'token': userToken
+		token: userToken
+	});
+
+	await UserServiceLinkings.update({
+		userId: user.id
 	}, {
-		$set: {
-			'discord': null
-		}
+		discord: false,
+		discordAccessToken: null,
+		discordRefreshToken: null,
+		discordExpiresDate: null,
+		discordId: null,
+		discordUsername: null,
+		discordDiscriminator: null,
 	});
 
 	ctx.body = `Discordの連携を解除しました :v:`;
 
 	// Publish i updated event
-	publishMainStream(user._id, 'meUpdated', await pack(user, user, {
+	publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
 		detail: true,
 		includeSecrets: true
 	}));
@@ -193,32 +202,30 @@ router.get('/dc/cb', async ctx => {
 			return;
 		}
 
-		let user = await User.findOne({
-			host: null,
-			'discord.id': id
-		}) as ILocalUser;
+		const link = await UserServiceLinkings.createQueryBuilder()
+			.where('discord @> :discord', {
+				discord: {
+					id: id,
+				},
+			})
+			.andWhere('userHost IS NULL')
+			.getOne();
 
-		if (!user) {
+		if (link == null) {
 			ctx.throw(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`);
 			return;
 		}
 
-		user = await User.findOneAndUpdate({
-			host: null,
-			'discord.id': id
-		}, {
-			$set: {
-				discord: {
-					accessToken,
-					refreshToken,
-					expiresDate,
-					username,
-					discriminator
-				}
-			}
-		}) as ILocalUser;
+		await UserServiceLinkings.update(link.id, {
+			discord: true,
+			discordAccessToken: accessToken,
+			discordRefreshToken: refreshToken,
+			discordExpiresDate: expiresDate,
+			discordUsername: username,
+			discordDiscriminator: discriminator
+		});
 
-		signin(ctx, user, true);
+		signin(ctx, await Users.findOne(link.userId) as ILocalUser, true);
 	} else {
 		const code = ctx.query.code;
 
@@ -277,26 +284,25 @@ router.get('/dc/cb', async ctx => {
 			return;
 		}
 
-		const user = await User.findOneAndUpdate({
+		const user = await Users.findOne({
 			host: null,
 			token: userToken
-		}, {
-			$set: {
-				discord: {
-					accessToken,
-					refreshToken,
-					expiresDate,
-					id,
-					username,
-					discriminator
-				}
-			}
+		});
+
+		await UserServiceLinkings.update({ userId: user.id }, {
+			discord: true,
+			discordAccessToken: accessToken,
+			discordRefreshToken: refreshToken,
+			discordExpiresDate: expiresDate,
+			discordId: id,
+			discordUsername: username,
+			discordDiscriminator: discriminator
 		});
 
 		ctx.body = `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました!`;
 
 		// Publish i updated event
-		publishMainStream(user._id, 'meUpdated', await pack(user, user, {
+		publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
 			detail: true,
 			includeSecrets: true
 		}));
diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts
index cf3589a4b7136edf6f9d5c764b235995a15e9442..e59b149d19b1432ad4851d8a5f19d95b9569738a 100644
--- a/src/server/api/service/github.ts
+++ b/src/server/api/service/github.ts
@@ -2,13 +2,14 @@ import * as Koa from 'koa';
 import * as Router from 'koa-router';
 import * as request from 'request';
 import { OAuth2 } from 'oauth';
-import User, { pack, ILocalUser } from '../../../models/user';
 import config from '../../../config';
 import { publishMainStream } from '../../../services/stream';
 import redis from '../../../db/redis';
 import * as uuid from 'uuid';
 import signin from '../common/signin';
 import fetchMeta from '../../../misc/fetch-meta';
+import { Users, UserServiceLinkings } from '../../../models';
+import { ILocalUser } from '../../../models/entities/user';
 
 function getUserToken(ctx: Koa.BaseContext) {
 	return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1];
@@ -39,19 +40,24 @@ router.get('/disconnect/github', async ctx => {
 		return;
 	}
 
-	const user = await User.findOneAndUpdate({
+	const user = await Users.findOne({
 		host: null,
-		'token': userToken
+		token: userToken
+	});
+
+	await UserServiceLinkings.update({
+		userId: user.id
 	}, {
-		$set: {
-			'github': null
-		}
+		github: false,
+		githubAccessToken: null,
+		githubId: null,
+		githubLogin: null,
 	});
 
 	ctx.body = `GitHubの連携を解除しました :v:`;
 
 	// Publish i updated event
-	publishMainStream(user._id, 'meUpdated', await pack(user, user, {
+	publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
 		detail: true,
 		includeSecrets: true
 	}));
@@ -185,17 +191,21 @@ router.get('/gh/cb', async ctx => {
 			return;
 		}
 
-		const user = await User.findOne({
-			host: null,
-			'github.id': id
-		}) as ILocalUser;
+		const link = await UserServiceLinkings.createQueryBuilder()
+			.where('github @> :github', {
+				github: {
+					id: id,
+				},
+			})
+			.andWhere('userHost IS NULL')
+			.getOne();
 
-		if (!user) {
+		if (link == null) {
 			ctx.throw(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`);
 			return;
 		}
 
-		signin(ctx, user, true);
+		signin(ctx, await Users.findOne(link.userId) as ILocalUser, true);
 	} else {
 		const code = ctx.query.code;
 
@@ -248,23 +258,22 @@ router.get('/gh/cb', async ctx => {
 			return;
 		}
 
-		const user = await User.findOneAndUpdate({
+		const user = await Users.findOne({
 			host: null,
 			token: userToken
-		}, {
-			$set: {
-				github: {
-					accessToken,
-					id,
-					login
-				}
-			}
+		});
+
+		await UserServiceLinkings.update({ userId: user.id }, {
+			github: true,
+			githubAccessToken: accessToken,
+			githubId: id,
+			githubLogin: login,
 		});
 
 		ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`;
 
 		// Publish i updated event
-		publishMainStream(user._id, 'meUpdated', await pack(user, user, {
+		publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
 			detail: true,
 			includeSecrets: true
 		}));
diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts
index fc23808e2156b81a8c0539b2f48d3d3d8ba052a8..77cf71395bf941868ed537231e7a330c37abdb73 100644
--- a/src/server/api/service/twitter.ts
+++ b/src/server/api/service/twitter.ts
@@ -3,11 +3,12 @@ import * as Router from 'koa-router';
 import * as uuid from 'uuid';
 import autwh from 'autwh';
 import redis from '../../../db/redis';
-import User, { pack, ILocalUser } from '../../../models/user';
 import { publishMainStream } from '../../../services/stream';
 import config from '../../../config';
 import signin from '../common/signin';
 import fetchMeta from '../../../misc/fetch-meta';
+import { Users, UserServiceLinkings } from '../../../models';
+import { ILocalUser } from '../../../models/entities/user';
 
 function getUserToken(ctx: Koa.BaseContext) {
 	return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1];
@@ -38,19 +39,25 @@ router.get('/disconnect/twitter', async ctx => {
 		return;
 	}
 
-	const user = await User.findOneAndUpdate({
+	const user = await Users.findOne({
 		host: null,
-		'token': userToken
+		token: userToken
+	});
+
+	await UserServiceLinkings.update({
+		userId: user.id
 	}, {
-		$set: {
-			'twitter': null
-		}
+		twitter: false,
+		twitterAccessToken: null,
+		twitterAccessTokenSecret: null,
+		twitterUserId: null,
+		twitterScreenName: null,
 	});
 
 	ctx.body = `Twitterの連携を解除しました :v:`;
 
 	// Publish i updated event
-	publishMainStream(user._id, 'meUpdated', await pack(user, user, {
+	publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
 		detail: true,
 		includeSecrets: true
 	}));
@@ -132,17 +139,21 @@ router.get('/tw/cb', async ctx => {
 
 		const result = await twAuth.done(JSON.parse(twCtx), ctx.query.oauth_verifier);
 
-		const user = await User.findOne({
-			host: null,
-			'twitter.userId': result.userId
-		}) as ILocalUser;
+		const link = await UserServiceLinkings.createQueryBuilder()
+			.where('twitter @> :twitter', {
+				twitter: {
+					userId: result.userId,
+				},
+			})
+			.andWhere('userHost IS NULL')
+			.getOne();
 
-		if (user == null) {
+		if (link == null) {
 			ctx.throw(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`);
 			return;
 		}
 
-		signin(ctx, user, true);
+		signin(ctx, await Users.findOne(link.userId) as ILocalUser, true);
 	} else {
 		const verifier = ctx.query.oauth_verifier;
 
@@ -161,24 +172,23 @@ router.get('/tw/cb', async ctx => {
 
 		const result = await twAuth.done(JSON.parse(twCtx), verifier);
 
-		const user = await User.findOneAndUpdate({
+		const user = await Users.findOne({
 			host: null,
 			token: userToken
-		}, {
-			$set: {
-				twitter: {
-					accessToken: result.accessToken,
-					accessTokenSecret: result.accessTokenSecret,
-					userId: result.userId,
-					screenName: result.screenName
-				}
-			}
+		});
+
+		await UserServiceLinkings.update({ userId: user.id }, {
+			twitter: true,
+			twitterAccessToken: result.accessToken,
+			twitterAccessTokenSecret: result.accessTokenSecret,
+			twitterUserId: result.userId,
+			twitterScreenName: result.screenName,
 		});
 
 		ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`;
 
 		// Publish i updated event
-		publishMainStream(user._id, 'meUpdated', await pack(user, user, {
+		publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
 			detail: true,
 			includeSecrets: true
 		}));
diff --git a/src/server/api/stream/channel.ts b/src/server/api/stream/channel.ts
index bdbe4605cf8852b3a70ad931b9ff96a1845dc9b3..18fa6518201bb098b195a063475698c23610d811 100644
--- a/src/server/api/stream/channel.ts
+++ b/src/server/api/stream/channel.ts
@@ -15,6 +15,14 @@ export default abstract class Channel {
 		return this.connection.user;
 	}
 
+	protected get following() {
+		return this.connection.following;
+	}
+
+	protected get muting() {
+		return this.connection.muting;
+	}
+
 	protected get subscriber() {
 		return this.connection.subscriber;
 	}
diff --git a/src/server/api/stream/channels/admin.ts b/src/server/api/stream/channels/admin.ts
index 6bcd1a7e0baf3db95d6361af28a51763c7a9fdce..e2eba10f7867fe5073d67369a2ad8e7b0801774e 100644
--- a/src/server/api/stream/channels/admin.ts
+++ b/src/server/api/stream/channels/admin.ts
@@ -9,7 +9,7 @@ export default class extends Channel {
 	@autobind
 	public async init(params: any) {
 		// Subscribe admin stream
-		this.subscriber.on(`adminStream:${this.user._id}`, data => {
+		this.subscriber.on(`adminStream:${this.user.id}`, data => {
 			this.send(data);
 		});
 	}
diff --git a/src/server/api/stream/channels/drive.ts b/src/server/api/stream/channels/drive.ts
index 391c4b5c325a093369a5018a936de38dc1f2153a..671aad43669f96655f1bc22003ecef0d3af129b6 100644
--- a/src/server/api/stream/channels/drive.ts
+++ b/src/server/api/stream/channels/drive.ts
@@ -9,7 +9,7 @@ export default class extends Channel {
 	@autobind
 	public async init(params: any) {
 		// Subscribe drive stream
-		this.subscriber.on(`driveStream:${this.user._id}`, data => {
+		this.subscriber.on(`driveStream:${this.user.id}`, data => {
 			this.send(data);
 		});
 	}
diff --git a/src/server/api/stream/channels/games/reversi-game.ts b/src/server/api/stream/channels/games/reversi-game.ts
index 87df9e194c6936cd84bd63ff491f59649c5d5700..158f108c4e1d90248584ee006568182000f5ad77 100644
--- a/src/server/api/stream/channels/games/reversi-game.ts
+++ b/src/server/api/stream/channels/games/reversi-game.ts
@@ -1,22 +1,22 @@
 import autobind from 'autobind-decorator';
 import * as CRC32 from 'crc-32';
-import * as mongo from 'mongodb';
-import ReversiGame, { pack } from '../../../../../models/games/reversi/game';
 import { publishReversiGameStream } from '../../../../../services/stream';
 import Reversi from '../../../../../games/reversi/core';
 import * as maps from '../../../../../games/reversi/maps';
 import Channel from '../../channel';
+import { ReversiGame } from '../../../../../models/entities/games/reversi/game';
+import { ReversiGames } from '../../../../../models';
 
 export default class extends Channel {
 	public readonly chName = 'gamesReversiGame';
 	public static shouldShare = false;
 	public static requireCredential = false;
 
-	private gameId: mongo.ObjectID;
+	private gameId: ReversiGame['id'];
 
 	@autobind
 	public async init(params: any) {
-		this.gameId = new mongo.ObjectID(params.gameId as string);
+		this.gameId = params.gameId;
 
 		// Subscribe game stream
 		this.subscriber.on(`reversiGameStream:${this.gameId}`, data => {
@@ -29,7 +29,7 @@ export default class extends Channel {
 		switch (type) {
 			case 'accept': this.accept(true); break;
 			case 'cancelAccept': this.accept(false); break;
-			case 'updateSettings': this.updateSettings(body.settings); break;
+			case 'updateSettings': this.updateSettings(body.key, body.value); break;
 			case 'initForm': this.initForm(body); break;
 			case 'updateForm': this.updateForm(body.id, body.value); break;
 			case 'message': this.message(body); break;
@@ -39,54 +39,55 @@ export default class extends Channel {
 	}
 
 	@autobind
-	private async updateSettings(settings: any) {
-		const game = await ReversiGame.findOne({ _id: this.gameId });
+	private async updateSettings(key: string, value: any) {
+		const game = await ReversiGames.findOne(this.gameId);
 
 		if (game.isStarted) return;
-		if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return;
-		if (game.user1Id.equals(this.user._id) && game.user1Accepted) return;
-		if (game.user2Id.equals(this.user._id) && game.user2Accepted) return;
+		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
+		if ((game.user1Id === this.user.id) && game.user1Accepted) return;
+		if ((game.user2Id === this.user.id) && game.user2Accepted) return;
 
-		await ReversiGame.update({ _id: this.gameId }, {
-			$set: {
-				settings
-			}
+		if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard'].includes(key)) return;
+
+		await ReversiGames.update({ id: this.gameId }, {
+			[key]: value
 		});
 
-		publishReversiGameStream(this.gameId, 'updateSettings', settings);
+		publishReversiGameStream(this.gameId, 'updateSettings', {
+			key: key,
+			value: value
+		});
 	}
 
 	@autobind
 	private async initForm(form: any) {
-		const game = await ReversiGame.findOne({ _id: this.gameId });
+		const game = await ReversiGames.findOne(this.gameId);
 
 		if (game.isStarted) return;
-		if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return;
+		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
 
-		const set = game.user1Id.equals(this.user._id) ? {
+		const set = game.user1Id === this.user.id ? {
 			form1: form
 		} : {
-				form2: form
-			};
+			form2: form
+		};
 
-		await ReversiGame.update({ _id: this.gameId }, {
-			$set: set
-		});
+		await ReversiGames.update({ id: this.gameId }, set);
 
 		publishReversiGameStream(this.gameId, 'initForm', {
-			userId: this.user._id,
+			userId: this.user.id,
 			form
 		});
 	}
 
 	@autobind
 	private async updateForm(id: string, value: any) {
-		const game = await ReversiGame.findOne({ _id: this.gameId });
+		const game = await ReversiGames.findOne({ id: this.gameId });
 
 		if (game.isStarted) return;
-		if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return;
+		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
 
-		const form = game.user1Id.equals(this.user._id) ? game.form2 : game.form1;
+		const form = game.user1Id === this.user.id ? game.form2 : game.form1;
 
 		const item = form.find((i: any) => i.id == id);
 
@@ -94,18 +95,16 @@ export default class extends Channel {
 
 		item.value = value;
 
-		const set = game.user1Id.equals(this.user._id) ? {
+		const set = game.user1Id === this.user.id ? {
 			form2: form
 		} : {
 				form1: form
 			};
 
-		await ReversiGame.update({ _id: this.gameId }, {
-			$set: set
-		});
+		await ReversiGames.update({ id: this.gameId }, set);
 
 		publishReversiGameStream(this.gameId, 'updateForm', {
-			userId: this.user._id,
+			userId: this.user.id,
 			id,
 			value
 		});
@@ -115,24 +114,22 @@ export default class extends Channel {
 	private async message(message: any) {
 		message.id = Math.random();
 		publishReversiGameStream(this.gameId, 'message', {
-			userId: this.user._id,
+			userId: this.user.id,
 			message
 		});
 	}
 
 	@autobind
 	private async accept(accept: boolean) {
-		const game = await ReversiGame.findOne({ _id: this.gameId });
+		const game = await ReversiGames.findOne(this.gameId);
 
 		if (game.isStarted) return;
 
 		let bothAccepted = false;
 
-		if (game.user1Id.equals(this.user._id)) {
-			await ReversiGame.update({ _id: this.gameId }, {
-				$set: {
-					user1Accepted: accept
-				}
+		if (game.user1Id === this.user.id) {
+			await ReversiGames.update({ id: this.gameId }, {
+				user1Accepted: accept
 			});
 
 			publishReversiGameStream(this.gameId, 'changeAccepts', {
@@ -141,11 +138,9 @@ export default class extends Channel {
 			});
 
 			if (accept && game.user2Accepted) bothAccepted = true;
-		} else if (game.user2Id.equals(this.user._id)) {
-			await ReversiGame.update({ _id: this.gameId }, {
-				$set: {
-					user2Accepted: accept
-				}
+		} else if (game.user2Id === this.user.id) {
+			await ReversiGames.update({ id: this.gameId }, {
+				user2Accepted: accept
 			});
 
 			publishReversiGameStream(this.gameId, 'changeAccepts', {
@@ -161,15 +156,15 @@ export default class extends Channel {
 		if (bothAccepted) {
 			// 3秒後、まだacceptされていたらゲーム開始
 			setTimeout(async () => {
-				const freshGame = await ReversiGame.findOne({ _id: this.gameId });
+				const freshGame = await ReversiGames.findOne(this.gameId);
 				if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return;
 				if (!freshGame.user1Accepted || !freshGame.user2Accepted) return;
 
 				let bw: number;
-				if (freshGame.settings.bw == 'random') {
+				if (freshGame.bw == 'random') {
 					bw = Math.random() > 0.5 ? 1 : 2;
 				} else {
-					bw = freshGame.settings.bw as number;
+					bw = parseInt(freshGame.bw, 10);
 				}
 
 				function getRandomMap() {
@@ -178,22 +173,20 @@ export default class extends Channel {
 					return Object.values(maps)[rnd].data;
 				}
 
-				const map = freshGame.settings.map != null ? freshGame.settings.map : getRandomMap();
+				const map = freshGame.map != null ? freshGame.map : getRandomMap();
 
-				await ReversiGame.update({ _id: this.gameId }, {
-					$set: {
-						startedAt: new Date(),
-						isStarted: true,
-						black: bw,
-						'settings.map': map
-					}
+				await ReversiGames.update({ id: this.gameId }, {
+					startedAt: new Date(),
+					isStarted: true,
+					black: bw,
+					map: map
 				});
 
 				//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
 				const o = new Reversi(map, {
-					isLlotheo: freshGame.settings.isLlotheo,
-					canPutEverywhere: freshGame.settings.canPutEverywhere,
-					loopedBoard: freshGame.settings.loopedBoard
+					isLlotheo: freshGame.isLlotheo,
+					canPutEverywhere: freshGame.canPutEverywhere,
+					loopedBoard: freshGame.loopedBoard
 				});
 
 				if (o.isEnded) {
@@ -206,23 +199,22 @@ export default class extends Channel {
 						winner = null;
 					}
 
-					await ReversiGame.update({
-						_id: this.gameId
+					await ReversiGames.update({
+						id: this.gameId
 					}, {
-							$set: {
-								isEnded: true,
-								winnerId: winner
-							}
-						});
+						isEnded: true,
+						winnerId: winner
+					});
 
 					publishReversiGameStream(this.gameId, 'ended', {
 						winnerId: winner,
-						game: await pack(this.gameId, this.user)
+						game: await ReversiGames.pack(this.gameId, this.user)
 					});
 				}
 				//#endregion
 
-				publishReversiGameStream(this.gameId, 'started', await pack(this.gameId, this.user));
+				publishReversiGameStream(this.gameId, 'started',
+					await ReversiGames.pack(this.gameId, this.user));
 			}, 3000);
 		}
 	}
@@ -230,16 +222,16 @@ export default class extends Channel {
 	// 石を打つ
 	@autobind
 	private async set(pos: number) {
-		const game = await ReversiGame.findOne({ _id: this.gameId });
+		const game = await ReversiGames.findOne(this.gameId);
 
 		if (!game.isStarted) return;
 		if (game.isEnded) return;
-		if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return;
+		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
 
-		const o = new Reversi(game.settings.map, {
-			isLlotheo: game.settings.isLlotheo,
-			canPutEverywhere: game.settings.canPutEverywhere,
-			loopedBoard: game.settings.loopedBoard
+		const o = new Reversi(game.map, {
+			isLlotheo: game.isLlotheo,
+			canPutEverywhere: game.canPutEverywhere,
+			loopedBoard: game.loopedBoard
 		});
 
 		for (const log of game.logs) {
@@ -247,7 +239,7 @@ export default class extends Channel {
 		}
 
 		const myColor =
-			(game.user1Id.equals(this.user._id) && game.black == 1) || (game.user2Id.equals(this.user._id) && game.black == 2)
+			((game.user1Id === this.user.id) && game.black == 1) || ((game.user2Id === this.user.id) && game.black == 2)
 				? true
 				: false;
 
@@ -271,20 +263,18 @@ export default class extends Channel {
 			pos
 		};
 
-		const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString());
+		const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()).toString();
 
-		await ReversiGame.update({
-			_id: this.gameId
+		game.logs.push(log);
+
+		await ReversiGames.update({
+			id: this.gameId
 		}, {
-				$set: {
-					crc32,
-					isEnded: o.isEnded,
-					winnerId: winner
-				},
-				$push: {
-					logs: log
-				}
-			});
+			crc32,
+			isEnded: o.isEnded,
+			winnerId: winner,
+			logs: game.logs
+		});
 
 		publishReversiGameStream(this.gameId, 'set', Object.assign(log, {
 			next: o.turn
@@ -293,14 +283,14 @@ export default class extends Channel {
 		if (o.isEnded) {
 			publishReversiGameStream(this.gameId, 'ended', {
 				winnerId: winner,
-				game: await pack(this.gameId, this.user)
+				game: await ReversiGames.pack(this.gameId, this.user)
 			});
 		}
 	}
 
 	@autobind
 	private async check(crc32: string) {
-		const game = await ReversiGame.findOne({ _id: this.gameId });
+		const game = await ReversiGames.findOne(this.gameId);
 
 		if (!game.isStarted) return;
 
@@ -308,7 +298,7 @@ export default class extends Channel {
 		if (game.crc32 == null) return;
 
 		if (crc32 !== game.crc32) {
-			this.send('rescue', await pack(game, this.user));
+			this.send('rescue', await ReversiGames.pack(game, this.user));
 		}
 	}
 }
diff --git a/src/server/api/stream/channels/games/reversi.ts b/src/server/api/stream/channels/games/reversi.ts
index 1b1ad187a35f916607bb252281ae85795e46c3c2..0498e5e0177d9476df21440c4b818c63db5a5ff4 100644
--- a/src/server/api/stream/channels/games/reversi.ts
+++ b/src/server/api/stream/channels/games/reversi.ts
@@ -1,8 +1,7 @@
 import autobind from 'autobind-decorator';
-import * as mongo from 'mongodb';
-import Matching, { pack } from '../../../../../models/games/reversi/matching';
 import { publishMainStream } from '../../../../../services/stream';
 import Channel from '../../channel';
+import { ReversiMatchings } from '../../../../../models';
 
 export default class extends Channel {
 	public readonly chName = 'gamesReversi';
@@ -12,7 +11,7 @@ export default class extends Channel {
 	@autobind
 	public async init(params: any) {
 		// Subscribe reversi stream
-		this.subscriber.on(`reversiStream:${this.user._id}`, data => {
+		this.subscriber.on(`reversiStream:${this.user.id}`, data => {
 			this.send(data);
 		});
 	}
@@ -22,12 +21,12 @@ export default class extends Channel {
 		switch (type) {
 			case 'ping':
 				if (body.id == null) return;
-				const matching = await Matching.findOne({
-					parentId: this.user._id,
-					childId: new mongo.ObjectID(body.id)
+				const matching = await ReversiMatchings.findOne({
+					parentId: this.user.id,
+					childId: body.id
 				});
 				if (matching == null) return;
-				publishMainStream(matching.childId, 'reversiInvited', await pack(matching, matching.childId));
+				publishMainStream(matching.childId, 'reversiInvited', await ReversiMatchings.pack(matching, matching.childId));
 				break;
 		}
 	}
diff --git a/src/server/api/stream/channels/global-timeline.ts b/src/server/api/stream/channels/global-timeline.ts
index b3689d47f57f843abfd047c26501263c553d43b0..bfb7697ba7689d41a77d3893a45e220584e0cf68 100644
--- a/src/server/api/stream/channels/global-timeline.ts
+++ b/src/server/api/stream/channels/global-timeline.ts
@@ -1,17 +1,14 @@
 import autobind from 'autobind-decorator';
-import Mute from '../../../../models/mute';
-import { pack } from '../../../../models/note';
 import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
 import Channel from '../channel';
 import fetchMeta from '../../../../misc/fetch-meta';
+import { Notes } from '../../../../models';
 
 export default class extends Channel {
 	public readonly chName = 'globalTimeline';
 	public static shouldShare = true;
 	public static requireCredential = false;
 
-	private mutedUserIds: string[] = [];
-
 	@autobind
 	public async init(params: any) {
 		const meta = await fetchMeta();
@@ -20,29 +17,26 @@ export default class extends Channel {
 		}
 
 		// Subscribe events
-		this.subscriber.on('globalTimeline', this.onNote);
-
-		const mute = await Mute.find({ muterId: this.user._id });
-		this.mutedUserIds = mute.map(m => m.muteeId.toString());
+		this.subscriber.on('notesStream', this.onNote);
 	}
 
 	@autobind
 	private async onNote(note: any) {
 		// リプライなら再pack
 		if (note.replyId != null) {
-			note.reply = await pack(note.replyId, this.user, {
+			note.reply = await Notes.pack(note.replyId, this.user, {
 				detail: true
 			});
 		}
 		// Renoteなら再pack
 		if (note.renoteId != null) {
-			note.renote = await pack(note.renoteId, this.user, {
+			note.renote = await Notes.pack(note.renoteId, this.user, {
 				detail: true
 			});
 		}
 
 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (shouldMuteThisNote(note, this.mutedUserIds)) return;
+		if (shouldMuteThisNote(note, this.muting)) return;
 
 		this.send('note', note);
 	}
@@ -50,6 +44,6 @@ export default class extends Channel {
 	@autobind
 	public dispose() {
 		// Unsubscribe events
-		this.subscriber.off('globalTimeline', this.onNote);
+		this.subscriber.off('notesStream', this.onNote);
 	}
 }
diff --git a/src/server/api/stream/channels/hashtag.ts b/src/server/api/stream/channels/hashtag.ts
index 586ce02f0666c98d1c11c94b8c59f60cc2aada74..36c56c7ab6bee77071d270dbd48774a8abb7aecc 100644
--- a/src/server/api/stream/channels/hashtag.ts
+++ b/src/server/api/stream/channels/hashtag.ts
@@ -1,40 +1,46 @@
 import autobind from 'autobind-decorator';
-import Mute from '../../../../models/mute';
-import { pack } from '../../../../models/note';
 import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
 import Channel from '../channel';
+import { Notes } from '../../../../models';
 
 export default class extends Channel {
 	public readonly chName = 'hashtag';
 	public static shouldShare = false;
 	public static requireCredential = false;
+	private q: string[][];
 
 	@autobind
 	public async init(params: any) {
-		const mute = this.user ? await Mute.find({ muterId: this.user._id }) : null;
-		const mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : [];
+		this.q = params.q;
 
-		const q: string[][] = params.q;
-
-		if (q == null) return;
+		if (this.q == null) return;
 
 		// Subscribe stream
-		this.subscriber.on('hashtag', async note => {
-			const noteTags = note.tags.map((t: string) => t.toLowerCase());
-			const matched = q.some(tags => tags.every(tag => noteTags.includes(tag.toLowerCase())));
-			if (!matched) return;
-
-			// Renoteなら再pack
-			if (note.renoteId != null) {
-				note.renote = await pack(note.renoteId, this.user, {
-					detail: true
-				});
-			}
-
-			// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-			if (shouldMuteThisNote(note, mutedUserIds)) return;
-
-			this.send('note', note);
-		});
+		this.subscriber.on('notesStream', this.onNote);
+	}
+
+	@autobind
+	private async onNote(note: any) {
+		const noteTags = note.tags.map((t: string) => t.toLowerCase());
+		const matched = this.q.some(tags => tags.every(tag => noteTags.includes(tag.toLowerCase())));
+		if (!matched) return;
+
+		// Renoteなら再pack
+		if (note.renoteId != null) {
+			note.renote = await Notes.pack(note.renoteId, this.user, {
+				detail: true
+			});
+		}
+
+		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+		if (shouldMuteThisNote(note, this.muting)) return;
+
+		this.send('note', note);
+	}
+
+	@autobind
+	public dispose() {
+		// Unsubscribe events
+		this.subscriber.off('notesStream', this.onNote);
 	}
 }
diff --git a/src/server/api/stream/channels/home-timeline.ts b/src/server/api/stream/channels/home-timeline.ts
index 3c0b23872047bbdfc73c6d031bfcabe1ec34d317..2cece0947f376f8ccafe32a718c9f267916a3a08 100644
--- a/src/server/api/stream/channels/home-timeline.ts
+++ b/src/server/api/stream/channels/home-timeline.ts
@@ -1,42 +1,49 @@
 import autobind from 'autobind-decorator';
-import Mute from '../../../../models/mute';
-import { pack } from '../../../../models/note';
 import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
 import Channel from '../channel';
+import { Notes } from '../../../../models';
 
 export default class extends Channel {
 	public readonly chName = 'homeTimeline';
 	public static shouldShare = true;
 	public static requireCredential = true;
 
-	private mutedUserIds: string[] = [];
-
 	@autobind
 	public async init(params: any) {
 		// Subscribe events
-		this.subscriber.on(`homeTimeline:${this.user._id}`, this.onNote);
-
-		const mute = await Mute.find({ muterId: this.user._id });
-		this.mutedUserIds = mute.map(m => m.muteeId.toString());
+		this.subscriber.on('notesStream', this.onNote);
 	}
 
 	@autobind
 	private async onNote(note: any) {
-		// リプライなら再pack
-		if (note.replyId != null) {
-			note.reply = await pack(note.replyId, this.user, {
-				detail: true
-			});
-		}
-		// Renoteなら再pack
-		if (note.renoteId != null) {
-			note.renote = await pack(note.renoteId, this.user, {
+		// その投稿のユーザーをフォローしていなかったら弾く
+		if (this.user.id !== note.userId && !this.following.includes(note.userId)) return;
+
+		if (['followers', 'specified'].includes(note.visibility)) {
+			note = await Notes.pack(note.id, this.user, {
 				detail: true
 			});
+
+			if (note.isHidden) {
+				return;
+			}
+		} else {
+			// リプライなら再pack
+			if (note.replyId != null) {
+				note.reply = await Notes.pack(note.replyId, this.user, {
+					detail: true
+				});
+			}
+			// Renoteなら再pack
+			if (note.renoteId != null) {
+				note.renote = await Notes.pack(note.renoteId, this.user, {
+					detail: true
+				});
+			}
 		}
 
 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (shouldMuteThisNote(note, this.mutedUserIds)) return;
+		if (shouldMuteThisNote(note, this.muting)) return;
 
 		this.send('note', note);
 	}
@@ -44,6 +51,6 @@ export default class extends Channel {
 	@autobind
 	public dispose() {
 		// Unsubscribe events
-		this.subscriber.off(`homeTimeline:${this.user._id}`, this.onNote);
+		this.subscriber.off('notesStream', this.onNote);
 	}
 }
diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts
deleted file mode 100644
index 35ef17b56baa1d22916b98d5c1e76f05a1f03154..0000000000000000000000000000000000000000
--- a/src/server/api/stream/channels/hybrid-timeline.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import autobind from 'autobind-decorator';
-import Mute from '../../../../models/mute';
-import { pack } from '../../../../models/note';
-import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
-import Channel from '../channel';
-import fetchMeta from '../../../../misc/fetch-meta';
-
-export default class extends Channel {
-	public readonly chName = 'hybridTimeline';
-	public static shouldShare = true;
-	public static requireCredential = true;
-
-	private mutedUserIds: string[] = [];
-
-	@autobind
-	public async init(params: any) {
-		const meta = await fetchMeta();
-		if (meta.disableLocalTimeline && !this.user.isAdmin && !this.user.isModerator) return;
-
-		// Subscribe events
-		this.subscriber.on('hybridTimeline', this.onNewNote);
-		this.subscriber.on(`hybridTimeline:${this.user._id}`, this.onNewNote);
-
-		const mute = await Mute.find({ muterId: this.user._id });
-		this.mutedUserIds = mute.map(m => m.muteeId.toString());
-	}
-
-	@autobind
-	private async onNewNote(note: any) {
-		// リプライなら再pack
-		if (note.replyId != null) {
-			note.reply = await pack(note.replyId, this.user, {
-				detail: true
-			});
-		}
-		// Renoteなら再pack
-		if (note.renoteId != null) {
-			note.renote = await pack(note.renoteId, this.user, {
-				detail: true
-			});
-		}
-
-		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (shouldMuteThisNote(note, this.mutedUserIds)) return;
-
-		this.send('note', note);
-	}
-
-	@autobind
-	public dispose() {
-		// Unsubscribe events
-		this.subscriber.off('hybridTimeline', this.onNewNote);
-		this.subscriber.off(`hybridTimeline:${this.user._id}`, this.onNewNote);
-	}
-}
diff --git a/src/server/api/stream/channels/index.ts b/src/server/api/stream/channels/index.ts
index 4527fb1e46dd0ecfb456b892a193e4a9096aad83..199ab0a80986ede310e31133a509f73148e3c547 100644
--- a/src/server/api/stream/channels/index.ts
+++ b/src/server/api/stream/channels/index.ts
@@ -1,7 +1,7 @@
 import main from './main';
 import homeTimeline from './home-timeline';
 import localTimeline from './local-timeline';
-import hybridTimeline from './hybrid-timeline';
+import socialTimeline from './social-timeline';
 import globalTimeline from './global-timeline';
 import notesStats from './notes-stats';
 import serverStats from './server-stats';
@@ -20,7 +20,7 @@ export default {
 	main,
 	homeTimeline,
 	localTimeline,
-	hybridTimeline,
+	socialTimeline,
 	globalTimeline,
 	notesStats,
 	serverStats,
diff --git a/src/server/api/stream/channels/local-timeline.ts b/src/server/api/stream/channels/local-timeline.ts
index 34020231922bca90a43d457e6b7f9b9fc3f91848..4aec2d66b4028cab2234e127a792ef967ec54578 100644
--- a/src/server/api/stream/channels/local-timeline.ts
+++ b/src/server/api/stream/channels/local-timeline.ts
@@ -1,17 +1,14 @@
 import autobind from 'autobind-decorator';
-import Mute from '../../../../models/mute';
-import { pack } from '../../../../models/note';
 import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
 import Channel from '../channel';
 import fetchMeta from '../../../../misc/fetch-meta';
+import { Notes } from '../../../../models';
 
 export default class extends Channel {
 	public readonly chName = 'localTimeline';
 	public static shouldShare = true;
 	public static requireCredential = false;
 
-	private mutedUserIds: string[] = [];
-
 	@autobind
 	public async init(params: any) {
 		const meta = await fetchMeta();
@@ -20,29 +17,39 @@ export default class extends Channel {
 		}
 
 		// Subscribe events
-		this.subscriber.on('localTimeline', this.onNote);
-
-		const mute = this.user ? await Mute.find({ muterId: this.user._id }) : null;
-		this.mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : [];
+		this.subscriber.on('notesStream', this.onNote);
 	}
 
 	@autobind
 	private async onNote(note: any) {
-		// リプライなら再pack
-		if (note.replyId != null) {
-			note.reply = await pack(note.replyId, this.user, {
-				detail: true
-			});
-		}
-		// Renoteなら再pack
-		if (note.renoteId != null) {
-			note.renote = await pack(note.renoteId, this.user, {
+		if (note.user.host !== null) return;
+		if (note.visibility === 'home') return;
+
+		if (['followers', 'specified'].includes(note.visibility)) {
+			note = await Notes.pack(note.id, this.user, {
 				detail: true
 			});
+
+			if (note.isHidden) {
+				return;
+			}
+		} else {
+			// リプライなら再pack
+			if (note.replyId != null) {
+				note.reply = await Notes.pack(note.replyId, this.user, {
+					detail: true
+				});
+			}
+			// Renoteなら再pack
+			if (note.renoteId != null) {
+				note.renote = await Notes.pack(note.renoteId, this.user, {
+					detail: true
+				});
+			}
 		}
 
 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (shouldMuteThisNote(note, this.mutedUserIds)) return;
+		if (shouldMuteThisNote(note, this.muting)) return;
 
 		this.send('note', note);
 	}
@@ -50,6 +57,6 @@ export default class extends Channel {
 	@autobind
 	public dispose() {
 		// Unsubscribe events
-		this.subscriber.off('localTimeline', this.onNote);
+		this.subscriber.off('notesStream', this.onNote);
 	}
 }
diff --git a/src/server/api/stream/channels/main.ts b/src/server/api/stream/channels/main.ts
index 175d914fa54ca4258e8c5cdc651efce51901257f..0d9bf3149d44a70fd0cdf50aef1a7471dc855262 100644
--- a/src/server/api/stream/channels/main.ts
+++ b/src/server/api/stream/channels/main.ts
@@ -1,6 +1,6 @@
 import autobind from 'autobind-decorator';
-import Mute from '../../../../models/mute';
 import Channel from '../channel';
+import { Mutings } from '../../../../models';
 
 export default class extends Channel {
 	public readonly chName = 'main';
@@ -9,16 +9,15 @@ export default class extends Channel {
 
 	@autobind
 	public async init(params: any) {
-		const mute = await Mute.find({ muterId: this.user._id });
-		const mutedUserIds = mute.map(m => m.muteeId.toString());
+		const mute = await Mutings.find({ muterId: this.user.id });
 
 		// Subscribe main stream channel
-		this.subscriber.on(`mainStream:${this.user._id}`, async data => {
+		this.subscriber.on(`mainStream:${this.user.id}`, async data => {
 			const { type, body } = data;
 
 			switch (type) {
 				case 'notification': {
-					if (mutedUserIds.includes(body.userId)) return;
+					if (mute.map(m => m.muteeId).includes(body.userId)) return;
 					if (body.note && body.note.isHidden) return;
 					break;
 				}
diff --git a/src/server/api/stream/channels/messaging-index.ts b/src/server/api/stream/channels/messaging-index.ts
index 148ff7f93508e59fa89b834e0fc74817daca8a43..648badc1dc169598f1ac3ba399e3857d3cb99e6b 100644
--- a/src/server/api/stream/channels/messaging-index.ts
+++ b/src/server/api/stream/channels/messaging-index.ts
@@ -9,7 +9,7 @@ export default class extends Channel {
 	@autobind
 	public async init(params: any) {
 		// Subscribe messaging index stream
-		this.subscriber.on(`messagingIndexStream:${this.user._id}`, data => {
+		this.subscriber.on(`messagingIndexStream:${this.user.id}`, data => {
 			this.send(data);
 		});
 	}
diff --git a/src/server/api/stream/channels/messaging.ts b/src/server/api/stream/channels/messaging.ts
index 0d81b4e45c12facf680213876bcf2e79e763a349..b81fbb9d4c857e462a3d88d1ceb67830a50d0d44 100644
--- a/src/server/api/stream/channels/messaging.ts
+++ b/src/server/api/stream/channels/messaging.ts
@@ -14,7 +14,7 @@ export default class extends Channel {
 		this.otherpartyId = params.otherparty as string;
 
 		// Subscribe messaging stream
-		this.subscriber.on(`messagingStream:${this.user._id}-${this.otherpartyId}`, data => {
+		this.subscriber.on(`messagingStream:${this.user.id}-${this.otherpartyId}`, data => {
 			this.send(data);
 		});
 	}
@@ -23,7 +23,7 @@ export default class extends Channel {
 	public onMessage(type: string, body: any) {
 		switch (type) {
 			case 'read':
-				read(this.user._id, this.otherpartyId, body.id);
+				read(this.user.id, this.otherpartyId, body.id);
 				break;
 		}
 	}
diff --git a/src/server/api/stream/channels/social-timeline.ts b/src/server/api/stream/channels/social-timeline.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1d76eed297c4a3bb4fc86af12cff1a54723e6084
--- /dev/null
+++ b/src/server/api/stream/channels/social-timeline.ts
@@ -0,0 +1,64 @@
+import autobind from 'autobind-decorator';
+import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
+import Channel from '../channel';
+import fetchMeta from '../../../../misc/fetch-meta';
+import { Notes } from '../../../../models';
+
+export default class extends Channel {
+	public readonly chName = 'socialTimeline';
+	public static shouldShare = true;
+	public static requireCredential = true;
+
+	@autobind
+	public async init(params: any) {
+		const meta = await fetchMeta();
+		if (meta.disableLocalTimeline && !this.user.isAdmin && !this.user.isModerator) return;
+
+		// Subscribe events
+		this.subscriber.on('notesStream', this.onNote);
+	}
+
+	@autobind
+	private async onNote(note: any) {
+		// 自分自身の投稿 または その投稿のユーザーをフォローしている または ローカルの投稿 の場合だけ
+		if (!(
+			this.user.id === note.userId ||
+			this.following.includes(note.userId) ||
+			note.user.host === null
+		)) return;
+
+		if (['followers', 'specified'].includes(note.visibility)) {
+			note = await Notes.pack(note.id, this.user, {
+				detail: true
+			});
+
+			if (note.isHidden) {
+				return;
+			}
+		} else {
+			// リプライなら再pack
+			if (note.replyId != null) {
+				note.reply = await Notes.pack(note.replyId, this.user, {
+					detail: true
+				});
+			}
+			// Renoteなら再pack
+			if (note.renoteId != null) {
+				note.renote = await Notes.pack(note.renoteId, this.user, {
+					detail: true
+				});
+			}
+	}
+
+		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+		if (shouldMuteThisNote(note, this.muting)) return;
+
+		this.send('note', note);
+	}
+
+	@autobind
+	public dispose() {
+		// Unsubscribe events
+		this.subscriber.off('notesStream', this.onNote);
+	}
+}
diff --git a/src/server/api/stream/channels/user-list.ts b/src/server/api/stream/channels/user-list.ts
index 5debf4177048dea96c2001705564d1ce048220c7..f5434b8f0862485f239c7d2f72e4b9c56df04334 100644
--- a/src/server/api/stream/channels/user-list.ts
+++ b/src/server/api/stream/channels/user-list.ts
@@ -1,23 +1,81 @@
 import autobind from 'autobind-decorator';
 import Channel from '../channel';
-import { pack } from '../../../../models/note';
+import { Notes, UserListJoinings } from '../../../../models';
+import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
+import { User } from '../../../../models/entities/user';
 
 export default class extends Channel {
 	public readonly chName = 'userList';
 	public static shouldShare = false;
 	public static requireCredential = false;
+	private listId: string;
+	public listUsers: User['id'][] = [];
+	private listUsersClock: NodeJS.Timer;
 
 	@autobind
 	public async init(params: any) {
-		const listId = params.listId as string;
+		this.listId = params.listId as string;
 
 		// Subscribe stream
-		this.subscriber.on(`userListStream:${listId}`, async data => {
-			// 再パック
-			if (data.type == 'note') data.body = await pack(data.body.id, this.user, {
+		this.subscriber.on(`userListStream:${this.listId}`, this.send);
+
+		this.subscriber.on('notesStream', this.onNote);
+
+		this.updateListUsers();
+		this.listUsersClock = setInterval(this.updateListUsers, 5000);
+	}
+
+	@autobind
+	private async updateListUsers() {
+		const users = await UserListJoinings.find({
+			where: {
+				userListId: this.listId,
+			},
+			select: ['userId']
+		});
+
+		this.listUsers = users.map(x => x.userId);
+	}
+
+	@autobind
+	private async onNote(note: any) {
+		if (!this.listUsers.includes(note.userId)) return;
+
+		if (['followers', 'specified'].includes(note.visibility)) {
+			note = await Notes.pack(note.id, this.user, {
 				detail: true
 			});
-			this.send(data);
-		});
+
+			if (note.isHidden) {
+				return;
+			}
+		} else {
+			// リプライなら再pack
+			if (note.replyId != null) {
+				note.reply = await Notes.pack(note.replyId, this.user, {
+					detail: true
+				});
+			}
+			// Renoteなら再pack
+			if (note.renoteId != null) {
+				note.renote = await Notes.pack(note.renoteId, this.user, {
+					detail: true
+				});
+			}
+		}
+
+		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+		if (shouldMuteThisNote(note, this.muting)) return;
+
+		this.send('note', note);
+	}
+
+	@autobind
+	public dispose() {
+		// Unsubscribe events
+		this.subscriber.off(`userListStream:${this.listId}`, this.send);
+		this.subscriber.off('notesStream', this.onNote);
+
+		clearInterval(this.listUsersClock);
 	}
 }
diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts
index 22f7646cb9ee5f19e9bbea0c2f46c8bc7f11cdb3..abbd91ec811a89389fedc08e3d950ae71a1352c2 100644
--- a/src/server/api/stream/index.ts
+++ b/src/server/api/stream/index.ts
@@ -1,33 +1,35 @@
 import autobind from 'autobind-decorator';
 import * as websocket from 'websocket';
-
-import User, { IUser } from '../../../models/user';
-import readNotification from '../common/read-notification';
+import { readNotification } from '../common/read-notification';
 import call from '../call';
-import { IApp } from '../../../models/app';
 import readNote from '../../../services/note/read';
-
 import Channel from './channel';
 import channels from './channels';
 import { EventEmitter } from 'events';
+import { User } from '../../../models/entities/user';
+import { App } from '../../../models/entities/app';
+import { Users, Followings, Mutings } from '../../../models';
 
 /**
  * Main stream connection
  */
 export default class Connection {
-	public user?: IUser;
-	public app: IApp;
+	public user?: User;
+	public following: User['id'][] = [];
+	public muting: User['id'][] = [];
+	public app: App;
 	private wsConnection: websocket.connection;
 	public subscriber: EventEmitter;
 	private channels: Channel[] = [];
 	private subscribingNotes: any = {};
-	public sendMessageToWsOverride: any = null; // 後方互換性のため
+	private followingClock: NodeJS.Timer;
+	private mutingClock: NodeJS.Timer;
 
 	constructor(
 		wsConnection: websocket.connection,
 		subscriber: EventEmitter,
-		user: IUser,
-		app: IApp
+		user: User,
+		app: App
 	) {
 		this.wsConnection = wsConnection;
 		this.user = user;
@@ -35,6 +37,14 @@ export default class Connection {
 		this.subscriber = subscriber;
 
 		this.wsConnection.on('message', this.onWsConnectionMessage);
+
+		if (this.user) {
+			this.updateFollowing();
+			this.followingClock = setInterval(this.updateFollowing, 5000);
+
+			this.updateMuting();
+			this.mutingClock = setInterval(this.updateMuting, 5000);
+		}
 	}
 
 	/**
@@ -64,7 +74,7 @@ export default class Connection {
 	@autobind
 	private async onApiRequest(payload: any) {
 		// 新鮮なデータを利用するためにユーザーをフェッチ
-		const user = this.user ? await User.findOne({ _id: this.user._id }) : null;
+		const user = this.user ? await Users.findOne(this.user.id) : null;
 
 		const endpoint = payload.endpoint || payload.ep; // alias
 
@@ -79,7 +89,7 @@ export default class Connection {
 	@autobind
 	private onReadNotification(payload: any) {
 		if (!payload.id) return;
-		readNotification(this.user._id, payload.id);
+		readNotification(this.user.id, [payload.id]);
 	}
 
 	/**
@@ -100,7 +110,7 @@ export default class Connection {
 		}
 
 		if (payload.read) {
-			readNote(this.user._id, payload.id);
+			readNote(this.user.id, payload.id);
 		}
 	}
 
@@ -150,7 +160,6 @@ export default class Connection {
 	 */
 	@autobind
 	public sendMessageToWs(type: string, payload: any) {
-		if (this.sendMessageToWsOverride) return this.sendMessageToWsOverride(type, payload); // 後方互換性のため
 		this.wsConnection.send(JSON.stringify({
 			type: type,
 			body: payload
@@ -208,6 +217,30 @@ export default class Connection {
 		}
 	}
 
+	@autobind
+	private async updateFollowing() {
+		const followings = await Followings.find({
+			where: {
+				followerId: this.user.id
+			},
+			select: ['followeeId']
+		});
+
+		this.following = followings.map(x => x.followeeId);
+	}
+
+	@autobind
+	private async updateMuting() {
+		const mutings = await Mutings.find({
+			where: {
+				muterId: this.user.id
+			},
+			select: ['muteeId']
+		});
+
+		this.muting = mutings.map(x => x.muteeId);
+	}
+
 	/**
 	 * ストリームが切れたとき
 	 */
@@ -216,5 +249,8 @@ export default class Connection {
 		for (const c of this.channels.filter(c => c.dispose)) {
 			c.dispose();
 		}
+
+		if (this.followingClock) clearInterval(this.followingClock);
+		if (this.mutingClock) clearInterval(this.mutingClock);
 	}
 }
diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts
index f8f3c0ff4aeeed398abf33697b0060441c3b0109..ab66f2b6d965a01d092be01c10be170e3f1371f7 100644
--- a/src/server/api/streaming.ts
+++ b/src/server/api/streaming.ts
@@ -48,33 +48,6 @@ module.exports = (server: http.Server) => {
 
 		const main = new MainStreamConnection(connection, ev, user, app);
 
-		// 後方互換性のため
-		if (request.resourceURL.pathname !== '/streaming') {
-			main.sendMessageToWsOverride = (type: string, payload: any) => {
-				if (type == 'channel') {
-					type = payload.type;
-					payload = payload.body;
-				}
-				if (type.startsWith('api:')) {
-					type = type.replace('api:', 'api-res:');
-				}
-				connection.send(JSON.stringify({
-					type: type,
-					body: payload
-				}));
-			};
-
-			main.connectChannel(Math.random().toString().substr(2, 8), null,
-				request.resourceURL.pathname === '/' ? 'homeTimeline' :
-				request.resourceURL.pathname === '/local-timeline' ? 'localTimeline' :
-				request.resourceURL.pathname === '/hybrid-timeline' ? 'hybridTimeline' :
-				request.resourceURL.pathname === '/global-timeline' ? 'globalTimeline' : null);
-
-			if (request.resourceURL.pathname === '/') {
-				main.connectChannel(Math.random().toString().substr(2, 8), null, 'main');
-			}
-		}
-
 		connection.once('close', () => {
 			ev.removeAllListeners();
 			main.dispose();
diff --git a/src/server/file/index.ts b/src/server/file/index.ts
index 973528da33d8d528bd9e1536dc4bd2fe74338328..e3487a2636319ba73173386a6f1c5ab48a3b906e 100644
--- a/src/server/file/index.ts
+++ b/src/server/file/index.ts
@@ -33,8 +33,8 @@ router.get('/app-default.jpg', ctx => {
 	ctx.body = file;
 });
 
-router.get('/:id', sendDriveFile);
-router.get('/:id/*', sendDriveFile);
+router.get('/:key', sendDriveFile);
+router.get('/:key/*', sendDriveFile);
 
 // Register router
 app.use(router.routes());
diff --git a/src/server/file/send-drive-file.ts b/src/server/file/send-drive-file.ts
index e0208f3fab8d595547cf43dbbbfd5112ec8df79a..f9b067b79ccb926398168389cece25ad92c2edf1 100644
--- a/src/server/file/send-drive-file.ts
+++ b/src/server/file/send-drive-file.ts
@@ -1,12 +1,10 @@
 import * as Koa from 'koa';
 import * as send from 'koa-send';
-import * as mongodb from 'mongodb';
 import * as rename from 'rename';
-import DriveFile, { getDriveFileBucket } from '../../models/drive-file';
-import DriveFileThumbnail, { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
-import DriveFileWebpublic, { getDriveFileWebpublicBucket } from '../../models/drive-file-webpublic';
 import { serverLogger } from '..';
 import { contentDisposition } from '../../misc/content-disposition';
+import { DriveFiles } from '../../models';
+import { InternalStorage } from '../../services/drive/internal-storage';
 
 const assets = `${__dirname}/../../server/file/assets/`;
 
@@ -16,16 +14,14 @@ const commonReadableHandlerGenerator = (ctx: Koa.BaseContext) => (e: Error): voi
 };
 
 export default async function(ctx: Koa.BaseContext) {
-	// Validate id
-	if (!mongodb.ObjectID.isValid(ctx.params.id)) {
-		ctx.throw(400, 'incorrect id');
-		return;
-	}
-
-	const fileId = new mongodb.ObjectID(ctx.params.id);
+	const key = ctx.params.key;
 
 	// Fetch drive file
-	const file = await DriveFile.findOne({ _id: fileId });
+	const file = await DriveFiles.createQueryBuilder('file')
+		.where('file.accessKey = :accessKey', { accessKey: key })
+		.orWhere('file.thumbnailAccessKey = :thumbnailAccessKey', { thumbnailAccessKey: key })
+		.orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key })
+		.getOne();
 
 	if (file == null) {
 		ctx.status = 404;
@@ -33,69 +29,30 @@ export default async function(ctx: Koa.BaseContext) {
 		return;
 	}
 
-	if (file.metadata.deletedAt) {
-		ctx.status = 410;
-		await send(ctx as any, '/tombstone.png', { root: assets });
-		return;
-	}
-
-	if (file.metadata.withoutChunks) {
+	if (!file.storedInternal) {
 		ctx.status = 204;
 		return;
 	}
 
-	const sendRaw = async () => {
-		if (file.metadata && file.metadata.accessKey && file.metadata.accessKey != ctx.query['original']) {
-			ctx.status = 403;
-			return;
-		}
-
-		const bucket = await getDriveFileBucket();
-		const readable = bucket.openDownloadStream(fileId);
-		readable.on('error', commonReadableHandlerGenerator(ctx));
-		ctx.set('Content-Type', file.contentType);
-		ctx.body = readable;
-	};
-
-	if ('thumbnail' in ctx.query) {
-		const thumb = await DriveFileThumbnail.findOne({
-			'metadata.originalId': fileId
-		});
-
-		if (thumb != null) {
-			ctx.set('Content-Type', 'image/jpeg');
-			ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.filename, { suffix: '-thumb', extname: '.jpeg' })}`));
-			const bucket = await getDriveFileThumbnailBucket();
-			ctx.body = bucket.openDownloadStream(thumb._id);
-		} else {
-			if (file.contentType.startsWith('image/')) {
-				ctx.set('Content-Disposition', contentDisposition('inline', `${file.filename}`));
-				await sendRaw();
-			} else {
-				ctx.status = 404;
-				await send(ctx as any, '/dummy.png', { root: assets });
-			}
-		}
-	} else if ('web' in ctx.query) {
-		const web = await DriveFileWebpublic.findOne({
-			'metadata.originalId': fileId
-		});
-
-		if (web != null) {
-			ctx.set('Content-Type', file.contentType);
-			ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.filename, { suffix: '-web' })}`));
-
-			const bucket = await getDriveFileWebpublicBucket();
-			ctx.body = bucket.openDownloadStream(web._id);
-		} else {
-			ctx.set('Content-Disposition', contentDisposition('inline', `${file.filename}`));
-			await sendRaw();
-		}
+	const isThumbnail = file.thumbnailAccessKey === key;
+	const isWebpublic = file.webpublicAccessKey === key;
+
+	if (isThumbnail) {
+		ctx.set('Content-Type', 'image/jpeg');
+		ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.name, { suffix: '-thumb', extname: '.jpeg' })}`));
+		ctx.body = InternalStorage.read(key);
+	} else if (isWebpublic) {
+		ctx.set('Content-Type', file.type);
+		ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.name, { suffix: '-web' })}`));
+		ctx.body = InternalStorage.read(key);
 	} else {
 		if ('download' in ctx.query) {
-			ctx.set('Content-Disposition', contentDisposition('attachment', `${file.filename}`));
+			ctx.set('Content-Disposition', contentDisposition('attachment', `${file.name}`));
 		}
 
-		await sendRaw();
+		const readable = InternalStorage.read(file.accessKey);
+		readable.on('error', commonReadableHandlerGenerator(ctx));
+		ctx.set('Content-Type', file.type);
+		ctx.body = readable;
 	}
 }
diff --git a/src/server/index.ts b/src/server/index.ts
index 7c51923f9eb75216050248f2666286eb9fe86b94..563117773e971d0f0068de2163661ae448de25c6 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -19,12 +19,12 @@ import activityPub from './activitypub';
 import nodeinfo from './nodeinfo';
 import wellKnown from './well-known';
 import config from '../config';
-import networkChart from '../services/chart/network';
 import apiServer from './api';
 import { sum } from '../prelude/array';
-import User from '../models/user';
 import Logger from '../services/logger';
 import { program } from '../argv';
+import { Users } from '../models';
+import { networkChart } from '../services/chart';
 
 export const serverLogger = new Logger('server', 'gray', false);
 
@@ -73,17 +73,17 @@ router.use(nodeinfo.routes());
 router.use(wellKnown.routes());
 
 router.get('/verify-email/:code', async ctx => {
-	const user = await User.findOne({ emailVerifyCode: ctx.params.code });
+	const user = await Users.findOne({
+		emailVerifyCode: ctx.params.code
+	});
 
 	if (user != null) {
 		ctx.body = 'Verify succeeded!';
 		ctx.status = 200;
 
-		User.update({ _id: user._id }, {
-			$set: {
-				emailVerified: true,
-				emailVerifyCode: null
-			}
+		Users.update(user.id, {
+			emailVerified: true,
+			emailVerifyCode: null
 		});
 	} else {
 		ctx.status = 404;
diff --git a/src/server/nodeinfo.ts b/src/server/nodeinfo.ts
index a783eea90b73b37d21a18d9d5484c66a38c166e3..686412383e4c7bc4eb7aab5099c4fcb9442e8c6f 100644
--- a/src/server/nodeinfo.ts
+++ b/src/server/nodeinfo.ts
@@ -20,7 +20,24 @@ export const links = [/* (awaiting release) {
 
 const nodeinfo2 = async () => {
 	const [
-		{ name, description, maintainer, langs, announcements, disableRegistration, disableLocalTimeline, disableGlobalTimeline, enableRecaptcha, maxNoteTextLength, enableTwitterIntegration, enableGithubIntegration, enableDiscordIntegration, enableEmail, enableServiceWorker },
+		{
+			name,
+			description,
+			maintainerName,
+			maintainerEmail,
+			langs,
+			announcements,
+			disableRegistration,
+			disableLocalTimeline,
+			disableGlobalTimeline,
+			enableRecaptcha,
+			maxNoteTextLength,
+			enableTwitterIntegration,
+			enableGithubIntegration,
+			enableDiscordIntegration,
+			enableEmail,
+			enableServiceWorker
+		},
 		// total,
 		// activeHalfyear,
 		// activeMonth,
@@ -52,7 +69,26 @@ const nodeinfo2 = async () => {
 			// localPosts,
 			// localComments
 		},
-		metadata: { name, description, maintainer, langs, announcements, disableRegistration, disableLocalTimeline, disableGlobalTimeline, enableRecaptcha, maxNoteTextLength, enableTwitterIntegration, enableGithubIntegration, enableDiscordIntegration, enableEmail, enableServiceWorker }
+		metadata: {
+			name,
+			description,
+			maintainer: {
+				name: maintainerName,
+				email: maintainerEmail
+			},
+			langs,
+			announcements,
+			disableRegistration,
+			disableLocalTimeline,
+			disableGlobalTimeline,
+			enableRecaptcha,
+			maxNoteTextLength,
+			enableTwitterIntegration,
+			enableGithubIntegration,
+			enableDiscordIntegration,
+			enableEmail,
+			enableServiceWorker
+		}
 	};
 };
 
diff --git a/src/server/web/feed.ts b/src/server/web/feed.ts
index 09ac10c576446900e989c090e84f68faf71f8ba9..4b4ea87973b2db089c35d42e669069f00940b165 100644
--- a/src/server/web/feed.ts
+++ b/src/server/web/feed.ts
@@ -1,25 +1,23 @@
 import { Feed } from 'feed';
 import config from '../../config';
-import Note from '../../models/note';
-import { IUser } from '../../models/user';
-import { getOriginalUrl } from '../../misc/get-drive-file-url';
+import { User } from '../../models/entities/user';
+import { Notes, DriveFiles } from '../../models';
+import { In } from 'typeorm';
 
-export default async function(user: IUser) {
+export default async function(user: User) {
 	const author: Author = {
 		link: `${config.url}/@${user.username}`,
 		name: user.name || user.username
 	};
 
-	const notes = await Note.find({
-		userId: user._id,
-		renoteId: null,
-		$or: [
-			{ visibility: 'public' },
-			{ visibility: 'home' }
-		]
-	}, {
-		sort: { createdAt: -1 },
-		limit: 20
+	const notes = await Notes.find({
+		where: {
+			userId: user.id,
+			renoteId: null,
+			visibility: In(['public', 'home'])
+		},
+		order: { createdAt: -1 },
+		take: 20
 	});
 
 	const feed = new Feed({
@@ -38,15 +36,18 @@ export default async function(user: IUser) {
 	} as FeedOptions);
 
 	for (const note of notes) {
-		const file = note._files && note._files.find(file => file.contentType.startsWith('image/'));
+		const files = note.fileIds.length > 0 ? await DriveFiles.find({
+			id: In(note.fileIds)
+		}) : [];
+		const file = files.find(file => file.type.startsWith('image/'));
 
 		feed.addItem({
 			title: `New note by ${author.name}`,
-			link: `${config.url}/notes/${note._id}`,
+			link: `${config.url}/notes/${note.id}`,
 			date: note.createdAt,
 			description: note.cw,
 			content: note.text,
-			image: file && getOriginalUrl(file)
+			image: file ? DriveFiles.getPublicUrl(file) : null
 		});
 	}
 
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index d8525ba1145fd027f438114093b81902606e4d9c..de0d65cf33551d3116b6c0adb56b92e05a11bd75 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -9,19 +9,16 @@ import * as Router from 'koa-router';
 import * as send from 'koa-send';
 import * as favicon from 'koa-favicon';
 import * as views from 'koa-views';
-import { ObjectID } from 'mongodb';
 
 import docs from './docs';
 import packFeed from './feed';
-import User from '../../models/user';
-import parseAcct from '../../misc/acct/parse';
-import config from '../../config';
-import Note, { pack as packNote } from '../../models/note';
-import getNoteSummary from '../../misc/get-note-summary';
 import fetchMeta from '../../misc/fetch-meta';
-import Emoji from '../../models/emoji';
 import * as pkg from '../../../package.json';
 import { genOpenapiSpec } from '../api/openapi/gen-spec';
+import config from '../../config';
+import { Users, Notes, Emojis } from '../../models';
+import parseAcct from '../../misc/acct/parse';
+import getNoteSummary from '../../misc/get-note-summary';
 
 const client = `${__dirname}/../../client/`;
 
@@ -100,7 +97,7 @@ router.get('/api.json', async ctx => {
 
 const getFeed = async (acct: string) => {
 	const { username, host } = parseAcct(acct);
-	const user = await User.findOne({
+	const user = await Users.findOne({
 		usernameLower: username.toLowerCase(),
 		host
 	});
@@ -148,7 +145,7 @@ router.get('/@:user.json', async ctx => {
 // User
 router.get('/@:user', async (ctx, next) => {
 	const { username, host } = parseAcct(ctx.params.user);
-	const user = await User.findOne({
+	const user = await Users.findOne({
 		usernameLower: username.toLowerCase(),
 		host
 	});
@@ -157,7 +154,7 @@ router.get('/@:user', async (ctx, next) => {
 		const meta = await fetchMeta();
 		await ctx.render('user', {
 			user,
-			instanceName: meta.name
+			instanceName: meta.name || 'Misskey'
 		});
 		ctx.set('Cache-Control', 'public, max-age=180');
 	} else {
@@ -167,19 +164,12 @@ router.get('/@:user', async (ctx, next) => {
 });
 
 router.get('/users/:user', async ctx => {
-	if (!ObjectID.isValid(ctx.params.user)) {
-		ctx.status = 404;
-		return;
-	}
-
-	const userId = new ObjectID(ctx.params.user);
-
-	const user = await User.findOne({
-		_id: userId,
+	const user = await Users.findOne({
+		id: ctx.params.user,
 		host: null
 	});
 
-	if (user === null) {
+	if (user == null) {
 		ctx.status = 404;
 		return;
 	}
@@ -189,26 +179,24 @@ router.get('/users/:user', async ctx => {
 
 // Note
 router.get('/notes/:note', async ctx => {
-	if (ObjectID.isValid(ctx.params.note)) {
-		const note = await Note.findOne({ _id: ctx.params.note });
-
-		if (note) {
-			const _note = await packNote(note);
-			const meta = await fetchMeta();
-			await ctx.render('note', {
-				note: _note,
-				summary: getNoteSummary(_note),
-				instanceName: meta.name
-			});
-
-			if (['public', 'home'].includes(note.visibility)) {
-				ctx.set('Cache-Control', 'public, max-age=180');
-			} else {
-				ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
-			}
-
-			return;
+	const note = await Notes.findOne(ctx.params.note);
+
+	if (note) {
+		const _note = await Notes.pack(note);
+		const meta = await fetchMeta();
+		await ctx.render('note', {
+			note: _note,
+			summary: getNoteSummary(_note),
+			instanceName: meta.name || 'Misskey'
+		});
+
+		if (['public', 'home'].includes(note.visibility)) {
+			ctx.set('Cache-Control', 'public, max-age=180');
+		} else {
+			ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
 		}
+
+		return;
 	}
 
 	ctx.status = 404;
@@ -217,10 +205,8 @@ router.get('/notes/:note', async ctx => {
 
 router.get('/info', async ctx => {
 	const meta = await fetchMeta();
-	const emojis = await Emoji.find({ host: null }, {
-		fields: {
-			_id: false
-		}
+	const emojis = await Emojis.find({
+		where: { host: null }
 	});
 	await ctx.render('info', {
 		version: pkg.version,
@@ -232,7 +218,9 @@ router.get('/info', async ctx => {
 			cores: os.cpus().length
 		},
 		emojis: emojis,
-		meta: meta
+		meta: meta,
+		originalUsersCount: await Users.count({ host: null }),
+		originalNotesCount: await Notes.count({ userHost: null })
 	});
 });
 
@@ -247,7 +235,7 @@ router.get('*', async ctx => {
 	const meta = await fetchMeta();
 	await ctx.render('base', {
 		img: meta.bannerUrl,
-		title: meta.name,
+		title: meta.name || 'Misskey',
 		desc: meta.description,
 		icon: meta.iconUrl
 	});
diff --git a/src/server/web/manifest.ts b/src/server/web/manifest.ts
index 35d3d1b666cef090de68c744838698fab116fa27..4acfb22de5dfb8b7746396f607f21c8442adc8c5 100644
--- a/src/server/web/manifest.ts
+++ b/src/server/web/manifest.ts
@@ -1,10 +1,9 @@
 import * as Koa from 'koa';
 import * as manifest from '../../client/assets/manifest.json';
-import * as deepcopy from 'deepcopy';
 import fetchMeta from '../../misc/fetch-meta';
 
 module.exports = async (ctx: Koa.BaseContext) => {
-	const json = deepcopy(manifest);
+	const json = JSON.parse(JSON.stringify(manifest));
 
 	const instance = await fetchMeta();
 
diff --git a/src/server/web/views/info.pug b/src/server/web/views/info.pug
index 1c4b272a627f8e2f9616d6e200ff014cb008eaf5..c8b0bd939afae54b99fe007878355a44990d6e34 100644
--- a/src/server/web/views/info.pug
+++ b/src/server/web/views/info.pug
@@ -70,15 +70,15 @@ html
 			table
 				tr
 					th Instance
-					td= meta.name
+					td= meta.name || 'Misskey'
 				tr
 					th Description
 					td= meta.description
 				tr
 					th Maintainer
 					td
-						= meta.maintainer.name
-						|  &lt;#{meta.maintainer.email}&gt;
+						= meta.maintainerName
+						|  &lt;#{meta.maintainerEmail}&gt;
 				tr
 					th System
 					td= os
@@ -93,10 +93,10 @@ html
 					td= cpu.model
 				tr
 					th Original users
-					td= meta.stats.originalUsersCount
+					td= originalUsersCount
 				tr
 					th Original notes
-					td= meta.stats.originalNotesCount
+					td= originalNotesCount
 				tr
 					th Registration
 					td= !meta.disableRegistration ? 'yes' : 'no'
diff --git a/src/server/well-known.ts b/src/server/well-known.ts
index 18c080acc7df302ec1e9e83d61319ba027c2f31e..7c5684d2cefc4e1ba71abe4e36eaab88c5619bb5 100644
--- a/src/server/well-known.ts
+++ b/src/server/well-known.ts
@@ -1,12 +1,12 @@
-import * as mongo from 'mongodb';
 import * as Router from 'koa-router';
 
 import config from '../config';
 import parseAcct from '../misc/acct/parse';
-import User from '../models/user';
 import Acct from '../misc/acct/type';
 import { links } from './nodeinfo';
 import { escapeAttribute, escapeValue } from '../prelude/xml';
+import { Users } from '../models';
+import { User } from '../models/entities/user';
 
 // Init router
 const router = new Router();
@@ -47,19 +47,19 @@ router.get('/.well-known/nodeinfo', async ctx => {
 });
 
 router.get(webFingerPath, async ctx => {
+	const fromId = (id: User['id']): Record<string, any> => ({
+		id,
+		host: null
+	});
+
 	const generateQuery = (resource: string) =>
 		resource.startsWith(`${config.url.toLowerCase()}/users/`) ?
-			fromId(new mongo.ObjectID(resource.split('/').pop())) :
+			fromId(resource.split('/').pop()) :
 			fromAcct(parseAcct(
 				resource.startsWith(`${config.url.toLowerCase()}/@`) ? resource.split('/').pop() :
 				resource.startsWith('acct:') ? resource.slice('acct:'.length) :
 				resource));
 
-	const fromId = (_id: mongo.ObjectID): Record<string, any> => ({
-			_id,
-			host: null
-		});
-
 	const fromAcct = (acct: Acct): Record<string, any> | number =>
 		!acct.host || acct.host === config.host.toLowerCase() ? {
 			usernameLower: acct.username,
@@ -78,9 +78,9 @@ router.get(webFingerPath, async ctx => {
 		return;
 	}
 
-	const user = await User.findOne(query);
+	const user = await Users.findOne(query);
 
-	if (user === null) {
+	if (user == null) {
 		ctx.status = 404;
 		return;
 	}
@@ -89,7 +89,7 @@ router.get(webFingerPath, async ctx => {
 	const self = {
 		rel: 'self',
 		type: 'application/activity+json',
-		href: `${config.url}/users/${user._id}`
+		href: `${config.url}/users/${user.id}`
 	};
 	const profilePage = {
 		rel: 'http://webfinger.net/rel/profile-page',
diff --git a/src/services/blocking/create.ts b/src/services/blocking/create.ts
index c20666ef2682c10d12a7bc27b90a4b6f1106bea2..79ca0d59f1680bb02cfbb508a6bad6344c96ef42 100644
--- a/src/services/blocking/create.ts
+++ b/src/services/blocking/create.ts
@@ -1,6 +1,3 @@
-import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user';
-import Following from '../../models/following';
-import FollowRequest from '../../models/follow-request';
 import { publishMainStream } from '../stream';
 import { renderActivity } from '../../remote/activitypub/renderer';
 import renderFollow from '../../remote/activitypub/renderer/follow';
@@ -8,11 +5,12 @@ import renderUndo from '../../remote/activitypub/renderer/undo';
 import renderBlock from '../../remote/activitypub/renderer/block';
 import { deliver } from '../../queue';
 import renderReject from '../../remote/activitypub/renderer/reject';
-import perUserFollowingChart from '../../services/chart/per-user-following';
-import Blocking from '../../models/blocking';
-
-export default async function(blocker: IUser, blockee: IUser) {
+import { User } from '../../models/entities/user';
+import { Blockings, Users, FollowRequests, Followings } from '../../models';
+import { perUserFollowingChart } from '../chart';
+import { genId } from '../../misc/gen-id';
 
+export default async function(blocker: User, blockee: User) {
 	await Promise.all([
 		cancelRequest(blocker, blockee),
 		cancelRequest(blockee, blocker),
@@ -20,105 +18,90 @@ export default async function(blocker: IUser, blockee: IUser) {
 		unFollow(blockee, blocker)
 	]);
 
-	await Blocking.insert({
+	await Blockings.save({
+		id: genId(),
 		createdAt: new Date(),
-		blockerId: blocker._id,
-		blockeeId: blockee._id,
+		blockerId: blocker.id,
+		blockeeId: blockee.id,
 	});
 
-	if (isLocalUser(blocker) && isRemoteUser(blockee)) {
+	if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
 		const content = renderActivity(renderBlock(blocker, blockee));
 		deliver(blocker, content, blockee.inbox);
 	}
 }
 
-async function cancelRequest(follower: IUser, followee: IUser) {
-	const request = await FollowRequest.findOne({
-		followeeId: followee._id,
-		followerId: follower._id
+async function cancelRequest(follower: User, followee: User) {
+	const request = await FollowRequests.findOne({
+		followeeId: followee.id,
+		followerId: follower.id
 	});
 
 	if (request == null) {
 		return;
 	}
 
-	await FollowRequest.remove({
-		followeeId: followee._id,
-		followerId: follower._id
-	});
-
-	await User.update({ _id: followee._id }, {
-		$inc: {
-			pendingReceivedFollowRequestsCount: -1
-		}
+	await FollowRequests.delete({
+		followeeId: followee.id,
+		followerId: follower.id
 	});
 
-	if (isLocalUser(followee)) {
-		packUser(followee, followee, {
+	if (Users.isLocalUser(followee)) {
+		Users.pack(followee, followee, {
 			detail: true
-		}).then(packed => publishMainStream(followee._id, 'meUpdated', packed));
+		}).then(packed => publishMainStream(followee.id, 'meUpdated', packed));
 	}
 
-	if (isLocalUser(follower)) {
-		packUser(followee, follower, {
+	if (Users.isLocalUser(follower)) {
+		Users.pack(followee, follower, {
 			detail: true
-		}).then(packed => publishMainStream(follower._id, 'unfollow', packed));
+		}).then(packed => publishMainStream(follower.id, 'unfollow', packed));
 	}
 
 	// リモートにフォローリクエストをしていたらUndoFollow送信
-	if (isLocalUser(follower) && isRemoteUser(followee)) {
+	if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
 		const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
 		deliver(follower, content, followee.inbox);
 	}
 
 	// リモートからフォローリクエストを受けていたらReject送信
-	if (isRemoteUser(follower) && isLocalUser(followee)) {
+	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
 		const content = renderActivity(renderReject(renderFollow(follower, followee, request.requestId), followee));
 		deliver(followee, content, follower.inbox);
 	}
 }
 
-async function unFollow(follower: IUser, followee: IUser) {
-	const following = await Following.findOne({
-		followerId: follower._id,
-		followeeId: followee._id
+async function unFollow(follower: User, followee: User) {
+	const following = await Followings.findOne({
+		followerId: follower.id,
+		followeeId: followee.id
 	});
 
 	if (following == null) {
 		return;
 	}
 
-	Following.remove({
-		_id: following._id
-	});
+	Followings.delete(following.id);
 
 	//#region Decrement following count
-	User.update({ _id: follower._id }, {
-		$inc: {
-			followingCount: -1
-		}
-	});
+	Users.decrement({ id: follower.id }, 'followingCount', 1);
 	//#endregion
 
 	//#region Decrement followers count
-	User.update({ _id: followee._id }, {
-		$inc: {
-			followersCount: -1
-		}
-	});
+	Users.decrement({ id: followee.id }, 'followersCount', 1);
 	//#endregion
 
 	perUserFollowingChart.update(follower, followee, false);
 
 	// Publish unfollow event
-	if (isLocalUser(follower)) {
-		packUser(followee, follower, {
+	if (Users.isLocalUser(follower)) {
+		Users.pack(followee, follower, {
 			detail: true
-		}).then(packed => publishMainStream(follower._id, 'unfollow', packed));
+		}).then(packed => publishMainStream(follower.id, 'unfollow', packed));
 	}
 
 	// リモートにフォローをしていたらUndoFollow送信
-	if (isLocalUser(follower) && isRemoteUser(followee)) {
+	if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
 		const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
 		deliver(follower, content, followee.inbox);
 	}
diff --git a/src/services/blocking/delete.ts b/src/services/blocking/delete.ts
index 099fa14b37a9a24c695eec402afc3619be20db1d..2c05cb7f3f8fc2430db3385aebaf976863db17bb 100644
--- a/src/services/blocking/delete.ts
+++ b/src/services/blocking/delete.ts
@@ -1,17 +1,17 @@
-import { isLocalUser, isRemoteUser, IUser } from '../../models/user';
-import Blocking from '../../models/blocking';
 import { renderActivity } from '../../remote/activitypub/renderer';
 import renderBlock from '../../remote/activitypub/renderer/block';
 import renderUndo from '../../remote/activitypub/renderer/undo';
 import { deliver } from '../../queue';
 import Logger from '../logger';
+import { User } from '../../models/entities/user';
+import { Blockings, Users } from '../../models';
 
 const logger = new Logger('blocking/delete');
 
-export default async function(blocker: IUser, blockee: IUser) {
-	const blocking = await Blocking.findOne({
-		blockerId: blocker._id,
-		blockeeId: blockee._id
+export default async function(blocker: User, blockee: User) {
+	const blocking = await Blockings.findOne({
+		blockerId: blocker.id,
+		blockeeId: blockee.id
 	});
 
 	if (blocking == null) {
@@ -19,12 +19,10 @@ export default async function(blocker: IUser, blockee: IUser) {
 		return;
 	}
 
-	Blocking.remove({
-		_id: blocking._id
-	});
+	Blockings.delete(blocking.id);
 
 	// deliver if remote bloking
-	if (isLocalUser(blocker) && isRemoteUser(blockee)) {
+	if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
 		const content = renderActivity(renderUndo(renderBlock(blocker, blockee), blocker));
 		deliver(blocker, content, blockee.inbox);
 	}
diff --git a/src/services/chart/active-users.ts b/src/services/chart/active-users.ts
deleted file mode 100644
index 2a4e1a97ac75335e5714ccb704fc523d1e061e5c..0000000000000000000000000000000000000000
--- a/src/services/chart/active-users.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from '.';
-import { IUser, isLocalUser } from '../../models/user';
-
-/**
- * アクティブユーザーに関するチャート
- */
-type ActiveUsersLog = {
-	local: {
-		/**
-		 * アクティブユーザー数
-		 */
-		count: number;
-	};
-
-	remote: ActiveUsersLog['local'];
-};
-
-class ActiveUsersChart extends Chart<ActiveUsersLog> {
-	constructor() {
-		super('activeUsers');
-	}
-
-	@autobind
-	protected async getTemplate(init: boolean, latest?: ActiveUsersLog): Promise<ActiveUsersLog> {
-		return {
-			local: {
-				count: 0
-			},
-			remote: {
-				count: 0
-			}
-		};
-	}
-
-	@autobind
-	public async update(user: IUser) {
-		const update: Obj = {
-			count: 1
-		};
-
-		await this.incIfUnique({
-			[isLocalUser(user) ? 'local' : 'remote']: update
-		}, 'users', user._id.toHexString());
-	}
-}
-
-export default new ActiveUsersChart();
diff --git a/src/services/chart/charts/classes/active-users.ts b/src/services/chart/charts/classes/active-users.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5128150de6cf0b1ed20c6730200d5198e439fa06
--- /dev/null
+++ b/src/services/chart/charts/classes/active-users.ts
@@ -0,0 +1,35 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { User } from '../../../../models/entities/user';
+import { SchemaType } from '../../../../misc/schema';
+import { Users } from '../../../../models';
+import { name, schema } from '../schemas/active-users';
+
+type ActiveUsersLog = SchemaType<typeof schema>;
+
+export default class ActiveUsersChart extends Chart<ActiveUsersLog> {
+	constructor() {
+		super(name, schema);
+	}
+
+	@autobind
+	protected genNewLog(latest: ActiveUsersLog): DeepPartial<ActiveUsersLog> {
+		return {};
+	}
+
+	@autobind
+	protected async fetchActual(): Promise<DeepPartial<ActiveUsersLog>> {
+		return {};
+	}
+
+	@autobind
+	public async update(user: User) {
+		const update: Obj = {
+			count: 1
+		};
+
+		await this.incIfUnique({
+			[Users.isLocalUser(user) ? 'local' : 'remote']: update
+		}, 'users', user.id);
+	}
+}
diff --git a/src/services/chart/charts/classes/drive.ts b/src/services/chart/charts/classes/drive.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ae52df19ac3ceb49f1176dd6d264055cbe02b184
--- /dev/null
+++ b/src/services/chart/charts/classes/drive.ts
@@ -0,0 +1,69 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { DriveFiles } from '../../../../models';
+import { Not } from 'typeorm';
+import { DriveFile } from '../../../../models/entities/drive-file';
+import { name, schema } from '../schemas/drive';
+
+type DriveLog = SchemaType<typeof schema>;
+
+export default class DriveChart extends Chart<DriveLog> {
+	constructor() {
+		super(name, schema);
+	}
+
+	@autobind
+	protected genNewLog(latest: DriveLog): DeepPartial<DriveLog> {
+		return {
+			local: {
+				totalCount: latest.local.totalCount,
+				totalSize: latest.local.totalSize,
+			},
+			remote: {
+				totalCount: latest.remote.totalCount,
+				totalSize: latest.remote.totalSize,
+			}
+		};
+	}
+
+	@autobind
+	protected async fetchActual(): Promise<DeepPartial<DriveLog>> {
+		const [localCount, remoteCount, localSize, remoteSize] = await Promise.all([
+			DriveFiles.count({ userHost: null }),
+			DriveFiles.count({ userHost: Not(null) }),
+			DriveFiles.clacDriveUsageOfLocal(),
+			DriveFiles.clacDriveUsageOfRemote()
+		]);
+
+		return {
+			local: {
+				totalCount: localCount,
+				totalSize: localSize,
+			},
+			remote: {
+				totalCount: remoteCount,
+				totalSize: remoteSize,
+			}
+		};
+	}
+
+	@autobind
+	public async update(file: DriveFile, isAdditional: boolean) {
+		const update: Obj = {};
+
+		update.totalCount = isAdditional ? 1 : -1;
+		update.totalSize = isAdditional ? file.size : -file.size;
+		if (isAdditional) {
+			update.incCount = 1;
+			update.incSize = file.size;
+		} else {
+			update.decCount = 1;
+			update.decSize = file.size;
+		}
+
+		await this.inc({
+			[file.userHost === null ? 'local' : 'remote']: update
+		});
+	}
+}
diff --git a/src/services/chart/charts/classes/federation.ts b/src/services/chart/charts/classes/federation.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bd2c497e7b8dfbdeaa05c4550908e536acaac255
--- /dev/null
+++ b/src/services/chart/charts/classes/federation.ts
@@ -0,0 +1,51 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { Instances } from '../../../../models';
+import { name, schema } from '../schemas/federation';
+
+type FederationLog = SchemaType<typeof schema>;
+
+export default class FederationChart extends Chart<FederationLog> {
+	constructor() {
+		super(name, schema);
+	}
+
+	@autobind
+	protected genNewLog(latest: FederationLog): DeepPartial<FederationLog> {
+		return {
+			instance: {
+				total: latest.instance.total,
+			}
+		};
+	}
+
+	@autobind
+	protected async fetchActual(): Promise<DeepPartial<FederationLog>> {
+		const [total] = await Promise.all([
+			Instances.count({})
+		]);
+
+		return {
+			instance: {
+				total: total,
+			}
+		};
+	}
+
+	@autobind
+	public async update(isAdditional: boolean) {
+		const update: Obj = {};
+
+		update.total = isAdditional ? 1 : -1;
+		if (isAdditional) {
+			update.inc = 1;
+		} else {
+			update.dec = 1;
+		}
+
+		await this.inc({
+			instance: update
+		});
+	}
+}
diff --git a/src/services/chart/charts/classes/hashtag.ts b/src/services/chart/charts/classes/hashtag.ts
new file mode 100644
index 0000000000000000000000000000000000000000..38c3a94f0cb21ce98301193a497389534dbbb8b8
--- /dev/null
+++ b/src/services/chart/charts/classes/hashtag.ts
@@ -0,0 +1,35 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { User } from '../../../../models/entities/user';
+import { SchemaType } from '../../../../misc/schema';
+import { Users } from '../../../../models';
+import { name, schema } from '../schemas/hashtag';
+
+type HashtagLog = SchemaType<typeof schema>;
+
+export default class HashtagChart extends Chart<HashtagLog> {
+	constructor() {
+		super(name, schema, true);
+	}
+
+	@autobind
+	protected genNewLog(latest: HashtagLog): DeepPartial<HashtagLog> {
+		return {};
+	}
+
+	@autobind
+	protected async fetchActual(): Promise<DeepPartial<HashtagLog>> {
+		return {};
+	}
+
+	@autobind
+	public async update(hashtag: string, user: User) {
+		const update: Obj = {
+			count: 1
+		};
+
+		await this.incIfUnique({
+			[Users.isLocalUser(user) ? 'local' : 'remote']: update
+		}, 'users', user.id, hashtag);
+	}
+}
diff --git a/src/services/chart/charts/classes/instance.ts b/src/services/chart/charts/classes/instance.ts
new file mode 100644
index 0000000000000000000000000000000000000000..974eac036b46eda7510bc5c273833ce47b2b8b8e
--- /dev/null
+++ b/src/services/chart/charts/classes/instance.ts
@@ -0,0 +1,160 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { DriveFiles, Followings, Users, Notes } from '../../../../models';
+import { DriveFile } from '../../../../models/entities/drive-file';
+import { name, schema } from '../schemas/instance';
+
+type InstanceLog = SchemaType<typeof schema>;
+
+export default class InstanceChart extends Chart<InstanceLog> {
+	constructor() {
+		super(name, schema);
+	}
+
+	@autobind
+	protected genNewLog(latest: InstanceLog): DeepPartial<InstanceLog> {
+		return {
+			notes: {
+				total: latest.notes.total,
+			},
+			users: {
+				total: latest.users.total,
+			},
+			following: {
+				total: latest.following.total,
+			},
+			followers: {
+				total: latest.followers.total,
+			},
+			drive: {
+				totalFiles: latest.drive.totalFiles,
+				totalUsage: latest.drive.totalUsage,
+			}
+		};
+	}
+
+	@autobind
+	protected async fetchActual(group: string): Promise<DeepPartial<InstanceLog>> {
+		const [
+			notesCount,
+			usersCount,
+			followingCount,
+			followersCount,
+			driveFiles,
+			driveUsage,
+		] = await Promise.all([
+			Notes.count({ userHost: group }),
+			Users.count({ host: group }),
+			Followings.count({ followerHost: group }),
+			Followings.count({ followeeHost: group }),
+			DriveFiles.count({ userHost: group }),
+			DriveFiles.clacDriveUsageOfHost(group),
+		]);
+
+		return {
+			notes: {
+				total: notesCount,
+			},
+			users: {
+				total: usersCount,
+			},
+			following: {
+				total: followingCount,
+			},
+			followers: {
+				total: followersCount,
+			},
+			drive: {
+				totalFiles: driveFiles,
+				totalUsage: driveUsage,
+			}
+		};
+	}
+
+	@autobind
+	public async requestReceived(host: string) {
+		await this.inc({
+			requests: {
+				received: 1
+			}
+		}, host);
+	}
+
+	@autobind
+	public async requestSent(host: string, isSucceeded: boolean) {
+		const update: Obj = {};
+
+		if (isSucceeded) {
+			update.succeeded = 1;
+		} else {
+			update.failed = 1;
+		}
+
+		await this.inc({
+			requests: update
+		}, host);
+	}
+
+	@autobind
+	public async newUser(host: string) {
+		await this.inc({
+			users: {
+				total: 1,
+				inc: 1
+			}
+		}, host);
+	}
+
+	@autobind
+	public async updateNote(host: string, isAdditional: boolean) {
+		await this.inc({
+			notes: {
+				total: isAdditional ? 1 : -1,
+				inc: isAdditional ? 1 : 0,
+				dec: isAdditional ? 0 : 1,
+			}
+		}, host);
+	}
+
+	@autobind
+	public async updateFollowing(host: string, isAdditional: boolean) {
+		await this.inc({
+			following: {
+				total: isAdditional ? 1 : -1,
+				inc: isAdditional ? 1 : 0,
+				dec: isAdditional ? 0 : 1,
+			}
+		}, host);
+	}
+
+	@autobind
+	public async updateFollowers(host: string, isAdditional: boolean) {
+		await this.inc({
+			followers: {
+				total: isAdditional ? 1 : -1,
+				inc: isAdditional ? 1 : 0,
+				dec: isAdditional ? 0 : 1,
+			}
+		}, host);
+	}
+
+	@autobind
+	public async updateDrive(file: DriveFile, isAdditional: boolean) {
+		const update: Obj = {};
+
+		update.totalFiles = isAdditional ? 1 : -1;
+		update.totalUsage = isAdditional ? file.size : -file.size;
+		if (isAdditional) {
+			update.incFiles = 1;
+			update.incUsage = file.size;
+		} else {
+			update.decFiles = 1;
+			update.decUsage = file.size;
+		}
+
+		await this.inc({
+			drive: update
+		}, file.userHost);
+	}
+}
diff --git a/src/services/chart/charts/classes/network.ts b/src/services/chart/charts/classes/network.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8b26e5c4c22f10c614c27f0e8dbe082088266322
--- /dev/null
+++ b/src/services/chart/charts/classes/network.ts
@@ -0,0 +1,34 @@
+import autobind from 'autobind-decorator';
+import Chart, { DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { name, schema } from '../schemas/network';
+
+type NetworkLog = SchemaType<typeof schema>;
+
+export default class NetworkChart extends Chart<NetworkLog> {
+	constructor() {
+		super(name, schema);
+	}
+
+	@autobind
+	protected genNewLog(latest: NetworkLog): DeepPartial<NetworkLog> {
+		return {};
+	}
+
+	@autobind
+	protected async fetchActual(): Promise<DeepPartial<NetworkLog>> {
+		return {};
+	}
+
+	@autobind
+	public async update(incomingRequests: number, time: number, incomingBytes: number, outgoingBytes: number) {
+		const inc: DeepPartial<NetworkLog> = {
+			incomingRequests: incomingRequests,
+			totalTime: time,
+			incomingBytes: incomingBytes,
+			outgoingBytes: outgoingBytes
+		};
+
+		await this.inc(inc);
+	}
+}
diff --git a/src/services/chart/charts/classes/notes.ts b/src/services/chart/charts/classes/notes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..85ccf000d8e33fb9c52fdb17a894b68eca91a640
--- /dev/null
+++ b/src/services/chart/charts/classes/notes.ts
@@ -0,0 +1,71 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { Notes } from '../../../../models';
+import { Not } from 'typeorm';
+import { Note } from '../../../../models/entities/note';
+import { name, schema } from '../schemas/notes';
+
+type NotesLog = SchemaType<typeof schema>;
+
+export default class NotesChart extends Chart<NotesLog> {
+	constructor() {
+		super(name, schema);
+	}
+
+	@autobind
+	protected genNewLog(latest: NotesLog): DeepPartial<NotesLog> {
+		return {
+			local: {
+				total: latest.local.total,
+			},
+			remote: {
+				total: latest.remote.total,
+			}
+		};
+	}
+
+	@autobind
+	protected async fetchActual(): Promise<DeepPartial<NotesLog>> {
+		const [localCount, remoteCount] = await Promise.all([
+			Notes.count({ userHost: null }),
+			Notes.count({ userHost: Not(null) })
+		]);
+
+		return {
+			local: {
+				total: localCount,
+			},
+			remote: {
+				total: remoteCount,
+			}
+		};
+	}
+
+	@autobind
+	public async update(note: Note, isAdditional: boolean) {
+		const update: Obj = {
+			diffs: {}
+		};
+
+		update.total = isAdditional ? 1 : -1;
+
+		if (isAdditional) {
+			update.inc = 1;
+		} else {
+			update.dec = 1;
+		}
+
+		if (note.replyId != null) {
+			update.diffs.reply = isAdditional ? 1 : -1;
+		} else if (note.renoteId != null) {
+			update.diffs.renote = isAdditional ? 1 : -1;
+		} else {
+			update.diffs.normal = isAdditional ? 1 : -1;
+		}
+
+		await this.inc({
+			[note.userHost === null ? 'local' : 'remote']: update
+		});
+	}
+}
diff --git a/src/services/chart/charts/classes/per-user-drive.ts b/src/services/chart/charts/classes/per-user-drive.ts
new file mode 100644
index 0000000000000000000000000000000000000000..822f4eda0f38fe034c097014324efa913d92d231
--- /dev/null
+++ b/src/services/chart/charts/classes/per-user-drive.ts
@@ -0,0 +1,52 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { DriveFiles } from '../../../../models';
+import { DriveFile } from '../../../../models/entities/drive-file';
+import { name, schema } from '../schemas/per-user-drive';
+
+type PerUserDriveLog = SchemaType<typeof schema>;
+
+export default class PerUserDriveChart extends Chart<PerUserDriveLog> {
+	constructor() {
+		super(name, schema, true);
+	}
+
+	@autobind
+	protected genNewLog(latest: PerUserDriveLog): DeepPartial<PerUserDriveLog> {
+		return {
+			totalCount: latest.totalCount,
+			totalSize: latest.totalSize,
+		};
+	}
+
+	@autobind
+	protected async fetchActual(group: string): Promise<DeepPartial<PerUserDriveLog>> {
+		const [count, size] = await Promise.all([
+			DriveFiles.count({ userId: group }),
+			DriveFiles.clacDriveUsageOf(group)
+		]);
+
+		return {
+			totalCount: count,
+			totalSize: size,
+		};
+	}
+
+	@autobind
+	public async update(file: DriveFile, isAdditional: boolean) {
+		const update: Obj = {};
+
+		update.totalCount = isAdditional ? 1 : -1;
+		update.totalSize = isAdditional ? file.size : -file.size;
+		if (isAdditional) {
+			update.incCount = 1;
+			update.incSize = file.size;
+		} else {
+			update.decCount = 1;
+			update.decSize = file.size;
+		}
+
+		await this.inc(update, file.userId);
+	}
+}
diff --git a/src/services/chart/charts/classes/per-user-following.ts b/src/services/chart/charts/classes/per-user-following.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f3809a7c946298cc1cff0674cb388ebd3a037a64
--- /dev/null
+++ b/src/services/chart/charts/classes/per-user-following.ts
@@ -0,0 +1,91 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { Followings, Users } from '../../../../models';
+import { Not } from 'typeorm';
+import { User } from '../../../../models/entities/user';
+import { name, schema } from '../schemas/per-user-following';
+
+type PerUserFollowingLog = SchemaType<typeof schema>;
+
+export default class PerUserFollowingChart extends Chart<PerUserFollowingLog> {
+	constructor() {
+		super(name, schema, true);
+	}
+
+	@autobind
+	protected genNewLog(latest: PerUserFollowingLog): DeepPartial<PerUserFollowingLog> {
+		return {
+			local: {
+				followings: {
+					total: latest.local.followings.total,
+				},
+				followers: {
+					total: latest.local.followers.total,
+				}
+			},
+			remote: {
+				followings: {
+					total: latest.remote.followings.total,
+				},
+				followers: {
+					total: latest.remote.followers.total,
+				}
+			}
+		};
+	}
+
+	@autobind
+	protected async fetchActual(group: string): Promise<DeepPartial<PerUserFollowingLog>> {
+		const [
+			localFollowingsCount,
+			localFollowersCount,
+			remoteFollowingsCount,
+			remoteFollowersCount
+		] = await Promise.all([
+			Followings.count({ followerId: group, followeeHost: null }),
+			Followings.count({ followeeId: group, followerHost: null }),
+			Followings.count({ followerId: group, followeeHost: Not(null) }),
+			Followings.count({ followeeId: group, followerHost: Not(null) })
+		]);
+
+		return {
+			local: {
+				followings: {
+					total: localFollowingsCount,
+				},
+				followers: {
+					total: localFollowersCount,
+				}
+			},
+			remote: {
+				followings: {
+					total: remoteFollowingsCount,
+				},
+				followers: {
+					total: remoteFollowersCount,
+				}
+			}
+		};
+	}
+
+	@autobind
+	public async update(follower: User, followee: User, isFollow: boolean) {
+		const update: Obj = {};
+
+		update.total = isFollow ? 1 : -1;
+
+		if (isFollow) {
+			update.inc = 1;
+		} else {
+			update.dec = 1;
+		}
+
+		this.inc({
+			[Users.isLocalUser(follower) ? 'local' : 'remote']: { followings: update }
+		}, follower.id);
+		this.inc({
+			[Users.isLocalUser(followee) ? 'local' : 'remote']: { followers: update }
+		}, followee.id);
+	}
+}
diff --git a/src/services/chart/charts/classes/per-user-notes.ts b/src/services/chart/charts/classes/per-user-notes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cccd49560453c1e3814ad1527e571296e0b86634
--- /dev/null
+++ b/src/services/chart/charts/classes/per-user-notes.ts
@@ -0,0 +1,58 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { User } from '../../../../models/entities/user';
+import { SchemaType } from '../../../../misc/schema';
+import { Notes } from '../../../../models';
+import { Note } from '../../../../models/entities/note';
+import { name, schema } from '../schemas/per-user-notes';
+
+type PerUserNotesLog = SchemaType<typeof schema>;
+
+export default class PerUserNotesChart extends Chart<PerUserNotesLog> {
+	constructor() {
+		super(name, schema, true);
+	}
+
+	@autobind
+	protected genNewLog(latest: PerUserNotesLog): DeepPartial<PerUserNotesLog> {
+		return {
+			total: latest.total,
+		};
+	}
+
+	@autobind
+	protected async fetchActual(group: string): Promise<DeepPartial<PerUserNotesLog>> {
+		const [count] = await Promise.all([
+			Notes.count({ userId: group }),
+		]);
+
+		return {
+			total: count,
+		};
+	}
+
+	@autobind
+	public async update(user: User, note: Note, isAdditional: boolean) {
+		const update: Obj = {
+			diffs: {}
+		};
+
+		update.total = isAdditional ? 1 : -1;
+
+		if (isAdditional) {
+			update.inc = 1;
+		} else {
+			update.dec = 1;
+		}
+
+		if (note.replyId != null) {
+			update.diffs.reply = isAdditional ? 1 : -1;
+		} else if (note.renoteId != null) {
+			update.diffs.renote = isAdditional ? 1 : -1;
+		} else {
+			update.diffs.normal = isAdditional ? 1 : -1;
+		}
+
+		await this.inc(update, user.id);
+	}
+}
diff --git a/src/services/chart/charts/classes/per-user-reactions.ts b/src/services/chart/charts/classes/per-user-reactions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..124fb4153cbd8302b8ea2c2d39c22cffa9a26772
--- /dev/null
+++ b/src/services/chart/charts/classes/per-user-reactions.ts
@@ -0,0 +1,32 @@
+import autobind from 'autobind-decorator';
+import Chart, { DeepPartial } from '../../core';
+import { User } from '../../../../models/entities/user';
+import { Note } from '../../../../models/entities/note';
+import { SchemaType } from '../../../../misc/schema';
+import { Users } from '../../../../models';
+import { name, schema } from '../schemas/per-user-reactions';
+
+type PerUserReactionsLog = SchemaType<typeof schema>;
+
+export default class PerUserReactionsChart extends Chart<PerUserReactionsLog> {
+	constructor() {
+		super(name, schema, true);
+	}
+
+	@autobind
+	protected genNewLog(latest: PerUserReactionsLog): DeepPartial<PerUserReactionsLog> {
+		return {};
+	}
+
+	@autobind
+	protected async fetchActual(group: string): Promise<DeepPartial<PerUserReactionsLog>> {
+		return {};
+	}
+
+	@autobind
+	public async update(user: User, note: Note) {
+		this.inc({
+			[Users.isLocalUser(user) ? 'local' : 'remote']: { count: 1 }
+		}, note.userId);
+	}
+}
diff --git a/src/services/chart/charts/classes/test-grouped.ts b/src/services/chart/charts/classes/test-grouped.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e32cbcf41630e0a1b3c9d3862c39482aba67951c
--- /dev/null
+++ b/src/services/chart/charts/classes/test-grouped.ts
@@ -0,0 +1,47 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { name, schema } from '../schemas/test-grouped';
+
+type TestGroupedLog = SchemaType<typeof schema>;
+
+export default class TestGroupedChart extends Chart<TestGroupedLog> {
+	private total = {} as Record<string, number>;
+
+	constructor() {
+		super(name, schema, true);
+	}
+
+	@autobind
+	protected genNewLog(latest: TestGroupedLog): DeepPartial<TestGroupedLog> {
+		return {
+			foo: {
+				total: latest.foo.total,
+			},
+		};
+	}
+
+	@autobind
+	protected async fetchActual(group: string): Promise<DeepPartial<TestGroupedLog>> {
+		return {
+			foo: {
+				total: this.total[group],
+			},
+		};
+	}
+
+	@autobind
+	public async increment(group: string) {
+		if (this.total[group] == null) this.total[group] = 0;
+
+		const update: Obj = {};
+
+		update.total = 1;
+		update.inc = 1;
+		this.total[group]++;
+
+		await this.inc({
+			foo: update
+		}, group);
+	}
+}
diff --git a/src/services/chart/charts/classes/test-unique.ts b/src/services/chart/charts/classes/test-unique.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1eb396c2936e22d19d3f1643418c503738a5ebe2
--- /dev/null
+++ b/src/services/chart/charts/classes/test-unique.ts
@@ -0,0 +1,29 @@
+import autobind from 'autobind-decorator';
+import Chart, { DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { name, schema } from '../schemas/test-unique';
+
+type TestUniqueLog = SchemaType<typeof schema>;
+
+export default class TestUniqueChart extends Chart<TestUniqueLog> {
+	constructor() {
+		super(name, schema);
+	}
+
+	@autobind
+	protected genNewLog(latest: TestUniqueLog): DeepPartial<TestUniqueLog> {
+		return {};
+	}
+
+	@autobind
+	protected async fetchActual(): Promise<DeepPartial<TestUniqueLog>> {
+		return {};
+	}
+
+	@autobind
+	public async uniqueIncrement(key: string) {
+		await this.incIfUnique({
+			foo: 1
+		}, 'foos', key);
+	}
+}
diff --git a/src/services/chart/charts/classes/test.ts b/src/services/chart/charts/classes/test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..57c22822f2c648b742f03a162bd541df2bdac322
--- /dev/null
+++ b/src/services/chart/charts/classes/test.ts
@@ -0,0 +1,45 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { name, schema } from '../schemas/test';
+
+type TestLog = SchemaType<typeof schema>;
+
+export default class TestChart extends Chart<TestLog> {
+	private total = 0;
+
+	constructor() {
+		super(name, schema);
+	}
+
+	@autobind
+	protected genNewLog(latest: TestLog): DeepPartial<TestLog> {
+		return {
+			foo: {
+				total: latest.foo.total,
+			},
+		};
+	}
+
+	@autobind
+	protected async fetchActual(): Promise<DeepPartial<TestLog>> {
+		return {
+			foo: {
+				total: this.total,
+			},
+		};
+	}
+
+	@autobind
+	public async increment() {
+		const update: Obj = {};
+
+		update.total = 1;
+		update.inc = 1;
+		this.total++;
+
+		await this.inc({
+			foo: update
+		});
+	}
+}
diff --git a/src/services/chart/charts/classes/users.ts b/src/services/chart/charts/classes/users.ts
new file mode 100644
index 0000000000000000000000000000000000000000..eec30de8dc63731a53b6155148024b1080c03402
--- /dev/null
+++ b/src/services/chart/charts/classes/users.ts
@@ -0,0 +1,60 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '../../../../misc/schema';
+import { Users } from '../../../../models';
+import { Not } from 'typeorm';
+import { User } from '../../../../models/entities/user';
+import { name, schema } from '../schemas/users';
+
+type UsersLog = SchemaType<typeof schema>;
+
+export default class UsersChart extends Chart<UsersLog> {
+	constructor() {
+		super(name, schema);
+	}
+
+	@autobind
+	protected genNewLog(latest: UsersLog): DeepPartial<UsersLog> {
+		return {
+			local: {
+				total: latest.local.total,
+			},
+			remote: {
+				total: latest.remote.total,
+			}
+		};
+	}
+
+	@autobind
+	protected async fetchActual(): Promise<DeepPartial<UsersLog>> {
+		const [localCount, remoteCount] = await Promise.all([
+			Users.count({ host: null }),
+			Users.count({ host: Not(null) })
+		]);
+
+		return {
+			local: {
+				total: localCount,
+			},
+			remote: {
+				total: remoteCount,
+			}
+		};
+	}
+
+	@autobind
+	public async update(user: User, isAdditional: boolean) {
+		const update: Obj = {};
+
+		update.total = isAdditional ? 1 : -1;
+		if (isAdditional) {
+			update.inc = 1;
+		} else {
+			update.dec = 1;
+		}
+
+		await this.inc({
+			[Users.isLocalUser(user) ? 'local' : 'remote']: update
+		});
+	}
+}
diff --git a/src/services/chart/charts/schemas/active-users.ts b/src/services/chart/charts/schemas/active-users.ts
new file mode 100644
index 0000000000000000000000000000000000000000..da8c63389c80c6d3ea602197a9bfc567ee062724
--- /dev/null
+++ b/src/services/chart/charts/schemas/active-users.ts
@@ -0,0 +1,28 @@
+export const logSchema = {
+	/**
+	 * アクティブユーザー数
+	 */
+	count: {
+		type: 'number' as 'number',
+		description: 'アクティブユーザー数',
+	},
+};
+
+/**
+ * アクティブユーザーに関するチャート
+ */
+export const schema = {
+	type: 'object' as 'object',
+	properties: {
+		local: {
+			type: 'object' as 'object',
+			properties: logSchema
+		},
+		remote: {
+			type: 'object' as 'object',
+			properties: logSchema
+		},
+	}
+};
+
+export const name = 'activeUsers';
diff --git a/src/services/chart/charts/schemas/drive.ts b/src/services/chart/charts/schemas/drive.ts
new file mode 100644
index 0000000000000000000000000000000000000000..47530e841761037967a62fd00c904fd8852b7215
--- /dev/null
+++ b/src/services/chart/charts/schemas/drive.ts
@@ -0,0 +1,65 @@
+const logSchema = {
+	/**
+	 * 集計期間時点での、全ドライブファイル数
+	 */
+	totalCount: {
+		type: 'number' as 'number',
+		description: '集計期間時点での、全ドライブファイル数'
+	},
+
+	/**
+	 * 集計期間時点での、全ドライブファイルの合計サイズ
+	 */
+	totalSize: {
+		type: 'number' as 'number',
+		description: '集計期間時点での、全ドライブファイルの合計サイズ'
+	},
+
+	/**
+	 * 増加したドライブファイル数
+	 */
+	incCount: {
+		type: 'number' as 'number',
+		description: '増加したドライブファイル数'
+	},
+
+	/**
+	 * 増加したドライブ使用量
+	 */
+	incSize: {
+		type: 'number' as 'number',
+		description: '増加したドライブ使用量'
+	},
+
+	/**
+	 * 減少したドライブファイル数
+	 */
+	decCount: {
+		type: 'number' as 'number',
+		description: '減少したドライブファイル数'
+	},
+
+	/**
+	 * 減少したドライブ使用量
+	 */
+	decSize: {
+		type: 'number' as 'number',
+		description: '減少したドライブ使用量'
+	},
+};
+
+export const schema = {
+	type: 'object' as 'object',
+	properties: {
+		local: {
+			type: 'object' as 'object',
+			properties: logSchema
+		},
+		remote: {
+			type: 'object' as 'object',
+			properties: logSchema
+		},
+	}
+};
+
+export const name = 'drive';
diff --git a/src/services/chart/charts/schemas/federation.ts b/src/services/chart/charts/schemas/federation.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d1d275fc95f5f56450d847260007c6e8c6bd610b
--- /dev/null
+++ b/src/services/chart/charts/schemas/federation.ts
@@ -0,0 +1,27 @@
+/**
+ * フェデレーションに関するチャート
+ */
+export const schema = {
+	type: 'object' as 'object',
+	properties: {
+		instance: {
+			type: 'object' as 'object',
+			properties: {
+				total: {
+					type: 'number' as 'number',
+					description: 'インスタンス数の合計'
+				},
+				inc: {
+					type: 'number' as 'number',
+					description: '増加インスタンス数'
+				},
+				dec: {
+					type: 'number' as 'number',
+					description: '減少インスタンス数'
+				},
+			}
+		}
+	}
+};
+
+export const name = 'federation';
diff --git a/src/services/chart/charts/schemas/hashtag.ts b/src/services/chart/charts/schemas/hashtag.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c1904b67012367dddd73d59ebef158c268b9a21b
--- /dev/null
+++ b/src/services/chart/charts/schemas/hashtag.ts
@@ -0,0 +1,28 @@
+export const logSchema = {
+	/**
+	 * 投稿された数
+	 */
+	count: {
+		type: 'number' as 'number',
+		description: '投稿された数',
+	},
+};
+
+/**
+ * ハッシュタグに関するチャート
+ */
+export const schema = {
+	type: 'object' as 'object',
+	properties: {
+		local: {
+			type: 'object' as 'object',
+			properties: logSchema
+		},
+		remote: {
+			type: 'object' as 'object',
+			properties: logSchema
+		},
+	}
+};
+
+export const name = 'hashtag';
diff --git a/src/services/chart/charts/schemas/instance.ts b/src/services/chart/charts/schemas/instance.ts
new file mode 100644
index 0000000000000000000000000000000000000000..af46b33629f5f387d6c826c389918dca8c03d9ff
--- /dev/null
+++ b/src/services/chart/charts/schemas/instance.ts
@@ -0,0 +1,124 @@
+/**
+ * インスタンスごとのチャート
+ */
+export const schema = {
+	type: 'object' as 'object',
+	properties: {
+		requests: {
+			type: 'object' as 'object',
+			properties: {
+				failed: {
+					type: 'number' as 'number',
+					description: '失敗したリクエスト数'
+				},
+				succeeded: {
+					type: 'number' as 'number',
+					description: '成功したリクエスト数'
+				},
+				received: {
+					type: 'number' as 'number',
+					description: '受信したリクエスト数'
+				},
+			}
+		},
+		notes: {
+			type: 'object' as 'object',
+			properties: {
+				total: {
+					type: 'number' as 'number',
+					description: '集計期間時点での、全投稿数'
+				},
+				inc: {
+					type: 'number' as 'number',
+					description: '増加した投稿数'
+				},
+				dec: {
+					type: 'number' as 'number',
+					description: '減少した投稿数'
+				},
+			}
+		},
+		users: {
+			type: 'object' as 'object',
+			properties: {
+				total: {
+					type: 'number' as 'number',
+					description: '集計期間時点での、全ユーザー数'
+				},
+				inc: {
+					type: 'number' as 'number',
+					description: '増加したユーザー数'
+				},
+				dec: {
+					type: 'number' as 'number',
+					description: '減少したユーザー数'
+				},
+			}
+		},
+		following: {
+			type: 'object' as 'object',
+			properties: {
+				total: {
+					type: 'number' as 'number',
+					description: '集計期間時点での、全フォロー数'
+				},
+				inc: {
+					type: 'number' as 'number',
+					description: '増加したフォロー数'
+				},
+				dec: {
+					type: 'number' as 'number',
+					description: '減少したフォロー数'
+				},
+			}
+		},
+		followers: {
+			type: 'object' as 'object',
+			properties: {
+				total: {
+					type: 'number' as 'number',
+					description: '集計期間時点での、全フォロワー数'
+				},
+				inc: {
+					type: 'number' as 'number',
+					description: '増加したフォロワー数'
+				},
+				dec: {
+					type: 'number' as 'number',
+					description: '減少したフォロワー数'
+				},
+			}
+		},
+		drive: {
+			type: 'object' as 'object',
+			properties: {
+				totalFiles: {
+					type: 'number' as 'number',
+					description: '集計期間時点での、全ドライブファイル数'
+				},
+				totalUsage: {
+					type: 'number' as 'number',
+					description: '集計期間時点での、全ドライブファイルの合計サイズ'
+				},
+				incFiles: {
+					type: 'number' as 'number',
+					description: '増加したドライブファイル数'
+				},
+				incUsage: {
+					type: 'number' as 'number',
+					description: '増加したドライブ使用量'
+				},
+				decFiles: {
+					type: 'number' as 'number',
+					description: '減少したドライブファイル数'
+				},
+				decUsage: {
+					type: 'number' as 'number',
+					description: '減少したドライブ使用量'
+				},
+			}
+		},
+	}
+};
+
+export const name = 'instance';
diff --git a/src/services/chart/charts/schemas/network.ts b/src/services/chart/charts/schemas/network.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4ef530c07cc0ca2f2b0ca426af59f4751afae902
--- /dev/null
+++ b/src/services/chart/charts/schemas/network.ts
@@ -0,0 +1,30 @@
+/**
+ * ネットワークに関するチャート
+ */
+export const schema = {
+	type: 'object' as 'object',
+	properties: {
+		incomingRequests: {
+			type: 'number' as 'number',
+			description: '受信したリクエスト数'
+		},
+		outgoingRequests: {
+			type: 'number' as 'number',
+			description: '送信したリクエスト数'
+		},
+		totalTime: {
+			type: 'number' as 'number',
+			description: '応答時間の合計' // TIP: (totalTime / incomingRequests) でひとつのリクエストに平均でどれくらいの時間がかかったか知れる
+		},
+		incomingBytes: {
+			type: 'number' as 'number',
+			description: '合計受信データ量'
+		},
+		outgoingBytes: {
+			type: 'number' as 'number',
+			description: '合計送信データ量'
+		},
+	}
+};
+
+export const name = 'network';
diff --git a/src/services/chart/charts/schemas/notes.ts b/src/services/chart/charts/schemas/notes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..133d1e3730a22c5ab086b8a309b024a595e0b527
--- /dev/null
+++ b/src/services/chart/charts/schemas/notes.ts
@@ -0,0 +1,52 @@
+const logSchema = {
+	total: {
+		type: 'number' as 'number',
+		description: '集計期間時点での、全投稿数'
+	},
+
+	inc: {
+		type: 'number' as 'number',
+		description: '増加した投稿数'
+	},
+
+	dec: {
+		type: 'number' as 'number',
+		description: '減少した投稿数'
+	},
+
+	diffs: {
+		type: 'object' as 'object',
+		properties: {
+			normal: {
+				type: 'number' as 'number',
+				description: '通常の投稿数の差分'
+			},
+
+			reply: {
+				type: 'number' as 'number',
+				description: 'リプライの投稿数の差分'
+			},
+
+			renote: {
+				type: 'number' as 'number',
+				description: 'Renoteの投稿数の差分'
+			},
+		}
+	},
+};
+
+export const schema = {
+	type: 'object' as 'object',
+	properties: {
+		local: {
+			type: 'object' as 'object',
+			properties: logSchema
+		},
+		remote: {
+			type: 'object' as 'object',
+			properties: logSchema
+		},
+	}
+};
+
+export const name = 'notes';
diff --git a/src/services/chart/charts/schemas/per-user-drive.ts b/src/services/chart/charts/schemas/per-user-drive.ts
new file mode 100644
index 0000000000000000000000000000000000000000..713bd7ed845e68ee2d5226c7193f6577d98006e4
--- /dev/null
+++ b/src/services/chart/charts/schemas/per-user-drive.ts
@@ -0,0 +1,54 @@
+export const schema = {
+	type: 'object' as 'object',
+	properties: {
+		/**
+		 * 集計期間時点での、全ドライブファイル数
+		 */
+		totalCount: {
+			type: 'number' as 'number',
+			description: '集計期間時点での、全ドライブファイル数'
+		},
+
+		/**
+		 * 集計期間時点での、全ドライブファイルの合計サイズ
+		 */
+		totalSize: {
+			type: 'number' as 'number',
+			description: '集計期間時点での、全ドライブファイルの合計サイズ'
+		},
+
+		/**
+		 * 増加したドライブファイル数
+		 */
+		incCount: {
+			type: 'number' as 'number',
+			description: '増加したドライブファイル数'
+		},
+
+		/**
+		 * 増加したドライブ使用量
+		 */
+		incSize: {
+			type: 'number' as 'number',
+			description: '増加したドライブ使用量'
+		},
+
+		/**
+		 * 減少したドライブファイル数
+		 */
+		decCount: {
+			type: 'number' as 'number',
+			description: '減少したドライブファイル数'
+		},
+
+		/**
+		 * 減少したドライブ使用量
+		 */
+		decSize: {
+			type: 'number' as 'number',
+			description: '減少したドライブ使用量'
+		},
+	}
+};
+
+export const name = 'perUserDrive';
diff --git a/src/services/chart/charts/schemas/per-user-following.ts b/src/services/chart/charts/schemas/per-user-following.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d6ca1130e0f1f2a8d1239d995c72d06c18bfa74e
--- /dev/null
+++ b/src/services/chart/charts/schemas/per-user-following.ts
@@ -0,0 +1,81 @@
+export const logSchema = {
+	/**
+	 * フォローしている
+	 */
+	followings: {
+		type: 'object' as 'object',
+		properties: {
+			/**
+			 * フォローしている合計
+			 */
+			total: {
+				type: 'number' as 'number',
+				description: 'フォローしている合計',
+			},
+
+			/**
+			 * フォローした数
+			 */
+			inc: {
+				type: 'number' as 'number',
+				description: 'フォローした数',
+			},
+
+			/**
+			 * フォロー解除した数
+			 */
+			dec: {
+				type: 'number' as 'number',
+				description: 'フォロー解除した数',
+			},
+		}
+	},
+
+	/**
+	 * フォローされている
+	 */
+	followers: {
+		type: 'object' as 'object',
+		properties: {
+			/**
+			 * フォローされている合計
+			 */
+			total: {
+				type: 'number' as 'number',
+				description: 'フォローされている合計',
+			},
+
+			/**
+			 * フォローされた数
+			 */
+			inc: {
+				type: 'number' as 'number',
+				description: 'フォローされた数',
+			},
+
+			/**
+			 * フォロー解除された数
+			 */
+			dec: {
+				type: 'number' as 'number',
+				description: 'フォロー解除された数',
+			},
+		}
+	},
+};
+
+export const schema = {
+	type: 'object' as 'object',
+	properties: {
+		local: {
+			type: 'object' as 'object',
+			properties: logSchema
+		},
+		remote: {
+			type: 'object' as 'object',
+			properties: logSchema
+		},
+	}
+};
+
+export const name = 'perUserFollowing';
diff --git a/src/services/chart/charts/schemas/per-user-notes.ts b/src/services/chart/charts/schemas/per-user-notes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3c448c4cee2464c83706320dcdc7e533b62e8736
--- /dev/null
+++ b/src/services/chart/charts/schemas/per-user-notes.ts
@@ -0,0 +1,41 @@
+export const schema = {
+	type: 'object' as 'object',
+	properties: {
+		total: {
+			type: 'number' as 'number',
+			description: '集計期間時点での、全投稿数'
+		},
+
+		inc: {
+			type: 'number' as 'number',
+			description: '増加した投稿数'
+		},
+
+		dec: {
+			type: 'number' as 'number',
+			description: '減少した投稿数'
+		},
+
+		diffs: {
+			type: 'object' as 'object',
+			properties: {
+				normal: {
+					type: 'number' as 'number',
+					description: '通常の投稿数の差分'
+				},
+
+				reply: {
+					type: 'number' as 'number',
+					description: 'リプライの投稿数の差分'
+				},
+
+				renote: {
+					type: 'number' as 'number',
+					description: 'Renoteの投稿数の差分'
+				},
+			}
+		},
+	}
+};
+
+export const name = 'perUserNotes';
diff --git a/src/services/chart/charts/schemas/per-user-reactions.ts b/src/services/chart/charts/schemas/per-user-reactions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1278184da65837f810ceb9a0cf122690d1d5d355
--- /dev/null
+++ b/src/services/chart/charts/schemas/per-user-reactions.ts
@@ -0,0 +1,28 @@
+export const logSchema = {
+	/**
+	 * フォローしている合計
+	 */
+	count: {
+		type: 'number' as 'number',
+		description: 'リアクションされた数',
+	},
+};
+
+/**
+ * ユーザーごとのリアクションに関するチャート
+ */
+export const schema = {
+	type: 'object' as 'object',
+	properties: {
+		local: {
+			type: 'object' as 'object',
+			properties: logSchema
+		},
+		remote: {
+			type: 'object' as 'object',
+			properties: logSchema
+		},
+	}
+};
+
+export const name = 'perUserReaction';
diff --git a/src/services/chart/charts/schemas/test-grouped.ts b/src/services/chart/charts/schemas/test-grouped.ts
new file mode 100644
index 0000000000000000000000000000000000000000..acf3fddb3157dbe34cece0a149bf3e92ad736e97
--- /dev/null
+++ b/src/services/chart/charts/schemas/test-grouped.ts
@@ -0,0 +1,26 @@
+export const schema = {
+	type: 'object' as 'object',
+	properties: {
+		foo: {
+			type: 'object' as 'object',
+			properties: {
+				total: {
+					type: 'number' as 'number',
+					description: ''
+				},
+
+				inc: {
+					type: 'number' as 'number',
+					description: ''
+				},
+
+				dec: {
+					type: 'number' as 'number',
+					description: ''
+				},
+			}
+		}
+	}
+};
+
+export const name = 'testGrouped';
diff --git a/src/services/chart/charts/schemas/test-unique.ts b/src/services/chart/charts/schemas/test-unique.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8fcfbf3c72342013e599c06ecf33a3098c898684
--- /dev/null
+++ b/src/services/chart/charts/schemas/test-unique.ts
@@ -0,0 +1,11 @@
+export const schema = {
+	type: 'object' as 'object',
+	properties: {
+		foo: {
+			type: 'number' as 'number',
+			description: ''
+		},
+	}
+};
+
+export const name = 'testUnique';
diff --git a/src/services/chart/charts/schemas/test.ts b/src/services/chart/charts/schemas/test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b1344500bfb3140c6bb4362c589161ee6af3ff59
--- /dev/null
+++ b/src/services/chart/charts/schemas/test.ts
@@ -0,0 +1,26 @@
+export const schema = {
+	type: 'object' as 'object',
+	properties: {
+		foo: {
+			type: 'object' as 'object',
+			properties: {
+				total: {
+					type: 'number' as 'number',
+					description: ''
+				},
+
+				inc: {
+					type: 'number' as 'number',
+					description: ''
+				},
+
+				dec: {
+					type: 'number' as 'number',
+					description: ''
+				},
+			}
+		}
+	}
+};
+
+export const name = 'test';
diff --git a/src/services/chart/charts/schemas/users.ts b/src/services/chart/charts/schemas/users.ts
new file mode 100644
index 0000000000000000000000000000000000000000..db7e2dd057b1e02aa432f6881e2c7db83b53620d
--- /dev/null
+++ b/src/services/chart/charts/schemas/users.ts
@@ -0,0 +1,41 @@
+const logSchema = {
+	/**
+	 * 集計期間時点での、全ユーザー数
+	 */
+	total: {
+		type: 'number' as 'number',
+		description: '集計期間時点での、全ユーザー数'
+	},
+
+	/**
+	 * 増加したユーザー数
+	 */
+	inc: {
+		type: 'number' as 'number',
+		description: '増加したユーザー数'
+	},
+
+	/**
+	 * 減少したユーザー数
+	 */
+	dec: {
+		type: 'number' as 'number',
+		description: '減少したユーザー数'
+	},
+};
+
+export const schema = {
+	type: 'object' as 'object',
+	properties: {
+		local: {
+			type: 'object' as 'object',
+			properties: logSchema
+		},
+		remote: {
+			type: 'object' as 'object',
+			properties: logSchema
+		},
+	}
+};
+
+export const name = 'users';
diff --git a/src/services/chart/core.ts b/src/services/chart/core.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2a60b1a0a3a78d4648478c683b5753d6dc20ae83
--- /dev/null
+++ b/src/services/chart/core.ts
@@ -0,0 +1,460 @@
+/**
+ * チャートエンジン
+ *
+ * Tests located in test/chart
+ */
+
+import * as moment from 'moment';
+import * as nestedProperty from 'nested-property';
+import autobind from 'autobind-decorator';
+import Logger from '../logger';
+import { Schema } from '../../misc/schema';
+import { EntitySchema, getRepository, Repository, LessThan, MoreThanOrEqual } from 'typeorm';
+import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error';
+
+const logger = new Logger('chart', 'white', process.env.NODE_ENV !== 'test');
+
+const utc = moment.utc;
+
+export type Obj = { [key: string]: any };
+
+export type DeepPartial<T> = {
+	[P in keyof T]?: DeepPartial<T[P]>;
+};
+
+type ArrayValue<T> = {
+	[P in keyof T]: T[P] extends number ? T[P][] : ArrayValue<T[P]>;
+};
+
+type Span = 'day' | 'hour';
+
+type Log = {
+	id: number;
+
+	/**
+	 * 集計のグループ
+	 */
+	group: string | null;
+
+	/**
+	 * 集計日時のUnixタイムスタンプ(秒)
+	 */
+	date: number;
+
+	/**
+	 * 集計期間
+	 */
+	span: Span;
+
+	/**
+	 * ユニークインクリメント用
+	 */
+	unique?: Record<string, any>;
+};
+
+const camelToSnake = (str: string) => {
+	return str.replace(/([A-Z])/g, s => '_' + s.charAt(0).toLowerCase());
+};
+
+/**
+ * 様々なチャートの管理を司るクラス
+ */
+export default abstract class Chart<T extends Record<string, any>> {
+	private static readonly columnPrefix = '___';
+	private static readonly columnDot = '_';
+
+	private name: string;
+	public schema: Schema;
+	protected repository: Repository<Log>;
+	protected abstract genNewLog(latest: T): DeepPartial<T>;
+	protected abstract async fetchActual(group?: string): Promise<DeepPartial<T>>;
+
+	@autobind
+	private static convertSchemaToFlatColumnDefinitions(schema: Schema) {
+		const columns = {} as any;
+		const flatColumns = (x: Obj, path?: string) => {
+			for (const [k, v] of Object.entries(x)) {
+				const p = path ? `${path}${this.columnDot}${k}` : k;
+				if (v.type === 'object') {
+					flatColumns(v.properties, p);
+				} else {
+					columns[this.columnPrefix + p] = {
+						type: 'integer',
+					};
+				}
+			}
+		};
+		flatColumns(schema.properties);
+		return columns;
+	}
+
+	@autobind
+	private static convertFlattenColumnsToObject(x: Record<string, number>) {
+		const obj = {} as any;
+		for (const k of Object.keys(x).filter(k => k.startsWith(Chart.columnPrefix))) {
+			// now k is ___x_y_z
+			const path = k.substr(Chart.columnPrefix.length).split(Chart.columnDot).join('.');
+			nestedProperty.set(obj, path, x[k]);
+		}
+		return obj;
+	}
+
+	@autobind
+	private static convertObjectToFlattenColumns(x: Record<string, any>) {
+		const columns = {} as Record<string, number>;
+		const flatten = (x: Obj, path?: string) => {
+			for (const [k, v] of Object.entries(x)) {
+				const p = path ? `${path}${this.columnDot}${k}` : k;
+				if (typeof v === 'object') {
+					flatten(v, p);
+				} else {
+					columns[this.columnPrefix + p] = v;
+				}
+			}
+		};
+		flatten(x);
+		return columns;
+	}
+
+	@autobind
+	private static convertQuery(x: Record<string, any>) {
+		const query: Record<string, Function> = {};
+
+		const columns = Chart.convertObjectToFlattenColumns(x);
+
+		for (const [k, v] of Object.entries(columns)) {
+			if (v > 0) query[k] = () => `"${k}" + ${v}`;
+			if (v < 0) query[k] = () => `"${k}" - ${v}`;
+		}
+
+		return query;
+	}
+
+	@autobind
+	private static momentToTimestamp(x: moment.Moment): Log['date'] {
+		return x.unix();
+	}
+
+	@autobind
+	public static schemaToEntity(name: string, schema: Schema): EntitySchema {
+		return new EntitySchema({
+			name: `__chart__${camelToSnake(name)}`,
+			columns: {
+				id: {
+					type: 'integer',
+					primary: true,
+					generated: true
+				},
+				date: {
+					type: 'integer',
+				},
+				group: {
+					type: 'varchar',
+					length: 128,
+					nullable: true
+				},
+				span: {
+					type: 'enum',
+					enum: ['hour', 'day']
+				},
+				unique: {
+					type: 'jsonb',
+					default: {}
+				},
+				...Chart.convertSchemaToFlatColumnDefinitions(schema)
+			},
+		});
+	}
+
+	constructor(name: string, schema: Schema, grouped = false) {
+		this.name = name;
+		this.schema = schema;
+		const entity = Chart.schemaToEntity(name, schema);
+
+		const keys = ['span', 'date'];
+		if (grouped) keys.push('group');
+
+		entity.options.uniques = [{
+			columns: keys
+		}];
+
+		this.repository = getRepository<Log>(entity);
+	}
+
+	@autobind
+	private getNewLog(latest?: T): T {
+		const log = latest ? this.genNewLog(latest) : {};
+		const flatColumns = (x: Obj, path?: string) => {
+			for (const [k, v] of Object.entries(x)) {
+				const p = path ? `${path}.${k}` : k;
+				if (v.type === 'object') {
+					flatColumns(v.properties, p);
+				} else {
+					if (nestedProperty.get(log, p) == null) {
+						nestedProperty.set(log, p, 0);
+					}
+				}
+			}
+		};
+		flatColumns(this.schema.properties);
+		return log as T;
+	}
+
+	@autobind
+	private getCurrentDate(): [number, number, number, number] {
+		const now = moment().utc();
+
+		const y = now.year();
+		const m = now.month();
+		const d = now.date();
+		const h = now.hour();
+
+		return [y, m, d, h];
+	}
+
+	@autobind
+	private getLatestLog(span: Span, group: string = null): Promise<Log> {
+		return this.repository.findOne({
+			group: group,
+			span: span
+		}, {
+			order: {
+				date: -1
+			}
+		});
+	}
+
+	@autobind
+	private async getCurrentLog(span: Span, group: string = null): Promise<Log> {
+		const [y, m, d, h] = this.getCurrentDate();
+
+		const current =
+			span == 'day' ? utc([y, m, d]) :
+			span == 'hour' ? utc([y, m, d, h]) :
+			null;
+
+		// 現在(今日または今のHour)のログ
+		const currentLog = await this.repository.findOne({
+			span: span,
+			date: Chart.momentToTimestamp(current),
+			...(group ? { group: group } : {})
+		});
+
+		// ログがあればそれを返して終了
+		if (currentLog != null) {
+			return currentLog;
+		}
+
+		let log: Log;
+		let data: T;
+
+		// 集計期間が変わってから、初めてのチャート更新なら
+		// 最も最近のログを持ってくる
+		// * 例えば集計期間が「日」である場合で考えると、
+		// * 昨日何もチャートを更新するような出来事がなかった場合は、
+		// * ログがそもそも作られずドキュメントが存在しないということがあり得るため、
+		// * 「昨日の」と決め打ちせずに「もっとも最近の」とします
+		const latest = await this.getLatestLog(span, group);
+
+		if (latest != null) {
+			const obj = Chart.convertFlattenColumnsToObject(
+				latest as Record<string, any>);
+
+			// 空ログデータを作成
+			data = await this.getNewLog(obj);
+		} else {
+			// ログが存在しなかったら
+			// (Misskeyインスタンスを建てて初めてのチャート更新時)
+
+			// 初期ログデータを作成
+			data = await this.getNewLog(null);
+
+			logger.info(`${this.name}: Initial commit created`);
+		}
+
+		try {
+			// 新規ログ挿入
+			log = await this.repository.save({
+				group: group,
+				span: span,
+				date: Chart.momentToTimestamp(current),
+				...Chart.convertObjectToFlattenColumns(data)
+			});
+		} catch (e) {
+			// duplicate key error
+			// 並列動作している他のチャートエンジンプロセスと処理が重なる場合がある
+			// その場合は再度最も新しいログを持ってくる
+			if (isDuplicateKeyValueError(e)) {
+				log = await this.getLatestLog(span, group);
+			} else {
+				logger.error(e);
+				throw e;
+			}
+		}
+
+		return log;
+	}
+
+	@autobind
+	protected commit(query: Record<string, Function>, group: string = null, uniqueKey?: string, uniqueValue?: string): Promise<any> {
+		const update = async (log: Log) => {
+			// ユニークインクリメントの場合、指定のキーに指定の値が既に存在していたら弾く
+			if (
+				uniqueKey &&
+				log.unique[uniqueKey] &&
+				log.unique[uniqueKey].includes(uniqueValue)
+			) return;
+
+			// ユニークインクリメントの指定のキーに値を追加
+			if (uniqueKey) {
+				if (log.unique[uniqueKey]) {
+					const sql = `jsonb_set("unique", '{${uniqueKey}}', ("unique"->>'${uniqueKey}')::jsonb || '["${uniqueValue}"]'::jsonb)`;
+					query['unique'] = () => sql;
+				} else {
+					const sql = `jsonb_set("unique", '{${uniqueKey}}', '["${uniqueValue}"]')`;
+					query['unique'] = () => sql;
+				}
+			}
+
+			// ログ更新
+			await this.repository.createQueryBuilder()
+				.update()
+				.set(query)
+				.where('id = :id', { id: log.id })
+				.execute();
+		};
+
+		return Promise.all([
+			this.getCurrentLog('day', group).then(log => update(log)),
+			this.getCurrentLog('hour', group).then(log => update(log)),
+		]);
+	}
+
+	@autobind
+	protected async inc(inc: DeepPartial<T>, group: string = null): Promise<void> {
+		await this.commit(Chart.convertQuery(inc as any), group);
+	}
+
+	@autobind
+	protected async incIfUnique(inc: DeepPartial<T>, key: string, value: string, group: string = null): Promise<void> {
+		await this.commit(Chart.convertQuery(inc as any), group, key, value);
+	}
+
+	@autobind
+	public async getChart(span: Span, range: number, group: string = null): Promise<ArrayValue<T>> {
+		const [y, m, d, h] = this.getCurrentDate();
+
+		const gt =
+			span == 'day' ? utc([y, m, d]).subtract(range, 'days') :
+			span == 'hour' ? utc([y, m, d, h]).subtract(range, 'hours') :
+			null;
+
+		// ログ取得
+		let logs = await this.repository.find({
+			where: {
+				group: group,
+				span: span,
+				date: MoreThanOrEqual(Chart.momentToTimestamp(gt))
+			},
+			order: {
+				date: -1
+			},
+		});
+
+		// 要求された範囲にログがひとつもなかったら
+		if (logs.length === 0) {
+			// もっとも新しいログを持ってくる
+			// (すくなくともひとつログが無いと隙間埋めできないため)
+			const recentLog = await this.repository.findOne({
+				group: group,
+				span: span
+			}, {
+				order: {
+					date: -1
+				},
+			});
+
+			if (recentLog) {
+				logs = [recentLog];
+			}
+
+		// 要求された範囲の最も古い箇所に位置するログが存在しなかったら
+		} else if (!utc(logs[logs.length - 1].date * 1000).isSame(gt)) {
+			// 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する
+			// (隙間埋めできないため)
+			const outdatedLog = await this.repository.findOne({
+				group: group,
+				span: span,
+				date: LessThan(Chart.momentToTimestamp(gt))
+			}, {
+				order: {
+					date: -1
+				},
+			});
+
+			if (outdatedLog) {
+				logs.push(outdatedLog);
+			}
+		}
+
+		const chart: T[] = [];
+
+		// æ•´å½¢
+		for (let i = (range - 1); i >= 0; i--) {
+			const current =
+				span == 'day' ? utc([y, m, d]).subtract(i, 'days') :
+				span == 'hour' ? utc([y, m, d, h]).subtract(i, 'hours') :
+				null;
+
+			const log = logs.find(l => utc(l.date * 1000).isSame(current));
+
+			if (log) {
+				const data = Chart.convertFlattenColumnsToObject(log as Record<string, any>);
+				chart.unshift(data);
+			} else {
+				// 隙間埋め
+				const latest = logs.find(l => utc(l.date * 1000).isBefore(current));
+				const data = latest ? Chart.convertFlattenColumnsToObject(latest as Record<string, any>) : null;
+				chart.unshift(this.getNewLog(data));
+			}
+		}
+
+		const res: ArrayValue<T> = {} as any;
+
+		/**
+		 * [{ foo: 1, bar: 5 }, { foo: 2, bar: 6 }, { foo: 3, bar: 7 }]
+		 * ã‚’
+		 * { foo: [1, 2, 3], bar: [5, 6, 7] }
+		 * にする
+		 */
+		const dive = (x: Obj, path?: string) => {
+			for (const [k, v] of Object.entries(x)) {
+				const p = path ? `${path}.${k}` : k;
+				if (typeof v == 'object') {
+					dive(v, p);
+				} else {
+					nestedProperty.set(res, p, chart.map(s => nestedProperty.get(s, p)));
+				}
+			}
+		};
+
+		dive(chart[0]);
+
+		return res;
+	}
+}
+
+export function convertLog(logSchema: Schema): Schema {
+	const v: Schema = JSON.parse(JSON.stringify(logSchema)); // copy
+	if (v.type === 'number') {
+		v.type = 'array';
+		v.items = {
+			type: 'number'
+		};
+	} else if (v.type === 'object') {
+		for (const k of Object.keys(v.properties)) {
+			v.properties[k] = convertLog(v.properties[k]);
+		}
+	}
+	return v;
+}
diff --git a/src/services/chart/drive.ts b/src/services/chart/drive.ts
deleted file mode 100644
index dd23412c7dcc8c2fcb142e53aa92d427b95b61eb..0000000000000000000000000000000000000000
--- a/src/services/chart/drive.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from './';
-import DriveFile, { IDriveFile } from '../../models/drive-file';
-import { isLocalUser } from '../../models/user';
-import { SchemaType } from '../../misc/schema';
-
-const logSchema = {
-	/**
-	 * 集計期間時点での、全ドライブファイル数
-	 */
-	totalCount: {
-		type: 'number' as 'number',
-		description: '集計期間時点での、全ドライブファイル数'
-	},
-
-	/**
-	 * 集計期間時点での、全ドライブファイルの合計サイズ
-	 */
-	totalSize: {
-		type: 'number' as 'number',
-		description: '集計期間時点での、全ドライブファイルの合計サイズ'
-	},
-
-	/**
-	 * 増加したドライブファイル数
-	 */
-	incCount: {
-		type: 'number' as 'number',
-		description: '増加したドライブファイル数'
-	},
-
-	/**
-	 * 増加したドライブ使用量
-	 */
-	incSize: {
-		type: 'number' as 'number',
-		description: '増加したドライブ使用量'
-	},
-
-	/**
-	 * 減少したドライブファイル数
-	 */
-	decCount: {
-		type: 'number' as 'number',
-		description: '減少したドライブファイル数'
-	},
-
-	/**
-	 * 減少したドライブ使用量
-	 */
-	decSize: {
-		type: 'number' as 'number',
-		description: '減少したドライブ使用量'
-	},
-};
-
-export const driveLogSchema = {
-	type: 'object' as 'object',
-	properties: {
-		local: {
-			type: 'object' as 'object',
-			properties: logSchema
-		},
-		remote: {
-			type: 'object' as 'object',
-			properties: logSchema
-		},
-	}
-};
-
-type DriveLog = SchemaType<typeof driveLogSchema>;
-
-class DriveChart extends Chart<DriveLog> {
-	constructor() {
-		super('drive');
-	}
-
-	@autobind
-	protected async getTemplate(init: boolean, latest?: DriveLog): Promise<DriveLog> {
-		const calcSize = (local: boolean) => DriveFile
-			.aggregate([{
-				$match: {
-					'metadata._user.host': local ? null : { $ne: null },
-					'metadata.deletedAt': { $exists: false }
-				}
-			}, {
-				$project: {
-					length: true
-				}
-			}, {
-				$group: {
-					_id: null,
-					usage: { $sum: '$length' }
-				}
-			}])
-			.then(res => res.length > 0 ? res[0].usage : 0);
-
-		const [localCount, remoteCount, localSize, remoteSize] = init ? await Promise.all([
-			DriveFile.count({ 'metadata._user.host': null }),
-			DriveFile.count({ 'metadata._user.host': { $ne: null } }),
-			calcSize(true),
-			calcSize(false)
-		]) : [
-			latest ? latest.local.totalCount : 0,
-			latest ? latest.remote.totalCount : 0,
-			latest ? latest.local.totalSize : 0,
-			latest ? latest.remote.totalSize : 0
-		];
-
-		return {
-			local: {
-				totalCount: localCount,
-				totalSize: localSize,
-				incCount: 0,
-				incSize: 0,
-				decCount: 0,
-				decSize: 0
-			},
-			remote: {
-				totalCount: remoteCount,
-				totalSize: remoteSize,
-				incCount: 0,
-				incSize: 0,
-				decCount: 0,
-				decSize: 0
-			}
-		};
-	}
-
-	@autobind
-	public async update(file: IDriveFile, isAdditional: boolean) {
-		const update: Obj = {};
-
-		update.totalCount = isAdditional ? 1 : -1;
-		update.totalSize = isAdditional ? file.length : -file.length;
-		if (isAdditional) {
-			update.incCount = 1;
-			update.incSize = file.length;
-		} else {
-			update.decCount = 1;
-			update.decSize = file.length;
-		}
-
-		await this.inc({
-			[isLocalUser(file.metadata._user) ? 'local' : 'remote']: update
-		});
-	}
-}
-
-export default new DriveChart();
diff --git a/src/services/chart/entities.ts b/src/services/chart/entities.ts
new file mode 100644
index 0000000000000000000000000000000000000000..14fd3adba084f0a5851441fb2b18f25f25b9950e
--- /dev/null
+++ b/src/services/chart/entities.ts
@@ -0,0 +1,8 @@
+import Chart from './core';
+
+export const entities = Object.values(require('require-all')({
+	dirname: __dirname + '/charts/schemas',
+	resolve: (x: any) => {
+		return Chart.schemaToEntity(x.name, x.schema);
+	}
+}));
diff --git a/src/services/chart/federation.ts b/src/services/chart/federation.ts
deleted file mode 100644
index 20da7a742125b0551fdcd4fc683f4aaa6a64dbe0..0000000000000000000000000000000000000000
--- a/src/services/chart/federation.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from '.';
-import Instance from '../../models/instance';
-
-/**
- * フェデレーションに関するチャート
- */
-type FederationLog = {
-	instance: {
-		/**
-		 * インスタンス数の合計
-		 */
-		total: number;
-
-		/**
-		 * 増加インスタンス数
-		 */
-		inc: number;
-
-		/**
-		 * 減少インスタンス数
-		 */
-		dec: number;
-	};
-};
-
-class FederationChart extends Chart<FederationLog> {
-	constructor() {
-		super('federation');
-	}
-
-	@autobind
-	protected async getTemplate(init: boolean, latest?: FederationLog): Promise<FederationLog> {
-		const [total] = init ? await Promise.all([
-			Instance.count({})
-		]) : [
-			latest ? latest.instance.total : 0
-		];
-
-		return {
-			instance: {
-				total: total,
-				inc: 0,
-				dec: 0
-			}
-		};
-	}
-
-	@autobind
-	public async update(isAdditional: boolean) {
-		const update: Obj = {};
-
-		update.total = isAdditional ? 1 : -1;
-		if (isAdditional) {
-			update.inc = 1;
-		} else {
-			update.dec = 1;
-		}
-
-		await this.inc({
-			instance: update
-		});
-	}
-}
-
-export default new FederationChart();
diff --git a/src/services/chart/hashtag.ts b/src/services/chart/hashtag.ts
deleted file mode 100644
index 7a31e9ccedb416eeb066b5a97ce7d5a852c9386c..0000000000000000000000000000000000000000
--- a/src/services/chart/hashtag.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from './';
-import { IUser, isLocalUser } from '../../models/user';
-import db from '../../db/mongodb';
-
-/**
- * ハッシュタグに関するチャート
- */
-type HashtagLog = {
-	local: {
-		/**
-		 * 投稿された数
-		 */
-		count: number;
-	};
-
-	remote: HashtagLog['local'];
-};
-
-class HashtagChart extends Chart<HashtagLog> {
-	constructor() {
-		super('hashtag', true);
-
-		// 後方互換性のため
-		db.get('chart.hashtag').findOne().then(doc => {
-			if (doc != null && doc.data.local == null) {
-				db.get('chart.hashtag').drop();
-			}
-		});
-	}
-
-	@autobind
-	protected async getTemplate(init: boolean, latest?: HashtagLog): Promise<HashtagLog> {
-		return {
-			local: {
-				count: 0
-			},
-			remote: {
-				count: 0
-			}
-		};
-	}
-
-	@autobind
-	public async update(hashtag: string, user: IUser) {
-		const update: Obj = {
-			count: 1
-		};
-
-		await this.incIfUnique({
-			[isLocalUser(user) ? 'local' : 'remote']: update
-		}, 'users', user._id.toHexString(), hashtag);
-	}
-}
-
-export default new HashtagChart();
diff --git a/src/services/chart/index.ts b/src/services/chart/index.ts
index 7a6470f4d87b092c443f5b9f8465eb451849d662..9626e3d6b3a308e904eac86a386b94c0ada0ba54 100644
--- a/src/services/chart/index.ts
+++ b/src/services/chart/index.ts
@@ -1,364 +1,25 @@
-/**
- * チャートエンジン
- */
-
-import * as moment from 'moment';
-import * as nestedProperty from 'nested-property';
-import autobind from 'autobind-decorator';
-import * as mongo from 'mongodb';
-import db from '../../db/mongodb';
-import { ICollection } from 'monk';
-import Logger from '../logger';
-import { Schema } from '../../misc/schema';
-
-const logger = new Logger('chart');
-
-const utc = moment.utc;
-
-export type Obj = { [key: string]: any };
-
-export type Partial<T> = {
-	[P in keyof T]?: Partial<T[P]>;
-};
-
-type ArrayValue<T> = {
-	[P in keyof T]: T[P] extends number ? T[P][] : ArrayValue<T[P]>;
-};
-
-type Span = 'day' | 'hour';
-
-type Log<T extends Obj> = {
-	_id: mongo.ObjectID;
-
-	/**
-	 * 集計のグループ
-	 */
-	group?: any;
-
-	/**
-	 * 集計日時
-	 */
-	date: Date;
-
-	/**
-	 * 集計期間
-	 */
-	span: Span;
-
-	/**
-	 * データ
-	 */
-	data: T;
-
-	/**
-	 * ユニークインクリメント用
-	 */
-	unique?: Obj;
-};
-
-/**
- * 様々なチャートの管理を司るクラス
- */
-export default abstract class Chart<T extends Obj> {
-	protected collection: ICollection<Log<T>>;
-	protected abstract async getTemplate(init: boolean, latest?: T, group?: any): Promise<T>;
-	private name: string;
-
-	constructor(name: string, grouped = false) {
-		this.name = name;
-		this.collection = db.get<Log<T>>(`chart.${name}`);
-
-		const keys = {
-			span: -1,
-			date: -1
-		} as { [key: string]: 1 | -1; };
-		if (grouped) keys.group = -1;
-
-		this.collection.createIndex(keys, { unique: true });
-	}
-
-	@autobind
-	private convertQuery(x: Obj, path: string): Obj {
-		const query: Obj = {};
-
-		const dive = (x: Obj, path: string) => {
-			for (const [k, v] of Object.entries(x)) {
-				const p = path ? `${path}.${k}` : k;
-				if (typeof v === 'number') {
-					query[p] = v;
-				} else {
-					dive(v, p);
-				}
-			}
-		};
-
-		dive(x, path);
-
-		return query;
-	}
-
-	@autobind
-	private getCurrentDate(): [number, number, number, number] {
-		const now = moment().utc();
-
-		const y = now.year();
-		const m = now.month();
-		const d = now.date();
-		const h = now.hour();
-
-		return [y, m, d, h];
-	}
-
-	@autobind
-	private getLatestLog(span: Span, group?: any): Promise<Log<T>> {
-		return this.collection.findOne({
-			group: group,
-			span: span
-		}, {
-			sort: {
-				date: -1
-			}
-		});
-	}
-
-	@autobind
-	private async getCurrentLog(span: Span, group?: any): Promise<Log<T>> {
-		const [y, m, d, h] = this.getCurrentDate();
-
-		const current =
-			span == 'day' ? utc([y, m, d]) :
-			span == 'hour' ? utc([y, m, d, h]) :
-			null;
-
-		// 現在(今日または今のHour)のログ
-		const currentLog = await this.collection.findOne({
-			group: group,
-			span: span,
-			date: current.toDate()
-		});
-
-		// ログがあればそれを返して終了
-		if (currentLog != null) {
-			return currentLog;
-		}
-
-		let log: Log<T>;
-		let data: T;
-
-		// 集計期間が変わってから、初めてのチャート更新なら
-		// 最も最近のログを持ってくる
-		// * 例えば集計期間が「日」である場合で考えると、
-		// * 昨日何もチャートを更新するような出来事がなかった場合は、
-		// * ログがそもそも作られずドキュメントが存在しないということがあり得るため、
-		// * 「昨日の」と決め打ちせずに「もっとも最近の」とします
-		const latest = await this.getLatestLog(span, group);
-
-		if (latest != null) {
-			// 空ログデータを作成
-			data = await this.getTemplate(false, latest.data);
-		} else {
-			// ログが存在しなかったら
-			// (Misskeyインスタンスを建てて初めてのチャート更新時など
-			// または何らかの理由でチャートコレクションを抹消した場合)
-
-			// 初期ログデータを作成
-			data = await this.getTemplate(true, null, group);
-
-			logger.info(`${this.name}: Initial commit created`);
-		}
-
-		try {
-			// 新規ログ挿入
-			log = await this.collection.insert({
-				group: group,
-				span: span,
-				date: current.toDate(),
-				data: data
-			});
-		} catch (e) {
-			// 11000 is duplicate key error
-			// 並列動作している他のチャートエンジンプロセスと処理が重なる場合がある
-			// その場合は再度最も新しいログを持ってくる
-			if (e.code === 11000) {
-				log = await this.getLatestLog(span, group);
-			} else {
-				logger.error(e);
-				throw e;
-			}
-		}
-
-		return log;
-	}
-
-	@autobind
-	protected commit(query: Obj, group?: any, uniqueKey?: string, uniqueValue?: string): void {
-		const update = (log: Log<T>) => {
-			// ユニークインクリメントの場合、指定のキーに指定の値が既に存在していたら弾く
-			if (
-				uniqueKey &&
-				log.unique &&
-				log.unique[uniqueKey] &&
-				log.unique[uniqueKey].includes(uniqueValue)
-			) return;
-
-			// ユニークインクリメントの指定のキーに値を追加
-			if (uniqueKey) {
-				query['$push'] = {
-					[`unique.${uniqueKey}`]: uniqueValue
-				};
-			}
-
-			// ログ更新
-			this.collection.update({
-				_id: log._id
-			}, query);
-		};
-
-		this.getCurrentLog('day', group).then(log => update(log));
-		this.getCurrentLog('hour', group).then(log => update(log));
-	}
-
-	@autobind
-	protected inc(inc: Partial<T>, group?: any): void {
-		this.commit({
-			$inc: this.convertQuery(inc, 'data')
-		}, group);
-	}
-
-	@autobind
-	protected incIfUnique(inc: Partial<T>, key: string, value: string, group?: any): void {
-		this.commit({
-			$inc: this.convertQuery(inc, 'data')
-		}, group, key, value);
-	}
-
-	@autobind
-	public async getChart(span: Span, range: number, group?: any): Promise<ArrayValue<T>> {
-		const promisedChart: Promise<T>[] = [];
-
-		const [y, m, d, h] = this.getCurrentDate();
-
-		const gt =
-			span == 'day' ? utc([y, m, d]).subtract(range, 'days') :
-			span == 'hour' ? utc([y, m, d, h]).subtract(range, 'hours') :
-			null;
-
-		// ログ取得
-		let logs = await this.collection.find({
-			group: group,
-			span: span,
-			date: {
-				$gte: gt.toDate()
-			}
-		}, {
-			sort: {
-				date: -1
-			},
-			fields: {
-				_id: 0
-			}
-		});
-
-		// 要求された範囲にログがひとつもなかったら
-		if (logs.length == 0) {
-			// もっとも新しいログを持ってくる
-			// (すくなくともひとつログが無いと隙間埋めできないため)
-			const recentLog = await this.collection.findOne({
-				group: group,
-				span: span
-			}, {
-				sort: {
-					date: -1
-				},
-				fields: {
-					_id: 0
-				}
-			});
-
-			if (recentLog) {
-				logs = [recentLog];
-			}
-
-		// 要求された範囲の最も古い箇所に位置するログが存在しなかったら
-		} else if (!utc(logs[logs.length - 1].date).isSame(gt)) {
-			// 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する
-			// (隙間埋めできないため)
-			const outdatedLog = await this.collection.findOne({
-				group: group,
-				span: span,
-				date: {
-					$lt: gt.toDate()
-				}
-			}, {
-				sort: {
-					date: -1
-				},
-				fields: {
-					_id: 0
-				}
-			});
-
-			if (outdatedLog) {
-				logs.push(outdatedLog);
-			}
-		}
-
-		// æ•´å½¢
-		for (let i = (range - 1); i >= 0; i--) {
-			const current =
-				span == 'day' ? utc([y, m, d]).subtract(i, 'days') :
-				span == 'hour' ? utc([y, m, d, h]).subtract(i, 'hours') :
-				null;
-
-			const log = logs.find(l => utc(l.date).isSame(current));
-
-			if (log) {
-				promisedChart.unshift(Promise.resolve(log.data));
-			} else {
-				// 隙間埋め
-				const latest = logs.find(l => utc(l.date).isBefore(current));
-				promisedChart.unshift(this.getTemplate(false, latest ? latest.data : null));
-			}
-		}
-
-		const chart = await Promise.all(promisedChart);
-
-		const res: ArrayValue<T> = {} as any;
-
-		/**
-		 * [{ foo: 1, bar: 5 }, { foo: 2, bar: 6 }, { foo: 3, bar: 7 }]
-		 * ã‚’
-		 * { foo: [1, 2, 3], bar: [5, 6, 7] }
-		 * にする
-		 */
-		const dive = (x: Obj, path?: string) => {
-			for (const [k, v] of Object.entries(x)) {
-				const p = path ? `${path}.${k}` : k;
-				if (typeof v == 'object') {
-					dive(v, p);
-				} else {
-					nestedProperty.set(res, p, chart.map(s => nestedProperty.get(s, p)));
-				}
-			}
-		};
-
-		dive(chart[0]);
-
-		return res;
-	}
-}
-
-export function convertLog(logSchema: Schema): Schema {
-	const v: Schema = JSON.parse(JSON.stringify(logSchema)); // copy
-	if (v.type === 'number') {
-		v.type = 'array';
-		v.items = {
-			type: 'number'
-		};
-	} else if (v.type === 'object') {
-		for (const k of Object.keys(v.properties)) {
-			v.properties[k] = convertLog(v.properties[k]);
-		}
-	}
-	return v;
-}
+import FederationChart from './charts/classes/federation';
+import NotesChart from './charts/classes/notes';
+import UsersChart from './charts/classes/users';
+import NetworkChart from './charts/classes/network';
+import ActiveUsersChart from './charts/classes/active-users';
+import InstanceChart from './charts/classes/instance';
+import PerUserNotesChart from './charts/classes/per-user-notes';
+import DriveChart from './charts/classes/drive';
+import PerUserReactionsChart from './charts/classes/per-user-reactions';
+import HashtagChart from './charts/classes/hashtag';
+import PerUserFollowingChart from './charts/classes/per-user-following';
+import PerUserDriveChart from './charts/classes/per-user-drive';
+
+export const federationChart = new FederationChart();
+export const notesChart = new NotesChart();
+export const usersChart = new UsersChart();
+export const networkChart = new NetworkChart();
+export const activeUsersChart = new ActiveUsersChart();
+export const instanceChart = new InstanceChart();
+export const perUserNotesChart = new PerUserNotesChart();
+export const driveChart = new DriveChart();
+export const perUserReactionsChart = new PerUserReactionsChart();
+export const hashtagChart = new HashtagChart();
+export const perUserFollowingChart = new PerUserFollowingChart();
+export const perUserDriveChart = new PerUserDriveChart();
diff --git a/src/services/chart/instance.ts b/src/services/chart/instance.ts
deleted file mode 100644
index 5af398b902b53628021d22d6d80a68276c9c1a0f..0000000000000000000000000000000000000000
--- a/src/services/chart/instance.ts
+++ /dev/null
@@ -1,302 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from '.';
-import User from '../../models/user';
-import Note from '../../models/note';
-import Following from '../../models/following';
-import DriveFile, { IDriveFile } from '../../models/drive-file';
-
-/**
- * インスタンスごとのチャート
- */
-type InstanceLog = {
-	requests: {
-		/**
-		 * 失敗したリクエスト数
-		 */
-		failed: number;
-
-		/**
-		 * 成功したリクエスト数
-		 */
-		succeeded: number;
-
-		/**
-		 * 受信したリクエスト数
-		 */
-		received: number;
-	};
-
-	notes: {
-		/**
-		 * 集計期間時点での、全投稿数
-		 */
-		total: number;
-
-		/**
-		 * 増加した投稿数
-		 */
-		inc: number;
-
-		/**
-		 * 減少した投稿数
-		 */
-		dec: number;
-	};
-
-	users: {
-		/**
-		 * 集計期間時点での、全ユーザー数
-		 */
-		total: number;
-
-		/**
-		 * 増加したユーザー数
-		 */
-		inc: number;
-
-		/**
-		 * 減少したユーザー数
-		 */
-		dec: number;
-	};
-
-	following: {
-		/**
-		 * 集計期間時点での、全フォロー数
-		 */
-		total: number;
-
-		/**
-		 * 増加したフォロー数
-		 */
-		inc: number;
-
-		/**
-		 * 減少したフォロー数
-		 */
-		dec: number;
-	};
-
-	followers: {
-		/**
-		 * 集計期間時点での、全フォロワー数
-		 */
-		total: number;
-
-		/**
-		 * 増加したフォロワー数
-		 */
-		inc: number;
-
-		/**
-		 * 減少したフォロワー数
-		 */
-		dec: number;
-	};
-
-	drive: {
-		/**
-		 * 集計期間時点での、全ドライブファイル数
-		 */
-		totalFiles: number;
-
-		/**
-		 * 集計期間時点での、全ドライブファイルの合計サイズ
-		 */
-		totalUsage: number;
-
-		/**
-		 * 増加したドライブファイル数
-		 */
-		incFiles: number;
-
-		/**
-		 * 増加したドライブ使用量
-		 */
-		incUsage: number;
-
-		/**
-		 * 減少したドライブファイル数
-		 */
-		decFiles: number;
-
-		/**
-		 * 減少したドライブ使用量
-		 */
-		decUsage: number;
-	};
-};
-
-class InstanceChart extends Chart<InstanceLog> {
-	constructor() {
-		super('instance', true);
-	}
-
-	@autobind
-	protected async getTemplate(init: boolean, latest?: InstanceLog, group?: any): Promise<InstanceLog> {
-		const calcUsage = () => DriveFile
-			.aggregate([{
-				$match: {
-					'metadata._user.host': group,
-					'metadata.deletedAt': { $exists: false }
-				}
-			}, {
-				$project: {
-					length: true
-				}
-			}, {
-				$group: {
-					_id: null,
-					usage: { $sum: '$length' }
-				}
-			}])
-			.then(res => res.length > 0 ? res[0].usage : 0);
-
-		const [
-			notesCount,
-			usersCount,
-			followingCount,
-			followersCount,
-			driveFiles,
-			driveUsage,
-		] = init ? await Promise.all([
-			Note.count({ '_user.host': group }),
-			User.count({ host: group }),
-			Following.count({ '_follower.host': group }),
-			Following.count({ '_followee.host': group }),
-			DriveFile.count({ 'metadata._user.host': group }),
-			calcUsage(),
-		]) : [
-			latest ? latest.notes.total : 0,
-			latest ? latest.users.total : 0,
-			latest ? latest.following.total : 0,
-			latest ? latest.followers.total : 0,
-			latest ? latest.drive.totalFiles : 0,
-			latest ? latest.drive.totalUsage : 0,
-		];
-
-		return {
-			requests: {
-				failed: 0,
-				succeeded: 0,
-				received: 0
-			},
-			notes: {
-				total: notesCount,
-				inc: 0,
-				dec: 0
-			},
-			users: {
-				total: usersCount,
-				inc: 0,
-				dec: 0
-			},
-			following: {
-				total: followingCount,
-				inc: 0,
-				dec: 0
-			},
-			followers: {
-				total: followersCount,
-				inc: 0,
-				dec: 0
-			},
-			drive: {
-				totalFiles: driveFiles,
-				totalUsage: driveUsage,
-				incFiles: 0,
-				incUsage: 0,
-				decFiles: 0,
-				decUsage: 0
-			}
-		};
-	}
-
-	@autobind
-	public async requestReceived(host: string) {
-		await this.inc({
-			requests: {
-				received: 1
-			}
-		}, host);
-	}
-
-	@autobind
-	public async requestSent(host: string, isSucceeded: boolean) {
-		const update: Obj = {};
-
-		if (isSucceeded) {
-			update.succeeded = 1;
-		} else {
-			update.failed = 1;
-		}
-
-		await this.inc({
-			requests: update
-		}, host);
-	}
-
-	@autobind
-	public async newUser(host: string) {
-		await this.inc({
-			users: {
-				total: 1,
-				inc: 1
-			}
-		}, host);
-	}
-
-	@autobind
-	public async updateNote(host: string, isAdditional: boolean) {
-		await this.inc({
-			notes: {
-				total: isAdditional ? 1 : -1,
-				inc: isAdditional ? 1 : 0,
-				dec: isAdditional ? 0 : 1,
-			}
-		}, host);
-	}
-
-	@autobind
-	public async updateFollowing(host: string, isAdditional: boolean) {
-		await this.inc({
-			following: {
-				total: isAdditional ? 1 : -1,
-				inc: isAdditional ? 1 : 0,
-				dec: isAdditional ? 0 : 1,
-			}
-		}, host);
-	}
-
-	@autobind
-	public async updateFollowers(host: string, isAdditional: boolean) {
-		await this.inc({
-			followers: {
-				total: isAdditional ? 1 : -1,
-				inc: isAdditional ? 1 : 0,
-				dec: isAdditional ? 0 : 1,
-			}
-		}, host);
-	}
-
-	@autobind
-	public async updateDrive(file: IDriveFile, isAdditional: boolean) {
-		const update: Obj = {};
-
-		update.totalFiles = isAdditional ? 1 : -1;
-		update.totalUsage = isAdditional ? file.length : -file.length;
-		if (isAdditional) {
-			update.incFiles = 1;
-			update.incUsage = file.length;
-		} else {
-			update.decFiles = 1;
-			update.decUsage = file.length;
-		}
-
-		await this.inc({
-			drive: update
-		}, file.metadata._user.host);
-	}
-}
-
-export default new InstanceChart();
diff --git a/src/services/chart/network.ts b/src/services/chart/network.ts
deleted file mode 100644
index fce47099d17d01d8268ee1aa76bba962f6ef5c3a..0000000000000000000000000000000000000000
--- a/src/services/chart/network.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Partial } from './';
-
-/**
- * ネットワークに関するチャート
- */
-type NetworkLog = {
-	/**
-	 * 受信したリクエスト数
-	 */
-	incomingRequests: number;
-
-	/**
-	 * 送信したリクエスト数
-	 */
-	outgoingRequests: number;
-
-	/**
-	 * 応答時間の合計
-	 * TIP: (totalTime / incomingRequests) でひとつのリクエストに平均でどれくらいの時間がかかったか知れる
-	 */
-	totalTime: number;
-
-	/**
-	 * 合計受信データ量
-	 */
-	incomingBytes: number;
-
-	/**
-	 * 合計送信データ量
-	 */
-	outgoingBytes: number;
-};
-
-class NetworkChart extends Chart<NetworkLog> {
-	constructor() {
-		super('network');
-	}
-
-	@autobind
-	protected async getTemplate(init: boolean, latest?: NetworkLog): Promise<NetworkLog> {
-		return {
-			incomingRequests: 0,
-			outgoingRequests: 0,
-			totalTime: 0,
-			incomingBytes: 0,
-			outgoingBytes: 0
-		};
-	}
-
-	@autobind
-	public async update(incomingRequests: number, time: number, incomingBytes: number, outgoingBytes: number) {
-		const inc: Partial<NetworkLog> = {
-			incomingRequests: incomingRequests,
-			totalTime: time,
-			incomingBytes: incomingBytes,
-			outgoingBytes: outgoingBytes
-		};
-
-		await this.inc(inc);
-	}
-}
-
-export default new NetworkChart();
diff --git a/src/services/chart/notes.ts b/src/services/chart/notes.ts
deleted file mode 100644
index b047ec273f01be2e5ad35d58057161deb394640d..0000000000000000000000000000000000000000
--- a/src/services/chart/notes.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from '.';
-import Note, { INote } from '../../models/note';
-import { isLocalUser } from '../../models/user';
-import { SchemaType } from '../../misc/schema';
-
-const logSchema = {
-	total: {
-		type: 'number' as 'number',
-		description: '集計期間時点での、全投稿数'
-	},
-
-	inc: {
-		type: 'number' as 'number',
-		description: '増加した投稿数'
-	},
-
-	dec: {
-		type: 'number' as 'number',
-		description: '減少した投稿数'
-	},
-
-	diffs: {
-		type: 'object' as 'object',
-		properties: {
-			normal: {
-				type: 'number' as 'number',
-				description: '通常の投稿数の差分'
-			},
-
-			reply: {
-				type: 'number' as 'number',
-				description: 'リプライの投稿数の差分'
-			},
-
-			renote: {
-				type: 'number' as 'number',
-				description: 'Renoteの投稿数の差分'
-			},
-		}
-	},
-};
-
-export const notesLogSchema = {
-	type: 'object' as 'object',
-	properties: {
-		local: {
-			type: 'object' as 'object',
-			properties: logSchema
-		},
-		remote: {
-			type: 'object' as 'object',
-			properties: logSchema
-		},
-	}
-};
-
-type NotesLog = SchemaType<typeof notesLogSchema>;
-
-class NotesChart extends Chart<NotesLog> {
-	constructor() {
-		super('notes');
-	}
-
-	@autobind
-	protected async getTemplate(init: boolean, latest?: NotesLog): Promise<NotesLog> {
-		const [localCount, remoteCount] = init ? await Promise.all([
-			Note.count({ '_user.host': null }),
-			Note.count({ '_user.host': { $ne: null } })
-		]) : [
-			latest ? latest.local.total : 0,
-			latest ? latest.remote.total : 0
-		];
-
-		return {
-			local: {
-				total: localCount,
-				inc: 0,
-				dec: 0,
-				diffs: {
-					normal: 0,
-					reply: 0,
-					renote: 0
-				}
-			},
-			remote: {
-				total: remoteCount,
-				inc: 0,
-				dec: 0,
-				diffs: {
-					normal: 0,
-					reply: 0,
-					renote: 0
-				}
-			}
-		};
-	}
-
-	@autobind
-	public async update(note: INote, isAdditional: boolean) {
-		const update: Obj = {
-			diffs: {}
-		};
-
-		update.total = isAdditional ? 1 : -1;
-
-		if (isAdditional) {
-			update.inc = 1;
-		} else {
-			update.dec = 1;
-		}
-
-		if (note.replyId != null) {
-			update.diffs.reply = isAdditional ? 1 : -1;
-		} else if (note.renoteId != null) {
-			update.diffs.renote = isAdditional ? 1 : -1;
-		} else {
-			update.diffs.normal = isAdditional ? 1 : -1;
-		}
-
-		await this.inc({
-			[isLocalUser(note._user) ? 'local' : 'remote']: update
-		});
-	}
-}
-
-export default new NotesChart();
diff --git a/src/services/chart/per-user-drive.ts b/src/services/chart/per-user-drive.ts
deleted file mode 100644
index 4f335f168898e3b7fce5cc350d30bf9b4c1cc545..0000000000000000000000000000000000000000
--- a/src/services/chart/per-user-drive.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from './';
-import DriveFile, { IDriveFile } from '../../models/drive-file';
-import { SchemaType } from '../../misc/schema';
-
-export const perUserDriveLogSchema = {
-	type: 'object' as 'object',
-	properties: {
-		/**
-		 * 集計期間時点での、全ドライブファイル数
-		 */
-		totalCount: {
-			type: 'number' as 'number',
-			description: '集計期間時点での、全ドライブファイル数'
-		},
-
-		/**
-		 * 集計期間時点での、全ドライブファイルの合計サイズ
-		 */
-		totalSize: {
-			type: 'number' as 'number',
-			description: '集計期間時点での、全ドライブファイルの合計サイズ'
-		},
-
-		/**
-		 * 増加したドライブファイル数
-		 */
-		incCount: {
-			type: 'number' as 'number',
-			description: '増加したドライブファイル数'
-		},
-
-		/**
-		 * 増加したドライブ使用量
-		 */
-		incSize: {
-			type: 'number' as 'number',
-			description: '増加したドライブ使用量'
-		},
-
-		/**
-		 * 減少したドライブファイル数
-		 */
-		decCount: {
-			type: 'number' as 'number',
-			description: '減少したドライブファイル数'
-		},
-
-		/**
-		 * 減少したドライブ使用量
-		 */
-		decSize: {
-			type: 'number' as 'number',
-			description: '減少したドライブ使用量'
-		},
-	}
-};
-
-type PerUserDriveLog = SchemaType<typeof perUserDriveLogSchema>;
-
-class PerUserDriveChart extends Chart<PerUserDriveLog> {
-	constructor() {
-		super('perUserDrive', true);
-	}
-
-	@autobind
-	protected async getTemplate(init: boolean, latest?: PerUserDriveLog, group?: any): Promise<PerUserDriveLog> {
-		const calcSize = () => DriveFile
-			.aggregate([{
-				$match: {
-					'metadata.userId': group,
-					'metadata.deletedAt': { $exists: false }
-				}
-			}, {
-				$project: {
-					length: true
-				}
-			}, {
-				$group: {
-					_id: null,
-					usage: { $sum: '$length' }
-				}
-			}])
-			.then(res => res.length > 0 ? res[0].usage : 0);
-
-		const [count, size] = init ? await Promise.all([
-			DriveFile.count({ 'metadata.userId': group }),
-			calcSize()
-		]) : [
-			latest ? latest.totalCount : 0,
-			latest ? latest.totalSize : 0
-		];
-
-		return {
-			totalCount: count,
-			totalSize: size,
-			incCount: 0,
-			incSize: 0,
-			decCount: 0,
-			decSize: 0
-		};
-	}
-
-	@autobind
-	public async update(file: IDriveFile, isAdditional: boolean) {
-		const update: Obj = {};
-
-		update.totalCount = isAdditional ? 1 : -1;
-		update.totalSize = isAdditional ? file.length : -file.length;
-		if (isAdditional) {
-			update.incCount = 1;
-			update.incSize = file.length;
-		} else {
-			update.decCount = 1;
-			update.decSize = file.length;
-		}
-
-		await this.inc(update, file.metadata.userId);
-	}
-}
-
-export default new PerUserDriveChart();
diff --git a/src/services/chart/per-user-following.ts b/src/services/chart/per-user-following.ts
deleted file mode 100644
index 8a94a4f155cdeae0d0b0ecb480a3586e09847aa3..0000000000000000000000000000000000000000
--- a/src/services/chart/per-user-following.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from './';
-import Following from '../../models/following';
-import { IUser, isLocalUser } from '../../models/user';
-import { SchemaType } from '../../misc/schema';
-
-export const logSchema = {
-	/**
-	 * フォローしている
-	 */
-	followings: {
-		type: 'object' as 'object',
-		properties: {
-			/**
-			 * フォローしている合計
-			 */
-			total: {
-				type: 'number',
-				description: 'フォローしている合計',
-			},
-
-			/**
-			 * フォローした数
-			 */
-			inc: {
-				type: 'number',
-				description: 'フォローした数',
-			},
-
-			/**
-			 * フォロー解除した数
-			 */
-			dec: {
-				type: 'number',
-				description: 'フォロー解除した数',
-			},
-		}
-	},
-
-	/**
-	 * フォローされている
-	 */
-	followers: {
-		type: 'object' as 'object',
-		properties: {
-			/**
-			 * フォローされている合計
-			 */
-			total: {
-				type: 'number',
-				description: 'フォローされている合計',
-			},
-
-			/**
-			 * フォローされた数
-			 */
-			inc: {
-				type: 'number',
-				description: 'フォローされた数',
-			},
-
-			/**
-			 * フォロー解除された数
-			 */
-			dec: {
-				type: 'number',
-				description: 'フォロー解除された数',
-			},
-		}
-	},
-};
-
-export const perUserFollowingLogSchema = {
-	type: 'object' as 'object',
-	properties: {
-		local: {
-			type: 'object' as 'object',
-			properties: logSchema
-		},
-		remote: {
-			type: 'object' as 'object',
-			properties: logSchema
-		},
-	}
-};
-
-type PerUserFollowingLog = SchemaType<typeof perUserFollowingLogSchema>;
-
-class PerUserFollowingChart extends Chart<PerUserFollowingLog> {
-	constructor() {
-		super('perUserFollowing', true);
-	}
-
-	@autobind
-	protected async getTemplate(init: boolean, latest?: PerUserFollowingLog, group?: any): Promise<PerUserFollowingLog> {
-		const [
-			localFollowingsCount,
-			localFollowersCount,
-			remoteFollowingsCount,
-			remoteFollowersCount
-		] = init ? await Promise.all([
-			Following.count({ followerId: group, '_followee.host': null }),
-			Following.count({ followeeId: group, '_follower.host': null }),
-			Following.count({ followerId: group, '_followee.host': { $ne: null } }),
-			Following.count({ followeeId: group, '_follower.host': { $ne: null } })
-		]) : [
-			latest ? latest.local.followings.total : 0,
-			latest ? latest.local.followers.total : 0,
-			latest ? latest.remote.followings.total : 0,
-			latest ? latest.remote.followers.total : 0
-		];
-
-		return {
-			local: {
-				followings: {
-					total: localFollowingsCount,
-					inc: 0,
-					dec: 0
-				},
-				followers: {
-					total: localFollowersCount,
-					inc: 0,
-					dec: 0
-				}
-			},
-			remote: {
-				followings: {
-					total: remoteFollowingsCount,
-					inc: 0,
-					dec: 0
-				},
-				followers: {
-					total: remoteFollowersCount,
-					inc: 0,
-					dec: 0
-				}
-			}
-		};
-	}
-
-	@autobind
-	public async update(follower: IUser, followee: IUser, isFollow: boolean) {
-		const update: Obj = {};
-
-		update.total = isFollow ? 1 : -1;
-
-		if (isFollow) {
-			update.inc = 1;
-		} else {
-			update.dec = 1;
-		}
-
-		this.inc({
-			[isLocalUser(follower) ? 'local' : 'remote']: { followings: update }
-		}, follower._id);
-		this.inc({
-			[isLocalUser(followee) ? 'local' : 'remote']: { followers: update }
-		}, followee._id);
-	}
-}
-
-export default new PerUserFollowingChart();
diff --git a/src/services/chart/per-user-notes.ts b/src/services/chart/per-user-notes.ts
deleted file mode 100644
index 2f4f88209104407c5712853f3f4da9298e7dafb6..0000000000000000000000000000000000000000
--- a/src/services/chart/per-user-notes.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from './';
-import Note, { INote } from '../../models/note';
-import { IUser } from '../../models/user';
-import { SchemaType } from '../../misc/schema';
-
-export const perUserNotesLogSchema = {
-	type: 'object' as 'object',
-	properties: {
-		total: {
-			type: 'number' as 'number',
-			description: '集計期間時点での、全投稿数'
-		},
-
-		inc: {
-			type: 'number' as 'number',
-			description: '増加した投稿数'
-		},
-
-		dec: {
-			type: 'number' as 'number',
-			description: '減少した投稿数'
-		},
-
-		diffs: {
-			type: 'object' as 'object',
-			properties: {
-				normal: {
-					type: 'number' as 'number',
-					description: '通常の投稿数の差分'
-				},
-
-				reply: {
-					type: 'number' as 'number',
-					description: 'リプライの投稿数の差分'
-				},
-
-				renote: {
-					type: 'number' as 'number',
-					description: 'Renoteの投稿数の差分'
-				},
-			}
-		},
-	}
-};
-
-type PerUserNotesLog = SchemaType<typeof perUserNotesLogSchema>;
-
-class PerUserNotesChart extends Chart<PerUserNotesLog> {
-	constructor() {
-		super('perUserNotes', true);
-	}
-
-	@autobind
-	protected async getTemplate(init: boolean, latest?: PerUserNotesLog, group?: any): Promise<PerUserNotesLog> {
-		const [count] = init ? await Promise.all([
-			Note.count({ userId: group, deletedAt: null }),
-		]) : [
-			latest ? latest.total : 0
-		];
-
-		return {
-			total: count,
-			inc: 0,
-			dec: 0,
-			diffs: {
-				normal: 0,
-				reply: 0,
-				renote: 0
-			}
-		};
-	}
-
-	@autobind
-	public async update(user: IUser, note: INote, isAdditional: boolean) {
-		const update: Obj = {
-			diffs: {}
-		};
-
-		update.total = isAdditional ? 1 : -1;
-
-		if (isAdditional) {
-			update.inc = 1;
-		} else {
-			update.dec = 1;
-		}
-
-		if (note.replyId != null) {
-			update.diffs.reply = isAdditional ? 1 : -1;
-		} else if (note.renoteId != null) {
-			update.diffs.renote = isAdditional ? 1 : -1;
-		} else {
-			update.diffs.normal = isAdditional ? 1 : -1;
-		}
-
-		await this.inc(update, user._id);
-	}
-}
-
-export default new PerUserNotesChart();
diff --git a/src/services/chart/per-user-reactions.ts b/src/services/chart/per-user-reactions.ts
deleted file mode 100644
index 60495aeb02daa99f64bda710eb79d2d740522e93..0000000000000000000000000000000000000000
--- a/src/services/chart/per-user-reactions.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart from './';
-import { IUser, isLocalUser } from '../../models/user';
-import { INote } from '../../models/note';
-
-/**
- * ユーザーごとのリアクションに関するチャート
- */
-type PerUserReactionsLog = {
-	local: {
-		/**
-		 * リアクションされた数
-		 */
-		count: number;
-	};
-
-	remote: PerUserReactionsLog['local'];
-};
-
-class PerUserReactionsChart extends Chart<PerUserReactionsLog> {
-	constructor() {
-		super('perUserReaction', true);
-	}
-
-	@autobind
-	protected async getTemplate(init: boolean, latest?: PerUserReactionsLog, group?: any): Promise<PerUserReactionsLog> {
-		return {
-			local: {
-				count: 0
-			},
-			remote: {
-				count: 0
-			}
-		};
-	}
-
-	@autobind
-	public async update(user: IUser, note: INote) {
-		this.inc({
-			[isLocalUser(user) ? 'local' : 'remote']: { count: 1 }
-		}, note.userId);
-	}
-}
-
-export default new PerUserReactionsChart();
diff --git a/src/services/chart/users.ts b/src/services/chart/users.ts
deleted file mode 100644
index cca9590842cc66b41a8e442f172f93e1b0779317..0000000000000000000000000000000000000000
--- a/src/services/chart/users.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj } from './';
-import User, { IUser, isLocalUser } from '../../models/user';
-import { SchemaType } from '../../misc/schema';
-
-const logSchema = {
-	/**
-	 * 集計期間時点での、全ユーザー数
-	 */
-	total: {
-		type: 'number' as 'number',
-		description: '集計期間時点での、全ユーザー数'
-	},
-
-	/**
-	 * 増加したユーザー数
-	 */
-	inc: {
-		type: 'number' as 'number',
-		description: '増加したユーザー数'
-	},
-
-	/**
-	 * 減少したユーザー数
-	 */
-	dec: {
-		type: 'number' as 'number',
-		description: '減少したユーザー数'
-	},
-};
-
-export const usersLogSchema = {
-	type: 'object' as 'object',
-	properties: {
-		local: {
-			type: 'object' as 'object',
-			properties: logSchema
-		},
-		remote: {
-			type: 'object' as 'object',
-			properties: logSchema
-		},
-	}
-};
-
-type UsersLog = SchemaType<typeof usersLogSchema>;
-
-class UsersChart extends Chart<UsersLog> {
-	constructor() {
-		super('users');
-	}
-
-	@autobind
-	protected async getTemplate(init: boolean, latest?: UsersLog): Promise<UsersLog> {
-		const [localCount, remoteCount] = init ? await Promise.all([
-			User.count({ host: null }),
-			User.count({ host: { $ne: null } })
-		]) : [
-			latest ? latest.local.total : 0,
-			latest ? latest.remote.total : 0
-		];
-
-		return {
-			local: {
-				total: localCount,
-				inc: 0,
-				dec: 0
-			},
-			remote: {
-				total: remoteCount,
-				inc: 0,
-				dec: 0
-			}
-		};
-	}
-
-	@autobind
-	public async update(user: IUser, isAdditional: boolean) {
-		const update: Obj = {};
-
-		update.total = isAdditional ? 1 : -1;
-		if (isAdditional) {
-			update.inc = 1;
-		} else {
-			update.dec = 1;
-		}
-
-		await this.inc({
-			[isLocalUser(user) ? 'local' : 'remote']: update
-		});
-	}
-}
-
-export default new UsersChart();
diff --git a/src/services/create-notification.ts b/src/services/create-notification.ts
index 3e000ef2ed1773a30248b366bb4403f78dbfa83e..bcb8214c56829eddb748e08b6e4b6c1e5aeeaa71 100644
--- a/src/services/create-notification.ts
+++ b/src/services/create-notification.ts
@@ -1,62 +1,66 @@
-import * as mongo from 'mongodb';
-import Notification from '../models/notification';
-import Mute from '../models/mute';
-import { pack } from '../models/notification';
 import { publishMainStream } from './stream';
-import User from '../models/user';
 import pushSw from './push-notification';
+import { Notifications, Mutings } from '../models';
+import { genId } from '../misc/gen-id';
+import { User } from '../models/entities/user';
+import { Note } from '../models/entities/note';
+import { Notification } from '../models/entities/notification';
 
-export default (
-	notifiee: mongo.ObjectID,
-	notifier: mongo.ObjectID,
+export async function createNotification(
+	notifieeId: User['id'],
+	notifierId: User['id'],
 	type: string,
-	content?: any
-) => new Promise<any>(async (resolve, reject) => {
-	if (notifiee.equals(notifier)) {
-		return resolve();
+	content?: {
+		noteId?: Note['id'];
+		reaction?: string;
+		choice?: number;
+	}
+) {
+	if (notifieeId === notifierId) {
+		return null;
 	}
 
-	// Create notification
-	const notification = await Notification.insert(Object.assign({
+	const data = {
+		id: genId(),
 		createdAt: new Date(),
-		notifieeId: notifiee,
-		notifierId: notifier,
+		notifieeId: notifieeId,
+		notifierId: notifierId,
 		type: type,
-		isRead: false
-	}, content));
+		isRead: false,
+	} as Partial<Notification>;
+
+	if (content) {
+		if (content.noteId) data.noteId = content.noteId;
+		if (content.reaction) data.reaction = content.reaction;
+		if (content.choice) data.choice = content.choice;
+	}
 
-	resolve(notification);
+	// Create notification
+	const notification = await Notifications.save(data);
 
-	const packed = await pack(notification);
+	const packed = await Notifications.pack(notification);
 
 	// Publish notification event
-	publishMainStream(notifiee, 'notification', packed);
-
-	// Update flag
-	User.update({ _id: notifiee }, {
-		$set: {
-			hasUnreadNotification: true
-		}
-	});
+	publishMainStream(notifieeId, 'notification', packed);
 
 	// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
 	setTimeout(async () => {
-		const fresh = await Notification.findOne({ _id: notification._id }, { isRead: true });
+		const fresh = await Notifications.findOne(notification.id);
 		if (!fresh.isRead) {
 			//#region ただしミュートしているユーザーからの通知なら無視
-			const mute = await Mute.find({
-				muterId: notifiee,
-				deletedAt: { $exists: false }
+			const mutings = await Mutings.find({
+				muterId: notifieeId
 			});
-			const mutedUserIds = mute.map(m => m.muteeId.toString());
-			if (mutedUserIds.indexOf(notifier.toString()) != -1) {
+			if (mutings.map(m => m.muteeId).includes(notifierId)) {
 				return;
 			}
 			//#endregion
 
-			publishMainStream(notifiee, 'unreadNotification', packed);
+			publishMainStream(notifieeId, 'unreadNotification', packed);
 
-			pushSw(notifiee, 'notification', packed);
+			pushSw(notifieeId, 'notification', packed);
 		}
 	}, 2000);
-});
+
+	return notification;
+}
diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts
index cdbcb34de40a2600ea5e2495d7d4aefa20d59b56..df5eedf4c8c248b8e6d3d38a871efc25c514837d 100644
--- a/src/services/drive/add-file.ts
+++ b/src/services/drive/add-file.ts
@@ -1,31 +1,27 @@
 import { Buffer } from 'buffer';
 import * as fs from 'fs';
 
-import * as mongodb from 'mongodb';
 import * as crypto from 'crypto';
 import * as Minio from 'minio';
 import * as uuid from 'uuid';
 import * as sharp from 'sharp';
 
-import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile } from '../../models/drive-file';
-import DriveFolder from '../../models/drive-folder';
-import { pack } from '../../models/drive-file';
 import { publishMainStream, publishDriveStream } from '../stream';
-import { isLocalUser, IUser, IRemoteUser, isRemoteUser } from '../../models/user';
 import delFile from './delete-file';
 import config from '../../config';
-import { getDriveFileWebpublicBucket } from '../../models/drive-file-webpublic';
-import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
-import driveChart from '../../services/chart/drive';
-import perUserDriveChart from '../../services/chart/per-user-drive';
-import instanceChart from '../../services/chart/instance';
 import fetchMeta from '../../misc/fetch-meta';
 import { GenerateVideoThumbnail } from './generate-video-thumbnail';
 import { driveLogger } from './logger';
 import { IImage, ConvertToJpeg, ConvertToWebp, ConvertToPng } from './image-processor';
-import Instance from '../../models/instance';
 import { contentDisposition } from '../../misc/content-disposition';
 import { detectMine } from '../../misc/detect-mine';
+import { DriveFiles, DriveFolders, Users, Instances } from '../../models';
+import { InternalStorage } from './internal-storage';
+import { DriveFile } from '../../models/entities/drive-file';
+import { IRemoteUser, User } from '../../models/entities/user';
+import { driveChart, perUserDriveChart, instanceChart } from '../chart';
+import { genId } from '../../misc/gen-id';
+import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error';
 
 const logger = driveLogger.createSubLogger('register', 'yellow');
 
@@ -36,11 +32,10 @@ const logger = driveLogger.createSubLogger('register', 'yellow');
  * @param type Content-Type for original
  * @param hash Hash for original
  * @param size Size for original
- * @param metadata
  */
-async function save(path: string, name: string, type: string, hash: string, size: number, metadata: IMetadata): Promise<IDriveFile> {
+async function save(file: DriveFile, path: string, name: string, type: string, hash: string, size: number): Promise<DriveFile> {
 	// thunbnail, webpublic を必要なら生成
-	const alts = await generateAlts(path, type, !metadata.uri);
+	const alts = await generateAlts(path, type, !file.uri);
 
 	if (config.drive && config.drive.storage == 'minio') {
 		//#region ObjectStorage params
@@ -60,10 +55,10 @@ async function save(path: string, name: string, type: string, hash: string, size
 		const url = `${ baseUrl }/${ key }`;
 
 		// for alts
-		let webpublicKey = null as string;
-		let webpublicUrl = null as string;
-		let thumbnailKey = null as string;
-		let thumbnailUrl = null as string;
+		let webpublicKey: string = null;
+		let webpublicUrl: string = null;
+		let thumbnailKey: string = null;
+		let thumbnailUrl: string = null;
 		//#endregion
 
 		//#region Uploads
@@ -91,58 +86,52 @@ async function save(path: string, name: string, type: string, hash: string, size
 		await Promise.all(uploads);
 		//#endregion
 
-		//#region DB
-		Object.assign(metadata, {
-			withoutChunks: true,
-			storage: 'minio',
-			storageProps: {
-				key,
-				webpublicKey,
-				thumbnailKey,
-			},
-			url,
-			webpublicUrl,
-			thumbnailUrl,
-		} as IMetadata);
-
-		const file = await DriveFile.insert({
-			length: size,
-			uploadDate: new Date(),
-			md5: hash,
-			filename: name,
-			metadata: metadata,
-			contentType: type
-		});
-		//#endregion
-
-		return file;
-	} else {	// use MongoDB GridFS
-		// #region store original
-		const originalDst = await getDriveFileBucket();
-
-		// web用(Exif削除済み)がある場合はオリジナルにアクセス制限
-		if (alts.webpublic) metadata.accessKey = uuid.v4();
+		file.url = url;
+		file.thumbnailUrl = thumbnailUrl;
+		file.webpublicUrl = webpublicUrl;
+		file.accessKey = key;
+		file.thumbnailAccessKey = thumbnailKey;
+		file.webpublicAccessKey = webpublicKey;
+		file.name = name;
+		file.type = type;
+		file.md5 = hash;
+		file.size = size;
+		file.storedInternal = false;
+
+		return await DriveFiles.save(file);
+	} else { // use internal storage
+		const accessKey = uuid.v4();
+		const thumbnailAccessKey = uuid.v4();
+		const webpublicAccessKey = uuid.v4();
+
+		const url = InternalStorage.saveFromPath(accessKey, path);
+
+		let thumbnailUrl: string;
+		let webpublicUrl: string;
 
-		const originalFile = await storeOriginal(originalDst, name, path, type, metadata);
-
-		logger.info(`original stored to ${originalFile._id}`);
-		// #endregion store original
-
-		// #region store webpublic
-		if (alts.webpublic) {
-			const webDst = await getDriveFileWebpublicBucket();
-			const webFile = await storeAlts(webDst, name, alts.webpublic.data, alts.webpublic.type, originalFile._id);
-			logger.info(`web stored ${webFile._id}`);
+		if (alts.thumbnail) {
+			thumbnailUrl = InternalStorage.saveFromBuffer(thumbnailAccessKey, alts.thumbnail.data);
+			logger.info(`thumbnail stored: ${thumbnailAccessKey}`);
 		}
-		// #endregion store webpublic
 
-		if (alts.thumbnail) {
-			const thumDst = await getDriveFileThumbnailBucket();
-			const thumFile = await storeAlts(thumDst, name, alts.thumbnail.data, alts.thumbnail.type, originalFile._id);
-			logger.info(`web stored ${thumFile._id}`);
+		if (alts.webpublic) {
+			webpublicUrl = InternalStorage.saveFromBuffer(webpublicAccessKey, alts.webpublic.data);
+			logger.info(`web stored: ${webpublicAccessKey}`);
 		}
 
-		return originalFile;
+		file.storedInternal = true;
+		file.url = url;
+		file.thumbnailUrl = thumbnailUrl;
+		file.webpublicUrl = webpublicUrl;
+		file.accessKey = accessKey;
+		file.thumbnailAccessKey = thumbnailAccessKey;
+		file.webpublicAccessKey = webpublicAccessKey;
+		file.name = name;
+		file.type = type;
+		file.md5 = hash;
+		file.size = size;
+
+		return await DriveFiles.save(file);
 	}
 }
 
@@ -211,51 +200,14 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string,
 	await minio.putObject(config.drive.bucket, key, stream, null, metadata);
 }
 
-/**
- * GridFSBucketにオリジナルを格納する
- */
-export async function storeOriginal(bucket: mongodb.GridFSBucket, name: string, path: string, contentType: string, metadata: any) {
-	return new Promise<IDriveFile>((resolve, reject) => {
-		const writeStream = bucket.openUploadStream(name, {
-			contentType,
-			metadata
-		});
-
-		writeStream.once('finish', resolve);
-		writeStream.on('error', reject);
-		fs.createReadStream(path).pipe(writeStream);
-	});
-}
-
-/**
- * GridFSBucketにオリジナル以外を格納する
- */
-export async function storeAlts(bucket: mongodb.GridFSBucket, name: string, data: Buffer, contentType: string, originalId: mongodb.ObjectID) {
-	return new Promise<IDriveFile>((resolve, reject) => {
-		const writeStream = bucket.openUploadStream(name, {
-			contentType,
-			metadata: {
-				originalId
-			}
-		});
-
-		writeStream.once('finish', resolve);
-		writeStream.on('error', reject);
-		writeStream.end(data);
-	});
-}
-
 async function deleteOldFile(user: IRemoteUser) {
-	const oldFile = await DriveFile.findOne({
-		_id: {
-			$nin: [user.avatarId, user.bannerId]
-		},
-		'metadata.userId': user._id
-	}, {
-		sort: {
-			_id: 1
-		}
-	});
+	const oldFile = await DriveFiles.createQueryBuilder()
+		.select('file')
+		.where('file.id != :avatarId', { avatarId: user.avatarId })
+		.andWhere('file.id != :bannerId', { bannerId: user.bannerId })
+		.andWhere('file.userId = :userId', { userId: user.id })
+		.orderBy('file.id', 'DESC')
+		.getOne();
 
 	if (oldFile) {
 		delFile(oldFile, true);
@@ -278,17 +230,17 @@ async function deleteOldFile(user: IRemoteUser) {
  * @return Created drive file
  */
 export default async function(
-	user: IUser,
+	user: User,
 	path: string,
 	name: string = null,
 	comment: string = null,
-	folderId: mongodb.ObjectID = null,
+	folderId: any = null,
 	force: boolean = false,
 	isLink: boolean = false,
 	url: string = null,
 	uri: string = null,
 	sensitive: boolean = null
-): Promise<IDriveFile> {
+): Promise<DriveFile> {
 	// Calc md5 hash
 	const calcHash = new Promise<string>((res, rej) => {
 		const readable = fs.createReadStream(path);
@@ -322,51 +274,29 @@ export default async function(
 
 	if (!force) {
 		// Check if there is a file with the same hash
-		const much = await DriveFile.findOne({
+		const much = await DriveFiles.findOne({
 			md5: hash,
-			'metadata.userId': user._id,
-			'metadata.deletedAt': { $exists: false }
+			userId: user.id,
 		});
 
 		if (much) {
-			logger.info(`file with same hash is found: ${much._id}`);
+			logger.info(`file with same hash is found: ${much.id}`);
 			return much;
 		}
 	}
 
 	//#region Check drive usage
 	if (!isLink) {
-		const usage = await DriveFile
-			.aggregate([{
-				$match: {
-					'metadata.userId': user._id,
-					'metadata.deletedAt': { $exists: false }
-				}
-			}, {
-				$project: {
-					length: true
-				}
-			}, {
-				$group: {
-					_id: null,
-					usage: { $sum: '$length' }
-				}
-			}])
-			.then((aggregates: any[]) => {
-				if (aggregates.length > 0) {
-					return aggregates[0].usage;
-				}
-				return 0;
-			});
-
-		logger.debug(`drive usage is ${usage}`);
+		const usage = await DriveFiles.clacDriveUsageOf(user);
 
 		const instance = await fetchMeta();
-		const driveCapacity = 1024 * 1024 * (isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb);
+		const driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb);
+
+		logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`);
 
 		// If usage limit exceeded
 		if (usage + size > driveCapacity) {
-			if (isLocalUser(user)) {
+			if (Users.isLocalUser(user)) {
 				throw 'no-free-space';
 			} else {
 				// (アバターまたはバナーを含まず)最も古いファイルを削除する
@@ -381,9 +311,9 @@ export default async function(
 			return null;
 		}
 
-		const driveFolder = await DriveFolder.findOne({
-			_id: folderId,
-			userId: user._id
+		const driveFolder = await DriveFolders.findOne({
+			id: folderId,
+			userId: user.id
 		});
 
 		if (driveFolder == null) throw 'folder-not-found';
@@ -437,54 +367,48 @@ export default async function(
 
 	const [folder] = await Promise.all([fetchFolder(), Promise.all(propPromises)]);
 
-	const metadata = {
-		userId: user._id,
-		_user: {
-			host: user.host
-		},
-		folderId: folder !== null ? folder._id : null,
-		comment: comment,
-		properties: properties,
-		withoutChunks: isLink,
-		isRemote: isLink,
-		isSensitive: isLocalUser(user) && user.settings.alwaysMarkNsfw ? true :
-			(sensitive !== null && sensitive !== undefined)
-				? sensitive
-				: false
-	} as IMetadata;
+	let file = new DriveFile();
+	file.id = genId();
+	file.createdAt = new Date();
+	file.userId = user.id;
+	file.userHost = user.host;
+	file.folderId = folder !== null ? folder.id : null;
+	file.comment = comment;
+	file.properties = properties;
+	file.isRemote = isLink;
+	file.isSensitive = Users.isLocalUser(user) && user.alwaysMarkNsfw ? true :
+		(sensitive !== null && sensitive !== undefined)
+			? sensitive
+			: false;
 
 	if (url !== null) {
-		metadata.src = url;
+		file.src = url;
 
 		if (isLink) {
-			metadata.url = url;
+			file.url = url;
 		}
 	}
 
 	if (uri !== null) {
-		metadata.uri = uri;
+		file.uri = uri;
 	}
 
-	let driveFile: IDriveFile;
-
 	if (isLink) {
 		try {
-			driveFile = await DriveFile.insert({
-				length: 0,
-				uploadDate: new Date(),
-				md5: hash,
-				filename: detectedName,
-				metadata: metadata,
-				contentType: mime
-			});
+			file.size = 0;
+			file.md5 = hash;
+			file.name = detectedName;
+			file.type = mime;
+
+			file = await DriveFiles.save(file);
 		} catch (e) {
 			// duplicate key error (when already registered)
-			if (e.code === 11000) {
-				logger.info(`already registered ${metadata.uri}`);
+			if (isDuplicateKeyValueError(e)) {
+				logger.info(`already registered ${file.uri}`);
 
-				driveFile = await DriveFile.findOne({
-					'metadata.uri': metadata.uri,
-					'metadata.userId': user._id
+				file = await DriveFiles.findOne({
+					uri: file.uri,
+					userId: user.id
 				});
 			} else {
 				logger.error(e);
@@ -492,29 +416,25 @@ export default async function(
 			}
 		}
 	} else {
-		driveFile = await (save(path, detectedName, mime, hash, size, metadata));
+		file = await (save(file, path, detectedName, mime, hash, size));
 	}
 
-	logger.succ(`drive file has been created ${driveFile._id}`);
+	logger.succ(`drive file has been created ${file.id}`);
 
-	pack(driveFile).then(packedFile => {
+	DriveFiles.pack(file).then(packedFile => {
 		// Publish driveFileCreated event
-		publishMainStream(user._id, 'driveFileCreated', packedFile);
-		publishDriveStream(user._id, 'fileCreated', packedFile);
+		publishMainStream(user.id, 'driveFileCreated', packedFile);
+		publishDriveStream(user.id, 'fileCreated', packedFile);
 	});
 
 	// 統計を更新
-	driveChart.update(driveFile, true);
-	perUserDriveChart.update(driveFile, true);
-	if (isRemoteUser(driveFile.metadata._user)) {
-		instanceChart.updateDrive(driveFile, true);
-		Instance.update({ host: driveFile.metadata._user.host }, {
-			$inc: {
-				driveUsage: driveFile.length,
-				driveFiles: 1
-			}
-		});
+	driveChart.update(file, true);
+	perUserDriveChart.update(file, true);
+	if (file.userHost !== null) {
+		instanceChart.updateDrive(file, true);
+		Instances.increment({ host: file.userHost }, 'driveUsage', file.size);
+		Instances.increment({ host: file.userHost }, 'driveFiles', 1);
 	}
 
-	return driveFile;
+	return file;
 }
diff --git a/src/services/drive/delete-file.ts b/src/services/drive/delete-file.ts
index c5c15ca20bdc3865660920bea8d2f26da363cdc6..adf57416fe3dd0167beeffe5c5ff299330355d6d 100644
--- a/src/services/drive/delete-file.ts
+++ b/src/services/drive/delete-file.ts
@@ -1,99 +1,53 @@
 import * as Minio from 'minio';
-import DriveFile, { DriveFileChunk, IDriveFile } from '../../models/drive-file';
-import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail';
 import config from '../../config';
-import driveChart from '../../services/chart/drive';
-import perUserDriveChart from '../../services/chart/per-user-drive';
-import instanceChart from '../../services/chart/instance';
-import DriveFileWebpublic, { DriveFileWebpublicChunk } from '../../models/drive-file-webpublic';
-import Instance from '../../models/instance';
-import { isRemoteUser } from '../../models/user';
+import { DriveFile } from '../../models/entities/drive-file';
+import { InternalStorage } from './internal-storage';
+import { DriveFiles, Instances } from '../../models';
+import { driveChart, perUserDriveChart, instanceChart } from '../chart';
 
-export default async function(file: IDriveFile, isExpired = false) {
-	if (file.metadata.storage == 'minio') {
-		const minio = new Minio.Client(config.drive.config);
-
-		// 後方互換性のため、file.metadata.storageProps.key があるかどうかチェックしています。
-		// 将来的には const obj = file.metadata.storageProps.key; とします。
-		const obj = file.metadata.storageProps.key ? file.metadata.storageProps.key : `${config.drive.prefix}/${file.metadata.storageProps.id}`;
-		await minio.removeObject(config.drive.bucket, obj);
+export default async function(file: DriveFile, isExpired = false) {
+	if (file.storedInternal) {
+		InternalStorage.del(file.accessKey);
 
-		if (file.metadata.thumbnailUrl) {
-			// 後方互換性のため、file.metadata.storageProps.thumbnailKey があるかどうかチェックしています。
-			// 将来的には const thumbnailObj = file.metadata.storageProps.thumbnailKey; とします。
-			const thumbnailObj = file.metadata.storageProps.thumbnailKey ? file.metadata.storageProps.thumbnailKey : `${config.drive.prefix}/${file.metadata.storageProps.id}-thumbnail`;
-			await minio.removeObject(config.drive.bucket, thumbnailObj);
+		if (file.thumbnailUrl) {
+			InternalStorage.del(file.thumbnailAccessKey);
 		}
 
-		if (file.metadata.webpublicUrl) {
-			const webpublicObj = file.metadata.storageProps.webpublicKey ? file.metadata.storageProps.webpublicKey : `${config.drive.prefix}/${file.metadata.storageProps.id}-original`;
-			await minio.removeObject(config.drive.bucket, webpublicObj);
+		if (file.webpublicUrl) {
+			InternalStorage.del(file.webpublicAccessKey);
 		}
-	}
+	} else {
+		const minio = new Minio.Client(config.drive.config);
 
-	// チャンクをすべて削除
-	await DriveFileChunk.remove({
-		files_id: file._id
-	});
+		await minio.removeObject(config.drive.bucket, file.accessKey);
 
-	const set = {
-		metadata: {
-			deletedAt: new Date(),
-			isExpired: isExpired
+		if (file.thumbnailUrl) {
+			await minio.removeObject(config.drive.bucket, file.thumbnailAccessKey);
 		}
-	} as any;
-
-	// リモートファイル期限切れ削除後は直リンクにする
-	if (isExpired && file.metadata && file.metadata._user && file.metadata._user.host != null) {
-		set.metadata.withoutChunks = true;
-		set.metadata.isRemote = true;
-		set.metadata.url = file.metadata.uri;
-		set.metadata.thumbnailUrl = undefined;
-		set.metadata.webpublicUrl = undefined;
-	}
-
-	await DriveFile.update({ _id: file._id }, {
-		$set: set
-	});
 
-	//#region サムネイルもあれば削除
-	const thumbnail = await DriveFileThumbnail.findOne({
-		'metadata.originalId': file._id
-	});
-
-	if (thumbnail) {
-		await DriveFileThumbnailChunk.remove({
-			files_id: thumbnail._id
-		});
-
-		await DriveFileThumbnail.remove({ _id: thumbnail._id });
+		if (file.webpublicUrl) {
+			await minio.removeObject(config.drive.bucket, file.webpublicAccessKey);
+		}
 	}
-	//#endregion
 
-	//#region Web公開用もあれば削除
-	const webpublic = await DriveFileWebpublic.findOne({
-		'metadata.originalId': file._id
-	});
-
-	if (webpublic) {
-		await DriveFileWebpublicChunk.remove({
-			files_id: webpublic._id
+	// リモートファイル期限切れ削除後は直リンクにする
+	if (isExpired && file.userHost !== null) {
+		DriveFiles.update(file.id, {
+			isRemote: true,
+			url: file.uri,
+			thumbnailUrl: null,
+			webpublicUrl: null
 		});
-
-		await DriveFileWebpublic.remove({ _id: webpublic._id });
+	} else {
+		DriveFiles.delete(file.id);
 	}
-	//#endregion
 
 	// 統計を更新
 	driveChart.update(file, false);
 	perUserDriveChart.update(file, false);
-	if (isRemoteUser(file.metadata._user)) {
+	if (file.userHost !== null) {
 		instanceChart.updateDrive(file, false);
-		Instance.update({ host: file.metadata._user.host }, {
-			$inc: {
-				driveUsage: -file.length,
-				driveFiles: -1
-			}
-		});
+		Instances.decrement({ host: file.userHost }, 'driveUsage', file.size);
+		Instances.decrement({ host: file.userHost }, 'driveFiles', 1);
 	}
 }
diff --git a/src/services/drive/internal-storage.ts b/src/services/drive/internal-storage.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ff890d7d471a28c9805dd442c7078a303c6a04dd
--- /dev/null
+++ b/src/services/drive/internal-storage.ts
@@ -0,0 +1,27 @@
+import * as fs from 'fs';
+import * as Path from 'path';
+import config from '../../config';
+
+export class InternalStorage {
+	private static readonly path = Path.resolve(`${__dirname}/../../../files`);
+
+	public static read(key: string) {
+		return fs.createReadStream(`${InternalStorage.path}/${key}`);
+	}
+
+	public static saveFromPath(key: string, srcPath: string) {
+		fs.mkdirSync(InternalStorage.path, { recursive: true });
+		fs.copyFileSync(srcPath, `${InternalStorage.path}/${key}`);
+		return `${config.url}/files/${key}`;
+	}
+
+	public static saveFromBuffer(key: string, data: Buffer) {
+		fs.mkdirSync(InternalStorage.path, { recursive: true });
+		fs.writeFileSync(`${InternalStorage.path}/${key}`, data);
+		return `${config.url}/files/${key}`;
+	}
+
+	public static del(key: string) {
+		fs.unlink(`${InternalStorage.path}/${key}`, () => {});
+	}
+}
diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts
index cdf6ba0cef2fdc6e33323e6f2ad91989622f9134..a7fe1fbd264acb77b3a63eb2e06b461d60e797e4 100644
--- a/src/services/drive/upload-from-url.ts
+++ b/src/services/drive/upload-from-url.ts
@@ -1,26 +1,26 @@
 import * as URL from 'url';
-
-import { IDriveFile, validateFileName } from '../../models/drive-file';
 import create from './add-file';
-import { IUser } from '../../models/user';
-import * as mongodb from 'mongodb';
+import { User } from '../../models/entities/user';
 import { driveLogger } from './logger';
 import { createTemp } from '../../misc/create-temp';
 import { downloadUrl } from '../../misc/donwload-url';
+import { DriveFolder } from '../../models/entities/drive-folder';
+import { DriveFile } from '../../models/entities/drive-file';
+import { DriveFiles } from '../../models';
 
 const logger = driveLogger.createSubLogger('downloader');
 
 export default async (
 	url: string,
-	user: IUser,
-	folderId: mongodb.ObjectID = null,
+	user: User,
+	folderId: DriveFolder['id'] = null,
 	uri: string = null,
 	sensitive = false,
 	force = false,
 	link = false
-): Promise<IDriveFile> => {
+): Promise<DriveFile> => {
 	let name = URL.parse(url).pathname.split('/').pop();
-	if (!validateFileName(name)) {
+	if (!DriveFiles.validateFileName(name)) {
 		name = null;
 	}
 
@@ -30,12 +30,12 @@ export default async (
 	// write content at URL to temp file
 	await downloadUrl(url, path);
 
-	let driveFile: IDriveFile;
+	let driveFile: DriveFile;
 	let error;
 
 	try {
 		driveFile = await create(user, path, name, null, folderId, force, link, url, uri, sensitive);
-		logger.succ(`Got: ${driveFile._id}`);
+		logger.succ(`Got: ${driveFile.id}`);
 	} catch (e) {
 		error = e;
 		logger.error(`Failed to create drive file: ${e}`, {
diff --git a/src/services/following/create.ts b/src/services/following/create.ts
index 1eaad750f7fcd3677e64b3a56178a1511344e698..28e4ba3c12ac8070b4837fa7e6ac0ba5250443bb 100644
--- a/src/services/following/create.ts
+++ b/src/services/following/create.ts
@@ -1,100 +1,70 @@
-import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user';
-import Following from '../../models/following';
-import Blocking from '../../models/blocking';
 import { publishMainStream } from '../stream';
-import notify from '../../services/create-notification';
 import { renderActivity } from '../../remote/activitypub/renderer';
 import renderFollow from '../../remote/activitypub/renderer/follow';
 import renderAccept from '../../remote/activitypub/renderer/accept';
 import renderReject from '../../remote/activitypub/renderer/reject';
 import { deliver } from '../../queue';
 import createFollowRequest from './requests/create';
-import perUserFollowingChart from '../../services/chart/per-user-following';
 import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
-import Instance from '../../models/instance';
-import instanceChart from '../../services/chart/instance';
 import Logger from '../logger';
-import FollowRequest from '../../models/follow-request';
 import { IdentifiableError } from '../../misc/identifiable-error';
+import { User } from '../../models/entities/user';
+import { Followings, Users, FollowRequests, Blockings, Instances } from '../../models';
+import { instanceChart, perUserFollowingChart } from '../chart';
+import { genId } from '../../misc/gen-id';
+import { createNotification } from '../create-notification';
+import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error';
 
 const logger = new Logger('following/create');
 
-export async function insertFollowingDoc(followee: IUser, follower: IUser) {
+export async function insertFollowingDoc(followee: User, follower: User) {
+	if (follower.id === followee.id) return;
+
 	let alreadyFollowed = false;
 
-	await Following.insert({
+	await Followings.save({
+		id: genId(),
 		createdAt: new Date(),
-		followerId: follower._id,
-		followeeId: followee._id,
+		followerId: follower.id,
+		followeeId: followee.id,
 
 		// 非正規化
-		_follower: {
-			host: follower.host,
-			inbox: isRemoteUser(follower) ? follower.inbox : undefined,
-			sharedInbox: isRemoteUser(follower) ? follower.sharedInbox : undefined
-		},
-		_followee: {
-			host: followee.host,
-			inbox: isRemoteUser(followee) ? followee.inbox : undefined,
-			sharedInbox: isRemoteUser(followee) ? followee.sharedInbox : undefined
-		}
+		followerHost: follower.host,
+		followerInbox: Users.isRemoteUser(follower) ? follower.inbox : null,
+		followerSharedInbox: Users.isRemoteUser(follower) ? follower.sharedInbox : null,
+		followeeHost: followee.host,
+		followeeInbox: Users.isRemoteUser(followee) ? followee.inbox : null,
+		followeeSharedInbox: Users.isRemoteUser(followee) ? followee.sharedInbox : null
 	}).catch(e => {
-		if (e.code === 11000 && isRemoteUser(follower) && isLocalUser(followee)) {
-			logger.info(`Insert duplicated ignore. ${follower._id} => ${followee._id}`);
+		if (isDuplicateKeyValueError(e) && Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
+			logger.info(`Insert duplicated ignore. ${follower.id} => ${followee.id}`);
 			alreadyFollowed = true;
 		} else {
 			throw e;
 		}
 	});
 
-	const removed = await FollowRequest.remove({
-		followeeId: followee._id,
-		followerId: follower._id
+	await FollowRequests.delete({
+		followeeId: followee.id,
+		followerId: follower.id
 	});
 
-	if (removed.deletedCount === 1) {
-		await User.update({ _id: followee._id }, {
-			$inc: {
-				pendingReceivedFollowRequestsCount: -1
-			}
-		});
-	}
-
 	if (alreadyFollowed) return;
 
 	//#region Increment counts
-	User.update({ _id: follower._id }, {
-		$inc: {
-			followingCount: 1
-		}
-	});
-
-	User.update({ _id: followee._id }, {
-		$inc: {
-			followersCount: 1
-		}
-	});
+	Users.increment({ id: follower.id }, 'followingCount', 1);
+	Users.increment({ id: followee.id }, 'followersCount', 1);
 	//#endregion
 
 	//#region Update instance stats
-	if (isRemoteUser(follower) && isLocalUser(followee)) {
+	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
 		registerOrFetchInstanceDoc(follower.host).then(i => {
-			Instance.update({ _id: i._id }, {
-				$inc: {
-					followingCount: 1
-				}
-			});
-
+			Instances.increment({ id: i.id }, 'followingCount', 1);
 			instanceChart.updateFollowing(i.host, true);
 		});
-	} else if (isLocalUser(follower) && isRemoteUser(followee)) {
+	} else if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
 		registerOrFetchInstanceDoc(followee.host).then(i => {
-			Instance.update({ _id: i._id }, {
-				$inc: {
-					followersCount: 1
-				}
-			});
-
+			Instances.increment({ id: i.id }, 'followersCount', 1);
 			instanceChart.updateFollowers(i.host, true);
 		});
 	}
@@ -103,44 +73,42 @@ export async function insertFollowingDoc(followee: IUser, follower: IUser) {
 	perUserFollowingChart.update(follower, followee, true);
 
 	// Publish follow event
-	if (isLocalUser(follower)) {
-		packUser(followee, follower, {
+	if (Users.isLocalUser(follower)) {
+		Users.pack(followee, follower, {
 			detail: true
-		}).then(packed => publishMainStream(follower._id, 'follow', packed));
+		}).then(packed => publishMainStream(follower.id, 'follow', packed));
 	}
 
 	// Publish followed event
-	if (isLocalUser(followee)) {
-		packUser(follower, followee).then(packed => publishMainStream(followee._id, 'followed', packed)),
+	if (Users.isLocalUser(followee)) {
+		Users.pack(follower, followee).then(packed => publishMainStream(followee.id, 'followed', packed)),
 
 		// 通知を作成
-		notify(followee._id, follower._id, 'follow');
+		createNotification(followee.id, follower.id, 'follow');
 	}
 }
 
-export default async function(follower: IUser, followee: IUser, requestId?: string) {
+export default async function(follower: User, followee: User, requestId?: string) {
 	// check blocking
 	const [blocking, blocked] = await Promise.all([
-		Blocking.findOne({
-			blockerId: follower._id,
-			blockeeId: followee._id,
+		Blockings.findOne({
+			blockerId: follower.id,
+			blockeeId: followee.id,
 		}),
-		Blocking.findOne({
-			blockerId: followee._id,
-			blockeeId: follower._id,
+		Blockings.findOne({
+			blockerId: followee.id,
+			blockeeId: follower.id,
 		})
 	]);
 
-	if (isRemoteUser(follower) && isLocalUser(followee) && blocked) {
+	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee) && blocked) {
 		// リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。
 		const content = renderActivity(renderReject(renderFollow(follower, followee, requestId), followee));
 		deliver(followee , content, follower.inbox);
 		return;
-	} else if (isRemoteUser(follower) && isLocalUser(followee) && blocking) {
+	} else if (Users.isRemoteUser(follower) && Users.isLocalUser(followee) && blocking) {
 		// リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。
-		await Blocking.remove({
-			_id: blocking._id
-		});
+		await Blockings.delete(blocking.id);
 	} else {
 		// それ以外は単純に例外
 		if (blocking != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking');
@@ -151,23 +119,23 @@ export default async function(follower: IUser, followee: IUser, requestId?: stri
 	// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
 	// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
 	// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
-	if (followee.isLocked || (followee.carefulBot && follower.isBot) || (isLocalUser(follower) && isRemoteUser(followee))) {
+	if (followee.isLocked || (followee.carefulBot && follower.isBot) || (Users.isLocalUser(follower) && Users.isRemoteUser(followee))) {
 		let autoAccept = false;
 
 		// 鍵アカウントであっても、既にフォローされていた場合はスルー
-		const following = await Following.findOne({
-			followerId: follower._id,
-			followeeId: followee._id,
+		const following = await Followings.findOne({
+			followerId: follower.id,
+			followeeId: followee.id,
 		});
 		if (following) {
 			autoAccept = true;
 		}
 
 		// フォローしているユーザーは自動承認オプション
-		if (!autoAccept && (isLocalUser(followee) && followee.autoAcceptFollowed)) {
-			const followed = await Following.findOne({
-				followerId: followee._id,
-				followeeId: follower._id
+		if (!autoAccept && (Users.isLocalUser(followee) && followee.autoAcceptFollowed)) {
+			const followed = await Followings.findOne({
+				followerId: followee.id,
+				followeeId: follower.id
 			});
 
 			if (followed) autoAccept = true;
@@ -181,7 +149,7 @@ export default async function(follower: IUser, followee: IUser, requestId?: stri
 
 	await insertFollowingDoc(followee, follower);
 
-	if (isRemoteUser(follower) && isLocalUser(followee)) {
+	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
 		const content = renderActivity(renderAccept(renderFollow(follower, followee, requestId), followee));
 		deliver(followee, content, follower.inbox);
 	}
diff --git a/src/services/following/delete.ts b/src/services/following/delete.ts
index d85c8472bb14b494a99935570897493cfd0777e5..ad09f0e6d12bc71087f328adc1da40d19400b135 100644
--- a/src/services/following/delete.ts
+++ b/src/services/following/delete.ts
@@ -1,22 +1,20 @@
-import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user';
-import Following from '../../models/following';
 import { publishMainStream } from '../stream';
 import { renderActivity } from '../../remote/activitypub/renderer';
 import renderFollow from '../../remote/activitypub/renderer/follow';
 import renderUndo from '../../remote/activitypub/renderer/undo';
 import { deliver } from '../../queue';
-import perUserFollowingChart from '../../services/chart/per-user-following';
 import Logger from '../logger';
 import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
-import Instance from '../../models/instance';
-import instanceChart from '../../services/chart/instance';
+import { User } from '../../models/entities/user';
+import { Followings, Users, Instances } from '../../models';
+import { instanceChart, perUserFollowingChart } from '../chart';
 
 const logger = new Logger('following/delete');
 
-export default async function(follower: IUser, followee: IUser, silent = false) {
-	const following = await Following.findOne({
-		followerId: follower._id,
-		followeeId: followee._id
+export default async function(follower: User, followee: User, silent = false) {
+	const following = await Followings.findOne({
+		followerId: follower.id,
+		followeeId: followee.id
 	});
 
 	if (following == null) {
@@ -24,45 +22,25 @@ export default async function(follower: IUser, followee: IUser, silent = false)
 		return;
 	}
 
-	Following.remove({
-		_id: following._id
-	});
+	Followings.delete(following.id);
 
 	//#region Decrement following count
-	User.update({ _id: follower._id }, {
-		$inc: {
-			followingCount: -1
-		}
-	});
+	Users.decrement({ id: follower.id }, 'followingCount', 1);
 	//#endregion
 
 	//#region Decrement followers count
-	User.update({ _id: followee._id }, {
-		$inc: {
-			followersCount: -1
-		}
-	});
+	Users.decrement({ id: followee.id }, 'followersCount', 1);
 	//#endregion
 
 	//#region Update instance stats
-	if (isRemoteUser(follower) && isLocalUser(followee)) {
+	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
 		registerOrFetchInstanceDoc(follower.host).then(i => {
-			Instance.update({ _id: i._id }, {
-				$inc: {
-					followingCount: -1
-				}
-			});
-
+			Instances.decrement({ id: i.id }, 'followingCount', 1);
 			instanceChart.updateFollowing(i.host, false);
 		});
-	} else if (isLocalUser(follower) && isRemoteUser(followee)) {
+	} else if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
 		registerOrFetchInstanceDoc(followee.host).then(i => {
-			Instance.update({ _id: i._id }, {
-				$inc: {
-					followersCount: -1
-				}
-			});
-
+			Instances.decrement({ id: i.id }, 'followersCount', 1);
 			instanceChart.updateFollowers(i.host, false);
 		});
 	}
@@ -71,13 +49,13 @@ export default async function(follower: IUser, followee: IUser, silent = false)
 	perUserFollowingChart.update(follower, followee, false);
 
 	// Publish unfollow event
-	if (!silent && isLocalUser(follower)) {
-		packUser(followee, follower, {
+	if (!silent && Users.isLocalUser(follower)) {
+		Users.pack(followee, follower, {
 			detail: true
-		}).then(packed => publishMainStream(follower._id, 'unfollow', packed));
+		}).then(packed => publishMainStream(follower.id, 'unfollow', packed));
 	}
 
-	if (isLocalUser(follower) && isRemoteUser(followee)) {
+	if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
 		const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
 		deliver(follower, content, followee.inbox);
 	}
diff --git a/src/services/following/requests/accept-all.ts b/src/services/following/requests/accept-all.ts
index cf1a9e923d1ad82071b343f6c4425971422c2432..b61c31a5139d6f0ba640a19f09e0dcab744cb9bc 100644
--- a/src/services/following/requests/accept-all.ts
+++ b/src/services/following/requests/accept-all.ts
@@ -1,24 +1,18 @@
-import User, { IUser } from '../../../models/user';
-import FollowRequest from '../../../models/follow-request';
 import accept from './accept';
+import { User } from '../../../models/entities/user';
+import { FollowRequests, Users } from '../../../models';
 
 /**
  * 指定したユーザー宛てのフォローリクエストをすべて承認
  * @param user ユーザー
  */
-export default async function(user: IUser) {
-	const requests = await FollowRequest.find({
-		followeeId: user._id
+export default async function(user: User) {
+	const requests = await FollowRequests.find({
+		followeeId: user.id
 	});
 
 	for (const request of requests) {
-		const follower = await User.findOne({ _id: request.followerId });
+		const follower = await Users.findOne(request.followerId);
 		accept(user, follower);
 	}
-
-	User.update({ _id: user._id }, {
-		$set: {
-			pendingReceivedFollowRequestsCount: 0
-		}
-	});
 }
diff --git a/src/services/following/requests/accept.ts b/src/services/following/requests/accept.ts
index 284c6d5e195cca1a24dac01dcd5aca028e0776e3..0be8e24e1a222385a1b373c61606e1842d3bdab8 100644
--- a/src/services/following/requests/accept.ts
+++ b/src/services/following/requests/accept.ts
@@ -1,26 +1,26 @@
-import { IUser, isRemoteUser, ILocalUser, pack as packUser } from '../../../models/user';
-import FollowRequest from '../../../models/follow-request';
 import { renderActivity } from '../../../remote/activitypub/renderer';
 import renderFollow from '../../../remote/activitypub/renderer/follow';
 import renderAccept from '../../../remote/activitypub/renderer/accept';
 import { deliver } from '../../../queue';
 import { publishMainStream } from '../../stream';
 import { insertFollowingDoc } from '../create';
+import { User, ILocalUser } from '../../../models/entities/user';
+import { FollowRequests, Users } from '../../../models';
 
-export default async function(followee: IUser, follower: IUser) {
-	const request = await FollowRequest.findOne({
-		followeeId: followee._id,
-		followerId: follower._id
+export default async function(followee: User, follower: User) {
+	const request = await FollowRequests.findOne({
+		followeeId: followee.id,
+		followerId: follower.id
 	});
 
 	await insertFollowingDoc(followee, follower);
 
-	if (isRemoteUser(follower) && request) {
+	if (Users.isRemoteUser(follower) && request) {
 		const content = renderActivity(renderAccept(renderFollow(follower, followee, request.requestId), followee as ILocalUser));
 		deliver(followee as ILocalUser, content, follower.inbox);
 	}
 
-	packUser(followee, followee, {
+	Users.pack(followee, followee, {
 		detail: true
-	}).then(packed => publishMainStream(followee._id, 'meUpdated', packed));
+	}).then(packed => publishMainStream(followee.id, 'meUpdated', packed));
 }
diff --git a/src/services/following/requests/cancel.ts b/src/services/following/requests/cancel.ts
index af4cca85fe1871b43b8deb72784d81f6e295b4b2..98fec5d331da2ac551c2a92e79bc1288c4d85164 100644
--- a/src/services/following/requests/cancel.ts
+++ b/src/services/following/requests/cancel.ts
@@ -1,39 +1,33 @@
-import User, { IUser, isRemoteUser, ILocalUser, pack as packUser } from '../../../models/user';
-import FollowRequest from '../../../models/follow-request';
 import { renderActivity } from '../../../remote/activitypub/renderer';
 import renderFollow from '../../../remote/activitypub/renderer/follow';
 import renderUndo from '../../../remote/activitypub/renderer/undo';
 import { deliver } from '../../../queue';
 import { publishMainStream } from '../../stream';
 import { IdentifiableError } from '../../../misc/identifiable-error';
+import { User, ILocalUser } from '../../../models/entities/user';
+import { Users, FollowRequests } from '../../../models';
 
-export default async function(followee: IUser, follower: IUser) {
-	if (isRemoteUser(followee)) {
+export default async function(followee: User, follower: User) {
+	if (Users.isRemoteUser(followee)) {
 		const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
 		deliver(follower as ILocalUser, content, followee.inbox);
 	}
 
-	const request = await FollowRequest.findOne({
-		followeeId: followee._id,
-		followerId: follower._id
+	const request = await FollowRequests.findOne({
+		followeeId: followee.id,
+		followerId: follower.id
 	});
 
 	if (request == null) {
 		throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
 	}
 
-	await FollowRequest.remove({
-		followeeId: followee._id,
-		followerId: follower._id
+	await FollowRequests.delete({
+		followeeId: followee.id,
+		followerId: follower.id
 	});
 
-	await User.update({ _id: followee._id }, {
-		$inc: {
-			pendingReceivedFollowRequestsCount: -1
-		}
-	});
-
-	packUser(followee, followee, {
+	Users.pack(followee, followee, {
 		detail: true
-	}).then(packed => publishMainStream(followee._id, 'meUpdated', packed));
+	}).then(packed => publishMainStream(followee.id, 'meUpdated', packed));
 }
diff --git a/src/services/following/requests/create.ts b/src/services/following/requests/create.ts
index 10c534f5298c0e0b7a95a3849fe22b96989cee93..32e79d136d72819d9f9e7bdd3ed40a314d28908d 100644
--- a/src/services/following/requests/create.ts
+++ b/src/services/following/requests/create.ts
@@ -1,66 +1,59 @@
-import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../../models/user';
 import { publishMainStream } from '../../stream';
-import notify from '../../../services/create-notification';
 import { renderActivity } from '../../../remote/activitypub/renderer';
 import renderFollow from '../../../remote/activitypub/renderer/follow';
 import { deliver } from '../../../queue';
-import FollowRequest from '../../../models/follow-request';
-import Blocking from '../../../models/blocking';
+import { User } from '../../../models/entities/user';
+import { Blockings, FollowRequests, Users } from '../../../models';
+import { genId } from '../../../misc/gen-id';
+import { createNotification } from '../../create-notification';
+
+export default async function(follower: User, followee: User, requestId?: string) {
+	if (follower.id === followee.id) return;
 
-export default async function(follower: IUser, followee: IUser, requestId?: string) {
 	// check blocking
 	const [blocking, blocked] = await Promise.all([
-		Blocking.findOne({
-			blockerId: follower._id,
-			blockeeId: followee._id,
+		Blockings.findOne({
+			blockerId: follower.id,
+			blockeeId: followee.id,
 		}),
-		Blocking.findOne({
-			blockerId: followee._id,
-			blockeeId: follower._id,
+		Blockings.findOne({
+			blockerId: followee.id,
+			blockeeId: follower.id,
 		})
 	]);
 
 	if (blocking != null) throw new Error('blocking');
 	if (blocked != null) throw new Error('blocked');
 
-	await FollowRequest.insert({
+	await FollowRequests.save({
+		id: genId(),
 		createdAt: new Date(),
-		followerId: follower._id,
-		followeeId: followee._id,
+		followerId: follower.id,
+		followeeId: followee.id,
 		requestId,
 
 		// 非正規化
-		_follower: {
-			host: follower.host,
-			inbox: isRemoteUser(follower) ? follower.inbox : undefined,
-			sharedInbox: isRemoteUser(follower) ? follower.sharedInbox : undefined
-		},
-		_followee: {
-			host: followee.host,
-			inbox: isRemoteUser(followee) ? followee.inbox : undefined,
-			sharedInbox: isRemoteUser(followee) ? followee.sharedInbox : undefined
-		}
-	});
-
-	await User.update({ _id: followee._id }, {
-		$inc: {
-			pendingReceivedFollowRequestsCount: 1
-		}
+		followerHost: follower.host,
+		followerInbox: Users.isRemoteUser(follower) ? follower.inbox : undefined,
+		followerSharedInbox: Users.isRemoteUser(follower) ? follower.sharedInbox : undefined,
+		followeeHost: followee.host,
+		followeeInbox: Users.isRemoteUser(followee) ? followee.inbox : undefined,
+		followeeSharedInbox: Users.isRemoteUser(followee) ? followee.sharedInbox : undefined
 	});
 
 	// Publish receiveRequest event
-	if (isLocalUser(followee)) {
-		packUser(follower, followee).then(packed => publishMainStream(followee._id, 'receiveFollowRequest', packed));
+	if (Users.isLocalUser(followee)) {
+		Users.pack(follower, followee).then(packed => publishMainStream(followee.id, 'receiveFollowRequest', packed));
 
-		packUser(followee, followee, {
+		Users.pack(followee, followee, {
 			detail: true
-		}).then(packed => publishMainStream(followee._id, 'meUpdated', packed));
+		}).then(packed => publishMainStream(followee.id, 'meUpdated', packed));
 
 		// 通知を作成
-		notify(followee._id, follower._id, 'receiveFollowRequest');
+		createNotification(followee.id, follower.id, 'receiveFollowRequest');
 	}
 
-	if (isLocalUser(follower) && isRemoteUser(followee)) {
+	if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
 		const content = renderActivity(renderFollow(follower, followee));
 		deliver(follower, content, followee.inbox);
 	}
diff --git a/src/services/following/requests/reject.ts b/src/services/following/requests/reject.ts
index cb924df81123ac333349cae55a423e81afa3050a..c590edcfd8677a55e549eece5bac385425e7f5d7 100644
--- a/src/services/following/requests/reject.ts
+++ b/src/services/following/requests/reject.ts
@@ -1,34 +1,28 @@
-import User, { IUser, isRemoteUser, ILocalUser, pack as packUser } from '../../../models/user';
-import FollowRequest from '../../../models/follow-request';
 import { renderActivity } from '../../../remote/activitypub/renderer';
 import renderFollow from '../../../remote/activitypub/renderer/follow';
 import renderReject from '../../../remote/activitypub/renderer/reject';
 import { deliver } from '../../../queue';
 import { publishMainStream } from '../../stream';
+import { User, ILocalUser } from '../../../models/entities/user';
+import { Users, FollowRequests } from '../../../models';
 
-export default async function(followee: IUser, follower: IUser) {
-	if (isRemoteUser(follower)) {
-		const request = await FollowRequest.findOne({
-			followeeId: followee._id,
-			followerId: follower._id
+export default async function(followee: User, follower: User) {
+	if (Users.isRemoteUser(follower)) {
+		const request = await FollowRequests.findOne({
+			followeeId: followee.id,
+			followerId: follower.id
 		});
 
 		const content = renderActivity(renderReject(renderFollow(follower, followee, request.requestId), followee as ILocalUser));
 		deliver(followee as ILocalUser, content, follower.inbox);
 	}
 
-	await FollowRequest.remove({
-		followeeId: followee._id,
-		followerId: follower._id
+	await FollowRequests.delete({
+		followeeId: followee.id,
+		followerId: follower.id
 	});
 
-	User.update({ _id: followee._id }, {
-		$inc: {
-			pendingReceivedFollowRequestsCount: -1
-		}
-	});
-
-	packUser(followee, follower, {
+	Users.pack(followee, follower, {
 		detail: true
-	}).then(packed => publishMainStream(follower._id, 'unfollow', packed));
+	}).then(packed => publishMainStream(follower.id, 'unfollow', packed));
 }
diff --git a/src/services/i/pin.ts b/src/services/i/pin.ts
index 4d0ae3c1491beb1a7c7f35a8b9000020f0077650..4e43421bdc9bbfbf930924a4e3ab8fa56af8a4cd 100644
--- a/src/services/i/pin.ts
+++ b/src/services/i/pin.ts
@@ -1,59 +1,51 @@
 import config from '../../config';
-import * as mongo from 'mongodb';
-import User, { isLocalUser, isRemoteUser, ILocalUser, IUser } from '../../models/user';
-import Note, { packMany } from '../../models/note';
-import Following from '../../models/following';
 import renderAdd from '../../remote/activitypub/renderer/add';
 import renderRemove from '../../remote/activitypub/renderer/remove';
 import { renderActivity } from '../../remote/activitypub/renderer';
 import { deliver } from '../../queue';
 import { IdentifiableError } from '../../misc/identifiable-error';
+import { User, ILocalUser } from '../../models/entities/user';
+import { Note } from '../../models/entities/note';
+import { Notes, UserNotePinings, Users, Followings } from '../../models';
+import { UserNotePining } from '../../models/entities/user-note-pinings';
+import { genId } from '../../misc/gen-id';
 
 /**
  * 指定した投稿をピン留めします
  * @param user
  * @param noteId
  */
-export async function addPinned(user: IUser, noteId: mongo.ObjectID) {
+export async function addPinned(user: User, noteId: Note['id']) {
 	// Fetch pinee
-	const note = await Note.findOne({
-		_id: noteId,
-		userId: user._id
+	const note = await Notes.findOne({
+		id: noteId,
+		userId: user.id
 	});
 
-	if (note === null) {
+	if (note == null) {
 		throw new IdentifiableError('70c4e51f-5bea-449c-a030-53bee3cce202', 'No such note.');
 	}
 
-	let pinnedNoteIds = user.pinnedNoteIds || [];
+	const pinings = await UserNotePinings.find({ userId: user.id });
 
-	//#region 現在ピン留め投稿している投稿が実際にデータベースに存在しているのかチェック
-	// データベースの欠損などで存在していない(または破損している)場合があるので。
-	// 存在していなかったらピン留め投稿から外す
-	const pinnedNotes = await packMany(pinnedNoteIds, null, { detail: true });
-
-	pinnedNoteIds = pinnedNoteIds.filter(id => pinnedNotes.some(n => n.id.toString() === id.toHexString()));
-	//#endregion
-
-	if (pinnedNoteIds.length >= 5) {
+	if (pinings.length >= 5) {
 		throw new IdentifiableError('15a018eb-58e5-4da1-93be-330fcc5e4e1a', 'You can not pin notes any more.');
 	}
 
-	if (pinnedNoteIds.some(id => id.equals(note._id))) {
+	if (pinings.some(pining => pining.noteId === note.id)) {
 		throw new IdentifiableError('23f0cf4e-59a3-4276-a91d-61a5891c1514', 'That note has already been pinned.');
 	}
 
-	pinnedNoteIds.unshift(note._id);
-
-	await User.update(user._id, {
-		$set: {
-			pinnedNoteIds: pinnedNoteIds
-		}
-	});
+	await UserNotePinings.save({
+		id: genId(),
+		createdAt: new Date(),
+		userId: user.id,
+		noteId: note.id
+	} as UserNotePining);
 
 	// Deliver to remote followers
-	if (isLocalUser(user)) {
-		deliverPinnedChange(user._id, note._id, true);
+	if (Users.isLocalUser(user)) {
+		deliverPinnedChange(user.id, note.id, true);
 	}
 }
 
@@ -62,43 +54,40 @@ export async function addPinned(user: IUser, noteId: mongo.ObjectID) {
  * @param user
  * @param noteId
  */
-export async function removePinned(user: IUser, noteId: mongo.ObjectID) {
+export async function removePinned(user: User, noteId: Note['id']) {
 	// Fetch unpinee
-	const note = await Note.findOne({
-		_id: noteId,
-		userId: user._id
+	const note = await Notes.findOne({
+		id: noteId,
+		userId: user.id
 	});
 
-	if (note === null) {
+	if (note == null) {
 		throw new IdentifiableError('b302d4cf-c050-400a-bbb3-be208681f40c', 'No such note.');
 	}
 
-	const pinnedNoteIds = (user.pinnedNoteIds || []).filter(id => !id.equals(note._id));
-
-	await User.update(user._id, {
-		$set: {
-			pinnedNoteIds: pinnedNoteIds
-		}
+	UserNotePinings.delete({
+		userId: user.id,
+		noteId: note.id
 	});
 
 	// Deliver to remote followers
-	if (isLocalUser(user)) {
-		deliverPinnedChange(user._id, noteId, false);
+	if (Users.isLocalUser(user)) {
+		deliverPinnedChange(user.id, noteId, false);
 	}
 }
 
-export async function deliverPinnedChange(userId: mongo.ObjectID, noteId: mongo.ObjectID, isAddition: boolean) {
-	const user = await User.findOne({
-		_id: userId
+export async function deliverPinnedChange(userId: User['id'], noteId: Note['id'], isAddition: boolean) {
+	const user = await Users.findOne({
+		id: userId
 	});
 
-	if (!isLocalUser(user)) return;
+	if (!Users.isLocalUser(user)) return;
 
 	const queue = await CreateRemoteInboxes(user);
 
 	if (queue.length < 1) return;
 
-	const target = `${config.url}/users/${user._id}/collections/featured`;
+	const target = `${config.url}/users/${user.id}/collections/featured`;
 
 	const item = `${config.url}/notes/${noteId}`;
 	const content = renderActivity(isAddition ? renderAdd(user, target, item) : renderRemove(user, target, item));
@@ -112,16 +101,20 @@ export async function deliverPinnedChange(userId: mongo.ObjectID, noteId: mongo.
  * @param user ローカルユーザー
  */
 async function CreateRemoteInboxes(user: ILocalUser): Promise<string[]> {
-	const followers = await Following.find({
-		followeeId: user._id
+	const followers = await Followings.find({
+		followeeId: user.id
 	});
 
 	const queue: string[] = [];
 
 	for (const following of followers) {
-		const follower = following._follower;
+		const follower = {
+			host: following.followerHost,
+			inbox: following.followerInbox,
+			sharedInbox: following.followerSharedInbox,
+		};
 
-		if (isRemoteUser(follower)) {
+		if (follower.host !== null) {
 			const inbox = follower.sharedInbox || follower.inbox;
 			if (!queue.includes(inbox)) queue.push(inbox);
 		}
diff --git a/src/services/i/update.ts b/src/services/i/update.ts
index 887cecb04c00c15f3ec86ae103fb0c7ed6d9610e..7dba472e78326ecc2b9d24b3b20c57b855be2c0f 100644
--- a/src/services/i/update.ts
+++ b/src/services/i/update.ts
@@ -1,29 +1,26 @@
-import * as mongo from 'mongodb';
-import User, { isLocalUser, isRemoteUser } from '../../models/user';
-import Following from '../../models/following';
-import renderPerson from '../../remote/activitypub/renderer/person';
 import renderUpdate from '../../remote/activitypub/renderer/update';
 import { renderActivity } from '../../remote/activitypub/renderer';
 import { deliver } from '../../queue';
+import { Followings, Users } from '../../models';
+import { User } from '../../models/entities/user';
+import { renderPerson } from '../../remote/activitypub/renderer/person';
 
-export async function publishToFollowers(userId: mongo.ObjectID) {
-	const user = await User.findOne({
-		_id: userId
+export async function publishToFollowers(userId: User['id']) {
+	const user = await Users.findOne({
+		id: userId
 	});
 
-	const followers = await Following.find({
-		followeeId: user._id
+	const followers = await Followings.find({
+		followeeId: user.id
 	});
 
 	const queue: string[] = [];
 
 	// フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信
-	if (isLocalUser(user)) {
+	if (Users.isLocalUser(user)) {
 		for (const following of followers) {
-			const follower = following._follower;
-
-			if (isRemoteUser(follower)) {
-				const inbox = follower.sharedInbox || follower.inbox;
+			if (following.followerHost !== null) {
+				const inbox = following.followerSharedInbox || following.followerInbox;
 				if (!queue.includes(inbox)) queue.push(inbox);
 			}
 		}
diff --git a/src/services/logger.ts b/src/services/logger.ts
index aa93954bc11e0081804cb1db3d1d71fb3386210c..e6a54e626df0d8140e8a86fc1427a3ddaeeaa494 100644
--- a/src/services/logger.ts
+++ b/src/services/logger.ts
@@ -3,7 +3,9 @@ import * as os from 'os';
 import chalk from 'chalk';
 import * as dateformat from 'dateformat';
 import { program } from '../argv';
-import Log from '../models/log';
+import { getRepository } from 'typeorm';
+import { Log } from '../models/entities/log';
+import { genId } from '../misc/gen-id';
 
 type Domain = {
 	name: string;
@@ -33,7 +35,6 @@ export default class Logger {
 
 	private log(level: Level, message: string, data: Record<string, any>, important = false, subDomains: Domain[] = [], store = true): void {
 		if (program.quiet) return;
-		if (process.env.NODE_ENV === 'test') return;
 		if (!this.store) store = false;
 
 		if (this.parentLogger) {
@@ -65,15 +66,17 @@ export default class Logger {
 		console.log(important ? chalk.bold(log) : log);
 
 		if (store) {
-			Log.insert({
+			const Logs = getRepository(Log);
+			Logs.insert({
+				id: genId(),
 				createdAt: new Date(),
 				machine: os.hostname(),
-				worker: worker,
+				worker: worker.toString(),
 				domain: [this.domain].concat(subDomains).map(d => d.name),
 				level: level,
 				message: message,
 				data: data,
-			});
+			} as Log);
 		}
 	}
 
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 85201086d40d02a9bad11569c8a8ece73030b9f7..9ac9223d3cce2aab6fec63ed34364daceddcf7f8 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -1,61 +1,54 @@
 import es from '../../db/elasticsearch';
-import Note, { pack, INote, IChoice } from '../../models/note';
-import User, { isLocalUser, IUser, isRemoteUser, IRemoteUser, ILocalUser } from '../../models/user';
-import { publishMainStream, publishHomeTimelineStream, publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream, publishHashtagStream } from '../stream';
-import Following from '../../models/following';
+import { publishMainStream, publishNotesStream } from '../stream';
 import { deliver } from '../../queue';
 import renderNote from '../../remote/activitypub/renderer/note';
 import renderCreate from '../../remote/activitypub/renderer/create';
 import renderAnnounce from '../../remote/activitypub/renderer/announce';
 import { renderActivity } from '../../remote/activitypub/renderer';
-import DriveFile, { IDriveFile } from '../../models/drive-file';
-import notify from '../../services/create-notification';
-import NoteWatching from '../../models/note-watching';
 import watch from './watch';
-import Mute from '../../models/mute';
 import { parse } from '../../mfm/parse';
-import { IApp } from '../../models/app';
-import UserList from '../../models/user-list';
 import resolveUser from '../../remote/resolve-user';
-import Meta from '../../models/meta';
 import config from '../../config';
 import { updateHashtag } from '../update-hashtag';
-import isQuote from '../../misc/is-quote';
-import notesChart from '../../services/chart/notes';
-import perUserNotesChart from '../../services/chart/per-user-notes';
-import activeUsersChart from '../../services/chart/active-users';
-import instanceChart from '../../services/chart/instance';
-import * as deepcopy from 'deepcopy';
-
 import { erase, concat } from '../../prelude/array';
 import insertNoteUnread from './unread';
 import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
-import Instance from '../../models/instance';
 import extractMentions from '../../misc/extract-mentions';
 import extractEmojis from '../../misc/extract-emojis';
 import extractHashtags from '../../misc/extract-hashtags';
+import { Note } from '../../models/entities/note';
+import { Mutings, Users, NoteWatchings, Followings, Notes, Instances, Polls } from '../../models';
+import { DriveFile } from '../../models/entities/drive-file';
+import { App } from '../../models/entities/app';
+import { Not } from 'typeorm';
+import { User, ILocalUser, IRemoteUser } from '../../models/entities/user';
+import { genId } from '../../misc/gen-id';
+import { notesChart, perUserNotesChart, activeUsersChart, instanceChart } from '../chart';
+import { Poll, IPoll } from '../../models/entities/poll';
+import { createNotification } from '../create-notification';
+import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error';
 
 type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
 
 class NotificationManager {
-	private notifier: IUser;
-	private note: INote;
+	private notifier: User;
+	private note: Note;
 	private queue: {
-		target: ILocalUser['_id'];
+		target: ILocalUser['id'];
 		reason: NotificationType;
 	}[];
 
-	constructor(notifier: IUser, note: INote) {
+	constructor(notifier: User, note: Note) {
 		this.notifier = notifier;
 		this.note = note;
 		this.queue = [];
 	}
 
-	public push(notifiee: ILocalUser['_id'], reason: NotificationType) {
+	public push(notifiee: ILocalUser['id'], reason: NotificationType) {
 		// 自分自身へは通知しない
-		if (this.notifier._id.equals(notifiee)) return;
+		if (this.notifier.id === notifiee) return;
 
-		const exist = this.queue.find(x => x.target.equals(notifiee));
+		const exist = this.queue.find(x => x.target === notifiee);
 
 		if (exist) {
 			// 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする
@@ -73,16 +66,16 @@ class NotificationManager {
 	public async deliver() {
 		for (const x of this.queue) {
 			// ミュート情報を取得
-			const mentioneeMutes = await Mute.find({
+			const mentioneeMutes = await Mutings.find({
 				muterId: x.target
 			});
 
-			const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString());
+			const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId);
 
 			// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
-			if (!mentioneesMutedUserIds.includes(this.notifier._id.toString())) {
-				notify(x.target, this.notifier._id, x.reason, {
-					noteId: this.note._id
+			if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
+				createNotification(x.target, this.notifier.id, x.reason, {
+					noteId: this.note.id
 				});
 			}
 		}
@@ -93,25 +86,25 @@ type Option = {
 	createdAt?: Date;
 	name?: string;
 	text?: string;
-	reply?: INote;
-	renote?: INote;
-	files?: IDriveFile[];
+	reply?: Note;
+	renote?: Note;
+	files?: DriveFile[];
 	geo?: any;
-	poll?: any;
+	poll?: IPoll;
 	viaMobile?: boolean;
 	localOnly?: boolean;
 	cw?: string;
 	visibility?: string;
-	visibleUsers?: IUser[];
-	apMentions?: IUser[];
+	visibleUsers?: User[];
+	apMentions?: User[];
 	apHashtags?: string[];
 	apEmojis?: string[];
 	questionUri?: string;
 	uri?: string;
-	app?: IApp;
+	app?: App;
 };
 
-export default async (user: IUser, data: Option, silent = false) => new Promise<INote>(async (res, rej) => {
+export default async (user: User, data: Option, silent = false) => new Promise<Note>(async (res, rej) => {
 	const isFirstNote = user.notesCount === 0;
 
 	if (data.createdAt == null) data.createdAt = new Date();
@@ -128,16 +121,6 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 		data.visibleUsers = erase(null, data.visibleUsers);
 	}
 
-	// リプライ対象が削除された投稿だったらreject
-	if (data.reply && data.reply.deletedAt != null) {
-		return rej('Reply target has been deleted');
-	}
-
-	// Renote対象が削除された投稿だったらreject
-	if (data.renote && data.renote.deletedAt != null) {
-		return rej('Renote target has been deleted');
-	}
-
 	// Renote対象が「ホームまたは全体」以外の公開範囲ならreject
 	if (data.renote && data.renote.visibility != 'public' && data.renote.visibility != 'home') {
 		return rej('Renote target is not public or home');
@@ -176,7 +159,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 		const tokens = data.text ? parse(data.text) : [];
 		const cwTokens = data.cw ? parse(data.cw) : [];
 		const choiceTokens = data.poll && data.poll.choices
-			? concat((data.poll.choices as IChoice[]).map(choice => parse(choice.text)))
+			? concat(data.poll.choices.map(choice => parse(choice)))
 			: [];
 
 		const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens);
@@ -188,24 +171,21 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 		mentionedUsers = data.apMentions || await extractMentionedUsers(user, combinedTokens);
 	}
 
-	// MongoDBのインデックス対象は128文字以上にできない
 	tags = tags.filter(tag => tag.length <= 100);
 
-	if (data.reply && !user._id.equals(data.reply.userId) && !mentionedUsers.some(u => u._id.equals(data.reply.userId))) {
-		mentionedUsers.push(await User.findOne({ _id: data.reply.userId }));
+	if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply.userId)) {
+		mentionedUsers.push(await Users.findOne(data.reply.userId));
 	}
 
 	if (data.visibility == 'specified') {
 		for (const u of data.visibleUsers) {
-			if (!mentionedUsers.some(x => x._id.equals(u._id))) {
+			if (!mentionedUsers.some(x => x.id === u.id)) {
 				mentionedUsers.push(u);
 			}
 		}
 
-		for (const u of mentionedUsers) {
-			if (!data.visibleUsers.some(x => x._id.equals(u._id))) {
-				data.visibleUsers.push(u);
-			}
+		if (data.reply && !data.visibleUsers.some(x => x.id === data.reply.userId)) {
+			data.visibleUsers.push(await Users.findOne(data.reply.userId));
 		}
 	}
 
@@ -221,17 +201,12 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 	notesChart.update(note, true);
 	perUserNotesChart.update(user, note, true);
 	// ローカルユーザーのチャートはタイムライン取得時に更新しているのでリモートユーザーの場合だけでよい
-	if (isRemoteUser(user)) activeUsersChart.update(user);
+	if (Users.isRemoteUser(user)) activeUsersChart.update(user);
 
 	// Register host
-	if (isRemoteUser(user)) {
+	if (Users.isRemoteUser(user)) {
 		registerOrFetchInstanceDoc(user.host).then(i => {
-			Instance.update({ _id: i._id }, {
-				$inc: {
-					notesCount: 1
-				}
-			});
-
+			Instances.increment({ id: i.id }, 'notesCount', 1);
 			instanceChart.updateNote(i.host, true);
 		});
 	}
@@ -239,20 +214,6 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 	// ハッシュタグ更新
 	for (const tag of tags) updateHashtag(user, tag);
 
-	// ファイルが添付されていた場合ドライブのファイルの「このファイルが添付された投稿一覧」プロパティにこの投稿を追加
-	if (data.files) {
-		for (const file of data.files) {
-			DriveFile.update({ _id: file._id }, {
-				$push: {
-					'metadata.attachedNoteIds': note._id
-				}
-			});
-		}
-	}
-
-	// Increment notes count
-	incNotesCount(user);
-
 	// Increment notes count (user)
 	incNotesCountOfUser(user);
 
@@ -275,20 +236,14 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 		incRenoteCount(data.renote);
 	}
 
-	if (isQuote(note)) {
-		saveQuote(data.renote, note);
-	}
-
 	// Pack the note
-	const noteObj = await pack(note);
+	const noteObj = await Notes.pack(note);
 
 	if (isFirstNote) {
 		noteObj.isFirstNote = true;
 	}
 
-	if (tags.length > 0) {
-		publishHashtagStream(noteObj);
-	}
+	publishNotesStream(noteObj);
 
 	const nm = new NotificationManager(user, note);
 	const nmRelatedPromises = [];
@@ -297,7 +252,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 
 	const noteActivity = await renderNoteOrRenoteActivity(data, note);
 
-	if (isLocalUser(user)) {
+	if (Users.isLocalUser(user)) {
 		deliverNoteToMentionedRemoteUsers(mentionedUsers, user, noteActivity);
 	}
 
@@ -307,12 +262,12 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 		nmRelatedPromises.push(notifyToWatchersOfReplyee(data.reply, user, nm));
 
 		// この投稿をWatchする
-		if (isLocalUser(user) && user.settings.autoWatch !== false) {
-			watch(user._id, data.reply);
+		if (Users.isLocalUser(user) && user.autoWatch !== false) {
+			watch(user.id, data.reply);
 		}
 
 		// 通知
-		if (isLocalUser(data.reply._user)) {
+		if (data.reply.userHost === null) {
 			nm.push(data.reply.userId, 'reply');
 			publishMainStream(data.reply.userId, 'reply', noteObj);
 		}
@@ -323,7 +278,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 		const type = data.text ? 'quote' : 'renote';
 
 		// Notify
-		if (isLocalUser(data.renote._user)) {
+		if (data.renote.userHost === null) {
 			nm.push(data.renote.userId, type);
 		}
 
@@ -331,18 +286,18 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 		nmRelatedPromises.push(notifyToWatchersOfRenotee(data.renote, user, nm, type));
 
 		// この投稿をWatchする
-		if (isLocalUser(user) && user.settings.autoWatch !== false) {
-			watch(user._id, data.renote);
+		if (Users.isLocalUser(user) && user.autoWatch !== false) {
+			watch(user.id, data.renote);
 		}
 
 		// Publish event
-		if (!user._id.equals(data.renote.userId) && isLocalUser(data.renote._user)) {
+		if ((user.id !== data.renote.userId) && data.renote.userHost === null) {
 			publishMainStream(data.renote.userId, 'renote', noteObj);
 		}
 	}
 
 	if (!silent) {
-		publish(user, note, noteObj, data.reply, data.renote, data.visibleUsers, noteActivity);
+		publish(user, note, data.reply, data.renote, noteActivity);
 	}
 
 	Promise.all(nmRelatedPromises).then(() => {
@@ -353,245 +308,166 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 	index(note);
 });
 
-async function renderNoteOrRenoteActivity(data: Option, note: INote) {
+async function renderNoteOrRenoteActivity(data: Option, note: Note) {
 	if (data.localOnly) return null;
 
 	const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length == 0)
-		? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote._id}`, note)
+		? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote.id}`, note)
 		: renderCreate(await renderNote(note, false), note);
 
 	return renderActivity(content);
 }
 
-function incRenoteCount(renote: INote) {
-	Note.update({ _id: renote._id }, {
-		$inc: {
-			renoteCount: 1,
-			score: 1
-		}
-	});
+function incRenoteCount(renote: Note) {
+	Notes.increment({ id: renote.id }, 'renoteCount', 1);
+	Notes.increment({ id: renote.id }, 'score', 1);
 }
 
-async function publish(user: IUser, note: INote, noteObj: any, reply: INote, renote: INote, visibleUsers: IUser[], noteActivity: any) {
-	if (isLocalUser(user)) {
+async function publish(user: User, note: Note, reply: Note, renote: Note, noteActivity: any) {
+	if (Users.isLocalUser(user)) {
 		// 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
-		if (reply && isRemoteUser(reply._user)) {
-			deliver(user, noteActivity, reply._user.inbox);
+		if (reply && reply.userHost !== null) {
+			deliver(user, noteActivity, reply.userInbox);
 		}
 
 		// 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送
-		if (renote && isRemoteUser(renote._user)) {
-			deliver(user, noteActivity, renote._user.inbox);
-		}
-
-		if (['followers', 'specified'].includes(note.visibility)) {
-			const detailPackedNote = await pack(note, user, {
-				detail: true
-			});
-			// Publish event to myself's stream
-			publishHomeTimelineStream(note.userId, detailPackedNote);
-			publishHybridTimelineStream(note.userId, detailPackedNote);
-
-			if (note.visibility == 'specified') {
-				for (const u of visibleUsers) {
-					if (!u._id.equals(user._id)) {
-						publishHomeTimelineStream(u._id, detailPackedNote);
-						publishHybridTimelineStream(u._id, detailPackedNote);
-					}
-				}
-			}
-		} else {
-			// Publish event to myself's stream
-			publishHomeTimelineStream(note.userId, noteObj);
-
-			// Publish note to local and hybrid timeline stream
-			if (note.visibility != 'home') {
-				publishLocalTimelineStream(noteObj);
-			}
-
-			if (note.visibility == 'public') {
-				publishHybridTimelineStream(null, noteObj);
-			} else {
-				// Publish event to myself's stream
-				publishHybridTimelineStream(note.userId, noteObj);
-			}
+		if (renote && renote.userHost !== null) {
+			deliver(user, noteActivity, renote.userInbox);
 		}
 	}
 
-	// Publish note to global timeline stream
-	if (note.visibility == 'public' && note.replyId == null) {
-		publishGlobalTimelineStream(noteObj);
-	}
-
 	if (['public', 'home', 'followers'].includes(note.visibility)) {
 		// フォロワーに配信
-		publishToFollowers(note, user, noteActivity);
+		publishToFollowers(note, user, noteActivity, reply);
 	}
-
-	// リストに配信
-	publishToUserLists(note, noteObj);
 }
 
-async function insertNote(user: IUser, data: Option, tags: string[], emojis: string[], mentionedUsers: IUser[]) {
-	const insert: any = {
+async function insertNote(user: User, data: Option, tags: string[], emojis: string[], mentionedUsers: User[]) {
+	const insert: Partial<Note> = {
+		id: genId(data.createdAt),
 		createdAt: data.createdAt,
-		fileIds: data.files ? data.files.map(file => file._id) : [],
-		replyId: data.reply ? data.reply._id : null,
-		renoteId: data.renote ? data.renote._id : null,
+		fileIds: data.files ? data.files.map(file => file.id) : [],
+		replyId: data.reply ? data.reply.id : null,
+		renoteId: data.renote ? data.renote.id : null,
 		name: data.name,
 		text: data.text,
-		poll: data.poll,
+		hasPoll: data.poll != null,
 		cw: data.cw == null ? null : data.cw,
-		tags,
-		tagsLower: tags.map(tag => tag.toLowerCase()),
+		tags: tags.map(tag => tag.toLowerCase()),
 		emojis,
-		userId: user._id,
+		userId: user.id,
 		viaMobile: data.viaMobile,
 		localOnly: data.localOnly,
 		geo: data.geo || null,
-		appId: data.app ? data.app._id : null,
-		visibility: data.visibility,
+		appId: data.app ? data.app.id : null,
+		visibility: data.visibility as any,
 		visibleUserIds: data.visibility == 'specified'
 			? data.visibleUsers
-				? data.visibleUsers.map(u => u._id)
+				? data.visibleUsers.map(u => u.id)
 				: []
 			: [],
 
+		attachedFileTypes: data.files ? data.files.map(file => file.type) : [],
+
 		// 以下非正規化データ
-		_reply: data.reply ? {
-			userId: data.reply.userId,
-			user: {
-				host: data.reply._user.host
-			}
-		} : null,
-		_renote: data.renote ? {
-			userId: data.renote.userId,
-			user: {
-				host: data.renote._user.host
-			}
-		} : null,
-		_user: {
-			host: user.host,
-			inbox: isRemoteUser(user) ? user.inbox : undefined
-		},
-		_files: data.files ? data.files : []
+		replyUserId: data.reply ? data.reply.userId : null,
+		replyUserHost: data.reply ? data.reply.userHost : null,
+		renoteUserId: data.renote ? data.renote.userId : null,
+		renoteUserHost: data.renote ? data.renote.userHost : null,
+		userHost: user.host,
+		userInbox: user.inbox,
 	};
 
 	if (data.uri != null) insert.uri = data.uri;
 
 	// Append mentions data
 	if (mentionedUsers.length > 0) {
-		insert.mentions = mentionedUsers.map(u => u._id);
-		insert.mentionedRemoteUsers = mentionedUsers.filter(u => isRemoteUser(u)).map(u => ({
+		insert.mentions = mentionedUsers.map(u => u.id);
+		insert.mentionedRemoteUsers = JSON.stringify(mentionedUsers.filter(u => Users.isRemoteUser(u)).map(u => ({
 			uri: (u as IRemoteUser).uri,
 			username: u.username,
 			host: u.host
-		}));
+		})));
 	}
 
 	// 投稿を作成
 	try {
-		return await Note.insert(insert);
+		const note = await Notes.save(insert);
+
+		if (note.hasPoll) {
+			await Polls.save({
+				id: genId(),
+				noteId: note.id,
+				choices: data.poll.choices,
+				expiresAt: data.poll.expiresAt,
+				multiple: data.poll.multiple,
+				votes: new Array(data.poll.choices.length).fill(0),
+				noteVisibility: note.visibility,
+				userId: user.id,
+				userHost: user.host
+			} as Poll);
+		}
+
+		return note;
 	} catch (e) {
 		// duplicate key error
-		if (e.code === 11000) {
+		if (isDuplicateKeyValueError(e)) {
 			return null;
 		}
 
+		console.error(e);
+
 		throw 'something happened';
 	}
 }
 
-function index(note: INote) {
+function index(note: Note) {
 	if (note.text == null || config.elasticsearch == null) return;
 
 	es.index({
 		index: 'misskey',
 		type: 'note',
-		id: note._id.toString(),
+		id: note.id.toString(),
 		body: {
 			text: note.text
 		}
 	});
 }
 
-async function notifyToWatchersOfRenotee(renote: INote, user: IUser, nm: NotificationManager, type: NotificationType) {
-	const watchers = await NoteWatching.find({
-		noteId: renote._id,
-		userId: { $ne: user._id }
-	}, {
-			fields: {
-				userId: true
-			}
-		});
+async function notifyToWatchersOfRenotee(renote: Note, user: User, nm: NotificationManager, type: NotificationType) {
+	const watchers = await NoteWatchings.find({
+		noteId: renote.id,
+		userId: Not(user.id)
+	});
 
 	for (const watcher of watchers) {
 		nm.push(watcher.userId, type);
 	}
 }
 
-async function notifyToWatchersOfReplyee(reply: INote, user: IUser, nm: NotificationManager) {
-	const watchers = await NoteWatching.find({
-		noteId: reply._id,
-		userId: { $ne: user._id }
-	}, {
-			fields: {
-				userId: true
-			}
-		});
+async function notifyToWatchersOfReplyee(reply: Note, user: User, nm: NotificationManager) {
+	const watchers = await NoteWatchings.find({
+		noteId: reply.id,
+		userId: Not(user.id)
+	});
 
 	for (const watcher of watchers) {
 		nm.push(watcher.userId, 'reply');
 	}
 }
 
-async function publishToUserLists(note: INote, noteObj: any) {
-	const lists = await UserList.find({
-		userIds: note.userId
-	});
-
-	for (const list of lists) {
-		if (note.visibility == 'specified') {
-			if (note.visibleUserIds.some(id => id.equals(list.userId))) {
-				publishUserListStream(list._id, 'note', noteObj);
-			}
-		} else {
-			publishUserListStream(list._id, 'note', noteObj);
-		}
-	}
-}
-
-async function publishToFollowers(note: INote, user: IUser, noteActivity: any) {
-	const detailPackedNote = await pack(note, null, {
-		detail: true,
-		skipHide: true
-	});
-
-	const followers = await Following.find({
-		followeeId: note.userId,
-		followerId: { $ne: note.userId }	// バグでフォロワーに自分がいることがあるため
+async function publishToFollowers(note: Note, user: User, noteActivity: any, reply: Note) {
+	const followers = await Followings.find({
+		followeeId: note.userId
 	});
 
 	const queue: string[] = [];
 
 	for (const following of followers) {
-		const follower = following._follower;
-
-		if (isLocalUser(follower)) {
-			// この投稿が返信ならスキップ
-			if (note.replyId && !note._reply.userId.equals(following.followerId) && !note._reply.userId.equals(note.userId))
-				continue;
-
-			// Publish event to followers stream
-			publishHomeTimelineStream(following.followerId, detailPackedNote);
-
-			if (isRemoteUser(user) || note.visibility != 'public') {
-				publishHybridTimelineStream(following.followerId, detailPackedNote);
-			}
-		} else {
+		if (following.followerHost !== null) {
 			// フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
-			if (isLocalUser(user)) {
-				const inbox = follower.sharedInbox || follower.inbox;
+			if (Users.isLocalUser(user)) {
+				const inbox = following.followerSharedInbox || following.followerInbox;
 				if (!queue.includes(inbox)) queue.push(inbox);
 			}
 		}
@@ -600,104 +476,52 @@ async function publishToFollowers(note: INote, user: IUser, noteActivity: any) {
 	for (const inbox of queue) {
 		deliver(user as any, noteActivity, inbox);
 	}
-
-	// 後方互換製のため、Questionは時間差でNoteでも送る
-	// Questionに対応してないインスタンスは、2つめのNoteだけを採用する
-	// Questionに対応しているインスタンスは、同IDで採番されている2つめのNoteを無視する
-	setTimeout(() => {
-		if (noteActivity.object.type === 'Question') {
-			const asNote = deepcopy(noteActivity);
-
-			asNote.object.type = 'Note';
-			asNote.object.content = asNote.object._misskey_fallback_content;
-
-			for (const inbox of queue) {
-				deliver(user as any, asNote, inbox);
-			}
-		}
-	}, 10 * 1000);
 }
 
-function deliverNoteToMentionedRemoteUsers(mentionedUsers: IUser[], user: ILocalUser, noteActivity: any) {
-	for (const u of mentionedUsers.filter(u => isRemoteUser(u))) {
+function deliverNoteToMentionedRemoteUsers(mentionedUsers: User[], user: ILocalUser, noteActivity: any) {
+	for (const u of mentionedUsers.filter(u => Users.isRemoteUser(u))) {
 		deliver(user, noteActivity, (u as IRemoteUser).inbox);
 	}
 }
 
-async function createMentionedEvents(mentionedUsers: IUser[], note: INote, nm: NotificationManager) {
-	for (const u of mentionedUsers.filter(u => isLocalUser(u))) {
-		const detailPackedNote = await pack(note, u, {
+async function createMentionedEvents(mentionedUsers: User[], note: Note, nm: NotificationManager) {
+	for (const u of mentionedUsers.filter(u => Users.isLocalUser(u))) {
+		const detailPackedNote = await Notes.pack(note, u, {
 			detail: true
 		});
 
-		publishMainStream(u._id, 'mention', detailPackedNote);
+		publishMainStream(u.id, 'mention', detailPackedNote);
 
 		// Create notification
-		nm.push(u._id, 'mention');
+		nm.push(u.id, 'mention');
 	}
 }
 
-function saveQuote(renote: INote, note: INote) {
-	Note.update({ _id: renote._id }, {
-		$push: {
-			_quoteIds: note._id
-		}
-	});
-}
-
-function saveReply(reply: INote, note: INote) {
-	Note.update({ _id: reply._id }, {
-		$inc: {
-			repliesCount: 1
-		}
-	});
+function saveReply(reply: Note, note: Note) {
+	Notes.increment({ id: reply.id }, 'repliesCount', 1);
 }
 
-function incNotesCountOfUser(user: IUser) {
-	User.update({ _id: user._id }, {
-		$set: {
-			updatedAt: new Date()
-		},
-		$inc: {
-			notesCount: 1
-		}
+function incNotesCountOfUser(user: User) {
+	Users.increment({ id: user.id }, 'notesCount', 1);
+	Users.update({ id: user.id }, {
+		updatedAt: new Date()
 	});
 }
 
-function incNotesCount(user: IUser) {
-	if (isLocalUser(user)) {
-		Meta.update({}, {
-			$inc: {
-				'stats.notesCount': 1,
-				'stats.originalNotesCount': 1
-			}
-		}, { upsert: true });
-	} else {
-		Meta.update({}, {
-			$inc: {
-				'stats.notesCount': 1
-			}
-		}, { upsert: true });
-	}
-}
-
-async function extractMentionedUsers(user: IUser, tokens: ReturnType<typeof parse>): Promise<IUser[]> {
+async function extractMentionedUsers(user: User, tokens: ReturnType<typeof parse>): Promise<User[]> {
 	if (tokens == null) return [];
 
 	const mentions = extractMentions(tokens);
 
-	let mentionedUsers =
-		erase(null, await Promise.all(mentions.map(async m => {
-			try {
-				return await resolveUser(m.username, m.host ? m.host : user.host);
-			} catch (e) {
-				return null;
-			}
-		})));
+	let mentionedUsers = await Promise.all(mentions.map(m =>
+		resolveUser(m.username, m.host || user.host).catch(() => null)
+	));
+
+	mentionedUsers = mentionedUsers.filter(x => x != null);
 
 	// Drop duplicate users
 	mentionedUsers = mentionedUsers.filter((u, i, self) =>
-		i === self.findIndex(u2 => u._id.equals(u2._id))
+		i === self.findIndex(u2 => u.id === u2.id)
 	);
 
 	return mentionedUsers;
diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts
index d71c97b2cab4dddf5a8660bc94f5fbfca5996499..7f04d12cd579d02c8f2c9ea336e6af3379b1b618 100644
--- a/src/services/note/delete.ts
+++ b/src/services/note/delete.ts
@@ -1,99 +1,50 @@
-import Note, { INote } from '../../models/note';
-import { IUser, isLocalUser, isRemoteUser } from '../../models/user';
 import { publishNoteStream } from '../stream';
 import renderDelete from '../../remote/activitypub/renderer/delete';
 import { renderActivity } from '../../remote/activitypub/renderer';
 import { deliver } from '../../queue';
-import Following from '../../models/following';
 import renderTombstone from '../../remote/activitypub/renderer/tombstone';
-import notesChart from '../../services/chart/notes';
-import perUserNotesChart from '../../services/chart/per-user-notes';
 import config from '../../config';
-import NoteUnread from '../../models/note-unread';
-import read from './read';
-import DriveFile from '../../models/drive-file';
 import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
-import Instance from '../../models/instance';
-import instanceChart from '../../services/chart/instance';
-import Favorite from '../../models/favorite';
+import { User } from '../../models/entities/user';
+import { Note } from '../../models/entities/note';
+import { Notes, Users, Followings, Instances } from '../../models';
+import { Not } from 'typeorm';
+import { notesChart, perUserNotesChart, instanceChart } from '../chart';
 
 /**
  * 投稿を削除します。
  * @param user 投稿者
  * @param note 投稿
  */
-export default async function(user: IUser, note: INote, quiet = false) {
+export default async function(user: User, note: Note, quiet = false) {
 	const deletedAt = new Date();
 
-	await Note.update({
-		_id: note._id,
-		userId: user._id
-	}, {
-		$set: {
-			deletedAt: deletedAt,
-			text: null,
-			tags: [],
-			fileIds: [],
-			renoteId: null,
-			poll: null,
-			geo: null,
-			cw: null
-		}
+	await Notes.delete({
+		id: note.id,
+		userId: user.id
 	});
 
 	if (note.renoteId) {
-		Note.update({ _id: note.renoteId }, {
-			$inc: {
-				renoteCount: -1,
-				score: -1
-			},
-			$pull: {
-				_quoteIds: note._id
-			}
-		});
-	}
-
-	// この投稿が関わる未読通知を削除
-	NoteUnread.find({
-		noteId: note._id
-	}).then(unreads => {
-		for (const unread of unreads) {
-			read(unread.userId, unread.noteId);
-		}
-	});
-
-	// この投稿をお気に入りから削除
-	Favorite.remove({
-		noteId: note._id
-	});
-
-	// ファイルが添付されていた場合ドライブのファイルの「このファイルが添付された投稿一覧」プロパティからこの投稿を削除
-	if (note.fileIds) {
-		for (const fileId of note.fileIds) {
-			DriveFile.update({ _id: fileId }, {
-				$pull: {
-					'metadata.attachedNoteIds': note._id
-				}
-			});
-		}
+		Notes.decrement({ id: note.renoteId }, 'renoteCount', 1);
+		Notes.decrement({ id: note.renoteId }, 'score', 1);
 	}
 
 	if (!quiet) {
-		publishNoteStream(note._id, 'deleted', {
+		publishNoteStream(note.id, 'deleted', {
 			deletedAt: deletedAt
 		});
 
 		//#region ローカルの投稿なら削除アクティビティを配送
-		if (isLocalUser(user)) {
-			const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${note._id}`), user));
+		if (Users.isLocalUser(user)) {
+			const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${note.id}`), user));
 
-			const followings = await Following.find({
-				followeeId: user._id,
-				'_follower.host': { $ne: null }
+			const followings = await Followings.find({
+				followeeId: user.id,
+				followerHost: Not(null)
 			});
 
 			for (const following of followings) {
-				deliver(user, content, following._follower.inbox);
+				deliver(user, content, following.followerInbox);
 			}
 		}
 		//#endregion
@@ -102,14 +53,9 @@ export default async function(user: IUser, note: INote, quiet = false) {
 		notesChart.update(note, false);
 		perUserNotesChart.update(user, note, false);
 
-		if (isRemoteUser(user)) {
+		if (Users.isRemoteUser(user)) {
 			registerOrFetchInstanceDoc(user.host).then(i => {
-				Instance.update({ _id: i._id }, {
-					$inc: {
-						notesCount: -1
-					}
-				});
-
+				Instances.decrement({ id: i.id }, 'notesCount', 1);
 				instanceChart.updateNote(i.host, false);
 			});
 		}
diff --git a/src/services/note/polls/update.ts b/src/services/note/polls/update.ts
index d4e183889d9b748f2ae9329a0f11f3fd4b61301e..ff8e8d59efd4f893604c05a66460e14bbde1f1ec 100644
--- a/src/services/note/polls/update.ts
+++ b/src/services/note/polls/update.ts
@@ -1,51 +1,48 @@
-import * as mongo from 'mongodb';
-import Note, { INote } from '../../../models/note';
 import { updateQuestion } from '../../../remote/activitypub/models/question';
 import ms = require('ms');
 import Logger from '../../logger';
-import User, { isLocalUser, isRemoteUser } from '../../../models/user';
-import Following from '../../../models/following';
 import renderUpdate from '../../../remote/activitypub/renderer/update';
 import { renderActivity } from '../../../remote/activitypub/renderer';
 import { deliver } from '../../../queue';
 import renderNote from '../../../remote/activitypub/renderer/note';
+import { Users, Notes, Followings } from '../../../models';
+import { Note } from '../../../models/entities/note';
 
 const logger = new Logger('pollsUpdate');
 
-export async function triggerUpdate(note: INote) {
+export async function triggerUpdate(note: Note) {
 	if (!note.updatedAt || Date.now() - new Date(note.updatedAt).getTime() > ms('1min')) {
-		logger.info(`Updating ${note._id}`);
+		logger.info(`Updating ${note.id}`);
 
 		try {
 			const updated = await updateQuestion(note.uri);
-			logger.info(`Updated ${note._id} ${updated ? 'changed' : 'nochange'}`);
+			logger.info(`Updated ${note.id} ${updated ? 'changed' : 'nochange'}`);
 		} catch (e) {
 			logger.error(e);
 		}
 	}
 }
 
-export async function deliverQuestionUpdate(noteId: mongo.ObjectID) {
-	const note = await Note.findOne({
-		_id: noteId,
-	});
+export async function deliverQuestionUpdate(noteId: Note['id']) {
+	const note = await Notes.findOne(noteId);
 
-	const user = await User.findOne({
-		_id: note.userId
-	});
+	const user = await Users.findOne(note.userId);
 
-	const followers = await Following.find({
-		followeeId: user._id
+	const followers = await Followings.find({
+		followeeId: user.id
 	});
 
 	const queue: string[] = [];
 
 	// フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信
-	if (isLocalUser(user)) {
+	if (Users.isLocalUser(user)) {
 		for (const following of followers) {
-			const follower = following._follower;
+			const follower = {
+				inbox: following.followerInbox,
+				sharedInbox: following.followerSharedInbox
+			};
 
-			if (isRemoteUser(follower)) {
+			if (following.followerHost !== null) {
 				const inbox = follower.sharedInbox || follower.inbox;
 				if (!queue.includes(inbox)) queue.push(inbox);
 			}
diff --git a/src/services/note/polls/vote.ts b/src/services/note/polls/vote.ts
index a23cdc1cb47bfc295331e4a9f8aa8b88ef12808c..15f1ddffbca87ab3ac0116fc918eced556e54bee 100644
--- a/src/services/note/polls/vote.ts
+++ b/src/services/note/polls/vote.ts
@@ -1,79 +1,74 @@
-import Vote from '../../../models/poll-vote';
-import Note, { INote } from '../../../models/note';
-import Watching from '../../../models/note-watching';
 import watch from '../../../services/note/watch';
 import { publishNoteStream } from '../../stream';
-import notify from '../../../services/create-notification';
-import { isLocalUser, IUser } from '../../../models/user';
+import { User } from '../../../models/entities/user';
+import { Note } from '../../../models/entities/note';
+import { PollVotes, Users, NoteWatchings, Polls } from '../../../models';
+import { Not } from 'typeorm';
+import { genId } from '../../../misc/gen-id';
+import { createNotification } from '../../create-notification';
 
-export default (user: IUser, note: INote, choice: number) => new Promise(async (res, rej) => {
-	if (!note.poll.choices.some(x => x.id == choice)) return rej('invalid choice param');
+export default (user: User, note: Note, choice: number) => new Promise(async (res, rej) => {
+	const poll = await Polls.findOne({ noteId: note.id });
+
+	// Check whether is valid choice
+	if (poll.choices[choice] == null) return rej('invalid choice param');
 
 	// if already voted
-	const exist = await Vote.find({
-		noteId: note._id,
-		userId: user._id
+	const exist = await PollVotes.find({
+		noteId: note.id,
+		userId: user.id
 	});
 
-	if (note.poll.multiple) {
-		if (exist.some(x => x.choice === choice))
+	if (poll.multiple) {
+		if (exist.some(x => x.choice === choice)) {
 			return rej('already voted');
-	} else if (exist.length) {
+		}
+	} else if (exist.length !== 0) {
 		return rej('already voted');
 	}
 
 	// Create vote
-	await Vote.insert({
+	await PollVotes.save({
+		id: genId(),
 		createdAt: new Date(),
-		noteId: note._id,
-		userId: user._id,
+		noteId: note.id,
+		userId: user.id,
 		choice: choice
 	});
 
 	res();
 
-	const inc: any = {};
-	inc[`poll.choices.${note.poll.choices.findIndex(c => c.id == choice)}.votes`] = 1;
-
 	// Increment votes count
-	await Note.update({ _id: note._id }, {
-		$inc: inc
-	});
+	const index = choice + 1; // In SQL, array index is 1 based
+	await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE id = '${poll.id}'`);
 
-	publishNoteStream(note._id, 'pollVoted', {
+	publishNoteStream(note.id, 'pollVoted', {
 		choice: choice,
-		userId: user._id.toHexString()
+		userId: user.id
 	});
 
 	// Notify
-	notify(note.userId, user._id, 'poll_vote', {
-		noteId: note._id,
+	createNotification(note.userId, user.id, 'pollVote', {
+		noteId: note.id,
 		choice: choice
 	});
 
 	// Fetch watchers
-	Watching
-		.find({
-			noteId: note._id,
-			userId: { $ne: user._id },
-			// 削除されたドキュメントは除く
-			deletedAt: { $exists: false }
-		}, {
-			fields: {
-				userId: true
-			}
-		})
-		.then(watchers => {
-			for (const watcher of watchers) {
-				notify(watcher.userId, user._id, 'poll_vote', {
-					noteId: note._id,
-					choice: choice
-				});
-			}
-		});
+	NoteWatchings.find({
+		noteId: note.id,
+		userId: Not(user.id),
+	})
+	.then(watchers => {
+		for (const watcher of watchers) {
+			createNotification(watcher.userId, user.id, 'pollVote', {
+				noteId: note.id,
+				choice: choice
+			});
+		}
+	});
 
 	// ローカルユーザーが投票した場合この投稿をWatchする
-	if (isLocalUser(user) && user.settings.autoWatch !== false) {
-		watch(user._id, note);
+	if (Users.isLocalUser(user) && user.autoWatch) {
+		watch(user.id, note);
 	}
 });
diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts
index 4fdaf92ac6fe74bb1b809e16a04ff7cbf6430daa..437b213ded31acacdee08ca50343c9a1ace6a9cc 100644
--- a/src/services/note/reaction/create.ts
+++ b/src/services/note/reaction/create.ts
@@ -1,21 +1,24 @@
-import { IUser, isLocalUser, isRemoteUser } from '../../../models/user';
-import Note, { INote } from '../../../models/note';
-import NoteReaction from '../../../models/note-reaction';
 import { publishNoteStream } from '../../stream';
-import notify from '../../create-notification';
-import NoteWatching from '../../../models/note-watching';
 import watch from '../watch';
 import renderLike from '../../../remote/activitypub/renderer/like';
 import { deliver } from '../../../queue';
 import { renderActivity } from '../../../remote/activitypub/renderer';
-import perUserReactionsChart from '../../../services/chart/per-user-reactions';
 import { IdentifiableError } from '../../../misc/identifiable-error';
 import { toDbReaction } from '../../../misc/reaction-lib';
 import fetchMeta from '../../../misc/fetch-meta';
+import { User } from '../../../models/entities/user';
+import { Note } from '../../../models/entities/note';
+import { NoteReactions, Users, NoteWatchings, Notes } from '../../../models';
+import { Not } from 'typeorm';
+import { perUserReactionsChart } from '../../chart';
+import { genId } from '../../../misc/gen-id';
+import { NoteReaction } from '../../../models/entities/note-reaction';
+import { createNotification } from '../../create-notification';
+import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error';
 
-export default async (user: IUser, note: INote, reaction: string) => {
+export default async (user: User, note: Note, reaction: string) => {
 	// Myself
-	if (note.userId.equals(user._id)) {
+	if (note.userId === user.id) {
 		throw new IdentifiableError('2d8e7297-1873-4c00-8404-792c68d7bef0', 'cannot react to my note');
 	}
 
@@ -23,14 +26,15 @@ export default async (user: IUser, note: INote, reaction: string) => {
 	reaction = await toDbReaction(reaction, meta.enableEmojiReaction);
 
 	// Create reaction
-	await NoteReaction.insert({
+	await NoteReactions.save({
+		id: genId(),
 		createdAt: new Date(),
-		noteId: note._id,
-		userId: user._id,
+		noteId: note.id,
+		userId: user.id,
 		reaction
-	}).catch(e => {
+	} as NoteReaction).catch(e => {
 		// duplicate key error
-		if (e.code === 11000) {
+		if (isDuplicateKeyValueError(e)) {
 			throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298', 'already reacted');
 		}
 
@@ -38,59 +42,53 @@ export default async (user: IUser, note: INote, reaction: string) => {
 	});
 
 	// Increment reactions count
-	await Note.update({ _id: note._id }, {
-		$inc: {
-			[`reactionCounts.${reaction}`]: 1,
-			score: 1
-		}
-	});
+	const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
+	await Notes.createQueryBuilder().update()
+		.set({
+			reactions: () => sql,
+		})
+		.where('id = :id', { id: note.id })
+		.execute();
+	// v11 inc score
 
 	perUserReactionsChart.update(user, note);
 
-	publishNoteStream(note._id, 'reacted', {
+	publishNoteStream(note.id, 'reacted', {
 		reaction: reaction,
-		userId: user._id
+		userId: user.id
 	});
 
 	// リアクションされたユーザーがローカルユーザーなら通知を作成
-	if (isLocalUser(note._user)) {
-		notify(note.userId, user._id, 'reaction', {
-			noteId: note._id,
+	if (note.userHost === null) {
+		createNotification(note.userId, user.id, 'reaction', {
+			noteId: note.id,
 			reaction: reaction
 		});
 	}
 
 	// Fetch watchers
-	NoteWatching
-		.find({
-			noteId: note._id,
-			userId: { $ne: user._id }
-		}, {
-			fields: {
-				userId: true
-			}
-		})
-		.then(watchers => {
-			for (const watcher of watchers) {
-				notify(watcher.userId, user._id, 'reaction', {
-					noteId: note._id,
-					reaction: reaction
-				});
-			}
-		});
+	NoteWatchings.find({
+		noteId: note.id,
+		userId: Not(user.id)
+	}).then(watchers => {
+		for (const watcher of watchers) {
+			createNotification(watcher.userId, user.id, 'reaction', {
+				noteId: note.id,
+				reaction: reaction
+			});
+		}
+	});
 
 	// ユーザーがローカルユーザーかつ自動ウォッチ設定がオンならばこの投稿をWatchする
-	if (isLocalUser(user) && user.settings.autoWatch !== false) {
-		watch(user._id, note);
+	if (Users.isLocalUser(user) && user.autoWatch !== false) {
+		watch(user.id, note);
 	}
 
 	//#region 配信
 	// リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送
-	if (isLocalUser(user) && isRemoteUser(note._user)) {
+	if (Users.isLocalUser(user) && note.userHost !== null) {
 		const content = renderActivity(renderLike(user, note, reaction));
-		deliver(user, content, note._user.inbox);
+		deliver(user, content, note.userInbox);
 	}
 	//#endregion
-
-	return;
 };
diff --git a/src/services/note/reaction/delete.ts b/src/services/note/reaction/delete.ts
index 695534db612794b0c057e5afc27b70e6a4543ad2..ce180aaecae409b8f40fe9ca3945d25022bae1ef 100644
--- a/src/services/note/reaction/delete.ts
+++ b/src/services/note/reaction/delete.ts
@@ -1,50 +1,47 @@
-import { IUser, isLocalUser, isRemoteUser } from '../../../models/user';
-import Note, { INote } from '../../../models/note';
-import NoteReaction from '../../../models/note-reaction';
 import { publishNoteStream } from '../../stream';
 import renderLike from '../../../remote/activitypub/renderer/like';
 import renderUndo from '../../../remote/activitypub/renderer/undo';
 import { renderActivity } from '../../../remote/activitypub/renderer';
 import { deliver } from '../../../queue';
 import { IdentifiableError } from '../../../misc/identifiable-error';
+import { User } from '../../../models/entities/user';
+import { Note } from '../../../models/entities/note';
+import { NoteReactions, Users, Notes } from '../../../models';
 
-export default async (user: IUser, note: INote) => {
+export default async (user: User, note: Note) => {
 	// if already unreacted
-	const exist = await NoteReaction.findOne({
-		noteId: note._id,
-		userId: user._id,
-		deletedAt: { $exists: false }
+	const exist = await NoteReactions.findOne({
+		noteId: note.id,
+		userId: user.id,
 	});
 
-	if (exist === null) {
+	if (exist == null) {
 		throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
 	}
 
 	// Delete reaction
-	await NoteReaction.remove({
-		_id: exist._id
-	});
-
-	const dec: any = {};
-	dec[`reactionCounts.${exist.reaction}`] = -1;
+	await NoteReactions.delete(exist.id);
 
 	// Decrement reactions count
-	Note.update({ _id: note._id }, {
-		$inc: dec
-	});
-
-	publishNoteStream(note._id, 'unreacted', {
+	const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
+	await Notes.createQueryBuilder().update()
+		.set({
+			reactions: () => sql,
+		})
+		.where('id = :id', { id: note.id })
+		.execute();
+	// v11 dec score
+
+	publishNoteStream(note.id, 'unreacted', {
 		reaction: exist.reaction,
-		userId: user._id
+		userId: user.id
 	});
 
 	//#region 配信
 	// リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送
-	if (isLocalUser(user) && isRemoteUser(note._user)) {
+	if (Users.isLocalUser(user) && (note.userHost !== null)) {
 		const content = renderActivity(renderUndo(renderLike(user, note, exist.reaction), user));
-		deliver(user, content, note._user.inbox);
+		deliver(user, content, note.userInbox);
 	}
 	//#endregion
-
-	return;
 };
diff --git a/src/services/note/read.ts b/src/services/note/read.ts
index 8b52445cf0b352d85375729692ce0c36fdece881..44d75bd850f8ef12299bd16f9feb43f66d865a67 100644
--- a/src/services/note/read.ts
+++ b/src/services/note/read.ts
@@ -1,59 +1,35 @@
-import * as mongo from 'mongodb';
-import isObjectId from '../../misc/is-objectid';
 import { publishMainStream } from '../stream';
-import User from '../../models/user';
-import NoteUnread from '../../models/note-unread';
+import { Note } from '../../models/entities/note';
+import { User } from '../../models/entities/user';
+import { NoteUnreads } from '../../models';
 
 /**
  * Mark a note as read
  */
 export default (
-	user: string | mongo.ObjectID,
-	note: string | mongo.ObjectID
+	userId: User['id'],
+	noteId: Note['id']
 ) => new Promise<any>(async (resolve, reject) => {
-
-	const userId: mongo.ObjectID = isObjectId(user)
-		? user as mongo.ObjectID
-		: new mongo.ObjectID(user);
-
-	const noteId: mongo.ObjectID = isObjectId(note)
-		? note as mongo.ObjectID
-		: new mongo.ObjectID(note);
-
 	// Remove document
-	const res = await NoteUnread.remove({
+	const res = await NoteUnreads.delete({
 		userId: userId,
 		noteId: noteId
 	});
 
-	if (res.deletedCount == 0) {
+	// v11 TODO: https://github.com/typeorm/typeorm/issues/2415
+	if (res.affected == 0) {
 		return;
 	}
 
-	const count1 = await NoteUnread
-		.count({
-			userId: userId,
-			isSpecified: false
-		}, {
-			limit: 1
-		});
-
-	const count2 = await NoteUnread
-		.count({
-			userId: userId,
-			isSpecified: true
-		}, {
-			limit: 1
-		});
+	const count1 = await NoteUnreads.count({
+		userId: userId,
+		isSpecified: false
+	});
 
-	if (count1 == 0 || count2 == 0) {
-		User.update({ _id: userId }, {
-			$set: {
-				hasUnreadMentions: count1 != 0 || count2 != 0,
-				hasUnreadSpecifiedNotes: count2 != 0
-			}
-		});
-	}
+	const count2 = await NoteUnreads.count({
+		userId: userId,
+		isSpecified: true
+	});
 
 	if (count1 == 0) {
 		// 全て既読になったイベントを発行
diff --git a/src/services/note/unread.ts b/src/services/note/unread.ts
index e70c63c76531e7e09d5a775c8afb25f81c067cda..203cff8d39045297c2e1246a38b257c1c2cc51c9 100644
--- a/src/services/note/unread.ts
+++ b/src/services/note/unread.ts
@@ -1,47 +1,34 @@
-import NoteUnread from '../../models/note-unread';
-import User, { IUser } from '../../models/user';
-import { INote } from '../../models/note';
-import Mute from '../../models/mute';
+import { Note } from '../../models/entities/note';
 import { publishMainStream } from '../stream';
+import { User } from '../../models/entities/user';
+import { Mutings, NoteUnreads } from '../../models';
+import { genId } from '../../misc/gen-id';
 
-export default async function(user: IUser, note: INote, isSpecified = false) {
+export default async function(user: User, note: Note, isSpecified = false) {
 	//#region ミュートしているなら無視
-	const mute = await Mute.find({
-		muterId: user._id
+	const mute = await Mutings.find({
+		muterId: user.id
 	});
-	const mutedUserIds = mute.map(m => m.muteeId.toString());
-	if (mutedUserIds.includes(note.userId.toString())) return;
+	if (mute.map(m => m.muteeId).includes(note.userId)) return;
 	//#endregion
 
-	const unread = await NoteUnread.insert({
-		noteId: note._id,
-		userId: user._id,
+	const unread = await NoteUnreads.save({
+		id: genId(),
+		noteId: note.id,
+		userId: user.id,
 		isSpecified,
-		_note: {
-			userId: note.userId
-		}
+		noteUserId: note.userId
 	});
 
 	// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
 	setTimeout(async () => {
-		const exist = await NoteUnread.findOne({ _id: unread._id });
+		const exist = await NoteUnreads.findOne(unread.id);
 		if (exist == null) return;
 
-		User.update({
-			_id: user._id
-		}, {
-			$set: isSpecified ? {
-				hasUnreadSpecifiedNotes: true,
-				hasUnreadMentions: true
-			} : {
-				hasUnreadMentions: true
-			}
-		});
-
-		publishMainStream(user._id, 'unreadMention', note._id);
+		publishMainStream(user.id, 'unreadMention', note.id);
 
 		if (isSpecified) {
-			publishMainStream(user._id, 'unreadSpecifiedNote', note._id);
+			publishMainStream(user.id, 'unreadSpecifiedNote', note.id);
 		}
 	}, 2000);
 }
diff --git a/src/services/note/unwatch.ts b/src/services/note/unwatch.ts
index ef5783231b05ee26d1410cfbb8d1a6a8f6549cd9..047ac343be367636c1766466eabab7d25cba8438 100644
--- a/src/services/note/unwatch.ts
+++ b/src/services/note/unwatch.ts
@@ -1,9 +1,10 @@
-import * as mongodb from 'mongodb';
-import Watching from '../../models/note-watching';
+import { User } from '../../models/entities/user';
+import { NoteWatchings } from '../../models';
+import { Note } from '../../models/entities/note';
 
-export default async (me: mongodb.ObjectID, note: object) => {
-	await Watching.remove({
-		noteId: (note as any)._id,
+export default async (me: User['id'], note: Note) => {
+	await NoteWatchings.delete({
+		noteId: note.id,
 		userId: me
 	});
 };
diff --git a/src/services/note/watch.ts b/src/services/note/watch.ts
index aad53610d83fc03979cd0b1d52c9fbb103d684ce..d3c955369646a4d0ead7049987536761676545a2 100644
--- a/src/services/note/watch.ts
+++ b/src/services/note/watch.ts
@@ -1,25 +1,20 @@
-import * as mongodb from 'mongodb';
-import Watching from '../../models/note-watching';
+import { User } from '../../models/entities/user';
+import { Note } from '../../models/entities/note';
+import { NoteWatchings } from '../../models';
+import { genId } from '../../misc/gen-id';
+import { NoteWatching } from '../../models/entities/note-watching';
 
-export default async (me: mongodb.ObjectID, note: object) => {
+export default async (me: User['id'], note: Note) => {
 	// 自分の投稿はwatchできない
-	if (me.equals((note as any).userId)) {
+	if (me === note.userId) {
 		return;
 	}
 
-	// if watching now
-	const exist = await Watching.findOne({
-		noteId: (note as any)._id,
-		userId: me
-	});
-
-	if (exist !== null) {
-		return;
-	}
-
-	await Watching.insert({
+	await NoteWatchings.save({
+		id: genId(),
 		createdAt: new Date(),
-		noteId: (note as any)._id,
-		userId: me
-	});
+		noteId: note.id,
+		userId: me,
+		noteUserId: note.userId
+	} as NoteWatching);
 };
diff --git a/src/services/push-notification.ts b/src/services/push-notification.ts
index ceb762b2fac1da6c8063e91fce7c590dd982b4c6..defd4d6e2de42f827cecb6d360b15bc3067e3896 100644
--- a/src/services/push-notification.ts
+++ b/src/services/push-notification.ts
@@ -1,11 +1,10 @@
 import * as push from 'web-push';
-import * as mongo from 'mongodb';
-import Subscription from '../models/sw-subscription';
 import config from '../config';
+import { SwSubscriptions } from '../models';
+import { Meta } from '../models/entities/meta';
 import fetchMeta from '../misc/fetch-meta';
-import { IMeta } from '../models/meta';
 
-let meta: IMeta = null;
+let meta: Meta = null;
 
 setInterval(() => {
 	fetchMeta().then(m => {
@@ -20,15 +19,11 @@ setInterval(() => {
 	});
 }, 3000);
 
-export default async function(userId: mongo.ObjectID | string, type: string, body?: any) {
+export default async function(userId: string, type: string, body?: any) {
 	if (!meta.enableServiceWorker) return;
 
-	if (typeof userId === 'string') {
-		userId = new mongo.ObjectID(userId);
-	}
-
 	// Fetch
-	const subscriptions = await Subscription.find({
+	const subscriptions = await SwSubscriptions.find({
 		userId: userId
 	});
 
@@ -49,7 +44,7 @@ export default async function(userId: mongo.ObjectID | string, type: string, bod
 			//swLogger.info(err.body);
 
 			if (err.statusCode == 410) {
-				Subscription.remove({
+				SwSubscriptions.delete({
 					userId: userId,
 					endpoint: subscription.endpoint,
 					auth: subscription.auth,
diff --git a/src/services/register-or-fetch-instance-doc.ts b/src/services/register-or-fetch-instance-doc.ts
index d418cd12cecfdc6767115f255411d6dff42de8aa..c96c8a1e325b126c14246f0f924f72a199900062 100644
--- a/src/services/register-or-fetch-instance-doc.ts
+++ b/src/services/register-or-fetch-instance-doc.ts
@@ -1,15 +1,19 @@
-import Instance, { IInstance } from '../models/instance';
-import federationChart from '../services/chart/federation';
+import { Instance } from '../models/entities/instance';
+import { Instances } from '../models';
+import { federationChart } from './chart';
+import { genId } from '../misc/gen-id';
 
-export async function registerOrFetchInstanceDoc(host: string): Promise<IInstance> {
+export async function registerOrFetchInstanceDoc(host: string): Promise<Instance> {
 	if (host == null) return null;
 
-	const index = await Instance.findOne({ host });
+	const index = await Instances.findOne({ host });
 
 	if (index == null) {
-		const i = await Instance.insert({
+		const i = await Instances.save({
+			id: genId(),
 			host,
 			caughtAt: new Date(),
+			lastCommunicatedAt: new Date(),
 			system: null // TODO
 		});
 
diff --git a/src/services/stream.ts b/src/services/stream.ts
index 813c9eb7c08935a91e0a52ddc86dcc811eb2e441..c1d14b27794c1d78358f635e1317236bb4c20871 100644
--- a/src/services/stream.ts
+++ b/src/services/stream.ts
@@ -1,8 +1,9 @@
-import * as mongo from 'mongodb';
 import redis from '../db/redis';
 import Xev from 'xev';
-
-type ID = string | mongo.ObjectID;
+import { User } from '../models/entities/user';
+import { Note } from '../models/entities/note';
+import { UserList } from '../models/entities/user-list';
+import { ReversiGame } from '../models/entities/games/reversi/game';
 
 class Publisher {
 	private ev: Xev;
@@ -29,66 +30,50 @@ class Publisher {
 		}
 	}
 
-	public publishMainStream = (userId: ID, type: string, value?: any): void => {
+	public publishMainStream = (userId: User['id'], type: string, value?: any): void => {
 		this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishDriveStream = (userId: ID, type: string, value?: any): void => {
+	public publishDriveStream = (userId: User['id'], type: string, value?: any): void => {
 		this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishNoteStream = (noteId: ID, type: string, value: any): void => {
+	public publishNoteStream = (noteId: Note['id'], type: string, value: any): void => {
 		this.publish(`noteStream:${noteId}`, type, {
 			id: noteId,
 			body: value
 		});
 	}
 
-	public publishUserListStream = (listId: ID, type: string, value?: any): void => {
+	public publishUserListStream = (listId: UserList['id'], type: string, value?: any): void => {
 		this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishMessagingStream = (userId: ID, otherpartyId: ID, type: string, value?: any): void => {
+	public publishMessagingStream = (userId: User['id'], otherpartyId: User['id'], type: string, value?: any): void => {
 		this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishMessagingIndexStream = (userId: ID, type: string, value?: any): void => {
+	public publishMessagingIndexStream = (userId: User['id'], type: string, value?: any): void => {
 		this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishReversiStream = (userId: ID, type: string, value?: any): void => {
+	public publishReversiStream = (userId: User['id'], type: string, value?: any): void => {
 		this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishReversiGameStream = (gameId: ID, type: string, value?: any): void => {
+	public publishReversiGameStream = (gameId: ReversiGame['id'], type: string, value?: any): void => {
 		this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishHomeTimelineStream = (userId: ID, note: any): void => {
-		this.publish(`homeTimeline:${userId}`, null, note);
-	}
-
-	public publishLocalTimelineStream = async (note: any): Promise<void> => {
-		this.publish('localTimeline', null, note);
-	}
-
-	public publishHybridTimelineStream = async (userId: ID, note: any): Promise<void> => {
-		this.publish(userId ? `hybridTimeline:${userId}` : 'hybridTimeline', null, note);
-	}
-
-	public publishGlobalTimelineStream = (note: any): void => {
-		this.publish('globalTimeline', null, note);
-	}
-
-	public publishHashtagStream = (note: any): void => {
-		this.publish('hashtag', null, note);
+	public publishNotesStream = (note: any): void => {
+		this.publish('notesStream', null, note);
 	}
 
 	public publishApLogStream = (log: any): void => {
 		this.publish('apLog', null, log);
 	}
 
-	public publishAdminStream = (userId: ID, type: string, value?: any): void => {
+	public publishAdminStream = (userId: User['id'], type: string, value?: any): void => {
 		this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 }
@@ -100,15 +85,11 @@ export default publisher;
 export const publishMainStream = publisher.publishMainStream;
 export const publishDriveStream = publisher.publishDriveStream;
 export const publishNoteStream = publisher.publishNoteStream;
+export const publishNotesStream = publisher.publishNotesStream;
 export const publishUserListStream = publisher.publishUserListStream;
 export const publishMessagingStream = publisher.publishMessagingStream;
 export const publishMessagingIndexStream = publisher.publishMessagingIndexStream;
 export const publishReversiStream = publisher.publishReversiStream;
 export const publishReversiGameStream = publisher.publishReversiGameStream;
-export const publishHomeTimelineStream = publisher.publishHomeTimelineStream;
-export const publishLocalTimelineStream = publisher.publishLocalTimelineStream;
-export const publishHybridTimelineStream = publisher.publishHybridTimelineStream;
-export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream;
-export const publishHashtagStream = publisher.publishHashtagStream;
 export const publishApLogStream = publisher.publishApLogStream;
 export const publishAdminStream = publisher.publishAdminStream;
diff --git a/src/services/update-hashtag.ts b/src/services/update-hashtag.ts
index 23c39312c051c7d9e30d69e5f4126a726e511a42..6f6d5c4691ee824298e825f025807c304be7e393 100644
--- a/src/services/update-hashtag.ts
+++ b/src/services/update-hashtag.ts
@@ -1,103 +1,104 @@
-import { IUser, isLocalUser, isRemoteUser } from '../models/user';
-import Hashtag from '../models/hashtag';
-import hashtagChart from './chart/hashtag';
+import { User } from '../models/entities/user';
+import { Hashtags, Users } from '../models';
+import { hashtagChart } from './chart';
+import { genId } from '../misc/gen-id';
+import { Hashtag } from '../models/entities/hashtag';
 
-export async function updateHashtag(user: IUser, tag: string, isUserAttached = false, inc = true) {
+export async function updateHashtag(user: User, tag: string, isUserAttached = false, inc = true) {
 	tag = tag.toLowerCase();
 
-	const index = await Hashtag.findOne({ tag });
+	const index = await Hashtags.findOne({ name: tag });
 
 	if (index == null && !inc) return;
 
 	if (index != null) {
-		const $push = {} as any;
-		const $pull = {} as any;
-		const $inc = {} as any;
+		const q = Hashtags.createQueryBuilder('tag').update()
+			.where('tag.name = :name', { name: tag });
+
+		const set = {} as any;
 
 		if (isUserAttached) {
 			if (inc) {
 				// 自分が初めてこのタグを使ったなら
-				if (!index.attachedUserIds.some(id => id.equals(user._id))) {
-					$push.attachedUserIds = user._id;
-					$inc.attachedUsersCount = 1;
+				if (!index.attachedUserIds.some(id => id === user.id)) {
+					set.attachedUserIds = () => `array_append(tag.attachedUserIds, '${user.id}')`;
+					set.attachedUsersCount = () => `tag.attachedUsersCount + 1`;
 				}
 				// 自分が(ローカル内で)初めてこのタグを使ったなら
-				if (isLocalUser(user) && !index.attachedLocalUserIds.some(id => id.equals(user._id))) {
-					$push.attachedLocalUserIds = user._id;
-					$inc.attachedLocalUsersCount = 1;
+				if (Users.isLocalUser(user) && !index.attachedLocalUserIds.some(id => id === user.id)) {
+					set.attachedLocalUserIds = () => `array_append(tag.attachedLocalUserIds, '${user.id}')`;
+					set.attachedLocalUsersCount = () => `tag.attachedLocalUsersCount + 1`;
 				}
 				// 自分が(リモートで)初めてこのタグを使ったなら
-				if (isRemoteUser(user) && !index.attachedRemoteUserIds.some(id => id.equals(user._id))) {
-					$push.attachedRemoteUserIds = user._id;
-					$inc.attachedRemoteUsersCount = 1;
+				if (Users.isRemoteUser(user) && !index.attachedRemoteUserIds.some(id => id === user.id)) {
+					set.attachedRemoteUserIds = () => `array_append(tag.attachedRemoteUserIds, '${user.id}')`;
+					set.attachedRemoteUsersCount = () => `tag.attachedRemoteUsersCount + 1`;
 				}
 			} else {
-				$pull.attachedUserIds = user._id;
-				$inc.attachedUsersCount = -1;
-				if (isLocalUser(user)) {
-					$pull.attachedLocalUserIds = user._id;
-					$inc.attachedLocalUsersCount = -1;
+				set.attachedUserIds = () => `array_remove(tag.attachedUserIds, '${user.id}')`;
+				set.attachedUsersCount = () => `tag.attachedUsersCount - 1`;
+				if (Users.isLocalUser(user)) {
+					set.attachedLocalUserIds = () => `array_remove(tag.attachedLocalUserIds, '${user.id}')`;
+					set.attachedLocalUsersCount = () => `tag.attachedLocalUsersCount - 1`;
 				} else {
-					$pull.attachedRemoteUserIds = user._id;
-					$inc.attachedRemoteUsersCount = -1;
+					set.attachedRemoteUserIds = () => `array_remove(tag.attachedRemoteUserIds, '${user.id}')`;
+					set.attachedRemoteUsersCount = () => `tag.attachedRemoteUsersCount - 1`;
 				}
 			}
 		} else {
 			// 自分が初めてこのタグを使ったなら
-			if (!index.mentionedUserIds.some(id => id.equals(user._id))) {
-				$push.mentionedUserIds = user._id;
-				$inc.mentionedUsersCount = 1;
+			if (!index.mentionedUserIds.some(id => id === user.id)) {
+				set.mentionedUserIds = () => `array_append(tag.mentionedUserIds, '${user.id}')`;
+				set.mentionedUsersCount = () => `tag.mentionedUsersCount + 1`;
 			}
 			// 自分が(ローカル内で)初めてこのタグを使ったなら
-			if (isLocalUser(user) && !index.mentionedLocalUserIds.some(id => id.equals(user._id))) {
-				$push.mentionedLocalUserIds = user._id;
-				$inc.mentionedLocalUsersCount = 1;
+			if (Users.isLocalUser(user) && !index.mentionedLocalUserIds.some(id => id === user.id)) {
+				set.mentionedLocalUserIds = () => `array_append(tag.mentionedLocalUserIds, '${user.id}')`;
+				set.mentionedLocalUsersCount = () => `tag.mentionedLocalUsersCount + 1`;
 			}
 			// 自分が(リモートで)初めてこのタグを使ったなら
-			if (isRemoteUser(user) && !index.mentionedRemoteUserIds.some(id => id.equals(user._id))) {
-				$push.mentionedRemoteUserIds = user._id;
-				$inc.mentionedRemoteUsersCount = 1;
+			if (Users.isRemoteUser(user) && !index.mentionedRemoteUserIds.some(id => id === user.id)) {
+				set.mentionedRemoteUserIds = () => `array_append(tag.mentionedRemoteUserIds, '${user.id}')`;
+				set.mentionedRemoteUsersCount = () => `tag.mentionedRemoteUsersCount + 1`;
 			}
 		}
 
-		const q = {} as any;
-		if (Object.keys($push).length > 0) q.$push = $push;
-		if (Object.keys($pull).length > 0) q.$pull = $pull;
-		if (Object.keys($inc).length > 0) q.$inc = $inc;
-		if (Object.keys(q).length > 0) Hashtag.update({ tag }, q);
+		q.execute();
 	} else {
 		if (isUserAttached) {
-			Hashtag.insert({
-				tag,
+			Hashtags.save({
+				id: genId(),
+				name: tag,
 				mentionedUserIds: [],
 				mentionedUsersCount: 0,
 				mentionedLocalUserIds: [],
 				mentionedLocalUsersCount: 0,
 				mentionedRemoteUserIds: [],
 				mentionedRemoteUsersCount: 0,
-				attachedUserIds: [user._id],
+				attachedUserIds: [user.id],
 				attachedUsersCount: 1,
-				attachedLocalUserIds: isLocalUser(user) ? [user._id] : [],
-				attachedLocalUsersCount: isLocalUser(user) ? 1 : 0,
-				attachedRemoteUserIds: isRemoteUser(user) ? [user._id] : [],
-				attachedRemoteUsersCount: isRemoteUser(user) ? 1 : 0,
-			});
+				attachedLocalUserIds: Users.isLocalUser(user) ? [user.id] : [],
+				attachedLocalUsersCount: Users.isLocalUser(user) ? 1 : 0,
+				attachedRemoteUserIds: Users.isRemoteUser(user) ? [user.id] : [],
+				attachedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0,
+			} as Hashtag);
 		} else {
-			Hashtag.insert({
-				tag,
-				mentionedUserIds: [user._id],
+			Hashtags.save({
+				id: genId(),
+				name: tag,
+				mentionedUserIds: [user.id],
 				mentionedUsersCount: 1,
-				mentionedLocalUserIds: isLocalUser(user) ? [user._id] : [],
-				mentionedLocalUsersCount: isLocalUser(user) ? 1 : 0,
-				mentionedRemoteUserIds: isRemoteUser(user) ? [user._id] : [],
-				mentionedRemoteUsersCount: isRemoteUser(user) ? 1 : 0,
+				mentionedLocalUserIds: Users.isLocalUser(user) ? [user.id] : [],
+				mentionedLocalUsersCount: Users.isLocalUser(user) ? 1 : 0,
+				mentionedRemoteUserIds: Users.isRemoteUser(user) ? [user.id] : [],
+				mentionedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0,
 				attachedUserIds: [],
 				attachedUsersCount: 0,
 				attachedLocalUserIds: [],
 				attachedLocalUsersCount: 0,
 				attachedRemoteUserIds: [],
 				attachedRemoteUsersCount: 0,
-			});
+			} as Hashtag);
 		}
 	}
 
diff --git a/src/services/user-list/push.ts b/src/services/user-list/push.ts
index 5ad4a148275928b32a27ac764c74dda95d2a4c05..958d54b0907331560b8c320f5a5fce768c9d55b1 100644
--- a/src/services/user-list/push.ts
+++ b/src/services/user-list/push.ts
@@ -1,21 +1,26 @@
-import { pack as packUser, IUser, isRemoteUser, fetchProxyAccount } from '../../models/user';
-import UserList, { IUserList } from '../../models/user-list';
 import { renderActivity } from '../../remote/activitypub/renderer';
 import { deliver } from '../../queue';
 import renderFollow from '../../remote/activitypub/renderer/follow';
 import { publishUserListStream } from '../stream';
+import { User } from '../../models/entities/user';
+import { UserList } from '../../models/entities/user-list';
+import { UserListJoinings, Users } from '../../models';
+import { UserListJoining } from '../../models/entities/user-list-joining';
+import { genId } from '../../misc/gen-id';
+import { fetchProxyAccount } from '../../misc/fetch-proxy-account';
 
-export async function pushUserToUserList(target: IUser, list: IUserList) {
-	await UserList.update({ _id: list._id }, {
-		$push: {
-			userIds: target._id
-		}
-	});
+export async function pushUserToUserList(target: User, list: UserList) {
+	await UserListJoinings.save({
+		id: genId(),
+		createdAt: new Date(),
+		userId: target.id,
+		userListId: list.id
+	} as UserListJoining);
 
-	publishUserListStream(list._id, 'userAdded', await packUser(target));
+	publishUserListStream(list.id, 'userAdded', await Users.pack(target));
 
 	// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
-	if (isRemoteUser(target)) {
+	if (Users.isRemoteUser(target)) {
 		const proxy = await fetchProxyAccount();
 		const content = renderActivity(renderFollow(proxy, target));
 		deliver(proxy, content, target.inbox);
diff --git a/src/tools/add-emoji.ts b/src/tools/add-emoji.ts
index 2aa99e37aec1fd7ff04f0bf9e6edefcbaa14e83f..a75798bdadbc03f2683fcbe37abc010e81ef40c3 100644
--- a/src/tools/add-emoji.ts
+++ b/src/tools/add-emoji.ts
@@ -1,9 +1,11 @@
-import Emoji from '../models/emoji';
+import { Emojis } from '../models';
+import { genId } from '../misc/gen-id';
 
 async function main(name: string, url: string, alias?: string): Promise<any> {
 	const aliases = alias != null ? [ alias ] : [];
 
-	await Emoji.insert({
+	await Emojis.save({
+		id: genId(),
 		host: null,
 		name,
 		url,
diff --git a/src/tools/clean-remote-files.ts b/src/tools/clean-remote-files.ts
index 28c76345c766c26186065dec0a76194654e9c7e1..f64affea978b3d57e89d3cf6c0e47a8e946f9e8b 100644
--- a/src/tools/clean-remote-files.ts
+++ b/src/tools/clean-remote-files.ts
@@ -1,18 +1,13 @@
 import * as promiseLimit from 'promise-limit';
-import DriveFile, { IDriveFile } from '../models/drive-file';
 import del from '../services/drive/delete-file';
+import { DriveFiles } from '../models';
+import { Not } from 'typeorm';
+import { DriveFile } from '../models/entities/drive-file';
 
 const limit = promiseLimit(16);
 
-DriveFile.find({
-	'metadata._user.host': {
-		$ne: null
-	},
-	'metadata.deletedAt': { $exists: false }
-}, {
-	fields: {
-		_id: true
-	}
+DriveFiles.find({
+	userHost: Not(null)
 }).then(async files => {
 	console.log(`there is ${files.length} files`);
 
@@ -21,10 +16,10 @@ DriveFile.find({
 	console.log('ALL DONE');
 });
 
-async function job(file: IDriveFile): Promise<any> {
-	file = await DriveFile.findOne({ _id: file._id });
+async function job(file: DriveFile): Promise<any> {
+	file = await DriveFiles.findOne(file.id);
 
 	await del(file, true);
 
-	console.log('done', file._id);
+	console.log('done', file.id);
 }
diff --git a/src/tools/move-drive-files.ts b/src/tools/move-drive-files.ts
deleted file mode 100644
index 8a1e944503e6ec8bb852524ea3c4ab7e62a4acf1..0000000000000000000000000000000000000000
--- a/src/tools/move-drive-files.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import * as Minio from 'minio';
-import * as uuid from 'uuid';
-import * as promiseLimit from 'promise-limit';
-import DriveFile, { DriveFileChunk, getDriveFileBucket, IDriveFile } from '../models/drive-file';
-import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../models/drive-file-thumbnail';
-import config from '../config';
-
-const limit = promiseLimit(16);
-
-DriveFile.find({
-	$or: [{
-		'metadata.withoutChunks': { $exists: false }
-	}, {
-		'metadata.withoutChunks': false
-	}],
-	'metadata.deletedAt': { $exists: false }
-}, {
-	fields: {
-		_id: true
-	}
-}).then(async files => {
-	console.log(`there is ${files.length} files`);
-
-	await Promise.all(files.map(file => limit(() => job(file))));
-
-	console.log('ALL DONE');
-});
-
-async function job(file: IDriveFile): Promise<any> {
-	file = await DriveFile.findOne({ _id: file._id });
-
-	const minio = new Minio.Client(config.drive.config);
-
-	const name = file.filename.substr(0, 50);
-	const keyDir = `${config.drive.prefix}/${uuid.v4()}`;
-	const key = `${keyDir}/${name}`;
-	const thumbnailKeyDir = `${config.drive.prefix}/${uuid.v4()}`;
-	const thumbnailKey = `${thumbnailKeyDir}/${name}.thumbnail.jpg`;
-
-	const baseUrl = config.drive.baseUrl
-		|| `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`;
-
-	const bucket = await getDriveFileBucket();
-	const readable = bucket.openDownloadStream(file._id);
-
-	await minio.putObject(config.drive.bucket, key, readable, file.length, {
-		'Content-Type': file.contentType,
-		'Cache-Control': 'max-age=31536000, immutable'
-	});
-
-	await DriveFile.findOneAndUpdate({ _id: file._id }, {
-		$set: {
-			'metadata.withoutChunks': true,
-			'metadata.storage': 'minio',
-			'metadata.storageProps': {
-				key: key,
-				thumbnailKey: thumbnailKey
-			},
-			'metadata.url': `${ baseUrl }/${ keyDir }/${ encodeURIComponent(name) }`,
-		}
-	});
-
-	// チャンクをすべて削除
-	await DriveFileChunk.remove({
-		files_id: file._id
-	});
-
-	//#region サムネイルもあれば削除
-	const thumbnail = await DriveFileThumbnail.findOne({
-		'metadata.originalId': file._id
-	});
-
-	if (thumbnail) {
-		await DriveFileThumbnailChunk.remove({
-			files_id: thumbnail._id
-		});
-
-		await DriveFileThumbnail.remove({ _id: thumbnail._id });
-	}
-	//#endregion
-
-	console.log('done', file._id);
-}
diff --git a/src/tools/show-signin-history.ts b/src/tools/show-signin-history.ts
index e770710322edb79cffeb8b8552d57e0fed63039c..584bece6bba87f3d57e0de11c29aafcbe4f4388e 100644
--- a/src/tools/show-signin-history.ts
+++ b/src/tools/show-signin-history.ts
@@ -1,3 +1,5 @@
+import { Users, Signins } from '../models';
+
 // node built/tools/show-signin-history username
 //  => {Success} {Date} {IPAddrsss}
 
@@ -7,19 +9,16 @@
 // node built/tools/show-signin-history username all
 //  with full request headers
 
-import User from '../models/user';
-import Signin from '../models/signin';
-
 async function main(username: string, headers: string[]) {
-	const user = await User.findOne({
+	const user = await Users.findOne({
 		host: null,
 		usernameLower: username.toLowerCase(),
 	});
 
-	if (user === null) throw 'User not found';
+	if (user == null) throw 'User not found';
 
-	const history = await Signin.find({
-		userId: user._id
+	const history = await Signins.find({
+		userId: user.id
 	});
 
 	for (const signin of history) {
diff --git a/test/api-visibility.ts b/test/api-visibility.ts
index 8380d54f1d8a7477430dc93e1159fba75cacfad5..894d0d07530f3ae9956dd8a44844832b3ba43f54 100644
--- a/test/api-visibility.ts
+++ b/test/api-visibility.ts
@@ -6,40 +6,33 @@
  *
  * To specify test:
  * > mocha test/api-visibility.ts --require ts-node/register -g 'test name'
+ *
+ * If the tests not start, try set following enviroment variables:
+ * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
+ * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
  */
-import * as http from 'http';
-import * as assert from 'chai';
-import { async, _signup, _request, _uploadFile, _post, _react, resetDb } from './utils';
-
-const expect = assert.expect;
-
-//#region process
-Error.stackTraceLimit = Infinity;
 
-// During the test the env variable is set to test
 process.env.NODE_ENV = 'test';
 
-// Display detail of unhandled promise rejection
-process.on('unhandledRejection', console.dir);
-//#endregion
-
-const app = require('../built/server/api').default;
-const db = require('../built/db/mongodb').default;
-
-const server = http.createServer(app.callback());
-
-//#region Utilities
-const request = _request(server);
-const signup = _signup(request);
-const post = _post(request);
-//#endregion
+import * as assert from 'assert';
+import * as childProcess from 'child_process';
+import { async, signup, request, post } from './utils';
 
 describe('API visibility', () => {
-	// Reset database each test
-	before(resetDb(db));
+	let p: childProcess.ChildProcess;
+
+	before(done => {
+		p = childProcess.spawn('node', [__dirname + '/../index.js'], {
+			stdio: ['inherit', 'inherit', 'ipc'],
+			env: { NODE_ENV: 'test' }
+		});
+		p.on('message', message => {
+			if (message === 'ok') done();
+		});
+	});
 
 	after(() => {
-		server.close();
+		p.kill();
 	});
 
 	describe('Note visibility', async () => {
@@ -61,8 +54,6 @@ describe('API visibility', () => {
 		let fol: any;
 		/** specified-post */
 		let spe: any;
-		/** private-post */
-		let pri: any;
 
 		/** public-reply to target's post */
 		let pubR: any;
@@ -72,8 +63,6 @@ describe('API visibility', () => {
 		let folR: any;
 		/** specified-reply to target's post */
 		let speR: any;
-		/** private-reply to target's post */
-		let priR: any;
 
 		/** public-mention to target */
 		let pubM: any;
@@ -83,8 +72,6 @@ describe('API visibility', () => {
 		let folM: any;
 		/** specified-mention to target */
 		let speM: any;
-		/** private-mention to target */
-		let priM: any;
 
 		/** reply target post */
 		let tgt: any;
@@ -112,7 +99,6 @@ describe('API visibility', () => {
 			home = await post(alice, { text: 'x', visibility: 'home' });
 			fol  = await post(alice, { text: 'x', visibility: 'followers' });
 			spe  = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] });
-			pri  = await post(alice, { text: 'x', visibility: 'private' });
 
 			// replies
 			tgt = await post(target, { text: 'y', visibility: 'public' });
@@ -120,14 +106,12 @@ describe('API visibility', () => {
 			homeR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'home' });
 			folR  = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' });
 			speR  = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' });
-			priR  = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'private' });
 
 			// mentions
 			pubM  = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' });
 			homeM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'home' });
 			folM  = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' });
 			speM  = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'specified' });
-			priM  = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'private' });
 			//#endregion
 		});
 
@@ -135,111 +119,90 @@ describe('API visibility', () => {
 		// public
 		it('[show] public-postを自分が見れる', async(async () => {
 			const res = await show(pub.id, alice);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] public-postをフォロワーが見れる', async(async () => {
 			const res = await show(pub.id, follower);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] public-postを非フォロワーが見れる', async(async () => {
 			const res = await show(pub.id, other);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] public-postを未認証が見れる', async(async () => {
 			const res = await show(pub.id, null);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		// home
 		it('[show] home-postを自分が見れる', async(async () => {
 			const res = await show(home.id, alice);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] home-postをフォロワーが見れる', async(async () => {
 			const res = await show(home.id, follower);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] home-postを非フォロワーが見れる', async(async () => {
 			const res = await show(home.id, other);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] home-postを未認証が見れる', async(async () => {
 			const res = await show(home.id, null);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		// followers
 		it('[show] followers-postを自分が見れる', async(async () => {
 			const res = await show(fol.id, alice);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] followers-postをフォロワーが見れる', async(async () => {
 			const res = await show(fol.id, follower);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] followers-postを非フォロワーが見れない', async(async () => {
 			const res = await show(fol.id, other);
-			expect(res.body).have.property('isHidden').eql(true);
+			assert.strictEqual(res.body.isHidden, true);
 		}));
 
 		it('[show] followers-postを未認証が見れない', async(async () => {
 			const res = await show(fol.id, null);
-			expect(res.body).have.property('isHidden').eql(true);
+			assert.strictEqual(res.body.isHidden, true);
 		}));
 
 		// specified
 		it('[show] specified-postを自分が見れる', async(async () => {
 			const res = await show(spe.id, alice);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] specified-postを指定ユーザーが見れる', async(async () => {
 			const res = await show(spe.id, target);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] specified-postをフォロワーが見れない', async(async () => {
 			const res = await show(spe.id, follower);
-			expect(res.body).have.property('isHidden').eql(true);
+			assert.strictEqual(res.body.isHidden, true);
 		}));
 
 		it('[show] specified-postを非フォロワーが見れない', async(async () => {
 			const res = await show(spe.id, other);
-			expect(res.body).have.property('isHidden').eql(true);
+			assert.strictEqual(res.body.isHidden, true);
 		}));
 
 		it('[show] specified-postを未認証が見れない', async(async () => {
 			const res = await show(spe.id, null);
-			expect(res.body).have.property('isHidden').eql(true);
-		}));
-
-		// private
-		it('[show] private-postを自分が見れる', async(async () => {
-			const res = await show(pri.id, alice);
-			expect(res.body).have.property('text').eql('x');
-		}));
-
-		it('[show] private-postをフォロワーが見れない', async(async () => {
-			const res = await show(pri.id, follower);
-			expect(res.body).have.property('isHidden').eql(true);
-		}));
-
-		it('[show] private-postを非フォロワーが見れない', async(async () => {
-			const res = await show(pri.id, other);
-			expect(res.body).have.property('isHidden').eql(true);
-		}));
-
-		it('[show] private-postを未認証が見れない', async(async () => {
-			const res = await show(pri.id, null);
-			expect(res.body).have.property('isHidden').eql(true);
+			assert.strictEqual(res.body.isHidden, true);
 		}));
 		//#endregion
 
@@ -247,131 +210,110 @@ describe('API visibility', () => {
 		// public
 		it('[show] public-replyを自分が見れる', async(async () => {
 			const res = await show(pubR.id, alice);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] public-replyをされた人が見れる', async(async () => {
 			const res = await show(pubR.id, target);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] public-replyをフォロワーが見れる', async(async () => {
 			const res = await show(pubR.id, follower);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] public-replyを非フォロワーが見れる', async(async () => {
 			const res = await show(pubR.id, other);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] public-replyを未認証が見れる', async(async () => {
 			const res = await show(pubR.id, null);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		// home
 		it('[show] home-replyを自分が見れる', async(async () => {
 			const res = await show(homeR.id, alice);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] home-replyをされた人が見れる', async(async () => {
 			const res = await show(homeR.id, target);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] home-replyをフォロワーが見れる', async(async () => {
 			const res = await show(homeR.id, follower);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] home-replyを非フォロワーが見れる', async(async () => {
 			const res = await show(homeR.id, other);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] home-replyを未認証が見れる', async(async () => {
 			const res = await show(homeR.id, null);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		// followers
 		it('[show] followers-replyを自分が見れる', async(async () => {
 			const res = await show(folR.id, alice);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] followers-replyを非フォロワーでもリプライされていれば見れる', async(async () => {
 			const res = await show(folR.id, target);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] followers-replyをフォロワーが見れる', async(async () => {
 			const res = await show(folR.id, follower);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] followers-replyを非フォロワーが見れない', async(async () => {
 			const res = await show(folR.id, other);
-			expect(res.body).have.property('isHidden').eql(true);
+			assert.strictEqual(res.body.isHidden, true);
 		}));
 
 		it('[show] followers-replyを未認証が見れない', async(async () => {
 			const res = await show(folR.id, null);
-			expect(res.body).have.property('isHidden').eql(true);
+			assert.strictEqual(res.body.isHidden, true);
 		}));
 
 		// specified
 		it('[show] specified-replyを自分が見れる', async(async () => {
 			const res = await show(speR.id, alice);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] specified-replyを指定ユーザーが見れる', async(async () => {
 			const res = await show(speR.id, target);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] specified-replyをされた人が指定されてなくても見れる', async(async () => {
 			const res = await show(speR.id, target);
-			expect(res.body).have.property('text').eql('x');
+			assert.strictEqual(res.body.text, 'x');
 		}));
 
 		it('[show] specified-replyをフォロワーが見れない', async(async () => {
 			const res = await show(speR.id, follower);
-			expect(res.body).have.property('isHidden').eql(true);
+			assert.strictEqual(res.body.isHidden, true);
 		}));
 
 		it('[show] specified-replyを非フォロワーが見れない', async(async () => {
 			const res = await show(speR.id, other);
-			expect(res.body).have.property('isHidden').eql(true);
+			assert.strictEqual(res.body.isHidden, true);
 		}));
 
 		it('[show] specified-replyを未認証が見れない', async(async () => {
 			const res = await show(speR.id, null);
-			expect(res.body).have.property('isHidden').eql(true);
-		}));
-
-		// private
-		it('[show] private-replyを自分が見れる', async(async () => {
-			const res = await show(priR.id, alice);
-			expect(res.body).have.property('text').eql('x');
-		}));
-
-		it('[show] private-replyをフォロワーが見れない', async(async () => {
-			const res = await show(priR.id, follower);
-			expect(res.body).have.property('isHidden').eql(true);
-		}));
-
-		it('[show] private-replyを非フォロワーが見れない', async(async () => {
-			const res = await show(priR.id, other);
-			expect(res.body).have.property('isHidden').eql(true);
-		}));
-
-		it('[show] private-replyを未認証が見れない', async(async () => {
-			const res = await show(priR.id, null);
-			expect(res.body).have.property('isHidden').eql(true);
+			assert.strictEqual(res.body.isHidden, true);
 		}));
 		//#endregion
 
@@ -379,193 +321,172 @@ describe('API visibility', () => {
 		// public
 		it('[show] public-mentionを自分が見れる', async(async () => {
 			const res = await show(pubM.id, alice);
-			expect(res.body).have.property('text').eql('@target x');
+			assert.strictEqual(res.body.text, '@target x');
 		}));
 
 		it('[show] public-mentionをされた人が見れる', async(async () => {
 			const res = await show(pubM.id, target);
-			expect(res.body).have.property('text').eql('@target x');
+			assert.strictEqual(res.body.text, '@target x');
 		}));
 
 		it('[show] public-mentionをフォロワーが見れる', async(async () => {
 			const res = await show(pubM.id, follower);
-			expect(res.body).have.property('text').eql('@target x');
+			assert.strictEqual(res.body.text, '@target x');
 		}));
 
 		it('[show] public-mentionを非フォロワーが見れる', async(async () => {
 			const res = await show(pubM.id, other);
-			expect(res.body).have.property('text').eql('@target x');
+			assert.strictEqual(res.body.text, '@target x');
 		}));
 
 		it('[show] public-mentionを未認証が見れる', async(async () => {
 			const res = await show(pubM.id, null);
-			expect(res.body).have.property('text').eql('@target x');
+			assert.strictEqual(res.body.text, '@target x');
 		}));
 
 		// home
 		it('[show] home-mentionを自分が見れる', async(async () => {
 			const res = await show(homeM.id, alice);
-			expect(res.body).have.property('text').eql('@target x');
+			assert.strictEqual(res.body.text, '@target x');
 		}));
 
 		it('[show] home-mentionをされた人が見れる', async(async () => {
 			const res = await show(homeM.id, target);
-			expect(res.body).have.property('text').eql('@target x');
+			assert.strictEqual(res.body.text, '@target x');
 		}));
 
 		it('[show] home-mentionをフォロワーが見れる', async(async () => {
 			const res = await show(homeM.id, follower);
-			expect(res.body).have.property('text').eql('@target x');
+			assert.strictEqual(res.body.text, '@target x');
 		}));
 
 		it('[show] home-mentionを非フォロワーが見れる', async(async () => {
 			const res = await show(homeM.id, other);
-			expect(res.body).have.property('text').eql('@target x');
+			assert.strictEqual(res.body.text, '@target x');
 		}));
 
 		it('[show] home-mentionを未認証が見れる', async(async () => {
 			const res = await show(homeM.id, null);
-			expect(res.body).have.property('text').eql('@target x');
+			assert.strictEqual(res.body.text, '@target x');
 		}));
 
 		// followers
 		it('[show] followers-mentionを自分が見れる', async(async () => {
 			const res = await show(folM.id, alice);
-			expect(res.body).have.property('text').eql('@target x');
+			assert.strictEqual(res.body.text, '@target x');
 		}));
 
-		it('[show] followers-mentionを非フォロワーでもメンションされていれば見れる', async(async () => {
+		it('[show] followers-mentionを非フォロワーがメンションされていても見れない', async(async () => {
 			const res = await show(folM.id, target);
-			expect(res.body).have.property('text').eql('@target x');
+			assert.strictEqual(res.body.isHidden, true);
 		}));
 
 		it('[show] followers-mentionをフォロワーが見れる', async(async () => {
 			const res = await show(folM.id, follower);
-			expect(res.body).have.property('text').eql('@target x');
+			assert.strictEqual(res.body.text, '@target x');
 		}));
 
 		it('[show] followers-mentionを非フォロワーが見れない', async(async () => {
 			const res = await show(folM.id, other);
-			expect(res.body).have.property('isHidden').eql(true);
+			assert.strictEqual(res.body.isHidden, true);
 		}));
 
 		it('[show] followers-mentionを未認証が見れない', async(async () => {
 			const res = await show(folM.id, null);
-			expect(res.body).have.property('isHidden').eql(true);
+			assert.strictEqual(res.body.isHidden, true);
 		}));
 
 		// specified
 		it('[show] specified-mentionを自分が見れる', async(async () => {
 			const res = await show(speM.id, alice);
-			expect(res.body).have.property('text').eql('@target x');
+			assert.strictEqual(res.body.text, '@target x');
 		}));
 
 		it('[show] specified-mentionを指定ユーザーが見れる', async(async () => {
 			const res = await show(speM.id, target);
-			expect(res.body).have.property('text').eql('@target x');
+			assert.strictEqual(res.body.text, '@target x');
 		}));
 
-		it('[show] specified-mentionをされた人が指定されてなくても見れる', async(async () => {
+		it('[show] specified-mentionをされた人が指定されてなかったら見れない', async(async () => {
 			const res = await show(speM.id, target);
-			expect(res.body).have.property('text').eql('@target x');
+			assert.strictEqual(res.body.isHidden, true);
 		}));
 
 		it('[show] specified-mentionをフォロワーが見れない', async(async () => {
 			const res = await show(speM.id, follower);
-			expect(res.body).have.property('isHidden').eql(true);
+			assert.strictEqual(res.body.isHidden, true);
 		}));
 
 		it('[show] specified-mentionを非フォロワーが見れない', async(async () => {
 			const res = await show(speM.id, other);
-			expect(res.body).have.property('isHidden').eql(true);
+			assert.strictEqual(res.body.isHidden, true);
 		}));
 
 		it('[show] specified-mentionを未認証が見れない', async(async () => {
 			const res = await show(speM.id, null);
-			expect(res.body).have.property('isHidden').eql(true);
-		}));
-
-		// private
-		it('[show] private-mentionを自分が見れる', async(async () => {
-			const res = await show(priM.id, alice);
-			expect(res.body).have.property('text').eql('@target x');
-		}));
-
-		it('[show] private-mentionをフォロワーが見れない', async(async () => {
-			const res = await show(priM.id, follower);
-			expect(res.body).have.property('isHidden').eql(true);
-		}));
-
-		it('[show] private-mentionを非フォロワーが見れない', async(async () => {
-			const res = await show(priM.id, other);
-			expect(res.body).have.property('isHidden').eql(true);
-		}));
-
-		it('[show] private-mentionを未認証が見れない', async(async () => {
-			const res = await show(priM.id, null);
-			expect(res.body).have.property('isHidden').eql(true);
+			assert.strictEqual(res.body.isHidden, true);
 		}));
 		//#endregion
 
 		//#region HTL
 		it('[HTL] public-post が 自分が見れる', async(async () => {
 			const res = await request('/notes/timeline', { limit: 100 }, alice);
-			expect(res).have.status(200);
+			assert.strictEqual(res.status, 200);
 			const notes = res.body.filter((n: any) => n.id == pub.id);
-			expect(notes[0]).have.property('text').eql('x');
+			assert.strictEqual(notes[0].text, 'x');
 		}));
 
 		it('[HTL] public-post が 非フォロワーから見れない', async(async () => {
 			const res = await request('/notes/timeline', { limit: 100 }, other);
-			expect(res).have.status(200);
+			assert.strictEqual(res.status, 200);
 			const notes = res.body.filter((n: any) => n.id == pub.id);
-			expect(notes).length(0);
+			assert.strictEqual(notes.length, 0);
 		}));
 
 		it('[HTL] followers-post が フォロワーから見れる', async(async () => {
 			const res = await request('/notes/timeline', { limit: 100 }, follower);
-			expect(res).have.status(200);
+			assert.strictEqual(res.status, 200);
 			const notes = res.body.filter((n: any) => n.id == fol.id);
-			expect(notes[0]).have.property('text').eql('x');
+			assert.strictEqual(notes[0].text, 'x');
 		}));
 		//#endregion
 
 		//#region RTL
 		it('[replies] followers-reply が フォロワーから見れる', async(async () => {
 			const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower);
-			expect(res).have.status(200);
+			assert.strictEqual(res.status, 200);
 			const notes = res.body.filter((n: any) => n.id == folR.id);
-			expect(notes[0]).have.property('text').eql('x');
+			assert.strictEqual(notes[0].text, 'x');
 		}));
 
 		it('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async(async () => {
 			const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, other);
-			expect(res).have.status(200);
+			assert.strictEqual(res.status, 200);
 			const notes = res.body.filter((n: any) => n.id == folR.id);
-			expect(notes).length(0);
+			assert.strictEqual(notes.length, 0);
 		}));
 
 		it('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async(async () => {
 			const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, target);
-			expect(res).have.status(200);
+			assert.strictEqual(res.status, 200);
 			const notes = res.body.filter((n: any) => n.id == folR.id);
-			expect(notes[0]).have.property('text').eql('x');
+			assert.strictEqual(notes[0].text, 'x');
 		}));
 		//#endregion
 
 		//#region MTL
 		it('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async(async () => {
 			const res = await request('/notes/mentions', { limit: 100 }, target);
-			expect(res).have.status(200);
+			assert.strictEqual(res.status, 200);
 			const notes = res.body.filter((n: any) => n.id == folR.id);
-			expect(notes[0]).have.property('text').eql('x');
+			assert.strictEqual(notes[0].text, 'x');
 		}));
 
 		it('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async(async () => {
 			const res = await request('/notes/mentions', { limit: 100 }, target);
-			expect(res).have.status(200);
+			assert.strictEqual(res.status, 200);
 			const notes = res.body.filter((n: any) => n.id == folM.id);
-			expect(notes[0]).have.property('text').eql('@target x');
+			assert.strictEqual(notes[0].text, '@target x');
 		}));
 		//#endregion
 	});
diff --git a/test/api.ts b/test/api.ts
index cc4521d3dc858e1c05d04873b3c1f5cf05ecc635..71443c5730fe4d4647a3e80b1d37bebe1294ce6a 100644
--- a/test/api.ts
+++ b/test/api.ts
@@ -6,44 +6,35 @@
  *
  * To specify test:
  * > mocha test/api.ts --require ts-node/register -g 'test name'
+ *
+ * If the tests not start, try set following enviroment variables:
+ * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
+ * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
  */
 
-import * as http from 'http';
-import * as fs from 'fs';
-import * as assert from 'chai';
-import { async, _signup, _request, _uploadFile, _post, _react, resetDb } from './utils';
-
-const expect = assert.expect;
-
-//#region process
-Error.stackTraceLimit = Infinity;
-
-// During the test the env variable is set to test
 process.env.NODE_ENV = 'test';
 
-// Display detail of unhandled promise rejection
-process.on('unhandledRejection', console.dir);
-//#endregion
-
-const app = require('../built/server/api').default;
-const db = require('../built/db/mongodb').default;
-
-const server = http.createServer(app.callback());
-
-//#region Utilities
-const request = _request(server);
-const signup = _signup(request);
-const post = _post(request);
-const react = _react(request);
-const uploadFile = _uploadFile(server);
-//#endregion
+import * as assert from 'assert';
+import * as childProcess from 'child_process';
+import { async, signup, request, post, react, uploadFile } from './utils';
 
 describe('API', () => {
-	// Reset database each test
-	beforeEach(resetDb(db));
+	let p: childProcess.ChildProcess;
+
+	beforeEach(done => {
+		p = childProcess.spawn('node', [__dirname + '/../index.js'], {
+			stdio: ['inherit', 'inherit', 'ipc'],
+			env: { NODE_ENV: 'test' }
+		});
+		p.on('message', message => {
+			if (message === 'ok') {
+				done();
+			}
+		});
+	});
 
-	after(() => {
-		server.close();
+	afterEach(() => {
+		p.kill();
 	});
 
 	describe('signup', () => {
@@ -52,7 +43,7 @@ describe('API', () => {
 				username: 'test.',
 				password: 'test'
 			});
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('空のパスワードでアカウントが作成できない', async(async () => {
@@ -60,7 +51,7 @@ describe('API', () => {
 				username: 'test',
 				password: ''
 			});
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('正しくアカウントが作成できる', async(async () => {
@@ -71,9 +62,9 @@ describe('API', () => {
 
 			const res = await request('/signup', me);
 
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('username').eql(me.username);
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+			assert.strictEqual(res.body.username, me.username);
 		}));
 
 		it('同じユーザー名のアカウントは作成できない', async(async () => {
@@ -86,7 +77,7 @@ describe('API', () => {
 				password: 'test'
 			});
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 	});
 
@@ -102,7 +93,7 @@ describe('API', () => {
 				password: 'bar'
 			});
 
-			expect(res).have.status(403);
+			assert.strictEqual(res.status, 403);
 		}));
 
 		it('クエリをインジェクションできない', async(async () => {
@@ -117,7 +108,7 @@ describe('API', () => {
 				}
 			});
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('正しい情報でサインインできる', async(async () => {
@@ -131,7 +122,7 @@ describe('API', () => {
 				password: 'foo'
 			});
 
-			expect(res).have.status(200);
+			assert.strictEqual(res.status, 200);
 		}));
 	});
 
@@ -149,12 +140,11 @@ describe('API', () => {
 				birthday: myBirthday
 			}, me);
 
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('name').eql(myName);
-			expect(res.body).have.nested.property('profile').a('object');
-			expect(res.body).have.nested.property('profile.location').eql(myLocation);
-			expect(res.body).have.nested.property('profile.birthday').eql(myBirthday);
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+			assert.strictEqual(res.body.name, myName);
+			assert.strictEqual(res.body.location, myLocation);
+			assert.strictEqual(res.body.birthday, myBirthday);
 		}));
 
 		it('名前を空白にできない', async(async () => {
@@ -162,7 +152,7 @@ describe('API', () => {
 			const res = await request('/i/update', {
 				name: ' '
 			}, me);
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('誕生日の設定を削除できる', async(async () => {
@@ -175,10 +165,9 @@ describe('API', () => {
 				birthday: null
 			}, me);
 
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.nested.property('profile').a('object');
-			expect(res.body).have.nested.property('profile.birthday').eql(null);
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+			assert.strictEqual(res.body.birthday, null);
 		}));
 
 		it('不正な誕生日の形式で怒られる', async(async () => {
@@ -186,7 +175,7 @@ describe('API', () => {
 			const res = await request('/i/update', {
 				birthday: '2000/09/07'
 			}, me);
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 	});
 
@@ -198,365 +187,23 @@ describe('API', () => {
 				userId: me.id
 			}, me);
 
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('id').eql(me.id);
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+			assert.strictEqual(res.body.id, me.id);
 		}));
 
 		it('ユーザーが存在しなかったら怒る', async(async () => {
 			const res = await request('/users/show', {
 				userId: '000000000000000000000000'
 			});
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('間違ったIDで怒られる', async(async () => {
 			const res = await request('/users/show', {
 				userId: 'kyoppie'
 			});
-			expect(res).have.status(400);
-		}));
-	});
-
-	describe('notes/create', () => {
-		it('投稿できる', async(async () => {
-			const me = await signup();
-			const post = {
-				text: 'test'
-			};
-
-			const res = await request('/notes/create', post, me);
-
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('createdNote');
-			expect(res.body.createdNote).have.property('text').eql(post.text);
-		}));
-
-		it('ファイルを添付できる', async(async () => {
-			const me = await signup();
-			const file = await uploadFile(me);
-
-			const res = await request('/notes/create', {
-				fileIds: [file.id]
-			}, me);
-
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('createdNote');
-			expect(res.body.createdNote).have.property('fileIds').eql([file.id]);
-		}));
-
-		it('他人のファイルは無視', async(async () => {
-			const me = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-			const file = await uploadFile(bob);
-
-			const res = await request('/notes/create', {
-				text: 'test',
-				fileIds: [file.id]
-			}, me);
-
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('createdNote');
-			expect(res.body.createdNote).have.property('fileIds').eql([]);
-		}));
-
-		it('存在しないファイルは無視', async(async () => {
-			const me = await signup();
-
-			const res = await request('/notes/create', {
-				text: 'test',
-				fileIds: ['000000000000000000000000']
-			}, me);
-
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('createdNote');
-			expect(res.body.createdNote).have.property('fileIds').eql([]);
-		}));
-
-		it('不正なファイルIDで怒られる', async(async () => {
-			const me = await signup();
-			const res = await request('/notes/create', {
-				fileIds: ['kyoppie']
-			}, me);
-			expect(res).have.status(400);
-		}));
-
-		it('返信できる', async(async () => {
-			const bob = await signup({ username: 'bob' });
-			const bobPost = await post(bob);
-
-			const alice = await signup({ username: 'alice' });
-			const alicePost = {
-				text: 'test',
-				replyId: bobPost.id
-			};
-
-			const res = await request('/notes/create', alicePost, alice);
-
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('createdNote');
-			expect(res.body.createdNote).have.property('text').eql(alicePost.text);
-			expect(res.body.createdNote).have.property('replyId').eql(alicePost.replyId);
-			expect(res.body.createdNote).have.property('reply');
-			expect(res.body.createdNote.reply).have.property('text').eql(alicePost.text);
-		}));
-
-		it('renoteできる', async(async () => {
-			const bob = await signup({ username: 'bob' });
-			const bobPost = await post(bob, {
-				text: 'test'
-			});
-
-			const alice = await signup({ username: 'alice' });
-			const alicePost = {
-				renoteId: bobPost.id
-			};
-
-			const res = await request('/notes/create', alicePost, alice);
-
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('createdNote');
-			expect(res.body.createdNote).have.property('renoteId').eql(alicePost.renoteId);
-			expect(res.body.createdNote).have.property('renote');
-			expect(res.body.createdNote.renote).have.property('text').eql(bobPost.text);
-		}));
-
-		it('引用renoteできる', async(async () => {
-			const bob = await signup({ username: 'bob' });
-			const bobPost = await post(bob, {
-				text: 'test'
-			});
-
-			const alice = await signup({ username: 'alice' });
-			const alicePost = {
-				text: 'test',
-				renoteId: bobPost.id
-			};
-
-			const res = await request('/notes/create', alicePost, alice);
-
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('createdNote');
-			expect(res.body.createdNote).have.property('text').eql(alicePost.text);
-			expect(res.body.createdNote).have.property('renoteId').eql(alicePost.renoteId);
-			expect(res.body.createdNote).have.property('renote');
-			expect(res.body.createdNote.renote).have.property('text').eql(bobPost.text);
-		}));
-
-		it('文字数ぎりぎりで怒られない', async(async () => {
-			const me = await signup();
-			const post = {
-				text: '!'.repeat(1000)
-			};
-			const res = await request('/notes/create', post, me);
-			expect(res).have.status(200);
-		}));
-
-		it('文字数オーバーで怒られる', async(async () => {
-			const me = await signup();
-			const post = {
-				text: '!'.repeat(1001)
-			};
-			const res = await request('/notes/create', post, me);
-			expect(res).have.status(400);
-		}));
-
-		it('存在しないリプライ先で怒られる', async(async () => {
-			const me = await signup();
-			const post = {
-				text: 'test',
-				replyId: '000000000000000000000000'
-			};
-			const res = await request('/notes/create', post, me);
-			expect(res).have.status(400);
-		}));
-
-		it('存在しないrenote対象で怒られる', async(async () => {
-			const me = await signup();
-			const post = {
-				renoteId: '000000000000000000000000'
-			};
-			const res = await request('/notes/create', post, me);
-			expect(res).have.status(400);
-		}));
-
-		it('不正なリプライ先IDで怒られる', async(async () => {
-			const me = await signup();
-			const post = {
-				text: 'test',
-				replyId: 'foo'
-			};
-			const res = await request('/notes/create', post, me);
-			expect(res).have.status(400);
-		}));
-
-		it('不正なrenote対象IDで怒られる', async(async () => {
-			const me = await signup();
-			const post = {
-				renoteId: 'foo'
-			};
-			const res = await request('/notes/create', post, me);
-			expect(res).have.status(400);
-		}));
-
-		it('投票を添付できる', async(async () => {
-			const me = await signup();
-
-			const res = await request('/notes/create', {
-				text: 'test',
-				poll: {
-					choices: ['foo', 'bar']
-				}
-			}, me);
-
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('createdNote');
-			expect(res.body.createdNote).have.property('poll');
-		}));
-
-		it('投票の選択肢が無くて怒られる', async(async () => {
-			const me = await signup();
-			const res = await request('/notes/create', {
-				poll: {}
-			}, me);
-			expect(res).have.status(400);
-		}));
-
-		it('投票の選択肢が無くて怒られる (空の配列)', async(async () => {
-			const me = await signup();
-			const res = await request('/notes/create', {
-				poll: {
-					choices: []
-				}
-			}, me);
-			expect(res).have.status(400);
-		}));
-
-		it('投票の選択肢が1つで怒られる', async(async () => {
-			const me = await signup();
-			const res = await request('/notes/create', {
-				poll: {
-					choices: ['Strawberry Pasta']
-				}
-			}, me);
-			expect(res).have.status(400);
-		}));
-
-		it('投票できる', async(async () => {
-			const me = await signup();
-
-			const { body } = await request('/notes/create', {
-				text: 'test',
-				poll: {
-					choices: ['sakura', 'izumi', 'ako']
-				}
-			}, me);
-
-			const res = await request('/notes/polls/vote', {
-				noteId: body.createdNote.id,
-				choice: 1
-			}, me);
-
-			expect(res).have.status(204);
-		}));
-
-		it('複数投票できない', async(async () => {
-			const me = await signup();
-
-			const { body } = await request('/notes/create', {
-				text: 'test',
-				poll: {
-					choices: ['sakura', 'izumi', 'ako']
-				}
-			}, me);
-
-			await request('/notes/polls/vote', {
-				noteId: body.createdNote.id,
-				choice: 0
-			}, me);
-
-			const res = await request('/notes/polls/vote', {
-				noteId: body.createdNote.id,
-				choice: 2
-			}, me);
-
-			expect(res).have.status(400);
-		}));
-
-		it('許可されている場合は複数投票できる', async(async () => {
-			const me = await signup();
-
-			const { body } = await request('/notes/create', {
-				text: 'test',
-				poll: {
-					choices: ['sakura', 'izumi', 'ako'],
-					multiple: true
-				}
-			}, me);
-
-			await request('/notes/polls/vote', {
-				noteId: body.createdNote.id,
-				choice: 0
-			}, me);
-
-			await request('/notes/polls/vote', {
-				noteId: body.createdNote.id,
-				choice: 1
-			}, me);
-
-			const res = await request('/notes/polls/vote', {
-				noteId: body.createdNote.id,
-				choice: 2
-			}, me);
-
-			expect(res).have.status(204);
-		}));
-
-		it('締め切られている場合は投票できない', async(async () => {
-			const me = await signup();
-
-			const { body } = await request('/notes/create', {
-				text: 'test',
-				poll: {
-					choices: ['sakura', 'izumi', 'ako'],
-					expiredAfter: 1
-				}
-			}, me);
-
-			await new Promise(x => setTimeout(x, 2));
-
-			const res = await request('/notes/polls/vote', {
-				noteId: body.createdNote.id,
-				choice: 1
-			}, me);
-
-			expect(res).have.status(400);
-		}));
-
-		it('同じユーザーに複数メンションしても内部的にまとめられる', async(async () => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-			const post = {
-				text: '@bob @bob @bob yo'
-			};
-
-			const res = await request('/notes/create', post, alice);
-
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('createdNote');
-			expect(res.body.createdNote).have.property('text').eql(post.text);
-
-			const noteDoc = await db.get('notes').findOne({ _id: res.body.createdNote.id });
-			expect(noteDoc.mentions.map((id: any) => id.toString())).eql([bob.id.toString()]);
+			assert.strictEqual(res.status, 400);
 		}));
 	});
 
@@ -571,24 +218,24 @@ describe('API', () => {
 				noteId: myPost.id
 			}, me);
 
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('id').eql(myPost.id);
-			expect(res.body).have.property('text').eql(myPost.text);
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+			assert.strictEqual(res.body.id, myPost.id);
+			assert.strictEqual(res.body.text, myPost.text);
 		}));
 
 		it('投稿が存在しなかったら怒る', async(async () => {
 			const res = await request('/notes/show', {
 				noteId: '000000000000000000000000'
 			});
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('間違ったIDで怒られる', async(async () => {
 			const res = await request('/notes/show', {
 				noteId: 'kyoppie'
 			});
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 	});
 
@@ -603,7 +250,7 @@ describe('API', () => {
 				reaction: 'like'
 			}, alice);
 
-			expect(res).have.status(204);
+			assert.strictEqual(res.status, 204);
 		}));
 
 		it('自分の投稿にはリアクションできない', async(async () => {
@@ -615,7 +262,7 @@ describe('API', () => {
 				reaction: 'like'
 			}, me);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('二重にリアクションできない', async(async () => {
@@ -630,7 +277,7 @@ describe('API', () => {
 				reaction: 'like'
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('存在しない投稿にはリアクションできない', async(async () => {
@@ -641,7 +288,7 @@ describe('API', () => {
 				reaction: 'like'
 			}, me);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('空のパラメータで怒られる', async(async () => {
@@ -649,7 +296,7 @@ describe('API', () => {
 
 			const res = await request('/notes/reactions/create', {}, me);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('間違ったIDで怒られる', async(async () => {
@@ -660,7 +307,7 @@ describe('API', () => {
 				reaction: 'like'
 			}, me);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 	});
 
@@ -673,7 +320,7 @@ describe('API', () => {
 				userId: alice.id
 			}, bob);
 
-			expect(res).have.status(200);
+			assert.strictEqual(res.status, 200);
 		}));
 
 		it('既にフォローしている場合は怒る', async(async () => {
@@ -687,7 +334,7 @@ describe('API', () => {
 				userId: alice.id
 			}, bob);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('存在しないユーザーはフォローできない', async(async () => {
@@ -697,7 +344,7 @@ describe('API', () => {
 				userId: '000000000000000000000000'
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('自分自身はフォローできない', async(async () => {
@@ -707,7 +354,7 @@ describe('API', () => {
 				userId: alice.id
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('空のパラメータで怒られる', async(async () => {
@@ -715,7 +362,7 @@ describe('API', () => {
 
 			const res = await request('/following/create', {}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('間違ったIDで怒られる', async(async () => {
@@ -725,7 +372,7 @@ describe('API', () => {
 				userId: 'foo'
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 	});
 
@@ -741,7 +388,7 @@ describe('API', () => {
 				userId: alice.id
 			}, bob);
 
-			expect(res).have.status(200);
+			assert.strictEqual(res.status, 200);
 		}));
 
 		it('フォローしていない場合は怒る', async(async () => {
@@ -752,7 +399,7 @@ describe('API', () => {
 				userId: alice.id
 			}, bob);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('存在しないユーザーはフォロー解除できない', async(async () => {
@@ -762,7 +409,7 @@ describe('API', () => {
 				userId: '000000000000000000000000'
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('自分自身はフォロー解除できない', async(async () => {
@@ -772,7 +419,7 @@ describe('API', () => {
 				userId: alice.id
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('空のパラメータで怒られる', async(async () => {
@@ -780,7 +427,7 @@ describe('API', () => {
 
 			const res = await request('/following/delete', {}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('間違ったIDで怒られる', async(async () => {
@@ -790,7 +437,7 @@ describe('API', () => {
 				userId: 'kyoppie'
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 	});
 
@@ -799,20 +446,20 @@ describe('API', () => {
 		it('ドライブ情報を取得できる', async(async () => {
 			const bob = await signup({ username: 'bob' });
 			await uploadFile({
-				userId: me._id,
-				datasize: 256
+				userId: me.id,
+				size: 256
 			});
 			await uploadFile({
-				userId: me._id,
-				datasize: 512
+				userId: me.id,
+				size: 512
 			});
 			await uploadFile({
-				userId: me._id,
-				datasize: 1024
+				userId: me.id,
+				size: 1024
 			});
 			const res = await request('/drive', {}, me);
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
 			expect(res.body).have.property('usage').eql(1792);
 		}));*/
 	});
@@ -821,14 +468,11 @@ describe('API', () => {
 		it('ファイルを作成できる', async(async () => {
 			const alice = await signup({ username: 'alice' });
 
-			const res = await assert.request(server)
-				.post('/drive/files/create')
-				.field('i', alice.token)
-				.attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png');
+			const res = await uploadFile(alice);
 
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('name').eql('Lenna.png');
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+			assert.strictEqual(res.body.name, 'Lenna.png');
 		}));
 
 		it('ファイル無しで怒られる', async(async () => {
@@ -836,21 +480,18 @@ describe('API', () => {
 
 			const res = await request('/drive/files/create', {}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('SVGファイルを作成できる', async(async () => {
 			const izumi = await signup({ username: 'izumi' });
 
-			const res = await assert.request(server)
-				.post('/drive/files/create')
-				.field('i', izumi.token)
-				.attach('file', fs.readFileSync(__dirname + '/resources/image.svg'), 'image.svg');
+			const res = await uploadFile(izumi, __dirname + '/resources/image.svg');
 
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('name').eql('image.svg');
-			expect(res.body).have.property('type').eql('image/svg+xml');
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+			assert.strictEqual(res.body.name, 'image.svg');
+			assert.strictEqual(res.body.type, 'image/svg+xml');
 		}));
 	});
 
@@ -865,9 +506,9 @@ describe('API', () => {
 				name: newName
 			}, alice);
 
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('name').eql(newName);
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+			assert.strictEqual(res.body.name, newName);
 		}));
 
 		it('他人のファイルは更新できない', async(async () => {
@@ -880,7 +521,7 @@ describe('API', () => {
 				name: 'いちごパスタ.png'
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('親フォルダを更新できる', async(async () => {
@@ -895,9 +536,9 @@ describe('API', () => {
 				folderId: folder.id
 			}, alice);
 
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('folderId').eql(folder.id);
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+			assert.strictEqual(res.body.folderId, folder.id);
 		}));
 
 		it('親フォルダを無しにできる', async(async () => {
@@ -918,9 +559,9 @@ describe('API', () => {
 				folderId: null
 			}, alice);
 
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('folderId').eql(null);
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+			assert.strictEqual(res.body.folderId, null);
 		}));
 
 		it('他人のフォルダには入れられない', async(async () => {
@@ -936,7 +577,7 @@ describe('API', () => {
 				folderId: folder.id
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('存在しないフォルダで怒られる', async(async () => {
@@ -948,7 +589,7 @@ describe('API', () => {
 				folderId: '000000000000000000000000'
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('不正なフォルダIDで怒られる', async(async () => {
@@ -960,7 +601,7 @@ describe('API', () => {
 				folderId: 'foo'
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('ファイルが存在しなかったら怒る', async(async () => {
@@ -971,7 +612,7 @@ describe('API', () => {
 				name: 'いちごパスタ.png'
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('間違ったIDで怒られる', async(async () => {
@@ -982,7 +623,7 @@ describe('API', () => {
 				name: 'いちごパスタ.png'
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 	});
 
@@ -994,9 +635,9 @@ describe('API', () => {
 				name: 'test'
 			}, alice);
 
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('name').eql('test');
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+			assert.strictEqual(res.body.name, 'test');
 		}));
 	});
 
@@ -1012,9 +653,9 @@ describe('API', () => {
 				name: 'new name'
 			}, alice);
 
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('name').eql('new name');
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+			assert.strictEqual(res.body.name, 'new name');
 		}));
 
 		it('他人のフォルダを更新できない', async(async () => {
@@ -1029,7 +670,7 @@ describe('API', () => {
 				name: 'new name'
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('親フォルダを更新できる', async(async () => {
@@ -1046,9 +687,9 @@ describe('API', () => {
 				parentId: parentFolder.id
 			}, alice);
 
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('parentId').eql(parentFolder.id);
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+			assert.strictEqual(res.body.parentId, parentFolder.id);
 		}));
 
 		it('親フォルダを無しに更新できる', async(async () => {
@@ -1069,9 +710,9 @@ describe('API', () => {
 				parentId: null
 			}, alice);
 
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('parentId').eql(null);
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+			assert.strictEqual(res.body.parentId, null);
 		}));
 
 		it('他人のフォルダを親フォルダに設定できない', async(async () => {
@@ -1089,7 +730,7 @@ describe('API', () => {
 				parentId: parentFolder.id
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('フォルダが循環するような構造にできない', async(async () => {
@@ -1110,7 +751,7 @@ describe('API', () => {
 				parentId: parentFolder.id
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('フォルダが循環するような構造にできない(再帰的)', async(async () => {
@@ -1138,7 +779,7 @@ describe('API', () => {
 				parentId: folderC.id
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('フォルダが循環するような構造にできない(自身)', async(async () => {
@@ -1166,7 +807,7 @@ describe('API', () => {
 				parentId: '000000000000000000000000'
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('不正な親フォルダIDで怒られる', async(async () => {
@@ -1180,7 +821,7 @@ describe('API', () => {
 				parentId: 'foo'
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('存在しないフォルダを更新できない', async(async () => {
@@ -1190,7 +831,7 @@ describe('API', () => {
 				folderId: '000000000000000000000000'
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('不正なフォルダIDで怒られる', async(async () => {
@@ -1200,7 +841,7 @@ describe('API', () => {
 				folderId: 'foo'
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 	});
 
@@ -1214,9 +855,9 @@ describe('API', () => {
 				text: 'test'
 			}, alice);
 
-			expect(res).have.status(200);
-			expect(res.body).be.a('object');
-			expect(res.body).have.property('text').eql('test');
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+			assert.strictEqual(res.body.text, 'test');
 		}));
 
 		it('自分自身にはメッセージを送信できない', async(async () => {
@@ -1227,7 +868,7 @@ describe('API', () => {
 				text: 'Yo'
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('存在しないユーザーにはメッセージを送信できない', async(async () => {
@@ -1238,7 +879,7 @@ describe('API', () => {
 				text: 'test'
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('不正なユーザーIDで怒られる', async(async () => {
@@ -1249,7 +890,7 @@ describe('API', () => {
 				text: 'test'
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('テキストが無くて怒られる', async(async () => {
@@ -1260,7 +901,7 @@ describe('API', () => {
 				userId: bob.id
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 
 		it('文字数オーバーで怒られる', async(async () => {
@@ -1272,7 +913,7 @@ describe('API', () => {
 				text: '!'.repeat(1001)
 			}, alice);
 
-			expect(res).have.status(400);
+			assert.strictEqual(res.status, 400);
 		}));
 	});
 
@@ -1297,9 +938,9 @@ describe('API', () => {
 				noteId: alicePost.id
 			}, carol);
 
-			expect(res).have.status(200);
-			expect(res.body).be.a('array');
-			expect(res.body).length(0);
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(Array.isArray(res.body), true);
+			assert.strictEqual(res.body.length, 0);
 		}));
 	});
 
@@ -1319,10 +960,10 @@ describe('API', () => {
 
 			const res = await request('/notes/timeline', {}, bob);
 
-			expect(res).have.status(200);
-			expect(res.body).be.a('array');
-			expect(res.body).length(1);
-			expect(res.body[0].id).equals(alicePost.id);
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(Array.isArray(res.body), true);
+			assert.strictEqual(res.body.length, 1);
+			assert.strictEqual(res.body[0].id, alicePost.id);
 		}));
 	});
 });
diff --git a/test/chart.ts b/test/chart.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b3976b03bae98559c0898d6caff0f136ff12a17b
--- /dev/null
+++ b/test/chart.ts
@@ -0,0 +1,323 @@
+/*
+ * Tests of chart engine
+ *
+ * How to run the tests:
+ * > mocha test/chart.ts --require ts-node/register
+ *
+ * To specify test:
+ * > mocha test/chart.ts --require ts-node/register -g 'test name'
+ *
+ * If the tests not start, try set following enviroment variables:
+ * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
+ * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
+ */
+
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import * as lolex from 'lolex';
+import { async } from './utils';
+import { getConnection, createConnection } from 'typeorm';
+const config = require('../built/config').default;
+const Chart = require('../built/services/chart/core').default;
+const _TestChart = require('../built/services/chart/charts/schemas/test');
+const _TestGroupedChart = require('../built/services/chart/charts/schemas/test-grouped');
+const _TestUniqueChart = require('../built/services/chart/charts/schemas/test-unique');
+
+function initDb() {
+	try {
+		const conn = getConnection();
+		return Promise.resolve(conn);
+	} catch (e) {}
+
+	return createConnection({
+		type: 'postgres',
+		host: config.db.host,
+		port: config.db.port,
+		username: config.db.user,
+		password: config.db.pass,
+		database: config.db.db,
+		synchronize: true,
+		dropSchema: true,
+		entities: [
+			Chart.schemaToEntity(_TestChart.name, _TestChart.schema),
+			Chart.schemaToEntity(_TestGroupedChart.name, _TestGroupedChart.schema),
+			Chart.schemaToEntity(_TestUniqueChart.name, _TestUniqueChart.schema)
+		]
+	});
+}
+
+describe('Chart', () => {
+	let testChart: any;
+	let testGroupedChart: any;
+	let testUniqueChart: any;
+	let connection: any;
+	let clock: lolex.InstalledClock<lolex.Clock>;
+
+	before(done => {
+		initDb().then(c => {
+			connection = c;
+			done();
+		});
+	});
+
+	beforeEach(done => {
+		const TestChart = require('../built/services/chart/charts/classes/test').default;
+		testChart = new TestChart();
+
+		const TestGroupedChart = require('../built/services/chart/charts/classes/test-grouped').default;
+		testGroupedChart = new TestGroupedChart();
+
+		const TestUniqueChart = require('../built/services/chart/charts/classes/test-unique').default;
+		testUniqueChart = new TestUniqueChart();
+
+		clock = lolex.install({
+			now: new Date('2000-01-01 00:00:00')
+		});
+
+		connection.synchronize().then(done);
+	});
+
+	afterEach(done => {
+		clock.uninstall();
+		connection.dropDatabase().then(done);
+	});
+
+	it('Can updates', async(async () => {
+		await testChart.increment();
+
+		const chartHours = await testChart.getChart('hour', 3);
+		const chartDays = await testChart.getChart('day', 3);
+
+		assert.deepStrictEqual(chartHours, {
+			foo: {
+				dec: [0, 0, 0],
+				inc: [1, 0, 0],
+				total: [1, 0, 0]
+			},
+		});
+
+		assert.deepStrictEqual(chartDays, {
+			foo: {
+				dec: [0, 0, 0],
+				inc: [1, 0, 0],
+				total: [1, 0, 0]
+			},
+		});
+	}));
+
+	it('Empty chart', async(async () => {
+		const chartHours = await testChart.getChart('hour', 3);
+		const chartDays = await testChart.getChart('day', 3);
+
+		assert.deepStrictEqual(chartHours, {
+			foo: {
+				dec: [0, 0, 0],
+				inc: [0, 0, 0],
+				total: [0, 0, 0]
+			},
+		});
+
+		assert.deepStrictEqual(chartDays, {
+			foo: {
+				dec: [0, 0, 0],
+				inc: [0, 0, 0],
+				total: [0, 0, 0]
+			},
+		});
+	}));
+
+	it('Can updates at multiple times at same time', async(async () => {
+		await testChart.increment();
+		await testChart.increment();
+		await testChart.increment();
+
+		const chartHours = await testChart.getChart('hour', 3);
+		const chartDays = await testChart.getChart('day', 3);
+
+		assert.deepStrictEqual(chartHours, {
+			foo: {
+				dec: [0, 0, 0],
+				inc: [3, 0, 0],
+				total: [3, 0, 0]
+			},
+		});
+
+		assert.deepStrictEqual(chartDays, {
+			foo: {
+				dec: [0, 0, 0],
+				inc: [3, 0, 0],
+				total: [3, 0, 0]
+			},
+		});
+	}));
+
+	it('Can updates at different times', async(async () => {
+		await testChart.increment();
+
+		clock.tick('01:00:00');
+
+		await testChart.increment();
+
+		const chartHours = await testChart.getChart('hour', 3);
+		const chartDays = await testChart.getChart('day', 3);
+
+		assert.deepStrictEqual(chartHours, {
+			foo: {
+				dec: [0, 0, 0],
+				inc: [1, 1, 0],
+				total: [2, 1, 0]
+			},
+		});
+
+		assert.deepStrictEqual(chartDays, {
+			foo: {
+				dec: [0, 0, 0],
+				inc: [2, 0, 0],
+				total: [2, 0, 0]
+			},
+		});
+	}));
+
+	it('Can padding', async(async () => {
+		await testChart.increment();
+
+		clock.tick('02:00:00');
+
+		await testChart.increment();
+
+		const chartHours = await testChart.getChart('hour', 3);
+		const chartDays = await testChart.getChart('day', 3);
+
+		assert.deepStrictEqual(chartHours, {
+			foo: {
+				dec: [0, 0, 0],
+				inc: [1, 0, 1],
+				total: [2, 1, 1]
+			},
+		});
+
+		assert.deepStrictEqual(chartDays, {
+			foo: {
+				dec: [0, 0, 0],
+				inc: [2, 0, 0],
+				total: [2, 0, 0]
+			},
+		});
+	}));
+
+	// 要求された範囲にログがひとつもない場合でもパディングできる
+	it('Can padding from past range', async(async () => {
+		await testChart.increment();
+
+		clock.tick('05:00:00');
+
+		const chartHours = await testChart.getChart('hour', 3);
+		const chartDays = await testChart.getChart('day', 3);
+
+		assert.deepStrictEqual(chartHours, {
+			foo: {
+				dec: [0, 0, 0],
+				inc: [0, 0, 0],
+				total: [1, 1, 1]
+			},
+		});
+
+		assert.deepStrictEqual(chartDays, {
+			foo: {
+				dec: [0, 0, 0],
+				inc: [1, 0, 0],
+				total: [1, 0, 0]
+			},
+		});
+	}));
+
+	// 要求された範囲の最も古い箇所に位置するログが存在しない場合でもパディングできる
+	// Issue #3190
+	it('Can padding from past range 2', async(async () => {
+		await testChart.increment();
+		clock.tick('05:00:00');
+		await testChart.increment();
+
+		const chartHours = await testChart.getChart('hour', 3);
+		const chartDays = await testChart.getChart('day', 3);
+
+		assert.deepStrictEqual(chartHours, {
+			foo: {
+				dec: [0, 0, 0],
+				inc: [1, 0, 0],
+				total: [2, 1, 1]
+			},
+		});
+
+		assert.deepStrictEqual(chartDays, {
+			foo: {
+				dec: [0, 0, 0],
+				inc: [2, 0, 0],
+				total: [2, 0, 0]
+			},
+		});
+	}));
+
+	describe('Grouped', () => {
+		it('Can updates', async(async () => {
+			await testGroupedChart.increment('alice');
+
+			const aliceChartHours = await testGroupedChart.getChart('hour', 3, 'alice');
+			const aliceChartDays = await testGroupedChart.getChart('day', 3, 'alice');
+			const bobChartHours = await testGroupedChart.getChart('hour', 3, 'bob');
+			const bobChartDays = await testGroupedChart.getChart('day', 3, 'bob');
+
+			assert.deepStrictEqual(aliceChartHours, {
+				foo: {
+					dec: [0, 0, 0],
+					inc: [1, 0, 0],
+					total: [1, 0, 0]
+				},
+			});
+
+			assert.deepStrictEqual(aliceChartDays, {
+				foo: {
+					dec: [0, 0, 0],
+					inc: [1, 0, 0],
+					total: [1, 0, 0]
+				},
+			});
+
+			assert.deepStrictEqual(bobChartHours, {
+				foo: {
+					dec: [0, 0, 0],
+					inc: [0, 0, 0],
+					total: [0, 0, 0]
+				},
+			});
+
+			assert.deepStrictEqual(bobChartDays, {
+				foo: {
+					dec: [0, 0, 0],
+					inc: [0, 0, 0],
+					total: [0, 0, 0]
+				},
+			});
+		}));
+	});
+
+	describe('Unique increment', () => {
+		it('Can updates', async(async () => {
+			await testUniqueChart.uniqueIncrement('alice');
+			await testUniqueChart.uniqueIncrement('alice');
+			await testUniqueChart.uniqueIncrement('bob');
+
+			const chartHours = await testUniqueChart.getChart('hour', 3);
+			const chartDays = await testUniqueChart.getChart('day', 3);
+
+			assert.deepStrictEqual(chartHours, {
+				foo: [2, 0, 0],
+			});
+
+			assert.deepStrictEqual(chartDays, {
+				foo: [2, 0, 0],
+			});
+		}));
+	});
+});
diff --git a/test/mfm.ts b/test/mfm.ts
index 191ee5e0ed0d4641b61a28b2d798ad21096e04b9..69260a54155d1004f421bc22191ee60a2500e8c4 100644
--- a/test/mfm.ts
+++ b/test/mfm.ts
@@ -6,6 +6,10 @@
  *
  * To specify test:
  * > mocha test/mfm.ts --require ts-node/register -g 'test name'
+ *
+ * If the tests not start, try set following enviroment variables:
+ * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
+ * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
  */
 
 import * as assert from 'assert';
diff --git a/test/mocha.opts b/test/mocha.opts
index 907011807d680edb272de32bab832a628e2d5064..e114c53bd8adf9df94559c0a223a4c4ed5031caa 100644
--- a/test/mocha.opts
+++ b/test/mocha.opts
@@ -1 +1,2 @@
---timeout 10000
+--timeout 30000
+--slow 1000
diff --git a/test/mute.ts b/test/mute.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bf24b55ee514aa4a8d41299dbe552a67341e9191
--- /dev/null
+++ b/test/mute.ts
@@ -0,0 +1,170 @@
+/*
+ * Tests of mute
+ *
+ * How to run the tests:
+ * > mocha test/mute.ts --require ts-node/register
+ *
+ * To specify test:
+ * > mocha test/mute.ts --require ts-node/register -g 'test name'
+ *
+ * If the tests not start, try set following enviroment variables:
+ * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
+ * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
+ */
+
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import * as childProcess from 'child_process';
+import { async, signup, request, post, react, connectStream } from './utils';
+
+describe('Mute', () => {
+	let p: childProcess.ChildProcess;
+
+	// alice mutes carol
+	let alice: any;
+	let bob: any;
+	let carol: any;
+
+	before(done => {
+		p = childProcess.spawn('node', [__dirname + '/../index.js'], {
+			stdio: ['inherit', 'inherit', 'ipc'],
+			env: { NODE_ENV: 'test' }
+		});
+		p.on('message', async message => {
+			if (message === 'ok') {
+				(p.channel as any).onread = () => {};
+				alice = await signup({ username: 'alice' });
+				bob = await signup({ username: 'bob' });
+				carol = await signup({ username: 'carol' });
+				done();
+			}
+		});
+	});
+
+	after(() => {
+		p.kill();
+	});
+
+	it('ミュート作成', async(async () => {
+		const res = await request('/mute/create', {
+			userId: carol.id
+		}, alice);
+
+		assert.strictEqual(res.status, 204);
+	}));
+
+	it('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async(async () => {
+		const bobNote = await post(bob, { text: '@alice hi' });
+		const carolNote = await post(carol, { text: '@alice hi' });
+
+		const res = await request('/notes/mentions', {}, alice);
+
+		assert.strictEqual(res.status, 200);
+		assert.strictEqual(Array.isArray(res.body), true);
+		assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+		assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
+	}));
+
+	it('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async(async () => {
+		// 状態リセット
+		await request('/i/read-all-unread-notes', {}, alice);
+
+		await post(carol, { text: '@alice hi' });
+
+		const res = await request('/i', {}, alice);
+
+		assert.strictEqual(res.status, 200);
+		assert.strictEqual(res.body.hasUnreadMentions, false);
+	}));
+
+	it('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => {
+		// 状態リセット
+		await request('/i/read-all-unread-notes', {}, alice);
+
+		let fired = false;
+
+		const ws = await connectStream(alice, 'main', ({ type }) => {
+			if (type == 'unreadMention') {
+				fired = true;
+			}
+		});
+
+		post(carol, { text: '@alice hi' });
+
+		setTimeout(() => {
+			assert.strictEqual(fired, false);
+			ws.close();
+			done();
+		}, 5000);
+	}));
+
+	it('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', () => new Promise(async done => {
+		// 状態リセット
+		await request('/i/read-all-unread-notes', {}, alice);
+		await request('/notifications/mark-all-as-read', {}, alice);
+
+		let fired = false;
+
+		const ws = await connectStream(alice, 'main', ({ type }) => {
+			if (type == 'unreadNotification') {
+				fired = true;
+			}
+		});
+
+		post(carol, { text: '@alice hi' });
+
+		setTimeout(() => {
+			assert.strictEqual(fired, false);
+			ws.close();
+			done();
+		}, 5000);
+	}));
+
+	describe('Timeline', () => {
+		it('タイムラインにミュートしているユーザーの投稿が含まれない', async(async () => {
+			const aliceNote = await post(alice);
+			const bobNote = await post(bob);
+			const carolNote = await post(carol);
+
+			const res = await request('/notes/local-timeline', {}, alice);
+
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(Array.isArray(res.body), true);
+			assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
+		}));
+
+		it('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async(async () => {
+			const aliceNote = await post(alice);
+			const carolNote = await post(carol);
+			const bobNote = await post(bob, {
+				renoteId: carolNote.id
+			});
+
+			const res = await request('/notes/local-timeline', {}, alice);
+
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(Array.isArray(res.body), true);
+			assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
+		}));
+	});
+
+	describe('Notification', () => {
+		it('通知にミュートしているユーザーの通知が含まれない(リアクション)', async(async () => {
+			const aliceNote = await post(alice);
+			await react(bob, aliceNote, 'like');
+			await react(carol, aliceNote, 'like');
+
+			const res = await request('/i/notifications', {}, alice);
+
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(Array.isArray(res.body), true);
+			assert.strictEqual(res.body.some(notification => notification.userId === bob.id), true);
+			assert.strictEqual(res.body.some(notification => notification.userId === carol.id), false);
+		}));
+	});
+});
diff --git a/test/note.ts b/test/note.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7a05930eaeb35f87f1bf6d294d5ff832e7bd9c27
--- /dev/null
+++ b/test/note.ts
@@ -0,0 +1,361 @@
+/*
+ * Tests of Note
+ *
+ * How to run the tests:
+ * > mocha test/note.ts --require ts-node/register
+ *
+ * To specify test:
+ * > mocha test/note.ts --require ts-node/register -g 'test name'
+ *
+ * If the tests not start, try set following enviroment variables:
+ * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
+ * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
+ */
+
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import * as childProcess from 'child_process';
+import { async, signup, request, post, uploadFile } from './utils';
+import { Note } from '../built/models/entities/note';
+const initDb = require('../built/db/postgre.js').initDb;
+
+describe('Note', () => {
+	let p: childProcess.ChildProcess;
+	let Notes: any;
+
+	let alice: any;
+	let bob: any;
+
+	before(done => {
+		p = childProcess.spawn('node', [__dirname + '/../index.js'], {
+			stdio: ['inherit', 'inherit', 'ipc'],
+			env: { NODE_ENV: 'test' }
+		});
+		p.on('message', message => {
+			if (message === 'ok') {
+				(p.channel as any).onread = () => {};
+				initDb(true).then(async connection => {
+					Notes = connection.getRepository(Note);
+					alice = await signup({ username: 'alice' });
+					bob = await signup({ username: 'bob' });
+					done();
+				});
+			}
+		});
+	});
+
+	after(() => {
+		p.kill();
+	});
+
+	it('投稿できる', async(async () => {
+		const post = {
+			text: 'test'
+		};
+
+		const res = await request('/notes/create', post, alice);
+
+		assert.strictEqual(res.status, 200);
+		assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+		assert.strictEqual(res.body.createdNote.text, post.text);
+	}));
+
+	it('ファイルを添付できる', async(async () => {
+		const file = await uploadFile(alice);
+
+		const res = await request('/notes/create', {
+			fileIds: [file.id]
+		}, alice);
+
+		assert.strictEqual(res.status, 200);
+		assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+		assert.deepStrictEqual(res.body.createdNote.fileIds, [file.id]);
+	}));
+
+	it('他人のファイルは無視', async(async () => {
+		const file = await uploadFile(bob);
+
+		const res = await request('/notes/create', {
+			text: 'test',
+			fileIds: [file.id]
+		}, alice);
+
+		assert.strictEqual(res.status, 200);
+		assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+		assert.deepStrictEqual(res.body.createdNote.fileIds, []);
+	}));
+
+	it('存在しないファイルは無視', async(async () => {
+		const res = await request('/notes/create', {
+			text: 'test',
+			fileIds: ['000000000000000000000000']
+		}, alice);
+
+		assert.strictEqual(res.status, 200);
+		assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+		assert.deepStrictEqual(res.body.createdNote.fileIds, []);
+	}));
+
+	it('不正なファイルIDで怒られる', async(async () => {
+		const res = await request('/notes/create', {
+			fileIds: ['kyoppie']
+		}, alice);
+		assert.strictEqual(res.status, 400);
+	}));
+
+	it('返信できる', async(async () => {
+		const bobPost = await post(bob, {
+			text: 'foo'
+		});
+
+		const alicePost = {
+			text: 'bar',
+			replyId: bobPost.id
+		};
+
+		const res = await request('/notes/create', alicePost, alice);
+
+		assert.strictEqual(res.status, 200);
+		assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+		assert.strictEqual(res.body.createdNote.text, alicePost.text);
+		assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId);
+		assert.strictEqual(res.body.createdNote.reply.text, bobPost.text);
+	}));
+
+	it('renoteできる', async(async () => {
+		const bobPost = await post(bob, {
+			text: 'test'
+		});
+
+		const alicePost = {
+			renoteId: bobPost.id
+		};
+
+		const res = await request('/notes/create', alicePost, alice);
+
+		assert.strictEqual(res.status, 200);
+		assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+		assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
+		assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
+	}));
+
+	it('引用renoteできる', async(async () => {
+		const bobPost = await post(bob, {
+			text: 'test'
+		});
+
+		const alicePost = {
+			text: 'test',
+			renoteId: bobPost.id
+		};
+
+		const res = await request('/notes/create', alicePost, alice);
+
+		assert.strictEqual(res.status, 200);
+		assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+		assert.strictEqual(res.body.createdNote.text, alicePost.text);
+		assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
+		assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
+	}));
+
+	it('文字数ぎりぎりで怒られない', async(async () => {
+		const post = {
+			text: '!'.repeat(1000)
+		};
+		const res = await request('/notes/create', post, alice);
+		assert.strictEqual(res.status, 200);
+	}));
+
+	it('文字数オーバーで怒られる', async(async () => {
+		const post = {
+			text: '!'.repeat(1001)
+		};
+		const res = await request('/notes/create', post, alice);
+		assert.strictEqual(res.status, 400);
+	}));
+
+	it('存在しないリプライ先で怒られる', async(async () => {
+		const post = {
+			text: 'test',
+			replyId: '000000000000000000000000'
+		};
+		const res = await request('/notes/create', post, alice);
+		assert.strictEqual(res.status, 400);
+	}));
+
+	it('存在しないrenote対象で怒られる', async(async () => {
+		const post = {
+			renoteId: '000000000000000000000000'
+		};
+		const res = await request('/notes/create', post, alice);
+		assert.strictEqual(res.status, 400);
+	}));
+
+	it('不正なリプライ先IDで怒られる', async(async () => {
+		const post = {
+			text: 'test',
+			replyId: 'foo'
+		};
+		const res = await request('/notes/create', post, alice);
+		assert.strictEqual(res.status, 400);
+	}));
+
+	it('不正なrenote対象IDで怒られる', async(async () => {
+		const post = {
+			renoteId: 'foo'
+		};
+		const res = await request('/notes/create', post, alice);
+		assert.strictEqual(res.status, 400);
+	}));
+
+	it('存在しないユーザーにメンションできる', async(async () => {
+		const post = {
+			text: '@ghost yo'
+		};
+
+		const res = await request('/notes/create', post, alice);
+
+		assert.strictEqual(res.status, 200);
+		assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+		assert.strictEqual(res.body.createdNote.text, post.text);
+	}));
+
+	it('同じユーザーに複数メンションしても内部的にまとめられる', async(async () => {
+		const post = {
+			text: '@bob @bob @bob yo'
+		};
+
+		const res = await request('/notes/create', post, alice);
+
+		assert.strictEqual(res.status, 200);
+		assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+		assert.strictEqual(res.body.createdNote.text, post.text);
+
+		const noteDoc = await Notes.findOne(res.body.createdNote.id);
+		assert.deepStrictEqual(noteDoc.mentions, [bob.id]);
+	}));
+
+	describe('notes/create', () => {
+		it('投票を添付できる', async(async () => {
+			const res = await request('/notes/create', {
+				text: 'test',
+				poll: {
+					choices: ['foo', 'bar']
+				}
+			}, alice);
+
+			assert.strictEqual(res.status, 200);
+			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+			assert.strictEqual(res.body.createdNote.poll != null, true);
+		}));
+
+		it('投票の選択肢が無くて怒られる', async(async () => {
+			const res = await request('/notes/create', {
+				poll: {}
+			}, alice);
+			assert.strictEqual(res.status, 400);
+		}));
+
+		it('投票の選択肢が無くて怒られる (空の配列)', async(async () => {
+			const res = await request('/notes/create', {
+				poll: {
+					choices: []
+				}
+			}, alice);
+			assert.strictEqual(res.status, 400);
+		}));
+
+		it('投票の選択肢が1つで怒られる', async(async () => {
+			const res = await request('/notes/create', {
+				poll: {
+					choices: ['Strawberry Pasta']
+				}
+			}, alice);
+			assert.strictEqual(res.status, 400);
+		}));
+
+		it('投票できる', async(async () => {
+			const { body } = await request('/notes/create', {
+				text: 'test',
+				poll: {
+					choices: ['sakura', 'izumi', 'ako']
+				}
+			}, alice);
+
+			const res = await request('/notes/polls/vote', {
+				noteId: body.createdNote.id,
+				choice: 1
+			}, alice);
+
+			assert.strictEqual(res.status, 204);
+		}));
+
+		it('複数投票できない', async(async () => {
+			const { body } = await request('/notes/create', {
+				text: 'test',
+				poll: {
+					choices: ['sakura', 'izumi', 'ako']
+				}
+			}, alice);
+
+			await request('/notes/polls/vote', {
+				noteId: body.createdNote.id,
+				choice: 0
+			}, alice);
+
+			const res = await request('/notes/polls/vote', {
+				noteId: body.createdNote.id,
+				choice: 2
+			}, alice);
+
+			assert.strictEqual(res.status, 400);
+		}));
+
+		it('許可されている場合は複数投票できる', async(async () => {
+			const { body } = await request('/notes/create', {
+				text: 'test',
+				poll: {
+					choices: ['sakura', 'izumi', 'ako'],
+					multiple: true
+				}
+			}, alice);
+
+			await request('/notes/polls/vote', {
+				noteId: body.createdNote.id,
+				choice: 0
+			}, alice);
+
+			await request('/notes/polls/vote', {
+				noteId: body.createdNote.id,
+				choice: 1
+			}, alice);
+
+			const res = await request('/notes/polls/vote', {
+				noteId: body.createdNote.id,
+				choice: 2
+			}, alice);
+
+			assert.strictEqual(res.status, 204);
+		}));
+
+		it('締め切られている場合は投票できない', async(async () => {
+			const { body } = await request('/notes/create', {
+				text: 'test',
+				poll: {
+					choices: ['sakura', 'izumi', 'ako'],
+					expiredAfter: 1
+				}
+			}, alice);
+
+			await new Promise(x => setTimeout(x, 2));
+
+			const res = await request('/notes/polls/vote', {
+				noteId: body.createdNote.id,
+				choice: 1
+			}, alice);
+
+			assert.strictEqual(res.status, 400);
+		}));
+	});
+});
diff --git a/test/reaction-lib.ts b/test/reaction-lib.ts
index 2f6c8ea81b278992ac08059f45e2035326bc45d0..3a7ff1ab33fcc88d2fb28fb6ecac93cce28868c6 100644
--- a/test/reaction-lib.ts
+++ b/test/reaction-lib.ts
@@ -6,6 +6,10 @@
  *
  * To specify test:
  * > mocha test/reaction-lib.ts --require ts-node/register -g 'test name'
+ *
+ * If the tests not start, try set following enviroment variables:
+ * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
+ * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
  */
 
 /*
diff --git a/test/streaming.ts b/test/streaming.ts
index 500324d520d4e5603b3d308210296821e19e2589..74a5aaa0b4bd9168cc6c45e43f1b4006a977a148 100644
--- a/test/streaming.ts
+++ b/test/streaming.ts
@@ -6,143 +6,844 @@
  *
  * To specify test:
  * > mocha test/streaming.ts --require ts-node/register -g 'test name'
+ *
+ * If the tests not start, try set following enviroment variables:
+ * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
+ * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
  */
 
-import * as http from 'http';
-import * as WebSocket from 'ws';
+process.env.NODE_ENV = 'test';
+
 import * as assert from 'assert';
-import { _signup, _request, _uploadFile, _post, _react, resetDb } from './utils';
+import * as childProcess from 'child_process';
+import { connectStream, signup, request, post } from './utils';
+import { Following } from '../built/models/entities/following';
+const initDb = require('../built/db/postgre.js').initDb;
 
-//#region process
-Error.stackTraceLimit = Infinity;
+describe('Streaming', () => {
+	let p: childProcess.ChildProcess;
+	let Followings: any;
 
-// During the test the env variable is set to test
-process.env.NODE_ENV = 'test';
+	beforeEach(done => {
+		p = childProcess.spawn('node', [__dirname + '/../index.js'], {
+			stdio: ['inherit', 'inherit', 'ipc'],
+			env: { NODE_ENV: 'test' }
+		});
+		p.on('message', message => {
+			if (message === 'ok') {
+				(p.channel as any).onread = () => {};
+				initDb(true).then(async connection => {
+					Followings = connection.getRepository(Following);
+					done();
+				});
+			}
+		});
+	});
 
-// Display detail of unhandled promise rejection
-process.on('unhandledRejection', console.dir);
-//#endregion
+	afterEach(() => {
+		p.kill();
+	});
 
-const app = require('../built/server/api').default;
-const server = require('../built/server').startServer();
-const db = require('../built/db/mongodb').default;
+	const follow = async (follower, followee) => {
+		await Followings.save({
+			id: 'a',
+			createdAt: new Date(),
+			followerId: follower.id,
+			followeeId: followee.id,
+			followerHost: follower.host,
+			followerInbox: null,
+			followerSharedInbox: null,
+			followeeHost: followee.host,
+			followeeInbox: null,
+			followeeSharedInbox: null
+		});
+	};
 
-const apiServer = http.createServer(app.callback());
+	it('mention event', () => new Promise(async done => {
+		const alice = await signup({ username: 'alice' });
+		const bob = await signup({ username: 'bob' });
 
-//#region Utilities
-const request = _request(apiServer);
-const signup = _signup(request);
-const post = _post(request);
-//#endregion
+		const ws = await connectStream(bob, 'main', ({ type, body }) => {
+			if (type == 'mention') {
+				assert.deepStrictEqual(body.userId, alice.id);
+				ws.close();
+				done();
+			}
+		});
 
-describe('Streaming', () => {
-	// Reset database each test
-	beforeEach(resetDb(db));
+		post(alice, {
+			text: 'foo @bob bar'
+		});
+	}));
+
+	it('renote event', () => new Promise(async done => {
+		const alice = await signup({ username: 'alice' });
+		const bob = await signup({ username: 'bob' });
+		const bobNote = await post(bob, {
+			text: 'foo'
+		});
+
+		const ws = await connectStream(bob, 'main', ({ type, body }) => {
+			if (type == 'renote') {
+				assert.deepStrictEqual(body.renoteId, bobNote.id);
+				ws.close();
+				done();
+			}
+		});
+
+		post(alice, {
+			renoteId: bobNote.id
+		});
+	}));
+
+	describe('Home Timeline', () => {
+		it('自分の投稿が流れる', () => new Promise(async done => {
+			const post = {
+				text: 'foo'
+			};
+
+			const me = await signup();
+
+			const ws = await connectStream(me, 'homeTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					assert.deepStrictEqual(body.text, post.text);
+					ws.close();
+					done();
+				}
+			});
+
+			request('/notes/create', post, me);
+		}));
+
+		it('フォローしているユーザーの投稿が流れる', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob' });
+
+			// Alice が Bob をフォロー
+			await request('/following/create', {
+				userId: bob.id
+			}, alice);
+
+			const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					assert.deepStrictEqual(body.userId, bob.id);
+					ws.close();
+					done();
+				}
+			});
+
+			post(bob, {
+				text: 'foo'
+			});
+		}));
+
+		it('フォローしていないユーザーの投稿は流れない', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob' });
+
+			let fired = false;
+
+			const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					fired = true;
+				}
+			});
+
+			post(bob, {
+				text: 'foo'
+			});
+
+			setTimeout(() => {
+				assert.strictEqual(fired, false);
+				ws.close();
+				done();
+			}, 3000);
+		}));
+
+		it('フォローしているユーザーのダイレクト投稿が流れる', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob' });
+
+			// Alice が Bob をフォロー
+			await request('/following/create', {
+				userId: bob.id
+			}, alice);
+
+			const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					assert.deepStrictEqual(body.userId, bob.id);
+					assert.deepStrictEqual(body.text, 'foo');
+					ws.close();
+					done();
+				}
+			});
+
+			// Bob が Alice 宛てのダイレクト投稿
+			post(bob, {
+				text: 'foo',
+				visibility: 'specified',
+				visibleUserIds: [alice.id]
+			});
+		}));
+
+		it('フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob' });
+			const carol = await signup({ username: 'carol' });
+
+			// Alice が Bob をフォロー
+			await request('/following/create', {
+				userId: bob.id
+			}, alice);
+
+			let fired = false;
+
+			const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					fired = true;
+				}
+			});
 
-	after(() => {
-		server.close();
+			// Bob が Carol 宛てのダイレクト投稿
+			post(bob, {
+				text: 'foo',
+				visibility: 'specified',
+				visibleUserIds: [carol.id]
+			});
+
+			setTimeout(() => {
+				assert.strictEqual(fired, false);
+				ws.close();
+				done();
+			}, 3000);
+		}));
 	});
 
-	it('投稿がタイムラインに流れる', () => new Promise(async done => {
-		const post = {
-			text: 'foo'
-		};
+	describe('Local Timeline', () => {
+		it('自分の投稿が流れる', () => new Promise(async done => {
+			const me = await signup();
+
+			const ws = await connectStream(me, 'localTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					assert.deepStrictEqual(body.userId, me.id);
+					ws.close();
+					done();
+				}
+			});
+
+			post(me, {
+				text: 'foo'
+			});
+		}));
 
-		const me = await signup();
-		const ws = new WebSocket(`ws://localhost/streaming?i=${me.token}`);
+		it('フォローしていないローカルユーザーの投稿が流れる', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob' });
 
-		ws.on('open', () => {
-			ws.on('message', data => {
-				const msg = JSON.parse(data.toString());
-				if (msg.type == 'channel' && msg.body.id == 'a') {
-					if (msg.body.type == 'note') {
-						assert.deepStrictEqual(msg.body.body.text, post.text);
-						ws.close();
-						done();
-					}
-				} else if (msg.type == 'connected' && msg.body.id == 'a') {
-					request('/notes/create', post, me);
+			const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					assert.deepStrictEqual(body.userId, bob.id);
+					ws.close();
+					done();
 				}
 			});
 
-			ws.send(JSON.stringify({
-				type: 'connect',
-				body: {
-					channel: 'homeTimeline',
-					id: 'a',
-					pong: true
+			post(bob, {
+				text: 'foo'
+			});
+		}));
+
+		it('リモートユーザーの投稿は流れない', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob', host: 'example.com' });
+
+			let fired = false;
+
+			const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					fired = true;
 				}
-			}));
-		});
-	}));
+			});
 
-	it('mention event', () => new Promise(async done => {
-		const alice = await signup({ username: 'alice' });
-		const bob = await signup({ username: 'bob' });
-		const aliceNote = {
-			text: 'foo @bob bar'
-		};
+			post(bob, {
+				text: 'foo'
+			});
 
-		const ws = new WebSocket(`ws://localhost/streaming?i=${bob.token}`);
+			setTimeout(() => {
+				assert.strictEqual(fired, false);
+				ws.close();
+				done();
+			}, 3000);
+		}));
 
-		ws.on('open', () => {
-			ws.on('message', data => {
-				const msg = JSON.parse(data.toString());
-				if (msg.type == 'channel' && msg.body.id == 'a') {
-					if (msg.body.type == 'mention') {
-						assert.deepStrictEqual(msg.body.body.text, aliceNote.text);
-						ws.close();
-						done();
-					}
-				} else if (msg.type == 'connected' && msg.body.id == 'a') {
-					request('/notes/create', aliceNote, alice);
+		it('フォローしてたとしてもリモートユーザーの投稿は流れない', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob', host: 'example.com' });
+
+			// Alice が Bob をフォロー
+			await request('/following/create', {
+				userId: bob.id
+			}, alice);
+
+			let fired = false;
+
+			const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					fired = true;
 				}
 			});
 
-			ws.send(JSON.stringify({
-				type: 'connect',
-				body: {
-					channel: 'main',
-					id: 'a',
-					pong: true
+			post(bob, {
+				text: 'foo'
+			});
+
+			setTimeout(() => {
+				assert.strictEqual(fired, false);
+				ws.close();
+				done();
+			}, 3000);
+		}));
+
+		it('ホーム指定の投稿は流れない', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob' });
+
+			let fired = false;
+
+			const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					fired = true;
 				}
-			}));
-		});
-	}));
+			});
 
-	it('renote event', () => new Promise(async done => {
-		const alice = await signup({ username: 'alice' });
-		const bob = await signup({ username: 'bob' });
-		const bobNote = await post(bob, {
-			text: 'foo'
-		});
+			// ホーム指定
+			post(bob, {
+				text: 'foo',
+				visibility: 'home'
+			});
+
+			setTimeout(() => {
+				assert.strictEqual(fired, false);
+				ws.close();
+				done();
+			}, 3000);
+		}));
+
+		it('フォローしているローカルユーザーのダイレクト投稿が流れる', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob' });
 
-		const ws = new WebSocket(`ws://localhost/streaming?i=${bob.token}`);
+			// Alice が Bob をフォロー
+			await request('/following/create', {
+				userId: bob.id
+			}, alice);
 
-		ws.on('open', () => {
-			ws.on('message', data => {
-				const msg = JSON.parse(data.toString());
-				if (msg.type == 'channel' && msg.body.id == 'a') {
-					if (msg.body.type == 'renote') {
-						assert.deepStrictEqual(msg.body.body.renoteId, bobNote.id);
-						ws.close();
-						done();
-					}
-				} else if (msg.type == 'connected' && msg.body.id == 'a') {
-					request('/notes/create', {
-						renoteId: bobNote.id
-					}, alice);
+			const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					assert.deepStrictEqual(body.userId, bob.id);
+					assert.deepStrictEqual(body.text, 'foo');
+					ws.close();
+					done();
 				}
 			});
 
-			ws.send(JSON.stringify({
-				type: 'connect',
-				body: {
-					channel: 'main',
-					id: 'a',
-					pong: true
+			// Bob が Alice 宛てのダイレクト投稿
+			post(bob, {
+				text: 'foo',
+				visibility: 'specified',
+				visibleUserIds: [alice.id]
+			});
+		}));
+
+		it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob' });
+
+			let fired = false;
+
+			const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					fired = true;
 				}
-			}));
-		});
-	}));
+			});
+
+			// フォロワー宛て投稿
+			post(bob, {
+				text: 'foo',
+				visibility: 'followers'
+			});
+
+			setTimeout(() => {
+				assert.strictEqual(fired, false);
+				ws.close();
+				done();
+			}, 3000);
+		}));
+	});
+
+	describe('Social Timeline', () => {
+		it('自分の投稿が流れる', () => new Promise(async done => {
+			const me = await signup();
+
+			const ws = await connectStream(me, 'socialTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					assert.deepStrictEqual(body.userId, me.id);
+					ws.close();
+					done();
+				}
+			});
+
+			post(me, {
+				text: 'foo'
+			});
+		}));
+
+		it('フォローしていないローカルユーザーの投稿が流れる', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob' });
+
+			const ws = await connectStream(alice, 'socialTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					assert.deepStrictEqual(body.userId, bob.id);
+					ws.close();
+					done();
+				}
+			});
+
+			post(bob, {
+				text: 'foo'
+			});
+		}));
+
+		it('フォローしているリモートユーザーの投稿が流れる', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob', host: 'example.com' });
+
+			// Alice が Bob をフォロー
+			await follow(alice, bob);
+
+			const ws = await connectStream(alice, 'socialTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					assert.deepStrictEqual(body.userId, bob.id);
+					ws.close();
+					done();
+				}
+			});
+
+			post(bob, {
+				text: 'foo'
+			});
+		}));
+
+		it('フォローしていないリモートユーザーの投稿は流れない', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob', host: 'example.com' });
+
+			let fired = false;
+
+			const ws = await connectStream(alice, 'socialTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					fired = true;
+				}
+			});
+
+			post(bob, {
+				text: 'foo'
+			});
+
+			setTimeout(() => {
+				assert.strictEqual(fired, false);
+				ws.close();
+				done();
+			}, 3000);
+		}));
+
+		it('フォローしているユーザーのダイレクト投稿が流れる', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob' });
+
+			// Alice が Bob をフォロー
+			await request('/following/create', {
+				userId: bob.id
+			}, alice);
+
+			const ws = await connectStream(alice, 'socialTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					assert.deepStrictEqual(body.userId, bob.id);
+					assert.deepStrictEqual(body.text, 'foo');
+					ws.close();
+					done();
+				}
+			});
+
+			// Bob が Alice 宛てのダイレクト投稿
+			post(bob, {
+				text: 'foo',
+				visibility: 'specified',
+				visibleUserIds: [alice.id]
+			});
+		}));
+
+		it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob' });
+
+			let fired = false;
+
+			const ws = await connectStream(alice, 'socialTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					fired = true;
+				}
+			});
+
+			// フォロワー宛て投稿
+			post(bob, {
+				text: 'foo',
+				visibility: 'followers'
+			});
+
+			setTimeout(() => {
+				assert.strictEqual(fired, false);
+				ws.close();
+				done();
+			}, 3000);
+		}));
+	});
+
+	describe('Global Timeline', () => {
+		it('フォローしていないローカルユーザーの投稿が流れる', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob' });
+
+			const ws = await connectStream(alice, 'globalTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					assert.deepStrictEqual(body.userId, bob.id);
+					ws.close();
+					done();
+				}
+			});
+
+			post(bob, {
+				text: 'foo'
+			});
+		}));
+
+		it('フォローしていないリモートユーザーの投稿が流れる', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob', host: 'example.com' });
+
+			const ws = await connectStream(alice, 'globalTimeline', ({ type, body }) => {
+				if (type == 'note') {
+					assert.deepStrictEqual(body.userId, bob.id);
+					ws.close();
+					done();
+				}
+			});
+
+			post(bob, {
+				text: 'foo'
+			});
+		}));
+	});
+
+	describe('UserList Timeline', () => {
+		it('リストに入れているユーザーの投稿が流れる', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob' });
+
+			// リスト作成
+			const list = await request('/users/lists/create', {
+				title: 'my list'
+			}, alice).then(x => x.body);
+
+			// Alice が Bob をリスイン
+			await request('/users/lists/push', {
+				listId: list.id,
+				userId: bob.id
+			}, alice);
+
+			const ws = await connectStream(alice, 'userList', ({ type, body }) => {
+				if (type == 'note') {
+					assert.deepStrictEqual(body.userId, bob.id);
+					ws.close();
+					done();
+				}
+			}, {
+				listId: list.id
+			});
+
+			post(bob, {
+				text: 'foo'
+			});
+		}));
+
+		it('リストに入れていないユーザーの投稿は流れない', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob' });
+
+			// リスト作成
+			const list = await request('/users/lists/create', {
+				title: 'my list'
+			}, alice).then(x => x.body);
+
+			let fired = false;
+
+			const ws = await connectStream(alice, 'userList', ({ type, body }) => {
+				if (type == 'note') {
+					fired = true;
+				}
+			}, {
+				listId: list.id
+			});
+
+			post(bob, {
+				text: 'foo'
+			});
+
+			setTimeout(() => {
+				assert.strictEqual(fired, false);
+				ws.close();
+				done();
+			}, 3000);
+		}));
+
+		// #4471
+		it('リストに入れているユーザーのダイレクト投稿が流れる', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob' });
+
+			// リスト作成
+			const list = await request('/users/lists/create', {
+				title: 'my list'
+			}, alice).then(x => x.body);
+
+			// Alice が Bob をリスイン
+			await request('/users/lists/push', {
+				listId: list.id,
+				userId: bob.id
+			}, alice);
+
+			const ws = await connectStream(alice, 'userList', ({ type, body }) => {
+				if (type == 'note') {
+					assert.deepStrictEqual(body.userId, bob.id);
+					assert.deepStrictEqual(body.text, 'foo');
+					ws.close();
+					done();
+				}
+			}, {
+				listId: list.id
+			});
+
+			// Bob が Alice 宛てのダイレクト投稿
+			post(bob, {
+				text: 'foo',
+				visibility: 'specified',
+				visibleUserIds: [alice.id]
+			});
+		}));
+
+		// #4335
+		it('リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => {
+			const alice = await signup({ username: 'alice' });
+			const bob = await signup({ username: 'bob' });
+
+			// リスト作成
+			const list = await request('/users/lists/create', {
+				title: 'my list'
+			}, alice).then(x => x.body);
+
+			// Alice が Bob をリスイン
+			await request('/users/lists/push', {
+				listId: list.id,
+				userId: bob.id
+			}, alice);
+
+			let fired = false;
+
+			const ws = await connectStream(alice, 'userList', ({ type, body }) => {
+				if (type == 'note') {
+					fired = true;
+				}
+			}, {
+				listId: list.id
+			});
+
+			// フォロワー宛て投稿
+			post(bob, {
+				text: 'foo',
+				visibility: 'followers'
+			});
+
+			setTimeout(() => {
+				assert.strictEqual(fired, false);
+				ws.close();
+				done();
+			}, 3000);
+		}));
+	});
+
+	describe('Hashtag Timeline', () => {
+		it('指定したハッシュタグの投稿が流れる', () => new Promise(async done => {
+			const me = await signup();
+
+			const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
+				if (type == 'note') {
+					assert.deepStrictEqual(body.text, '#foo');
+					ws.close();
+					done();
+				}
+			}, {
+				q: [
+					['foo']
+				]
+			});
+
+			post(me, {
+				text: '#foo'
+			});
+		}));
+
+		it('指定したハッシュタグの投稿が流れる (AND)', () => new Promise(async done => {
+			const me = await signup();
+
+			let fooCount = 0;
+			let barCount = 0;
+			let fooBarCount = 0;
+
+			const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
+				if (type == 'note') {
+					if (body.text === '#foo') fooCount++;
+					if (body.text === '#bar') barCount++;
+					if (body.text === '#foo #bar') fooBarCount++;
+				}
+			}, {
+				q: [
+					['foo', 'bar']
+				]
+			});
+
+			post(me, {
+				text: '#foo'
+			});
+
+			post(me, {
+				text: '#bar'
+			});
+
+			post(me, {
+				text: '#foo #bar'
+			});
+
+			setTimeout(() => {
+				assert.strictEqual(fooCount, 0);
+				assert.strictEqual(barCount, 0);
+				assert.strictEqual(fooBarCount, 1);
+				ws.close();
+				done();
+			}, 3000);
+		}));
+
+		it('指定したハッシュタグの投稿が流れる (OR)', () => new Promise(async done => {
+			const me = await signup();
+
+			let fooCount = 0;
+			let barCount = 0;
+			let fooBarCount = 0;
+			let piyoCount = 0;
+
+			const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
+				if (type == 'note') {
+					if (body.text === '#foo') fooCount++;
+					if (body.text === '#bar') barCount++;
+					if (body.text === '#foo #bar') fooBarCount++;
+					if (body.text === '#piyo') piyoCount++;
+				}
+			}, {
+				q: [
+					['foo'],
+					['bar']
+				]
+			});
+
+			post(me, {
+				text: '#foo'
+			});
+
+			post(me, {
+				text: '#bar'
+			});
+
+			post(me, {
+				text: '#foo #bar'
+			});
+
+			post(me, {
+				text: '#piyo'
+			});
+
+			setTimeout(() => {
+				assert.strictEqual(fooCount, 1);
+				assert.strictEqual(barCount, 1);
+				assert.strictEqual(fooBarCount, 1);
+				assert.strictEqual(piyoCount, 0);
+				ws.close();
+				done();
+			}, 3000);
+		}));
+
+		it('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise(async done => {
+			const me = await signup();
+
+			let fooCount = 0;
+			let barCount = 0;
+			let fooBarCount = 0;
+			let piyoCount = 0;
+			let waaaCount = 0;
+
+			const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
+				if (type == 'note') {
+					if (body.text === '#foo') fooCount++;
+					if (body.text === '#bar') barCount++;
+					if (body.text === '#foo #bar') fooBarCount++;
+					if (body.text === '#piyo') piyoCount++;
+					if (body.text === '#waaa') waaaCount++;
+				}
+			}, {
+				q: [
+					['foo', 'bar'],
+					['piyo']
+				]
+			});
+
+			post(me, {
+				text: '#foo'
+			});
+
+			post(me, {
+				text: '#bar'
+			});
+
+			post(me, {
+				text: '#foo #bar'
+			});
+
+			post(me, {
+				text: '#piyo'
+			});
+
+			post(me, {
+				text: '#waaa'
+			});
+
+			setTimeout(() => {
+				assert.strictEqual(fooCount, 0);
+				assert.strictEqual(barCount, 0);
+				assert.strictEqual(fooBarCount, 1);
+				assert.strictEqual(piyoCount, 1);
+				assert.strictEqual(waaaCount, 0);
+				ws.close();
+				done();
+			}, 3000);
+		}));
+	});
 });
diff --git a/test/user-notes.ts b/test/user-notes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5e457d66921f8a894047a73e874411230343d33c
--- /dev/null
+++ b/test/user-notes.ts
@@ -0,0 +1,86 @@
+/*
+ * Tests of Note
+ *
+ * How to run the tests:
+ * > mocha test/user-notes.ts --require ts-node/register
+ *
+ * To specify test:
+ * > mocha test/user-notes.ts --require ts-node/register -g 'test name'
+ *
+ * If the tests not start, try set following enviroment variables:
+ * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true
+ * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754
+ */
+
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import * as childProcess from 'child_process';
+import { async, signup, request, post, uploadFile } from './utils';
+
+describe('users/notes', () => {
+	let p: childProcess.ChildProcess;
+
+	let alice: any;
+	let jpgNote: any;
+	let pngNote: any;
+	let jpgPngNote: any;
+
+	before(done => {
+		p = childProcess.spawn('node', [__dirname + '/../index.js'], {
+			stdio: ['inherit', 'inherit', 'ipc'],
+			env: { NODE_ENV: 'test' }
+		});
+		p.on('message', async message => {
+			if (message === 'ok') {
+				(p.channel as any).onread = () => {};
+
+				alice = await signup({ username: 'alice' });
+				const jpg = await uploadFile(alice, __dirname + '/resources/Lenna.jpg');
+				const png = await uploadFile(alice, __dirname + '/resources/Lenna.png');
+				jpgNote = await post(alice, {
+					fileIds: [jpg.id]
+				});
+				pngNote = await post(alice, {
+					fileIds: [png.id]
+				});
+				jpgPngNote = await post(alice, {
+					fileIds: [jpg.id, png.id]
+				});
+
+				done();
+			}
+		});
+	});
+
+	after(() => {
+		p.kill();
+	});
+
+	it('ファイルタイプ指定 (jpg)', async(async () => {
+		const res = await request('/users/notes', {
+			userId: alice.id,
+			fileType: ['image/jpeg']
+		}, alice);
+
+		assert.strictEqual(res.status, 200);
+		assert.strictEqual(Array.isArray(res.body), true);
+		assert.strictEqual(res.body.length, 2);
+		assert.strictEqual(res.body.some(note => note.id === jpgNote.id), true);
+		assert.strictEqual(res.body.some(note => note.id === jpgPngNote.id), true);
+	}));
+
+	it('ファイルタイプ指定 (jpg or png)', async(async () => {
+		const res = await request('/users/notes', {
+			userId: alice.id,
+			fileType: ['image/jpeg', 'image/png']
+		}, alice);
+
+		assert.strictEqual(res.status, 200);
+		assert.strictEqual(Array.isArray(res.body), true);
+		assert.strictEqual(res.body.length, 3);
+		assert.strictEqual(res.body.some(note => note.id === jpgNote.id), true);
+		assert.strictEqual(res.body.some(note => note.id === pngNote.id), true);
+		assert.strictEqual(res.body.some(note => note.id === jpgPngNote.id), true);
+	}));
+});
diff --git a/test/utils.ts b/test/utils.ts
index 1377122478d604a0f2f24f856ae8e603f57682a2..fbba9a68c932846212a8542b54b9addfba5e25a8 100644
--- a/test/utils.ts
+++ b/test/utils.ts
@@ -1,7 +1,7 @@
 import * as fs from 'fs';
-import * as http from 'http';
-import * as assert from 'chai';
-assert.use(require('chai-http'));
+import * as WebSocket from 'ws';
+const fetch = require('node-fetch');
+import * as req from 'request';
 
 export const async = (fn: Function) => (done: Function) => {
 	fn().then(() => {
@@ -11,19 +11,31 @@ export const async = (fn: Function) => (done: Function) => {
 	});
 };
 
-export const _request = (server: http.Server) => async (endpoint: string, params: any, me?: any): Promise<ChaiHttp.Response> => {
+export const request = async (endpoint: string, params: any, me?: any): Promise<{ body: any, status: number }> => {
 	const auth = me ? {
 		i: me.token
 	} : {};
 
-	const res = await assert.request(server)
-		.post(endpoint)
-		.send(Object.assign(auth, params));
+	try {
+		const res = await fetch('http://localhost:80/api' + endpoint, {
+			method: 'POST',
+			body: JSON.stringify(Object.assign(auth, params))
+		});
 
-	return res;
+		const status = res.status;
+		const body = res.status !== 204 ? await res.json().catch() : null;
+
+		return {
+			body, status
+		};
+	} catch (e) {
+		return {
+			body: null, status: 500
+		};
+	}
 };
 
-export const _signup = (request: ReturnType<typeof _request>) => async (params?: any): Promise<any> => {
+export const signup = async (params?: any): Promise<any> => {
 	const q = Object.assign({
 		username: 'test',
 		password: 'test'
@@ -34,50 +46,59 @@ export const _signup = (request: ReturnType<typeof _request>) => async (params?:
 	return res.body;
 };
 
-export const _post = (request: ReturnType<typeof _request>) => async (user: any, params?: any): Promise<any> => {
+export const post = async (user: any, params?: any): Promise<any> => {
 	const q = Object.assign({
 		text: 'test'
 	}, params);
 
 	const res = await request('/notes/create', q, user);
 
-	return res.body.createdNote;
+	return res.body ? res.body.createdNote : null;
 };
 
-export const _react = (request: ReturnType<typeof _request>) => async (user: any, note: any, reaction: string): Promise<any> => {
+export const react = async (user: any, note: any, reaction: string): Promise<any> => {
 	await request('/notes/reactions/create', {
 		noteId: note.id,
 		reaction: reaction
 	}, user);
 };
 
-export const _uploadFile = (server: http.Server) => async (user: any): Promise<any> => {
-	const res = await assert.request(server)
-		.post('/drive/files/create')
-		.field('i', user.token)
-		.attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png');
+export const uploadFile = (user: any, path?: string): Promise<any> => new Promise((ok, rej) => {
+	req.post({
+		url: 'http://localhost:80/api/drive/files/create',
+		formData: {
+			i: user.token,
+			file: fs.createReadStream(path || __dirname + '/resources/Lenna.png')
+		},
+		json: true
+	}, (err, httpResponse, body) => {
+		ok(body);
+	});
+});
 
-	return res.body;
-};
+export function connectStream(user: any, channel: string, listener: any, params?: any): Promise<WebSocket> {
+	return new Promise((res, rej) => {
+		const ws = new WebSocket(`ws://localhost/streaming?i=${user.token}`);
 
-export const resetDb = (db: any) => () => new Promise(res => {
-	// APIがなにかレスポンスを返した後に、後処理を行う場合があり、
-	// レスポンスを受け取ってすぐデータベースをリセットすると
-	// その後処理と競合し(テスト自体は合格するものの)エラーがコンソールに出力され
-	// 見た目的に気持ち悪くなるので、後処理が終るのを待つために500msくらい待ってから
-	// データベースをリセットするようにする
-	setTimeout(async () => {
-		await Promise.all([
-			db.get('users').drop(),
-			db.get('notes').drop(),
-			db.get('driveFiles.files').drop(),
-			db.get('driveFiles.chunks').drop(),
-			db.get('driveFolders').drop(),
-			db.get('apps').drop(),
-			db.get('accessTokens').drop(),
-			db.get('authSessions').drop()
-		]);
-
-		res();
-	}, 500);
-});
+		ws.on('open', () => {
+			ws.on('message', data => {
+				const msg = JSON.parse(data.toString());
+				if (msg.type == 'channel' && msg.body.id == 'a') {
+					listener(msg.body);
+				} else if (msg.type == 'connected' && msg.body.id == 'a') {
+					res(ws);
+				}
+			});
+
+			ws.send(JSON.stringify({
+				type: 'connect',
+				body: {
+					channel: channel,
+					id: 'a',
+					pong: true,
+					params: params
+				}
+			}));
+		});
+	});
+}
diff --git a/tsconfig.json b/tsconfig.json
index 09da750c35ec933ad650d390090369c2ae1bab7c..6bd857120707ece10ef0d65e17238c8fb040e54f 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -16,6 +16,7 @@
     "strict": true,
     "strictNullChecks": false,
     "experimentalDecorators": true,
+    "emitDecoratorMetadata": true,
     "resolveJsonModule": true,
     "typeRoots": [
       "node_modules/@types",