diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d934f6758a..2e973b55b5 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -98,6 +98,7 @@ test:build:backend_ts_only:
       changes:
         paths:
           - packages/backend/**/*
+          - packages/firefish-js/**/*
           - packages/megalodon/**/*
       when: always
   before_script:
@@ -115,7 +116,7 @@ test:build:backend_ts_only:
     - psql --host postgres --user "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" --command 'CREATE EXTENSION pgroonga'
   script:
     - pnpm install --frozen-lockfile
-    - pnpm --filter 'backend' --filter 'megalodon' run build:debug
+    - pnpm --filter 'backend' --filter 'firefish-js' --filter 'megalodon' run build:debug
     - pnpm run migrate
 
 test:build:client_only:
diff --git a/packages/backend/package.json b/packages/backend/package.json
index c427084f05..ddc28deca1 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -55,6 +55,7 @@
 		"escape-regexp": "0.0.1",
 		"feed": "4.2.2",
 		"file-type": "19.0.0",
+		"firefish-js": "workspace:*",
 		"fluent-ffmpeg": "2.1.2",
 		"form-data": "4.0.0",
 		"got": "14.2.1",
diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts
index 73600832ce..b4148e7e27 100644
--- a/packages/backend/src/misc/schema.ts
+++ b/packages/backend/src/misc/schema.ts
@@ -1,234 +1,7 @@
-import {
-	packedUserLiteSchema,
-	packedUserDetailedNotMeOnlySchema,
-	packedMeDetailedOnlySchema,
-	packedUserDetailedNotMeSchema,
-	packedMeDetailedSchema,
-	packedUserDetailedSchema,
-	packedUserSchema,
-} from "@/models/schema/user.js";
-import { packedNoteSchema } from "@/models/schema/note.js";
-import { packedUserListSchema } from "@/models/schema/user-list.js";
-import { packedAppSchema } from "@/models/schema/app.js";
-import { packedMessagingMessageSchema } from "@/models/schema/messaging-message.js";
-import { packedNotificationSchema } from "@/models/schema/notification.js";
-import { packedDriveFileSchema } from "@/models/schema/drive-file.js";
-import { packedDriveFolderSchema } from "@/models/schema/drive-folder.js";
-import { packedFollowingSchema } from "@/models/schema/following.js";
-import { packedMutingSchema } from "@/models/schema/muting.js";
-import { packedRenoteMutingSchema } from "@/models/schema/renote-muting.js";
-import { packedReplyMutingSchema } from "@/models/schema/reply-muting.js";
-import { packedBlockingSchema } from "@/models/schema/blocking.js";
-import { packedNoteReactionSchema } from "@/models/schema/note-reaction.js";
-import { packedHashtagSchema } from "@/models/schema/hashtag.js";
-import { packedPageSchema } from "@/models/schema/page.js";
-import { packedUserGroupSchema } from "@/models/schema/user-group.js";
-import { packedNoteFavoriteSchema } from "@/models/schema/note-favorite.js";
-import { packedChannelSchema } from "@/models/schema/channel.js";
-import { packedAntennaSchema } from "@/models/schema/antenna.js";
-import { packedClipSchema } from "@/models/schema/clip.js";
-import { packedFederationInstanceSchema } from "@/models/schema/federation-instance.js";
-import { packedQueueCountSchema } from "@/models/schema/queue.js";
-import { packedGalleryPostSchema } from "@/models/schema/gallery-post.js";
-import { packedEmojiSchema } from "@/models/schema/emoji.js";
-import { packedNoteEdit } from "@/models/schema/note-edit.js";
-import { packedNoteFileSchema } from "@/models/schema/note-file.js";
-import { packedAbuseUserReportSchema } from "@/models/schema/abuse-user-report.js";
+// TODO: use firefish-js
+import { Schema as _Schema } from "firefish-js";
 
-export const refs = {
-	AbuseUserReport: packedAbuseUserReportSchema,
-	UserLite: packedUserLiteSchema,
-	UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema,
-	MeDetailedOnly: packedMeDetailedOnlySchema,
-	UserDetailedNotMe: packedUserDetailedNotMeSchema,
-	MeDetailed: packedMeDetailedSchema,
-	UserDetailed: packedUserDetailedSchema,
-	User: packedUserSchema,
-
-	UserList: packedUserListSchema,
-	UserGroup: packedUserGroupSchema,
-	App: packedAppSchema,
-	MessagingMessage: packedMessagingMessageSchema,
-	Note: packedNoteSchema,
-	NoteFile: packedNoteFileSchema,
-	NoteEdit: packedNoteEdit,
-	NoteReaction: packedNoteReactionSchema,
-	NoteFavorite: packedNoteFavoriteSchema,
-	Notification: packedNotificationSchema,
-	DriveFile: packedDriveFileSchema,
-	DriveFolder: packedDriveFolderSchema,
-	Following: packedFollowingSchema,
-	Muting: packedMutingSchema,
-	RenoteMuting: packedRenoteMutingSchema,
-	ReplyMuting: packedReplyMutingSchema,
-	Blocking: packedBlockingSchema,
-	Hashtag: packedHashtagSchema,
-	Page: packedPageSchema,
-	Channel: packedChannelSchema,
-	QueueCount: packedQueueCountSchema,
-	Antenna: packedAntennaSchema,
-	Clip: packedClipSchema,
-	FederationInstance: packedFederationInstanceSchema,
-	GalleryPost: packedGalleryPostSchema,
-	Emoji: packedEmojiSchema,
-};
-
-export type Packed<x extends keyof typeof refs> = SchemaType<(typeof refs)[x]>;
-
-type TypeStringef =
-	| "null"
-	| "boolean"
-	| "integer"
-	| "number"
-	| "string"
-	| "array"
-	| "object"
-	| "any";
-type StringDefToType<T extends TypeStringef> = T extends "null"
-	? null
-	: T extends "boolean"
-		? boolean
-		: T extends "integer"
-			? number
-			: T extends "number"
-				? number
-				: T extends "string"
-					? string | Date
-					: T extends "array"
-						? ReadonlyArray<any>
-						: T extends "object"
-							? Record<string, any>
-							: any;
-
-// https://swagger.io/specification/?sbsearch=optional#schema-object
-type OfSchema = {
-	readonly anyOf?: ReadonlyArray<Schema>;
-	readonly oneOf?: ReadonlyArray<Schema>;
-	readonly allOf?: ReadonlyArray<Schema>;
-};
-
-export interface Schema extends OfSchema {
-	readonly type?: TypeStringef;
-	readonly nullable?: boolean;
-	readonly optional?: boolean;
-	readonly items?: Schema;
-	readonly properties?: Obj;
-	readonly required?: ReadonlyArray<
-		Extract<keyof NonNullable<this["properties"]>, string>
-	>;
-	readonly description?: string;
-	readonly example?: any;
-	readonly format?: string;
-	readonly ref?: keyof typeof refs;
-	readonly enum?: ReadonlyArray<string>;
-	readonly default?:
-		| (this["type"] extends TypeStringef ? StringDefToType<this["type"]> : any)
-		| null;
-	readonly maxLength?: number;
-	readonly minLength?: number;
-	readonly maximum?: number;
-	readonly minimum?: number;
-	readonly pattern?: string;
-}
-
-type RequiredPropertyNames<s extends Obj> = {
-	[K in keyof s]: // K is not optional
-	s[K]["optional"] extends false
-		? K
-		: // K has default value
-			s[K]["default"] extends
-					| null
-					| string
-					| number
-					| boolean
-					| Record<string, unknown>
-			? K
-			: never;
-}[keyof s];
-
-export type Obj = Record<string, Schema>;
-
-// https://github.com/misskey-dev/misskey/issues/8535
-// To avoid excessive stack depth error,
-// deceive TypeScript with UnionToIntersection (or more precisely, `infer` expression within it).
-export type ObjType<
-	s extends Obj,
-	RequiredProps extends keyof s,
-> = UnionToIntersection<
-	{
-		-readonly [R in RequiredPropertyNames<s>]-?: SchemaType<s[R]>;
-	} & {
-		-readonly [R in RequiredProps]-?: SchemaType<s[R]>;
-	} & {
-		-readonly [P in keyof s]?: SchemaType<s[P]>;
-	}
->;
-
-type NullOrUndefined<p extends Schema, T> =
-	| (p["nullable"] extends true ? null : never)
-	| (p["optional"] extends true ? undefined : never)
-	| T;
-
-// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
-// Get intersection from union
-type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
-	k: infer I,
-) => void
-	? I
-	: never;
-
-// https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552
-// To get union, we use `Foo extends any ? Hoge<Foo> : never`
-type UnionSchemaType<
-	a extends readonly any[],
-	X extends Schema = a[number],
-> = X extends any ? SchemaType<X> : never;
-type ArrayUnion<T> = T extends any ? Array<T> : never;
-
-export type SchemaTypeDef<p extends Schema> = p["type"] extends "null"
-	? null
-	: p["type"] extends "integer"
-		? number
-		: p["type"] extends "number"
-			? number
-			: p["type"] extends "string"
-				? p["enum"] extends readonly string[]
-					? p["enum"][number]
-					: p["format"] extends "date-time"
-						? string
-						: // Dateにする??
-							string
-				: p["type"] extends "boolean"
-					? boolean
-					: p["type"] extends "object"
-						? p["ref"] extends keyof typeof refs
-							? Packed<p["ref"]>
-							: p["properties"] extends NonNullable<Obj>
-								? ObjType<p["properties"], NonNullable<p["required"]>[number]>
-								: p["anyOf"] extends ReadonlyArray<Schema>
-									? UnionSchemaType<p["anyOf"]> &
-											Partial<UnionToIntersection<UnionSchemaType<p["anyOf"]>>>
-									: p["allOf"] extends ReadonlyArray<Schema>
-										? UnionToIntersection<UnionSchemaType<p["allOf"]>>
-										: any
-						: p["type"] extends "array"
-							? p["items"] extends OfSchema
-								? p["items"]["anyOf"] extends ReadonlyArray<Schema>
-									? UnionSchemaType<NonNullable<p["items"]["anyOf"]>>[]
-									: p["items"]["oneOf"] extends ReadonlyArray<Schema>
-										? ArrayUnion<
-												UnionSchemaType<NonNullable<p["items"]["oneOf"]>>
-											>
-										: p["items"]["allOf"] extends ReadonlyArray<Schema>
-											? UnionToIntersection<
-													UnionSchemaType<NonNullable<p["items"]["allOf"]>>
-												>[]
-											: never
-								: p["items"] extends NonNullable<Schema>
-									? SchemaTypeDef<p["items"]>[]
-									: any[]
-							: p["oneOf"] extends ReadonlyArray<Schema>
-								? UnionSchemaType<p["oneOf"]>
-								: any;
-
-export type SchemaType<p extends Schema> = NullOrUndefined<p, SchemaTypeDef<p>>;
+export const refs = _Schema.refs;
+export type Packed<T extends keyof typeof refs> = _Schema.Packed<T>;
+export type Schema = _Schema.Schema;
+export type SchemaType<P extends _Schema.Schema> = _Schema.SchemaType<P>;
diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts
index 04684ebc10..65ad217d51 100644
--- a/packages/backend/src/remote/activitypub/models/note.ts
+++ b/packages/backend/src/remote/activitypub/models/note.ts
@@ -53,7 +53,7 @@ import { UserProfiles } from "@/models/index.js";
 import { In } from "typeorm";
 import { config } from "@/config.js";
 import { truncate } from "@/misc/truncate.js";
-import { langmap } from "@/misc/langmap.js";
+import { langmap } from "firefish-js";
 import { inspect } from "node:util";
 
 export function validateNote(object: any, uri: string) {
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index eb4d9ca5a2..7f07bb2336 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -17,7 +17,7 @@ import { ApiError } from "@/server/api/error.js";
 import define from "@/server/api/define.js";
 import { HOUR } from "backend-rs";
 import { getNote } from "@/server/api/common/getters.js";
-import { langmap } from "@/misc/langmap.js";
+import { langmap } from "firefish-js";
 
 export const meta = {
 	tags: ["notes"],
diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts
index 8e92d43bf4..4698d3d473 100644
--- a/packages/backend/src/server/api/endpoints/notes/edit.ts
+++ b/packages/backend/src/server/api/endpoints/notes/edit.ts
@@ -33,7 +33,7 @@ import renderNote from "@/remote/activitypub/renderer/note.js";
 import renderUpdate from "@/remote/activitypub/renderer/update.js";
 import { deliverToRelays } from "@/services/relay.js";
 // import { deliverQuestionUpdate } from "@/services/note/polls/update.js";
-import { langmap } from "@/misc/langmap.js";
+import { langmap } from "firefish-js";
 
 export const meta = {
 	tags: ["notes"],
diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts
index 094b7fed33..8b84baa0b2 100644
--- a/packages/backend/src/server/api/endpoints/notes/translate.ts
+++ b/packages/backend/src/server/api/endpoints/notes/translate.ts
@@ -1,7 +1,7 @@
 import { ApiError } from "@/server/api/error.js";
 import { getNote } from "@/server/api/common/getters.js";
 import { translate } from "@/misc/translate.js";
-import type { PostLanguage } from "@/misc/langmap.js";
+import type { PostLanguage } from "firefish-js";
 import define from "@/server/api/define.js";
 
 export const meta = {
diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts
index 2096f8b1a2..0e484ffc3c 100644
--- a/packages/backend/src/services/note/create.ts
+++ b/packages/backend/src/services/note/create.ts
@@ -63,7 +63,7 @@ import { db } from "@/db/postgre.js";
 import { getActiveWebhooks } from "@/misc/webhook-cache.js";
 import { redisClient } from "@/db/redis.js";
 import { Mutex } from "redis-semaphore";
-import { langmap } from "@/misc/langmap.js";
+import { langmap } from "firefish-js";
 import Logger from "@/services/logger.js";
 import { inspect } from "node:util";
 import { toRustObject } from "@/prelude/undefined-to-null.js";
diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts
index fb3f70c34e..1d38b2798f 100644
--- a/packages/client/src/account.ts
+++ b/packages/client/src/account.ts
@@ -7,6 +7,7 @@ import { alert, api, popup, popupMenu, waiting } from "@/os";
 import icon from "@/scripts/icon";
 import { del, get, set } from "@/scripts/idb-proxy";
 import { reloadChannel, unisonReload } from "@/scripts/unison-reload";
+import type { MenuButton, MenuUser } from "./types/menu";
 
 // TODO: 他のタブと永続化されたstateを同期
 
@@ -16,7 +17,7 @@ export async function signOut() {
 	waiting();
 	localStorage.removeItem("account");
 
-	await removeAccount(me.id);
+	await removeAccount(me!.id);
 
 	const accounts = await getAccounts();
 
@@ -26,12 +27,9 @@ export async function signOut() {
 			const registration = await navigator.serviceWorker.ready;
 			const push = await registration.pushManager.getSubscription();
 			if (push) {
-				await fetch(`${apiUrl}/sw/unregister`, {
-					method: "POST",
-					body: JSON.stringify({
-						i: me.token,
-						endpoint: push.endpoint,
-					}),
+				await api("sw/unregister", {
+					endpoint: push.endpoint,
+					i: me!.token, // FIXME: This parameter seems to be removable but I didn't test it
 				});
 			}
 		}
@@ -117,13 +115,13 @@ function showSuspendedDialog() {
 
 export function updateAccount(accountData) {
 	for (const [key, value] of Object.entries(accountData)) {
-		me[key] = value;
+		me![key] = value;
 	}
 	localStorage.setItem("account", JSON.stringify(me));
 }
 
 export async function refreshAccount() {
-	const accountData = await fetchAccount(me.token);
+	const accountData = await fetchAccount(me!.token);
 	return updateAccount(accountData);
 }
 
@@ -189,7 +187,7 @@ export async function openAccountMenu(
 
 	async function switchAccount(account: entities.UserDetailed) {
 		const storedAccounts = await getAccounts();
-		const token = storedAccounts.find((x) => x.id === account.id).token;
+		const token = storedAccounts.find((x) => x.id === account.id)!.token;
 		switchAccountWithToken(token);
 	}
 
@@ -198,15 +196,15 @@ export async function openAccountMenu(
 	}
 
 	const storedAccounts = await getAccounts().then((accounts) =>
-		accounts.filter((x) => x.id !== me.id),
+		accounts.filter((x) => x.id !== me!.id),
 	);
 	const accountsPromise = api("users/show", {
 		userIds: storedAccounts.map((x) => x.id),
 	});
 
-	function createItem(account: entities.UserDetailed) {
+	function createItem(account: entities.UserDetailed): MenuUser {
 		return {
-			type: "user",
+			type: "user" as const,
 			user: account,
 			active: opts.active != null ? opts.active === account.id : false,
 			action: () => {
@@ -221,10 +219,14 @@ export async function openAccountMenu(
 
 	const accountItemPromises = storedAccounts.map(
 		(a) =>
-			new Promise((res) => {
+			new Promise<MenuUser>((res) => {
 				accountsPromise.then((accounts) => {
 					const account = accounts.find((x) => x.id === a.id);
-					if (account == null) return res(null);
+					if (account == null) {
+						// The user is deleted, remove it
+						removeAccount(a.id);
+						return res(null as unknown as MenuUser);
+					}
 					res(createItem(account));
 				});
 			}),
@@ -233,74 +235,72 @@ export async function openAccountMenu(
 	if (opts.withExtraOperation) {
 		popupMenu(
 			[
-				...[
-					...(isMobile ?? false
-						? [
-								{
-									type: "parent",
-									icon: `${icon("ph-plus")}`,
-									text: i18n.ts.addAccount,
-									children: [
-										{
-											text: i18n.ts.existingAccount,
-											action: () => {
-												showSigninDialog();
-											},
+				...(isMobile ?? false
+					? [
+							{
+								type: "parent" as const,
+								icon: `${icon("ph-plus")}`,
+								text: i18n.ts.addAccount,
+								children: [
+									{
+										text: i18n.ts.existingAccount,
+										action: () => {
+											showSigninDialog();
 										},
-										{
-											text: i18n.ts.createAccount,
-											action: () => {
-												createAccount();
-											},
+									},
+									{
+										text: i18n.ts.createAccount,
+										action: () => {
+											createAccount();
 										},
-									],
-								},
-							]
-						: [
-								{
-									type: "link",
-									text: i18n.ts.profile,
-									to: `/@${me.username}`,
-									avatar: me,
-								},
-								null,
-							]),
-					...(opts.includeCurrentAccount ? [createItem(me)] : []),
-					...accountItemPromises,
-					...(isMobile ?? false
-						? [
-								null,
-								{
-									type: "link",
-									text: i18n.ts.profile,
-									to: `/@${me.username}`,
-									avatar: me,
-								},
-							]
-						: [
-								{
-									type: "parent",
-									icon: `${icon("ph-plus")}`,
-									text: i18n.ts.addAccount,
-									children: [
-										{
-											text: i18n.ts.existingAccount,
-											action: () => {
-												showSigninDialog();
-											},
+									},
+								],
+							},
+						]
+					: [
+							{
+								type: "link" as const,
+								text: i18n.ts.profile,
+								to: `/@${me!.username}`,
+								avatar: me!,
+							},
+							null,
+						]),
+				...(opts.includeCurrentAccount ? [createItem(me!)] : []),
+				...accountItemPromises,
+				...(isMobile ?? false
+					? [
+							null,
+							{
+								type: "link" as const,
+								text: i18n.ts.profile,
+								to: `/@${me!.username}`,
+								avatar: me!,
+							},
+						]
+					: [
+							{
+								type: "parent" as const,
+								icon: `${icon("ph-plus")}`,
+								text: i18n.ts.addAccount,
+								children: [
+									{
+										text: i18n.ts.existingAccount,
+										action: () => {
+											showSigninDialog();
 										},
-										{
-											text: i18n.ts.createAccount,
-											action: () => {
-												createAccount();
-											},
+									},
+									{
+										text: i18n.ts.createAccount,
+										action: () => {
+											createAccount();
 										},
-									],
-								},
-							]),
-				],
+									},
+								],
+							},
+						]),
 			],
-			ev.currentTarget ?? ev.target,
+			(ev.currentTarget ?? ev.target) as HTMLElement,
 			{
 				align: "left",
 			},
@@ -308,10 +308,10 @@ export async function openAccountMenu(
 	} else {
 		popupMenu(
 			[
-				...(opts.includeCurrentAccount ? [createItem(me)] : []),
+				...(opts.includeCurrentAccount ? [createItem(me!)] : []),
 				...accountItemPromises,
 			],
-			ev.currentTarget ?? ev.target,
+			(ev.currentTarget ?? ev.target) as HTMLElement,
 			{
 				align: "left",
 			},
diff --git a/packages/client/src/cold-store.ts b/packages/client/src/cold-store.ts
new file mode 100644
index 0000000000..63e7782264
--- /dev/null
+++ b/packages/client/src/cold-store.ts
@@ -0,0 +1,121 @@
+import { ref as vueRef } from "vue";
+import type { UnwrapRef } from "vue";
+
+// TODO: 他のタブと永続化されたstateを同期
+
+const PREFIX = "miux:";
+
+interface Plugin {
+	id: string;
+	name: string;
+	active: boolean;
+	configData: Record<string, unknown>;
+	token: string;
+	ast: unknown[];
+}
+
+import darkTheme from "@/themes/d-rosepine.json5";
+/**
+ * Storage for configuration information that does not need to be constantly loaded into memory (non-reactive)
+ */
+import lightTheme from "@/themes/l-rosepinedawn.json5";
+
+const ColdStoreDefault = {
+	lightTheme,
+	darkTheme,
+	syncDeviceDarkMode: true,
+	plugins: [] as Plugin[],
+	mediaVolume: 0.5,
+	vibrate: false,
+	sound_masterVolume: 0.3,
+	sound_note: { type: "none", volume: 0 },
+	sound_noteMy: { type: "syuilo/up", volume: 1 },
+	sound_notification: { type: "syuilo/pope2", volume: 1 },
+	sound_chat: { type: "syuilo/pope1", volume: 1 },
+	sound_chatBg: { type: "syuilo/waon", volume: 1 },
+	sound_antenna: { type: "syuilo/triple", volume: 1 },
+	sound_channel: { type: "syuilo/square-pico", volume: 1 },
+};
+
+const watchers: {
+	key: string;
+	callback: (value) => void;
+}[] = [];
+
+function get<T extends keyof typeof ColdStoreDefault>(
+	key: T,
+): (typeof ColdStoreDefault)[T] {
+	// TODO: indexedDBにする
+	//       ただしその際はnullチェックではなくキー存在チェックにしないとダメ
+	//       (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある)
+	const value = localStorage.getItem(PREFIX + key);
+	if (value == null) {
+		return ColdStoreDefault[key];
+	} else {
+		return JSON.parse(value);
+	}
+}
+
+function set<T extends keyof typeof ColdStoreDefault>(
+	key: T,
+	value: (typeof ColdStoreDefault)[T],
+): void {
+	// 呼び出し側のバグ等で undefined が来ることがある
+	// undefined を文字列として localStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視
+	if (value === undefined) {
+		console.error(`attempt to store undefined value for key '${key}'`);
+		return;
+	}
+
+	localStorage.setItem(PREFIX + key, JSON.stringify(value));
+
+	for (const watcher of watchers) {
+		if (watcher.key === key) watcher.callback(value);
+	}
+}
+
+function watch<T extends keyof typeof ColdStoreDefault>(
+	key: T,
+	callback: (value: (typeof ColdStoreDefault)[T]) => void,
+) {
+	watchers.push({ key, callback });
+}
+
+// TODO: VueのcustomRef使うと良い感じになるかも
+function ref<T extends keyof typeof ColdStoreDefault>(key: T) {
+	const v = get(key);
+	const r = vueRef(v);
+	// TODO: このままではwatcherがリークするので開放する方法を考える
+	watch(key, (v) => {
+		r.value = v as UnwrapRef<typeof v>;
+	});
+	return r;
+}
+
+/**
+ * 特定のキーの、簡易的なgetter/setterを作ります
+ * 主にvue場で設定コントロールのmodelとして使う用
+ */
+function makeGetterSetter<K extends keyof typeof ColdStoreDefault>(key: K) {
+	// TODO: VueのcustomRef使うと良い感じになるかも
+	const valueRef = ref(key);
+	return {
+		get: () => {
+			return valueRef.value;
+		},
+		set: (value: (typeof ColdStoreDefault)[K]) => {
+			const val = value;
+			set(key, val);
+		},
+	};
+}
+
+export default {
+	default: ColdStoreDefault,
+	watchers,
+	get,
+	set,
+	watch,
+	ref,
+	makeGetterSetter,
+};
diff --git a/packages/client/src/components/MkAnnouncement.vue b/packages/client/src/components/MkAnnouncement.vue
index 3af8e0163f..783d511bc8 100644
--- a/packages/client/src/components/MkAnnouncement.vue
+++ b/packages/client/src/components/MkAnnouncement.vue
@@ -1,5 +1,5 @@
 <template>
-	<MkModal ref="modal" :z-priority="'middle'" @closed="$emit('closed')">
+	<MkModal ref="modal" :z-priority="'middle'" @closed="emit('closed')">
 		<div :class="$style.root">
 			<div :class="$style.title">
 				<MkSparkle v-if="isGoodNews">{{ title }}</MkSparkle>
@@ -41,6 +41,10 @@ const props = defineProps<{
 	announcement: entities.Announcement;
 }>();
 
+const emit = defineEmits<{
+	closed: [];
+}>();
+
 const { id, text, title, imageUrl, isGoodNews } = props.announcement;
 
 const modal = shallowRef<InstanceType<typeof MkModal>>();
diff --git a/packages/client/src/components/MkAutocomplete.vue b/packages/client/src/components/MkAutocomplete.vue
index 332785b467..4e890cc03e 100644
--- a/packages/client/src/components/MkAutocomplete.vue
+++ b/packages/client/src/components/MkAutocomplete.vue
@@ -182,7 +182,7 @@ export default {
 const props = defineProps<{
 	type: string;
 	q: string | null;
-	textarea: HTMLTextAreaElement;
+	textarea: HTMLTextAreaElement | HTMLInputElement;
 	close: () => void;
 	x: number;
 	y: number;
@@ -435,7 +435,7 @@ onUpdated(() => {
 onMounted(() => {
 	setPosition();
 
-	props.textarea.addEventListener("keydown", onKeydown);
+	(props.textarea as HTMLTextAreaElement).addEventListener("keydown", onKeydown);
 	document.body.addEventListener("mousedown", onMousedown);
 
 	nextTick(() => {
@@ -453,7 +453,7 @@ onMounted(() => {
 });
 
 onBeforeUnmount(() => {
-	props.textarea.removeEventListener("keydown", onKeydown);
+	(props.textarea as HTMLTextAreaElement).removeEventListener("keydown", onKeydown);
 	document.body.removeEventListener("mousedown", onMousedown);
 });
 </script>
diff --git a/packages/client/src/components/MkManyAnnouncements.vue b/packages/client/src/components/MkManyAnnouncements.vue
index 903891b64c..047c92787c 100644
--- a/packages/client/src/components/MkManyAnnouncements.vue
+++ b/packages/client/src/components/MkManyAnnouncements.vue
@@ -1,5 +1,5 @@
 <template>
-	<MkModal ref="modal" :z-priority="'middle'" @closed="$emit('closed')">
+	<MkModal ref="modal" :z-priority="'middle'" @closed="emit('closed')">
 		<div :class="$style.root">
 			<p :class="$style.title">
 				{{ i18n.ts.youHaveUnreadAnnouncements }}
@@ -21,6 +21,10 @@ import MkModal from "@/components/MkModal.vue";
 import MkButton from "@/components/MkButton.vue";
 import { i18n } from "@/i18n";
 
+const emit = defineEmits<{
+	closed: [];
+}>();
+
 const modal = shallowRef<InstanceType<typeof MkModal>>();
 const checkAnnouncements = () => {
 	modal.value!.close();
diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue
index 78cb6350f7..702627f8b7 100644
--- a/packages/client/src/components/MkPostForm.vue
+++ b/packages/client/src/components/MkPostForm.vue
@@ -810,13 +810,17 @@ function setLanguage() {
 		actions.push(null);
 	}
 
-	if (language.value != null)
+	if (language.value != null && langmap[language.value] != null) {
 		actions.push({
 			text: langmap[language.value].nativeName,
 			danger: false,
 			active: true,
 			action: () => {},
 		});
+	} else {
+		// Unrecognized language, set to null
+		language.value = null;
+	}
 
 	const langs = Object.keys(langmap);
 
diff --git a/packages/client/src/components/MkSignin.vue b/packages/client/src/components/MkSignin.vue
index d2c1159642..35770a2d13 100644
--- a/packages/client/src/components/MkSignin.vue
+++ b/packages/client/src/components/MkSignin.vue
@@ -160,7 +160,7 @@ const hCaptchaResponse = ref(null);
 const reCaptchaResponse = ref(null);
 
 const emit = defineEmits<{
-	(ev: "login", v: any): void;
+	login: [v: { id: string; i: string }];
 }>();
 
 const props = defineProps({
diff --git a/packages/client/src/components/MkSigninDialog.vue b/packages/client/src/components/MkSigninDialog.vue
index b0fc7700e8..f63e7c2565 100644
--- a/packages/client/src/components/MkSigninDialog.vue
+++ b/packages/client/src/components/MkSigninDialog.vue
@@ -30,7 +30,7 @@ withDefaults(
 );
 
 const emit = defineEmits<{
-	(ev: "done"): void;
+	(ev: "done", res: { id: string; i: string }): void;
 	(ev: "closed"): void;
 	(ev: "cancelled"): void;
 }>();
@@ -39,11 +39,11 @@ const dialog = ref<InstanceType<typeof XModalWindow>>();
 
 function onClose() {
 	emit("cancelled");
-	dialog.value.close();
+	dialog.value!.close();
 }
 
-function onLogin(res) {
+function onLogin(res: { id: string; i: string }) {
 	emit("done", res);
-	dialog.value.close();
+	dialog.value!.close();
 }
 </script>
diff --git a/packages/client/src/components/MkSignup.vue b/packages/client/src/components/MkSignup.vue
index 0807935be3..05119809d4 100644
--- a/packages/client/src/components/MkSignup.vue
+++ b/packages/client/src/components/MkSignup.vue
@@ -248,7 +248,7 @@
 				v-model="hCaptchaResponse"
 				class="_formBlock captcha"
 				provider="hcaptcha"
-				:sitekey="instance.hcaptchaSiteKey"
+				:sitekey="instance.hcaptchaSiteKey!"
 			/>
 			<MkCaptcha
 				v-if="instance.enableRecaptcha"
@@ -256,7 +256,7 @@
 				v-model="reCaptchaResponse"
 				class="_formBlock captcha"
 				provider="recaptcha"
-				:sitekey="instance.recaptchaSiteKey"
+				:sitekey="instance.recaptchaSiteKey!"
 			/>
 			<MkButton
 				class="_formBlock"
@@ -296,7 +296,7 @@ const props = withDefaults(
 );
 
 const emit = defineEmits<{
-	(ev: "signup", user: Record<string, any>): void;
+	(ev: "signup", user: { id: string; i: string }): void;
 	(ev: "signupEmailPending"): void;
 }>();
 
diff --git a/packages/client/src/components/MkSignupDialog.vue b/packages/client/src/components/MkSignupDialog.vue
index f9829c1040..5614188198 100644
--- a/packages/client/src/components/MkSignupDialog.vue
+++ b/packages/client/src/components/MkSignupDialog.vue
@@ -36,13 +36,13 @@ withDefaults(
 );
 
 const emit = defineEmits<{
-	(ev: "done"): void;
+	(ev: "done", res: { id: string; i: string }): void;
 	(ev: "closed"): void;
 }>();
 
 const dialog = ref<InstanceType<typeof XModalWindow>>();
 
-function onSignup(res) {
+function onSignup(res: { id: string; i: string }) {
 	emit("done", res);
 	dialog.value?.close();
 }
diff --git a/packages/client/src/components/MkUpdated.vue b/packages/client/src/components/MkUpdated.vue
index 7f519b407a..5061e9d91a 100644
--- a/packages/client/src/components/MkUpdated.vue
+++ b/packages/client/src/components/MkUpdated.vue
@@ -2,8 +2,8 @@
 	<MkModal
 		ref="modal"
 		:z-priority="'middle'"
-		@click="$refs.modal.close()"
-		@closed="$emit('closed')"
+		@click="modal!.close()"
+		@closed="emit('closed')"
 	>
 		<div :class="$style.root">
 			<div :class="$style.title">
@@ -14,7 +14,7 @@
 				:class="$style.gotIt"
 				primary
 				full
-				@click="$refs.modal.close()"
+				@click="modal!.close()"
 				>{{ i18n.ts.gotIt }}</MkButton
 			>
 		</div>
diff --git a/packages/client/src/i18n.ts b/packages/client/src/i18n.ts
index 6d352ba03e..4dd989a64a 100644
--- a/packages/client/src/i18n.ts
+++ b/packages/client/src/i18n.ts
@@ -1,6 +1,7 @@
 import { markRaw } from "vue";
 import { locale } from "@/config";
 
+// biome-ignore lint/suspicious/noExplicitAny: temporary use any
 class I18n<T extends Record<string, any>> {
 	public ts: T;
 
diff --git a/packages/client/src/init.ts b/packages/client/src/init.ts
index 08d424cffc..c3fff47e9a 100644
--- a/packages/client/src/init.ts
+++ b/packages/client/src/init.ts
@@ -245,7 +245,12 @@ function checkForSplash() {
 
 		try {
 			// 変なバージョン文字列来るとcompareVersionsでエラーになるため
-			if (lastVersion < version && defaultStore.state.showUpdates) {
+			// If a strange version string comes, an error will occur in compareVersions.
+			if (
+				lastVersion != null &&
+				lastVersion < version &&
+				defaultStore.state.showUpdates
+			) {
 				// ログインしてる場合だけ
 				if (me) {
 					popup(
@@ -281,7 +286,7 @@ function checkForSplash() {
 						"closed",
 					);
 				} else {
-					unreadAnnouncements.forEach((item) => {
+					for (const item of unreadAnnouncements) {
 						if (item.showPopup)
 							popup(
 								defineAsyncComponent(
@@ -291,7 +296,7 @@ function checkForSplash() {
 								{},
 								"closed",
 							);
-					});
+					}
 				}
 			})
 			.catch((err) => console.log(err));
diff --git a/packages/client/src/nirax.ts b/packages/client/src/nirax.ts
index da162338b6..90348b06ba 100644
--- a/packages/client/src/nirax.ts
+++ b/packages/client/src/nirax.ts
@@ -1,7 +1,7 @@
 // NIRAX --- A lightweight router
 
 import { EventEmitter } from "eventemitter3";
-import type { Component, ShallowRef } from "vue";
+import type { Component } from "vue";
 import { shallowRef } from "vue";
 import { safeURIDecode } from "@/scripts/safe-uri-decode";
 import { pleaseLogin } from "@/scripts/please-login";
@@ -36,6 +36,7 @@ export interface Resolved {
 function parsePath(path: string): ParsedPath {
 	const res = [] as ParsedPath;
 
+	// biome-ignore lint/style/noParameterAssign: assign it intentionally
 	path = path.substring(1);
 
 	for (const part of path.split("/")) {
@@ -76,13 +77,13 @@ export class Router extends EventEmitter<{
 	same: () => void;
 }> {
 	private routes: RouteDef[];
-	public current: Resolved;
-	public currentRef: ShallowRef<Resolved> = shallowRef();
-	public currentRoute: ShallowRef<RouteDef> = shallowRef();
+	public current!: Resolved; // It is assigned in this.navigate
+	public currentRef = shallowRef<Resolved>();
+	public currentRoute = shallowRef<RouteDef>();
 	private currentPath: string;
 	private currentKey = Date.now().toString();
 
-	public navHook: ((path: string, flag?: any) => boolean) | null = null;
+	public navHook: ((path: string, flag?: unknown) => boolean) | null = null;
 
 	constructor(routes: Router["routes"], currentPath: Router["currentPath"]) {
 		super();
@@ -92,9 +93,10 @@ export class Router extends EventEmitter<{
 		this.navigate(currentPath, null, false);
 	}
 
-	public resolve(path: string): Resolved | null {
+	public resolve(_path: string): Resolved | null {
 		let queryString: string | null = null;
 		let hash: string | null = null;
+		let path = _path;
 		if (path[0] === "/") path = path.substring(1);
 		if (path.includes("#")) {
 			hash = path.substring(path.indexOf("#") + 1);
@@ -168,9 +170,16 @@ export class Router extends EventEmitter<{
 					}
 
 					if (route.query != null && queryString != null) {
-						const queryObject = [
-							...new URLSearchParams(queryString).entries(),
-						].reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {});
+						// const queryObject = [
+						// 	...new URLSearchParams(queryString).entries(),
+						// ].reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {});
+
+						const queryObject: Record<string, string> = Object.assign(
+							{},
+							...[...new URLSearchParams(queryString).entries()].map(
+								(entry) => ({ [entry[0]]: entry[1] }),
+							),
+						);
 
 						for (const q in route.query) {
 							const as = route.query[q];
@@ -227,6 +236,7 @@ export class Router extends EventEmitter<{
 		}
 
 		const isSamePath = beforePath === path;
+		// biome-ignore lint/style/noParameterAssign: assign it intentionally
 		if (isSamePath && key == null) key = this.currentKey;
 		this.current = res;
 		this.currentRef.value = res;
@@ -253,7 +263,7 @@ export class Router extends EventEmitter<{
 		return this.currentKey;
 	}
 
-	public push(path: string, flag?: any) {
+	public push(path: string, flag?: unknown) {
 		const beforePath = this.currentPath;
 		if (path === beforePath) {
 			this.emit("same");
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index 4ccd5e47b1..b997da28ac 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -896,9 +896,6 @@ export async function openEmojiPicker(
 			...opts,
 		},
 		{
-			chosen: (emoji) => {
-				insertTextAtCursor(activeTextarea, emoji);
-			},
 			done: (emoji) => {
 				insertTextAtCursor(activeTextarea, emoji);
 			},
diff --git a/packages/client/src/pages/settings/delete-account.vue b/packages/client/src/pages/settings/delete-account.vue
index 9c24dd4009..48b0b3448e 100644
--- a/packages/client/src/pages/settings/delete-account.vue
+++ b/packages/client/src/pages/settings/delete-account.vue
@@ -7,7 +7,7 @@
 			i18n.ts._accountDelete.sendEmail
 		}}</FormInfo>
 		<FormButton
-			v-if="!me?.isDeleted"
+			v-if="!me!.isDeleted"
 			danger
 			class="_formBlock"
 			@click="deleteAccount"
diff --git a/packages/client/src/pizzax.ts b/packages/client/src/pizzax.ts
index ce35dbeb24..0185071f30 100644
--- a/packages/client/src/pizzax.ts
+++ b/packages/client/src/pizzax.ts
@@ -5,12 +5,13 @@ import { onUnmounted, ref, watch } from "vue";
 import { api } from "./os";
 import { useStream } from "./stream";
 import { isSignedIn, me } from "@/me";
+import type { TypeUtils } from "firefish-js";
 
 type StateDef = Record<
 	string,
 	{
 		where: "account" | "device" | "deviceAccount";
-		default: any;
+		default: unknown;
 	}
 >;
 
@@ -82,11 +83,12 @@ export class Storage<T extends StateDef> {
 		for (const [k, v] of Object.entries(state)) {
 			reactiveState[k] = ref(v);
 		}
-		this.state = state as any;
-		this.reactiveState = reactiveState as any;
+		this.state = state as typeof this.state;
+		this.reactiveState = reactiveState as typeof this.reactiveState;
 
 		if (isSignedIn(me)) {
 			// なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう)
+			// For some reason, if I don't setTimeout, an error occurs in the api function (probably caused by circular references)
 			window.setTimeout(() => {
 				api("i/registry/get-all", { scope: ["client", this.key] }).then(
 					(kvs) => {
@@ -104,7 +106,7 @@ export class Storage<T extends StateDef> {
 							}
 						}
 						localStorage.setItem(
-							`${this.keyForLocalStorage}::cache::${me.id}`,
+							`${this.keyForLocalStorage}::cache::${me!.id}`,
 							JSON.stringify(cache),
 						);
 					},
@@ -118,11 +120,12 @@ export class Storage<T extends StateDef> {
 					key,
 					value,
 				}: {
-					scope: string[];
+					scope?: string[];
 					key: keyof T;
 					value: T[typeof key]["default"];
 				}) => {
 					if (
+						scope == null ||
 						scope.length !== 2 ||
 						scope[0] !== "client" ||
 						scope[1] !== this.key ||
@@ -135,13 +138,13 @@ export class Storage<T extends StateDef> {
 
 					const cache = JSON.parse(
 						localStorage.getItem(
-							`${this.keyForLocalStorage}::cache::${me.id}`,
+							`${this.keyForLocalStorage}::cache::${me!.id}`,
 						) || "{}",
 					);
 					if (cache[key] !== value) {
 						cache[key] = value;
 						localStorage.setItem(
-							`${this.keyForLocalStorage}::cache::${me.id}`,
+							`${this.keyForLocalStorage}::cache::${me!.id}`,
 							JSON.stringify(cache),
 						);
 					}
@@ -150,7 +153,7 @@ export class Storage<T extends StateDef> {
 		}
 	}
 
-	public set<K extends keyof T>(key: K, value: T[K]["default"]): void {
+	public set<K extends keyof T>(key: K & string, value: T[K]["default"]): void {
 		if (_DEV_) console.log("set", key, value);
 
 		this.state[key] = value;
@@ -201,15 +204,15 @@ export class Storage<T extends StateDef> {
 		}
 	}
 
-	public push<K extends keyof T>(
-		key: K,
+	public push<K extends TypeUtils.PropertyOfType<T, { default: unknown[] }>>(
+		key: K & string,
 		value: ArrayElement<T[K]["default"]>,
 	): void {
-		const currentState = this.state[key];
+		const currentState = this.state[key] as unknown[];
 		this.set(key, [...currentState, value]);
 	}
 
-	public reset(key: keyof T) {
+	public reset(key: keyof T & string) {
 		this.set(key, this.def[key].default);
 	}
 
@@ -218,11 +221,11 @@ export class Storage<T extends StateDef> {
 	 * 主にvue場で設定コントロールのmodelとして使う用
 	 */
 	public makeGetterSetter<K extends keyof T>(
-		key: K,
-		getter?: (v: T[K]) => unknown,
-		setter?: (v: unknown) => T[K],
+		key: K & string,
+		getter?: (oldV: T[K]["default"]) => T[K]["default"],
+		setter?: (oldV: T[K]["default"]) => T[K]["default"],
 	) {
-		const valueRef = ref(this.state[key]);
+		const valueRef = ref(this.state[key]) as Ref<T[K]["default"]>;
 
 		const stop = watch(this.reactiveState[key], (val) => {
 			valueRef.value = val;
@@ -242,7 +245,7 @@ export class Storage<T extends StateDef> {
 					return valueRef.value;
 				}
 			},
-			set: (value: unknown) => {
+			set: (value: T[K]["default"]) => {
 				const val = setter ? setter(value) : value;
 				this.set(key, val);
 				valueRef.value = val;
diff --git a/packages/client/src/plugin.ts b/packages/client/src/plugin.ts
index c38aaa365a..000071b7d8 100644
--- a/packages/client/src/plugin.ts
+++ b/packages/client/src/plugin.ts
@@ -42,7 +42,20 @@ export function install(plugin) {
 	aiscript.exec(parser.parse(plugin.src));
 }
 
-function createPluginEnv(opts) {
+interface Plugin {
+	config?: Record<
+		string,
+		{
+			default: unknown;
+			[k: string]: unknown;
+		}
+	>;
+	configData: Record<string, unknown>;
+	token: string;
+	id: string;
+}
+
+function createPluginEnv(opts: { plugin: Plugin; storageKey: string }) {
 	const config = new Map<string, values.Value>();
 	for (const [k, v] of Object.entries(opts.plugin.config ?? {})) {
 		config.set(
@@ -172,7 +185,7 @@ function registerNoteAction({ pluginId, title, handler }) {
 			if (!pluginContext) {
 				return;
 			}
-			pluginContext.execFn(handler, [utils.jsToVal(user)]);
+			pluginContext.execFn(handler, [utils.jsToVal(note)]);
 		},
 	});
 }
@@ -205,16 +218,18 @@ function registerNotePostInterruptor({ pluginId, handler }) {
 	});
 }
 
+// FIXME: where is pageViewInterruptors?
+// This function currently can't do anything
 function registerPageViewInterruptor({ pluginId, handler }): void {
-	pageViewInterruptors.push({
-		handler: async (page) => {
-			const pluginContext = pluginContexts.get(pluginId);
-			if (!pluginContext) {
-				return;
-			}
-			return utils.valToJs(
-				await pluginContext.execFn(handler, [utils.jsToVal(page)]),
-			);
-		},
-	});
+	// pageViewInterruptors.push({
+	// 	handler: async (page) => {
+	// 		const pluginContext = pluginContexts.get(pluginId);
+	// 		if (!pluginContext) {
+	// 			return;
+	// 		}
+	// 		return utils.valToJs(
+	// 			await pluginContext.execFn(handler, [utils.jsToVal(page)]),
+	// 		);
+	// 	},
+	// });
 }
diff --git a/packages/client/src/scripts/2fa.ts b/packages/client/src/scripts/2fa.ts
index 9c34c8fb70..f5d1f57538 100644
--- a/packages/client/src/scripts/2fa.ts
+++ b/packages/client/src/scripts/2fa.ts
@@ -9,7 +9,7 @@ export function byteify(string: string, encoding: "ascii" | "base64" | "hex") {
 			);
 		case "hex":
 			return new Uint8Array(
-				string.match(/.{1,2}/g).map((byte) => Number.parseInt(byte, 16)),
+				string.match(/.{1,2}/g)!.map((byte) => Number.parseInt(byte, 16)),
 			);
 	}
 }
diff --git a/packages/client/src/scripts/array.ts b/packages/client/src/scripts/array.ts
index ef3dfb298e..f9c7525638 100644
--- a/packages/client/src/scripts/array.ts
+++ b/packages/client/src/scripts/array.ts
@@ -1,4 +1,4 @@
-import type { EndoRelation, Predicate } from "./relation";
+import type { EndoRelation, Predicate } from "@/types/relation";
 
 /**
  * Count the number of elements that satisfy the predicate
@@ -126,7 +126,7 @@ export function lessThan(xs: number[], ys: number[]): boolean {
  * Returns the longest prefix of elements that satisfy the predicate
  */
 export function takeWhile<T>(f: Predicate<T>, xs: T[]): T[] {
-	const ys = [];
+	const ys: T[] = [];
 	for (const x of xs) {
 		if (f(x)) {
 			ys.push(x);
diff --git a/packages/client/src/scripts/autocomplete.ts b/packages/client/src/scripts/autocomplete.ts
index 18e56ba38d..9eedb33d4d 100644
--- a/packages/client/src/scripts/autocomplete.ts
+++ b/packages/client/src/scripts/autocomplete.ts
@@ -13,7 +13,7 @@ export class Autocomplete {
 	} | null;
 
 	private textarea: HTMLInputElement | HTMLTextAreaElement;
-	private currentType: string;
+	private currentType?: string;
 	private textRef: Ref<string>;
 	private opening: boolean;
 
@@ -69,7 +69,7 @@ export class Autocomplete {
 	 * テキスト入力時
 	 */
 	private onInput() {
-		const caretPos = this.textarea.selectionStart;
+		const caretPos = this.textarea.selectionStart!;
 		const text = this.text.substring(0, caretPos).split("\n").pop()!;
 
 		const mentionIndex = text.lastIndexOf("@");
@@ -147,10 +147,10 @@ export class Autocomplete {
 		this.opening = true;
 		this.currentType = type;
 
-		// #region サジェストを表示すべき位置を計算
+		// #region Calculate the position where suggestions should be displayed
 		const caretPosition = getCaretCoordinates(
 			this.textarea,
-			this.textarea.selectionStart,
+			this.textarea.selectionStart!,
 		);
 
 		const rect = this.textarea.getBoundingClientRect();
@@ -216,7 +216,7 @@ export class Autocomplete {
 	private complete({ type, value }) {
 		this.close();
 
-		const caret = this.textarea.selectionStart;
+		const caret = this.textarea.selectionStart!;
 
 		if (type === "user") {
 			const source = this.text;
diff --git a/packages/client/src/scripts/check-word-mute.ts b/packages/client/src/scripts/check-word-mute.ts
index b8e17c4a6f..1a45be2571 100644
--- a/packages/client/src/scripts/check-word-mute.ts
+++ b/packages/client/src/scripts/check-word-mute.ts
@@ -1,4 +1,5 @@
 import type { entities } from "firefish-js";
+import { detectLanguage, languageContains } from "./language-utils";
 
 export interface Muted {
 	muted: boolean;
@@ -12,11 +13,12 @@ function checkLangMute(
 	note: entities.Note,
 	mutedLangs: Array<string | string[]>,
 ): Muted {
-	const mutedLangList = new Set(
-		mutedLangs.reduce((arr, x) => [...arr, ...(Array.isArray(x) ? x : [x])]),
-	);
-	if (mutedLangList.has((note.lang?.[0]?.lang || "").split("-")[0])) {
-		return { muted: true, matched: [note.lang?.[0]?.lang] };
+	const mutedLangList = mutedLangs.flat();
+	const noteLang = note.lang ?? detectLanguage(note.text ?? "") ?? "no-lang";
+	for (const mutedLang of mutedLangList) {
+		if (languageContains(mutedLang, noteLang)) {
+			return { muted: true, matched: [noteLang] };
+		}
 	}
 	return NotMuted;
 }
@@ -32,7 +34,7 @@ function checkWordMute(
 
 	if (text === "") return NotMuted;
 
-	const result = { muted: false, matched: [] };
+	const result = { muted: false, matched: [] as string[] };
 
 	for (const mutePattern of mutedWords) {
 		if (Array.isArray(mutePattern)) {
@@ -74,7 +76,7 @@ function checkWordMute(
 }
 
 export function getWordSoftMute(
-	note: firefish.entities.Note,
+	note: entities.Note,
 	meId: string | null | undefined,
 	mutedWords: Array<string | string[]>,
 	mutedLangs: Array<string | string[]>,
diff --git a/packages/client/src/scripts/collect-page-vars.ts b/packages/client/src/scripts/collect-page-vars.ts
index e7d4a6d0e6..d410981e9a 100644
--- a/packages/client/src/scripts/collect-page-vars.ts
+++ b/packages/client/src/scripts/collect-page-vars.ts
@@ -1,6 +1,8 @@
-export function collectPageVars(content) {
-	const pageVars = [];
-	const collect = (xs: any[]) => {
+import type { PageContent, PageVar } from "@/types/page";
+
+export function collectPageVars(content: PageContent[]) {
+	const pageVars: PageVar[] = [];
+	const collect = (xs: PageContent[]) => {
 		for (const x of xs) {
 			if (x.type === "textInput") {
 				pageVars.push({
@@ -24,7 +26,7 @@ export function collectPageVars(content) {
 				pageVars.push({
 					name: x.name,
 					type: "boolean",
-					value: x.default,
+					value: x.default!,
 				});
 			} else if (x.type === "counter") {
 				pageVars.push({
diff --git a/packages/client/src/scripts/copy-to-clipboard.ts b/packages/client/src/scripts/copy-to-clipboard.ts
index a4835d8e79..b989a1bc43 100644
--- a/packages/client/src/scripts/copy-to-clipboard.ts
+++ b/packages/client/src/scripts/copy-to-clipboard.ts
@@ -1,7 +1,7 @@
 /**
  * Clipboardに値をコピー(TODO: 文字列以外も対応)
  */
-export default (val) => {
+function obsoleteCopyToClipboard(val: string) {
 	// 空div 生成
 	const tmp = document.createElement("div");
 	// 選択用のタグ生成
@@ -21,7 +21,7 @@ export default (val) => {
 	// body に追加
 	document.body.appendChild(tmp);
 	// 要素を選択
-	document.getSelection().selectAllChildren(tmp);
+	document.getSelection()?.selectAllChildren(tmp);
 
 	// クリップボードにコピー
 	const result = document.execCommand("copy");
@@ -30,4 +30,20 @@ export default (val) => {
 	document.body.removeChild(tmp);
 
 	return result;
-};
+}
+
+export default async function (val?: string | null) {
+	if (val == null) return true;
+	const clipboardObj = window.navigator?.clipboard;
+	if (clipboardObj == null) {
+		// not supported
+		return obsoleteCopyToClipboard(val);
+	} else {
+		return new Promise<boolean>((res) => {
+			clipboardObj
+				.writeText(val)
+				.then(() => res(true))
+				.catch(() => res(obsoleteCopyToClipboard(val)));
+		});
+	}
+}
diff --git a/packages/client/src/scripts/extract-mentions.ts b/packages/client/src/scripts/extract-mentions.ts
index 259f78e576..cdf04c1106 100644
--- a/packages/client/src/scripts/extract-mentions.ts
+++ b/packages/client/src/scripts/extract-mentions.ts
@@ -6,7 +6,10 @@ export function extractMentions(
 	nodes: mfm.MfmNode[],
 ): mfm.MfmMention["props"][] {
 	// TODO: 重複を削除
-	const mentionNodes = mfm.extract(nodes, (node) => node.type === "mention");
+	const mentionNodes = mfm.extract(
+		nodes,
+		(node) => node.type === "mention",
+	) as mfm.MfmMention[];
 	const mentions = mentionNodes.map((x) => x.props);
 
 	return mentions;
diff --git a/packages/client/src/scripts/extract-mfm.ts b/packages/client/src/scripts/extract-mfm.ts
index b02557b341..e5dfb7029b 100644
--- a/packages/client/src/scripts/extract-mfm.ts
+++ b/packages/client/src/scripts/extract-mfm.ts
@@ -15,7 +15,8 @@ const animatedMfm = [
 export function extractMfmWithAnimation(nodes: mfm.MfmNode[]): string[] {
 	const mfmNodes = mfm.extract(nodes, (node) => {
 		return node.type === "fn" && animatedMfm.includes(node.props.name);
-	});
+	}) as mfm.MfmFn[];
+	// FIXME: mfm type error
 	const mfms = mfmNodes.map((x) => x.props.fn);
 
 	return mfms;
diff --git a/packages/client/src/scripts/extract-url-from-mfm.ts b/packages/client/src/scripts/extract-url-from-mfm.ts
index 0c580b6d32..e142640df3 100644
--- a/packages/client/src/scripts/extract-url-from-mfm.ts
+++ b/packages/client/src/scripts/extract-url-from-mfm.ts
@@ -14,7 +14,7 @@ export function extractUrlFromMfm(
 			node.type === "url" ||
 			(node.type === "link" && !(respectSilentFlag && node.props.silent))
 		);
-	});
+	}) as (mfm.MfmLink | mfm.MfmUrl)[];
 	const urls: string[] = unique(urlNodes.map((x) => x.props.url));
 
 	return urls.reduce((array, url) => {
diff --git a/packages/client/src/scripts/focus.ts b/packages/client/src/scripts/focus.ts
index 878132fbe8..d490a1045d 100644
--- a/packages/client/src/scripts/focus.ts
+++ b/packages/client/src/scripts/focus.ts
@@ -1,5 +1,6 @@
 export function focusPrev(el: Element | null, self = false, scroll = true) {
 	if (el == null) return;
+	// biome-ignore lint/style/noParameterAssign: assign it intentionally
 	if (!self) el = el.previousElementSibling;
 	if (el) {
 		if (el.hasAttribute("tabindex")) {
@@ -14,6 +15,7 @@ export function focusPrev(el: Element | null, self = false, scroll = true) {
 
 export function focusNext(el: Element | null, self = false, scroll = true) {
 	if (el == null) return;
+	// biome-ignore lint/style/noParameterAssign: assign it intentionally
 	if (!self) el = el.nextElementSibling;
 	if (el) {
 		if (el.hasAttribute("tabindex")) {
diff --git a/packages/client/src/scripts/gen-search-query.ts b/packages/client/src/scripts/gen-search-query.ts
deleted file mode 100644
index 21bc5df34e..0000000000
--- a/packages/client/src/scripts/gen-search-query.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { acct } from "firefish-js";
-import { host as localHost } from "@/config";
-
-export async function genSearchQuery(v: any, q: string) {
-	let host: string;
-	let userId: string;
-	if (q.split(" ").some((x) => x.startsWith("@"))) {
-		for (const at of q
-			.split(" ")
-			.filter((x) => x.startsWith("@"))
-			.map((x) => x.slice(1))) {
-			if (at.includes(".")) {
-				if (at === localHost || at === ".") {
-					host = null;
-				} else {
-					host = at;
-				}
-			} else {
-				const user = await v.os
-					.api("users/show", acct.parse(at))
-					.catch((x) => null);
-				if (user) {
-					userId = user.id;
-				} else {
-					// todo: show error
-				}
-			}
-		}
-	}
-	return {
-		query: q
-			.split(" ")
-			.filter((x) => !(x.startsWith("/") || x.startsWith("@")))
-			.join(" "),
-		host,
-		userId,
-	};
-}
diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts
index 2945cb16b3..ade43a3e22 100644
--- a/packages/client/src/scripts/get-note-menu.ts
+++ b/packages/client/src/scripts/get-note-menu.ts
@@ -15,6 +15,7 @@ import { useRouter } from "@/router";
 import { notePage } from "@/filters/note";
 import type { NoteTranslation } from "@/types/note";
 import type { MenuItem } from "@/types/menu";
+import type { NoteDraft } from "@/types/post-form";
 
 const router = useRouter();
 
@@ -72,7 +73,7 @@ export function getNoteMenu(props: {
 			});
 
 			os.post({
-				initialNote: appearNote,
+				initialNote: appearNote as NoteDraft,
 				renote: appearNote.renote,
 				reply: appearNote.reply,
 				channel: appearNote.channel,
@@ -83,7 +84,7 @@ export function getNoteMenu(props: {
 
 	async function edit() {
 		os.post({
-			initialNote: appearNote,
+			initialNote: appearNote as NoteDraft,
 			renote: appearNote.renote,
 			reply: appearNote.reply,
 			channel: appearNote.channel,
diff --git a/packages/client/src/scripts/langmap.ts b/packages/client/src/scripts/langmap.ts
index bfb6bec00d..ae82146bec 100644
--- a/packages/client/src/scripts/langmap.ts
+++ b/packages/client/src/scripts/langmap.ts
@@ -1,385 +1,6 @@
-// TODO: sharedに置いてバックエンドのと統合したい
-export const iso639Langs1 = {
-	af: {
-		nativeName: "Afrikaans",
-	},
-	ak: {
-		nativeName: "Tɕɥi",
-	},
-	ar: {
-		nativeName: "العربية",
-		rtl: true,
-	},
-	ay: {
-		nativeName: "Aymar aru",
-	},
-	az: {
-		nativeName: "Azərbaycan dili",
-	},
-	be: {
-		nativeName: "Беларуская",
-	},
-	bg: {
-		nativeName: "Български",
-	},
-	bn: {
-		nativeName: "বাংলা",
-	},
-	br: {
-		nativeName: "Brezhoneg",
-	},
-	bs: {
-		nativeName: "Bosanski",
-	},
-	ca: {
-		nativeName: "Català",
-	},
-	cs: {
-		nativeName: "Čeština",
-	},
-	cy: {
-		nativeName: "Cymraeg",
-	},
-	da: {
-		nativeName: "Dansk",
-	},
-	de: {
-		nativeName: "Deutsch",
-	},
-	el: {
-		nativeName: "Ελληνικά",
-	},
-	en: {
-		nativeName: "English",
-	},
-	eo: {
-		nativeName: "Esperanto",
-	},
-	es: {
-		nativeName: "Español",
-	},
-	et: {
-		nativeName: "eesti keel",
-	},
-	eu: {
-		nativeName: "Euskara",
-	},
-	fa: {
-		nativeName: "فارسی",
-		rtl: true,
-	},
-	ff: {
-		nativeName: "Fulah",
-	},
-	fi: {
-		nativeName: "Suomi",
-	},
-	fo: {
-		nativeName: "Føroyskt",
-	},
-	fr: {
-		nativeName: "Français",
-	},
-	fy: {
-		nativeName: "Frysk",
-	},
-	ga: {
-		nativeName: "Gaeilge",
-	},
-	gd: {
-		nativeName: "Gàidhlig",
-	},
-	gl: {
-		nativeName: "Galego",
-	},
-	gn: {
-		nativeName: "Avañe'ẽ",
-	},
-	gu: {
-		nativeName: "ગુજરાતી",
-	},
-	gv: {
-		nativeName: "Gaelg",
-	},
-	he: {
-		nativeName: "עברית‏",
-		rtl: true,
-	},
-	hi: {
-		nativeName: "हिन्दी",
-	},
-	hr: {
-		nativeName: "Hrvatski",
-	},
-	ht: {
-		nativeName: "Kreyòl",
-	},
-	hu: {
-		nativeName: "Magyar",
-	},
-	hy: {
-		nativeName: "Հայերեն",
-	},
-	id: {
-		nativeName: "Bahasa Indonesia",
-	},
-	is: {
-		nativeName: "Íslenska",
-	},
-	it: {
-		nativeName: "Italiano",
-	},
-	ja: {
-		nativeName: "日本語",
-	},
-	jv: {
-		nativeName: "Basa Jawa",
-	},
-	ka: {
-		nativeName: "ქართული",
-	},
-	kk: {
-		nativeName: "Қазақша",
-	},
-	kl: {
-		nativeName: "kalaallisut",
-	},
-	km: {
-		nativeName: "ភាសាខ្មែរ",
-	},
-	kn: {
-		nativeName: "ಕನ್ನಡ",
-	},
-	ko: {
-		nativeName: "한국어",
-	},
-	ku: {
-		nativeName: "Kurdî",
-	},
-	kw: {
-		nativeName: "Kernewek",
-	},
-	la: {
-		nativeName: "Latin",
-	},
-	lb: {
-		nativeName: "Lëtzebuergesch",
-	},
-	li: {
-		nativeName: "Lèmbörgs",
-	},
-	lt: {
-		nativeName: "Lietuvių",
-	},
-	lv: {
-		nativeName: "Latviešu",
-	},
-	mg: {
-		nativeName: "Malagasy",
-	},
-	mk: {
-		nativeName: "Македонски",
-	},
-	ml: {
-		nativeName: "മലയാളം",
-	},
-	mn: {
-		nativeName: "Монгол",
-	},
-	mr: {
-		nativeName: "मराठी",
-	},
-	ms: {
-		nativeName: "Bahasa Melayu",
-	},
-	mt: {
-		nativeName: "Malti",
-	},
-	my: {
-		nativeName: "ဗမာစကာ",
-	},
-	no: {
-		nativeName: "Norsk",
-	},
-	nb: {
-		nativeName: "Norsk (bokmål)",
-	},
-	ne: {
-		nativeName: "नेपाली",
-	},
-	nl: {
-		nativeName: "Nederlands",
-	},
-	nn: {
-		nativeName: "Norsk (nynorsk)",
-	},
-	oc: {
-		nativeName: "Occitan",
-	},
-	or: {
-		nativeName: "ଓଡ଼ିଆ",
-	},
-	pa: {
-		nativeName: "ਪੰਜਾਬੀ",
-	},
-	pl: {
-		nativeName: "Polski",
-	},
-	ps: {
-		nativeName: "پښتو",
-		rtl: true,
-	},
-	pt: {
-		nativeName: "Português",
-	},
-	qu: {
-		nativeName: "Qhichwa",
-	},
-	rm: {
-		nativeName: "Rumantsch",
-	},
-	ro: {
-		nativeName: "Română",
-	},
-	ru: {
-		nativeName: "Русский",
-	},
-	sa: {
-		nativeName: "संस्कृतम्",
-	},
-	se: {
-		nativeName: "Davvisámegiella",
-	},
-	sh: {
-		nativeName: "српскохрватски",
-	},
-	si: {
-		nativeName: "සිංහල",
-	},
-	sk: {
-		nativeName: "Slovenčina",
-	},
-	sl: {
-		nativeName: "Slovenščina",
-	},
-	so: {
-		nativeName: "Soomaaliga",
-	},
-	sq: {
-		nativeName: "Shqip",
-	},
-	sr: {
-		nativeName: "Српски",
-	},
-	su: {
-		nativeName: "Basa Sunda",
-	},
-	sv: {
-		nativeName: "Svenska",
-	},
-	sw: {
-		nativeName: "Kiswahili",
-	},
-	ta: {
-		nativeName: "தமிழ்",
-	},
-	te: {
-		nativeName: "తెలుగు",
-	},
-	tg: {
-		nativeName: "забо́ни тоҷикӣ́",
-	},
-	th: {
-		nativeName: "ภาษาไทย",
-	},
-	tr: {
-		nativeName: "Türkçe",
-	},
-	tt: {
-		nativeName: "татарча",
-	},
-	uk: {
-		nativeName: "Українська",
-	},
-	ur: {
-		nativeName: "اردو",
-		rtl: true,
-	},
-	uz: {
-		nativeName: "O'zbek",
-	},
-	vi: {
-		nativeName: "Tiếng Việt",
-	},
-	xh: {
-		nativeName: "isiXhosa",
-	},
-	yi: {
-		nativeName: "ייִדיש",
-		rtl: true,
-	},
-	zh: {
-		nativeName: "中文",
-	},
-	zu: {
-		nativeName: "isiZulu",
-	},
-};
+import { langmap as _langmap } from "firefish-js";
 
-export const iso639Langs3 = {
-	ach: {
-		nativeName: "Lwo",
-	},
-	ady: {
-		nativeName: "Адыгэбзэ",
-	},
-	cak: {
-		nativeName: "Maya Kaqchikel",
-	},
-	chr: {
-		nativeName: "ᏣᎳᎩ (tsalagi)",
-	},
-	dsb: {
-		nativeName: "Dolnoserbšćina",
-	},
-	fil: {
-		nativeName: "Filipino",
-	},
-	hsb: {
-		nativeName: "Hornjoserbšćina",
-	},
-	kab: {
-		nativeName: "Taqbaylit",
-	},
-	mai: {
-		nativeName: "मैथिली, মৈথিলী",
-	},
-	tlh: {
-		nativeName: "tlhIngan-Hol",
-	},
-	tok: {
-		nativeName: "Toki Pona",
-	},
-	yue: {
-		nativeName: "粵語",
-	},
-	nan: {
-		nativeName: "閩南語",
-	},
-};
-
-export const langmapNoRegion = Object.assign({}, iso639Langs1, iso639Langs3);
-
-export const iso639Regional = {
-	"zh-hans": {
-		nativeName: "中文(简体)",
-	},
-	"zh-hant": {
-		nativeName: "中文(繁體)",
-	},
-};
-
-export const langmap = Object.assign({}, langmapNoRegion, iso639Regional);
+export const langmap = _langmap;
 
 /**
  * @see https://github.com/komodojp/tinyld/blob/develop/docs/langs.md
diff --git a/packages/client/src/scripts/language-utils.ts b/packages/client/src/scripts/language-utils.ts
index 93696028de..b73348ba8f 100644
--- a/packages/client/src/scripts/language-utils.ts
+++ b/packages/client/src/scripts/language-utils.ts
@@ -39,7 +39,7 @@ export function languageContains(
 ) {
 	if (!langCode1 || !langCode2) return false;
 
-	return parentLanguage(langCode2) === langCode1;
+	return langCode1 === langCode2 || parentLanguage(langCode2) === langCode1;
 }
 
 export function parentLanguage(langCode: string | null) {
diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts
index a742a13ddc..a25e52ef8f 100644
--- a/packages/client/src/store.ts
+++ b/packages/client/src/store.ts
@@ -1,9 +1,12 @@
-import { markRaw, ref } from "vue";
+import { markRaw } from "vue";
 import type { ApiTypes, entities } from "firefish-js";
 import { isSignedIn, me } from "./me";
 import { Storage } from "./pizzax";
 import type { NoteVisibility } from "@/types/note";
 
+// biome-ignore lint/suspicious/noExplicitAny: <explanation>
+type TODO = any;
+
 export const postFormActions: {
 	title: string;
 	handler: (from, update) => void | Promise<void>;
@@ -152,7 +155,7 @@ export const defaultStore = markRaw(
 				type: string;
 				size: "verySmall" | "small" | "medium" | "large" | "veryLarge";
 				black: boolean;
-				props: Record<string, any>;
+				props: Record<string, TODO>;
 			}[],
 		},
 		widgets: {
@@ -161,7 +164,7 @@ export const defaultStore = markRaw(
 				name: string;
 				id: string;
 				place: string | null;
-				data: Record<string, any>;
+				data: Record<string, TODO>;
 			}[],
 		},
 		tl: {
@@ -465,109 +468,6 @@ export const defaultStore = markRaw(
 	}),
 );
 
-// TODO: 他のタブと永続化されたstateを同期
+import ColdStore from "./cold-store";
 
-const PREFIX = "miux:";
-
-interface Plugin {
-	id: string;
-	name: string;
-	active: boolean;
-	configData: Record<string, any>;
-	token: string;
-	ast: any[];
-}
-
-import darkTheme from "@/themes/d-rosepine.json5";
-/**
- * Storage for configuration information that does not need to be constantly loaded into memory (non-reactive)
- */
-import lightTheme from "@/themes/l-rosepinedawn.json5";
-
-export class ColdDeviceStorage {
-	public static default = {
-		lightTheme,
-		darkTheme,
-		syncDeviceDarkMode: true,
-		plugins: [] as Plugin[],
-		mediaVolume: 0.5,
-		vibrate: false,
-		sound_masterVolume: 0.3,
-		sound_note: { type: "none", volume: 0 },
-		sound_noteMy: { type: "syuilo/up", volume: 1 },
-		sound_notification: { type: "syuilo/pope2", volume: 1 },
-		sound_chat: { type: "syuilo/pope1", volume: 1 },
-		sound_chatBg: { type: "syuilo/waon", volume: 1 },
-		sound_antenna: { type: "syuilo/triple", volume: 1 },
-		sound_channel: { type: "syuilo/square-pico", volume: 1 },
-	};
-
-	public static watchers = [];
-
-	public static get<T extends keyof typeof ColdDeviceStorage.default>(
-		key: T,
-	): (typeof ColdDeviceStorage.default)[T] {
-		// TODO: indexedDBにする
-		//       ただしその際はnullチェックではなくキー存在チェックにしないとダメ
-		//       (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある)
-		const value = localStorage.getItem(PREFIX + key);
-		if (value == null) {
-			return ColdDeviceStorage.default[key];
-		} else {
-			return JSON.parse(value);
-		}
-	}
-
-	public static set<T extends keyof typeof ColdDeviceStorage.default>(
-		key: T,
-		value: (typeof ColdDeviceStorage.default)[T],
-	): void {
-		// 呼び出し側のバグ等で undefined が来ることがある
-		// undefined を文字列として localStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視
-		if (value === undefined) {
-			console.error(`attempt to store undefined value for key '${key}'`);
-			return;
-		}
-
-		localStorage.setItem(PREFIX + key, JSON.stringify(value));
-
-		for (const watcher of this.watchers) {
-			if (watcher.key === key) watcher.callback(value);
-		}
-	}
-
-	public static watch(key, callback) {
-		this.watchers.push({ key, callback });
-	}
-
-	// TODO: VueのcustomRef使うと良い感じになるかも
-	public static ref<T extends keyof typeof ColdDeviceStorage.default>(key: T) {
-		const v = ColdDeviceStorage.get(key);
-		const r = ref(v);
-		// TODO: このままではwatcherがリークするので開放する方法を考える
-		this.watch(key, (v) => {
-			r.value = v;
-		});
-		return r;
-	}
-
-	/**
-	 * 特定のキーの、簡易的なgetter/setterを作ります
-	 * 主にvue場で設定コントロールのmodelとして使う用
-	 */
-	public static makeGetterSetter<
-		K extends keyof typeof ColdDeviceStorage.default,
-	>(key: K) {
-		// TODO: VueのcustomRef使うと良い感じになるかも
-		const valueRef = ColdDeviceStorage.ref(key);
-		return {
-			get: () => {
-				return valueRef.value;
-			},
-			set: (value: unknown) => {
-				const val = value;
-				ColdDeviceStorage.set(key, val);
-			},
-		};
-	}
-}
+export const ColdDeviceStorage = ColdStore;
diff --git a/packages/client/src/stream.ts b/packages/client/src/stream.ts
index 89dda63f08..b620c60c9d 100644
--- a/packages/client/src/stream.ts
+++ b/packages/client/src/stream.ts
@@ -31,6 +31,7 @@ export function reloadStream() {
 
 	isReloading = true;
 	stream.close();
+	// biome-ignore lint/suspicious/noAssignInExpressions: assign intentionally
 	stream.once("_connected_", () => (isReloading = false));
 	stream.stream.reconnect();
 	isReloading = false;
diff --git a/packages/client/src/theme-store.ts b/packages/client/src/theme-store.ts
index bc344b339d..6f4616878f 100644
--- a/packages/client/src/theme-store.ts
+++ b/packages/client/src/theme-store.ts
@@ -17,7 +17,8 @@ export async function fetchThemes(): Promise<void> {
 			key: "themes",
 		});
 		localStorage.setItem(lsCacheKey, JSON.stringify(themes));
-	} catch (err) {
+		// biome-ignore lint/suspicious/noExplicitAny: Safely any
+	} catch (err: any) {
 		if (err.code === "NO_SUCH_KEY") return;
 		throw err;
 	}
diff --git a/packages/client/src/types/page.ts b/packages/client/src/types/page.ts
new file mode 100644
index 0000000000..30750e104d
--- /dev/null
+++ b/packages/client/src/types/page.ts
@@ -0,0 +1,78 @@
+import type { TypeUtils } from "firefish-js";
+
+export type BasePageContent = {
+	name: string;
+};
+
+export type PageContentTextInput = BasePageContent & {
+	type: "textInput";
+	default: string;
+};
+
+export type PageContentTextareaInput = BasePageContent & {
+	type: "textareaInput";
+	default?: string;
+};
+
+export type PageContentNumberInput = BasePageContent & {
+	type: "numberInput";
+	default?: number;
+};
+
+export type PageContentSwitch = BasePageContent & {
+	type: "switch";
+	default?: boolean;
+};
+export type PageContentCounter = BasePageContent & {
+	type: "counter";
+	default?: number;
+};
+
+export type PageContentRadioButton = BasePageContent & {
+	type: "radioButton";
+	default?: string;
+};
+
+export type PageContentChildren =
+	| PageContentTextInput
+	| PageContentTextareaInput
+	| PageContentNumberInput
+	| PageContentSwitch
+	| PageContentCounter
+	| PageContentRadioButton;
+
+export type PageContentParent = {
+	type: "parent";
+	children: PageContentChildren[];
+};
+
+export type PageContent = PageContentParent | PageContentChildren;
+
+export type GetPageVar<T extends PageContentChildren> = {
+	name: string;
+	type: TypeUtils.NonUndefinedAble<T["default"]> extends string
+		? "string"
+		: TypeUtils.NonUndefinedAble<T["default"]> extends boolean
+			? "boolean"
+			: TypeUtils.NonUndefinedAble<T["default"]> extends number
+				? "number"
+				: never;
+	value: TypeUtils.NonUndefinedAble<T["default"]>;
+};
+
+export type PageVar =
+	| {
+			name: string;
+			type: "string";
+			value: string;
+	  }
+	| {
+			name: string;
+			type: "boolean";
+			value: boolean;
+	  }
+	| {
+			name: string;
+			type: "number";
+			value: number;
+	  };
diff --git a/packages/client/src/types/relation.ts b/packages/client/src/types/relation.ts
new file mode 100644
index 0000000000..1f4703f52f
--- /dev/null
+++ b/packages/client/src/types/relation.ts
@@ -0,0 +1,5 @@
+export type Predicate<T> = (a: T) => boolean;
+
+export type Relation<T, U> = (a: T, b: U) => boolean;
+
+export type EndoRelation<T> = Relation<T, T>;
diff --git a/packages/firefish-js/.swcrc b/packages/firefish-js/.swcrc
index 89800c7b67..f398cdce5c 100644
--- a/packages/firefish-js/.swcrc
+++ b/packages/firefish-js/.swcrc
@@ -17,7 +17,7 @@
 	},
 	"minify": false,
 	"module": {
-		"type": "commonjs",
+		"type": "es6",
 		"strict": true,
 		"resolveFully": true
 	}
diff --git a/packages/firefish-js/package.json b/packages/firefish-js/package.json
index 01e5487988..b6512b1b79 100644
--- a/packages/firefish-js/package.json
+++ b/packages/firefish-js/package.json
@@ -4,6 +4,7 @@
 	"description": "Firefish SDK for JavaScript",
 	"homepage": "https://firefish.dev/firefish/firefish/-/tree/develop/packages/firefish-js",
 	"main": "./built/index.js",
+	"type": "module",
 	"types": "./src/index.ts",
 	"license": "MIT",
 	"scripts": {
@@ -35,7 +36,7 @@
 		"typescript": "5.4.5"
 	},
 	"files": [
-		"built"
+		"built", "src"
 	],
 	"dependencies": {
 		"eventemitter3": "5.0.1",
diff --git a/packages/firefish-js/src/api.ts b/packages/firefish-js/src/api.ts
index 4a7e1f0b64..5553c1feeb 100644
--- a/packages/firefish-js/src/api.ts
+++ b/packages/firefish-js/src/api.ts
@@ -1,4 +1,4 @@
-import type { Endpoints } from "./api.types";
+import type { Endpoints } from "./api.types.js";
 
 const MK_API_ERROR = Symbol();
 
@@ -7,10 +7,12 @@ export type APIError = {
 	code: string;
 	message: string;
 	kind: "client" | "server";
-	info: Record<string, any>;
+	info: Record<string, unknown>;
 };
 
-export function isAPIError(reason: any): reason is APIError {
+// biome-ignore lint/suspicious/noExplicitAny: used it intentially
+type ExplicitlyUsedAny = any;
+export function isAPIError(reason: ExplicitlyUsedAny): reason is APIError {
 	return reason[MK_API_ERROR] === true;
 }
 
@@ -24,7 +26,7 @@ export type FetchLike = (
 	},
 ) => Promise<{
 	status: number;
-	json(): Promise<any>;
+	json(): Promise<ExplicitlyUsedAny>;
 }>;
 
 type IsNeverType<T> = [T] extends [never] ? true : false;
@@ -36,7 +38,10 @@ type IsCaseMatched<
 	P extends Endpoints[E]["req"],
 	C extends number,
 > = IsNeverType<
-	StrictExtract<Endpoints[E]["res"]["$switch"]["$cases"][C], [P, any]>
+	StrictExtract<
+		Endpoints[E]["res"]["$switch"]["$cases"][C],
+		[P, ExplicitlyUsedAny]
+	>
 > extends false
 	? true
 	: false;
@@ -45,7 +50,10 @@ type GetCaseResult<
 	E extends keyof Endpoints,
 	P extends Endpoints[E]["req"],
 	C extends number,
-> = StrictExtract<Endpoints[E]["res"]["$switch"]["$cases"][C], [P, any]>[1];
+> = StrictExtract<
+	Endpoints[E]["res"]["$switch"]["$cases"][C],
+	[P, ExplicitlyUsedAny]
+>[1];
 
 export class APIClient {
 	public origin: string;
@@ -70,7 +78,7 @@ export class APIClient {
 		credential?: string | null | undefined,
 	): Promise<
 		Endpoints[E]["res"] extends {
-			$switch: { $cases: [any, any][]; $default: any };
+			$switch: { $cases: [unknown, unknown][]; $default: unknown };
 		}
 			? IsCaseMatched<E, P, 0> extends true
 				? GetCaseResult<E, P, 0>
diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts
index 1ee94b9954..c458e46a09 100644
--- a/packages/firefish-js/src/api.types.ts
+++ b/packages/firefish-js/src/api.types.ts
@@ -38,7 +38,7 @@ import type {
 	UserList,
 	UserLite,
 	UserSorting,
-} from "./entities";
+} from "./entities.js";
 
 import type * as consts from "./consts";
 
@@ -263,9 +263,9 @@ export type Endpoints = {
 
 	// clips
 	"clips/add-note": { req: TODO; res: TODO };
-	"clips/create": { req: TODO; res: TODO };
+	"clips/create": { req: TODO; res: Clip };
 	"clips/delete": { req: { clipId: Clip["id"] }; res: null };
-	"clips/list": { req: TODO; res: TODO };
+	"clips/list": { req: TODO; res: Clip[] };
 	"clips/notes": { req: TODO; res: TODO };
 	"clips/show": { req: TODO; res: TODO };
 	"clips/update": { req: TODO; res: TODO };
@@ -362,6 +362,16 @@ export type Endpoints = {
 		res: DriveFile[];
 	};
 
+	"email-address/available": {
+		req: {
+			emailAddress: string;
+		};
+		res: {
+			available?: boolean;
+			reason: string | null;
+		};
+	};
+
 	// endpoint
 	endpoint: {
 		req: { endpoint: string };
@@ -738,6 +748,18 @@ export type Endpoints = {
 		};
 		res: Note[];
 	};
+	"notes/thread-muting/create": {
+		req: {
+			noteId: Note["id"];
+		};
+		res: null;
+	};
+	"notes/thread-muting/delete": {
+		req: {
+			noteId: Note["id"];
+		};
+		res: null;
+	};
 	"notes/hybrid-timeline": {
 		req: {
 			limit?: number;
@@ -758,6 +780,12 @@ export type Endpoints = {
 		};
 		res: Note[];
 	};
+	"notes/make-private": {
+		req: {
+			noteId: Note["id"];
+		};
+		res: null;
+	};
 	"notes/mentions": {
 		req: {
 			following?: boolean;
@@ -899,6 +927,16 @@ export type Endpoints = {
 	// promo
 	"promo/read": { req: TODO; res: TODO };
 
+	// release
+	release: {
+		req: null;
+		res: {
+			version: string;
+			notes: string;
+			screenshots: string[];
+		};
+	};
+
 	// request-reset-password
 	"request-reset-password": {
 		req: { username: string; email: string };
@@ -921,8 +959,36 @@ export type Endpoints = {
 	// ck specific
 	"latest-version": { req: NoParams; res: TODO };
 
+	// signin
+	signin: {
+		req: {
+			username: string;
+			password: string;
+			"hcaptcha-response"?: null | string;
+			"g-recaptcha-response"?: null | string;
+		};
+		res:
+			| {
+					id: User["id"];
+					i: string;
+			  }
+			| {
+					challenge: string;
+					challengeId: string;
+					securityKeys: {
+						id: string;
+					}[];
+			  };
+	};
+
 	// sw
 	"sw/register": { req: TODO; res: TODO };
+	"sw/unregister": {
+		req: {
+			endpoint: string;
+		};
+		res: null;
+	};
 
 	// username
 	"username/available": {
diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index 9ab3c2fff6..93e11285ba 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -1,4 +1,5 @@
-import type * as consts from "./consts";
+import type * as consts from "./consts.js";
+import type { Packed } from "./misc/schema.js";
 
 export type ID = string;
 export type DateString = string;
@@ -116,6 +117,7 @@ export type MeDetailed = UserDetailed & {
 	preventAiLearning: boolean;
 	receiveAnnouncementEmail: boolean;
 	usePasswordLessLogin: boolean;
+	token: string;
 	[other: string]: any;
 };
 
@@ -481,7 +483,7 @@ export type Announcement = {
 	imageUrl: string | null;
 	isRead?: boolean;
 	isGoodNews: boolean;
-	showPopUp: boolean;
+	showPopup: boolean;
 };
 
 export type Antenna = {
@@ -512,7 +514,7 @@ export type AuthSession = {
 
 export type Ad = TODO;
 
-export type Clip = TODO;
+export type Clip = Packed<"Clip">;
 
 export type NoteFavorite = {
 	id: ID;
diff --git a/packages/firefish-js/src/index.ts b/packages/firefish-js/src/index.ts
index 3398ed8a2e..b5baa85989 100644
--- a/packages/firefish-js/src/index.ts
+++ b/packages/firefish-js/src/index.ts
@@ -1,14 +1,17 @@
-import * as acct from "./acct";
-import type { Acct } from "./acct";
-import { Endpoints } from "./api.types";
-import type * as ApiTypes from "./api.types";
-import * as consts from "./consts";
-import Stream, { Connection } from "./streaming";
-import * as StreamTypes from "./streaming.types";
-import type * as TypeUtils from "./type-utils";
+import * as acct from "./acct.js";
+import type { Acct } from "./acct.js";
+import type { Endpoints } from "./api.types.js";
+import type * as ApiTypes from "./api.types.js";
+import * as consts from "./consts.js";
+import Stream, { Connection } from "./streaming.js";
+import * as StreamTypes from "./streaming.types.js";
+import type * as TypeUtils from "./type-utils.js";
+
+import type * as SchemaTypes from "./misc/schema.js";
+import * as Schema from "./misc/schema.js";
 
 export {
-	Endpoints,
+	type Endpoints,
 	type ApiTypes,
 	Stream,
 	Connection as ChannelConnection,
@@ -16,6 +19,8 @@ export {
 	acct,
 	type Acct,
 	type TypeUtils,
+	Schema,
+	type SchemaTypes,
 };
 
 export const permissions = consts.permissions;
@@ -26,9 +31,12 @@ export const languages = consts.languages;
 export const ffVisibility = consts.ffVisibility;
 export const instanceSortParam = consts.instanceSortParam;
 
+import { langmap, type PostLanguage } from "./misc/langmap.js";
+export { langmap, type PostLanguage };
+
 // api extractor not supported yet
 //export * as api from './api';
 //export * as entities from './entities';
-import * as api from "./api";
-import * as entities from "./entities";
+import * as api from "./api.js";
+import * as entities from "./entities.js";
 export { api, entities };
diff --git a/packages/backend/src/misc/langmap.ts b/packages/firefish-js/src/misc/langmap.ts
similarity index 98%
rename from packages/backend/src/misc/langmap.ts
rename to packages/firefish-js/src/misc/langmap.ts
index 2506a36151..16d169d914 100644
--- a/packages/backend/src/misc/langmap.ts
+++ b/packages/firefish-js/src/misc/langmap.ts
@@ -1,4 +1,3 @@
-// TODO: sharedに置いてバックエンドのと統合したい
 export const iso639Langs1 = {
 	af: {
 		nativeName: "Afrikaans",
diff --git a/packages/firefish-js/src/misc/schema.ts b/packages/firefish-js/src/misc/schema.ts
new file mode 100644
index 0000000000..811190e3e1
--- /dev/null
+++ b/packages/firefish-js/src/misc/schema.ts
@@ -0,0 +1,241 @@
+import {
+	packedUserLiteSchema,
+	packedUserDetailedNotMeOnlySchema,
+	packedMeDetailedOnlySchema,
+	packedUserDetailedNotMeSchema,
+	packedMeDetailedSchema,
+	packedUserDetailedSchema,
+	packedUserSchema,
+} from "../schema/user.js";
+import { packedNoteSchema } from "../schema/note.js";
+import { packedUserListSchema } from "../schema/user-list.js";
+import { packedAppSchema } from "../schema/app.js";
+import { packedMessagingMessageSchema } from "../schema/messaging-message.js";
+import { packedNotificationSchema } from "../schema/notification.js";
+import { packedDriveFileSchema } from "../schema/drive-file.js";
+import { packedDriveFolderSchema } from "../schema/drive-folder.js";
+import { packedFollowingSchema } from "../schema/following.js";
+import { packedMutingSchema } from "../schema/muting.js";
+import { packedRenoteMutingSchema } from "../schema/renote-muting.js";
+import { packedReplyMutingSchema } from "../schema/reply-muting.js";
+import { packedBlockingSchema } from "../schema/blocking.js";
+import { packedNoteReactionSchema } from "../schema/note-reaction.js";
+import { packedHashtagSchema } from "../schema/hashtag.js";
+import { packedPageSchema } from "../schema/page.js";
+import { packedUserGroupSchema } from "../schema/user-group.js";
+import { packedNoteFavoriteSchema } from "../schema/note-favorite.js";
+import { packedChannelSchema } from "../schema/channel.js";
+import { packedAntennaSchema } from "../schema/antenna.js";
+import { packedClipSchema } from "../schema/clip.js";
+import { packedFederationInstanceSchema } from "../schema/federation-instance.js";
+import { packedQueueCountSchema } from "../schema/queue.js";
+import { packedGalleryPostSchema } from "../schema/gallery-post.js";
+import { packedEmojiSchema } from "../schema/emoji.js";
+import { packedNoteEdit } from "../schema/note-edit.js";
+import { packedNoteFileSchema } from "../schema/note-file.js";
+import { packedAbuseUserReportSchema } from "../schema/abuse-user-report.js";
+
+export const refs = {
+	AbuseUserReport: packedAbuseUserReportSchema,
+	UserLite: packedUserLiteSchema,
+	UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema,
+	MeDetailedOnly: packedMeDetailedOnlySchema,
+	UserDetailedNotMe: packedUserDetailedNotMeSchema,
+	MeDetailed: packedMeDetailedSchema,
+	UserDetailed: packedUserDetailedSchema,
+	User: packedUserSchema,
+
+	UserList: packedUserListSchema,
+	UserGroup: packedUserGroupSchema,
+	App: packedAppSchema,
+	MessagingMessage: packedMessagingMessageSchema,
+	Note: packedNoteSchema,
+	NoteFile: packedNoteFileSchema,
+	NoteEdit: packedNoteEdit,
+	NoteReaction: packedNoteReactionSchema,
+	NoteFavorite: packedNoteFavoriteSchema,
+	Notification: packedNotificationSchema,
+	DriveFile: packedDriveFileSchema,
+	DriveFolder: packedDriveFolderSchema,
+	Following: packedFollowingSchema,
+	Muting: packedMutingSchema,
+	RenoteMuting: packedRenoteMutingSchema,
+	ReplyMuting: packedReplyMutingSchema,
+	Blocking: packedBlockingSchema,
+	Hashtag: packedHashtagSchema,
+	Page: packedPageSchema,
+	Channel: packedChannelSchema,
+	QueueCount: packedQueueCountSchema,
+	Antenna: packedAntennaSchema,
+	Clip: packedClipSchema,
+	FederationInstance: packedFederationInstanceSchema,
+	GalleryPost: packedGalleryPostSchema,
+	Emoji: packedEmojiSchema,
+};
+
+// biome-ignore lint/suspicious/noExplicitAny: used it intentially
+type ExplicitlyUsedAny = any;
+
+export type Packed<x extends keyof typeof refs> = SchemaType<(typeof refs)[x]>;
+
+type TypeStringef =
+	| "null"
+	| "boolean"
+	| "integer"
+	| "number"
+	| "string"
+	| "array"
+	| "object"
+	| "any";
+type StringDefToType<T extends TypeStringef> = T extends "null"
+	? null
+	: T extends "boolean"
+		? boolean
+		: T extends "integer"
+			? number
+			: T extends "number"
+				? number
+				: T extends "string"
+					? string | Date
+					: T extends "array"
+						? ReadonlyArray<ExplicitlyUsedAny>
+						: T extends "object"
+							? Record<string, ExplicitlyUsedAny>
+							: ExplicitlyUsedAny;
+
+// https://swagger.io/specification/?sbsearch=optional#schema-object
+type OfSchema = {
+	readonly anyOf?: ReadonlyArray<Schema>;
+	readonly oneOf?: ReadonlyArray<Schema>;
+	readonly allOf?: ReadonlyArray<Schema>;
+};
+
+export interface Schema extends OfSchema {
+	readonly type?: TypeStringef;
+	readonly nullable?: boolean;
+	readonly optional?: boolean;
+	readonly items?: Schema;
+	readonly properties?: Obj;
+	readonly required?: ReadonlyArray<
+		Extract<keyof NonNullable<this["properties"]>, string>
+	>;
+	readonly description?: string;
+	readonly example?: ExplicitlyUsedAny;
+	readonly format?: string;
+	readonly ref?: keyof typeof refs;
+	readonly enum?: ReadonlyArray<string>;
+	readonly default?:
+		| (this["type"] extends TypeStringef
+				? StringDefToType<this["type"]>
+				: ExplicitlyUsedAny)
+		| null;
+	readonly maxLength?: number;
+	readonly minLength?: number;
+	readonly maximum?: number;
+	readonly minimum?: number;
+	readonly pattern?: string;
+}
+
+type RequiredPropertyNames<s extends Obj> = {
+	[K in keyof s]: // K is not optional
+	s[K]["optional"] extends false
+		? K
+		: // K has default value
+			s[K]["default"] extends
+					| null
+					| string
+					| number
+					| boolean
+					| Record<string, unknown>
+			? K
+			: never;
+}[keyof s];
+
+export type Obj = Record<string, Schema>;
+
+// https://github.com/misskey-dev/misskey/issues/8535
+// To avoid excessive stack depth error,
+// deceive TypeScript with UnionToIntersection (or more precisely, `infer` expression within it).
+export type ObjType<
+	s extends Obj,
+	RequiredProps extends keyof s,
+> = UnionToIntersection<
+	{
+		-readonly [R in RequiredPropertyNames<s>]-?: SchemaType<s[R]>;
+	} & {
+		-readonly [R in RequiredProps]-?: SchemaType<s[R]>;
+	} & {
+		-readonly [P in keyof s]?: SchemaType<s[P]>;
+	}
+>;
+
+type NullOrUndefined<p extends Schema, T> =
+	| (p["nullable"] extends true ? null : never)
+	| (p["optional"] extends true ? undefined : never)
+	| T;
+
+// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
+// Get intersection from union
+type UnionToIntersection<U> = (
+	U extends ExplicitlyUsedAny
+		? (k: U) => void
+		: never
+) extends (k: infer I) => void
+	? I
+	: never;
+
+// https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552
+// To get union, we use `Foo extends ExplicitlyUsedAny ? Hoge<Foo> : never`
+type UnionSchemaType<
+	a extends readonly ExplicitlyUsedAny[],
+	X extends Schema = a[number],
+> = X extends ExplicitlyUsedAny ? SchemaType<X> : never;
+type ArrayUnion<T> = T extends ExplicitlyUsedAny ? Array<T> : never;
+
+export type SchemaTypeDef<p extends Schema> = p["type"] extends "null"
+	? null
+	: p["type"] extends "integer"
+		? number
+		: p["type"] extends "number"
+			? number
+			: p["type"] extends "string"
+				? p["enum"] extends readonly string[]
+					? p["enum"][number]
+					: p["format"] extends "date-time"
+						? string
+						: // Dateにする??
+							string
+				: p["type"] extends "boolean"
+					? boolean
+					: p["type"] extends "object"
+						? p["ref"] extends keyof typeof refs
+							? Packed<p["ref"]>
+							: p["properties"] extends NonNullable<Obj>
+								? ObjType<p["properties"], NonNullable<p["required"]>[number]>
+								: p["anyOf"] extends ReadonlyArray<Schema>
+									? UnionSchemaType<p["anyOf"]> &
+											Partial<UnionToIntersection<UnionSchemaType<p["anyOf"]>>>
+									: p["allOf"] extends ReadonlyArray<Schema>
+										? UnionToIntersection<UnionSchemaType<p["allOf"]>>
+										: ExplicitlyUsedAny
+						: p["type"] extends "array"
+							? p["items"] extends OfSchema
+								? p["items"]["anyOf"] extends ReadonlyArray<Schema>
+									? UnionSchemaType<NonNullable<p["items"]["anyOf"]>>[]
+									: p["items"]["oneOf"] extends ReadonlyArray<Schema>
+										? ArrayUnion<
+												UnionSchemaType<NonNullable<p["items"]["oneOf"]>>
+											>
+										: p["items"]["allOf"] extends ReadonlyArray<Schema>
+											? UnionToIntersection<
+													UnionSchemaType<NonNullable<p["items"]["allOf"]>>
+												>[]
+											: never
+								: p["items"] extends NonNullable<Schema>
+									? SchemaTypeDef<p["items"]>[]
+									: ExplicitlyUsedAny[]
+							: p["oneOf"] extends ReadonlyArray<Schema>
+								? UnionSchemaType<p["oneOf"]>
+								: ExplicitlyUsedAny;
+
+export type SchemaType<p extends Schema> = NullOrUndefined<p, SchemaTypeDef<p>>;
diff --git a/packages/backend/src/models/schema/abuse-user-report.ts b/packages/firefish-js/src/schema/abuse-user-report.ts
similarity index 100%
rename from packages/backend/src/models/schema/abuse-user-report.ts
rename to packages/firefish-js/src/schema/abuse-user-report.ts
diff --git a/packages/backend/src/models/schema/antenna.ts b/packages/firefish-js/src/schema/antenna.ts
similarity index 100%
rename from packages/backend/src/models/schema/antenna.ts
rename to packages/firefish-js/src/schema/antenna.ts
diff --git a/packages/backend/src/models/schema/app.ts b/packages/firefish-js/src/schema/app.ts
similarity index 100%
rename from packages/backend/src/models/schema/app.ts
rename to packages/firefish-js/src/schema/app.ts
diff --git a/packages/backend/src/models/schema/blocking.ts b/packages/firefish-js/src/schema/blocking.ts
similarity index 100%
rename from packages/backend/src/models/schema/blocking.ts
rename to packages/firefish-js/src/schema/blocking.ts
diff --git a/packages/backend/src/models/schema/channel.ts b/packages/firefish-js/src/schema/channel.ts
similarity index 100%
rename from packages/backend/src/models/schema/channel.ts
rename to packages/firefish-js/src/schema/channel.ts
diff --git a/packages/backend/src/models/schema/clip.ts b/packages/firefish-js/src/schema/clip.ts
similarity index 100%
rename from packages/backend/src/models/schema/clip.ts
rename to packages/firefish-js/src/schema/clip.ts
diff --git a/packages/backend/src/models/schema/drive-file.ts b/packages/firefish-js/src/schema/drive-file.ts
similarity index 100%
rename from packages/backend/src/models/schema/drive-file.ts
rename to packages/firefish-js/src/schema/drive-file.ts
diff --git a/packages/backend/src/models/schema/drive-folder.ts b/packages/firefish-js/src/schema/drive-folder.ts
similarity index 100%
rename from packages/backend/src/models/schema/drive-folder.ts
rename to packages/firefish-js/src/schema/drive-folder.ts
diff --git a/packages/backend/src/models/schema/emoji.ts b/packages/firefish-js/src/schema/emoji.ts
similarity index 100%
rename from packages/backend/src/models/schema/emoji.ts
rename to packages/firefish-js/src/schema/emoji.ts
diff --git a/packages/backend/src/models/schema/federation-instance.ts b/packages/firefish-js/src/schema/federation-instance.ts
similarity index 97%
rename from packages/backend/src/models/schema/federation-instance.ts
rename to packages/firefish-js/src/schema/federation-instance.ts
index 338e079e28..9ef0d337b5 100644
--- a/packages/backend/src/models/schema/federation-instance.ts
+++ b/packages/firefish-js/src/schema/federation-instance.ts
@@ -1,5 +1,3 @@
-import { config } from "@/config.js";
-
 export const packedFederationInstanceSchema = {
 	type: "object",
 	properties: {
@@ -83,7 +81,7 @@ export const packedFederationInstanceSchema = {
 			type: "string",
 			optional: false,
 			nullable: true,
-			example: config.version,
+			example: "20240424",
 		},
 		openRegistrations: {
 			type: "boolean",
diff --git a/packages/backend/src/models/schema/following.ts b/packages/firefish-js/src/schema/following.ts
similarity index 100%
rename from packages/backend/src/models/schema/following.ts
rename to packages/firefish-js/src/schema/following.ts
diff --git a/packages/backend/src/models/schema/gallery-post.ts b/packages/firefish-js/src/schema/gallery-post.ts
similarity index 100%
rename from packages/backend/src/models/schema/gallery-post.ts
rename to packages/firefish-js/src/schema/gallery-post.ts
diff --git a/packages/backend/src/models/schema/hashtag.ts b/packages/firefish-js/src/schema/hashtag.ts
similarity index 100%
rename from packages/backend/src/models/schema/hashtag.ts
rename to packages/firefish-js/src/schema/hashtag.ts
diff --git a/packages/backend/src/models/schema/messaging-message.ts b/packages/firefish-js/src/schema/messaging-message.ts
similarity index 100%
rename from packages/backend/src/models/schema/messaging-message.ts
rename to packages/firefish-js/src/schema/messaging-message.ts
diff --git a/packages/backend/src/models/schema/muting.ts b/packages/firefish-js/src/schema/muting.ts
similarity index 100%
rename from packages/backend/src/models/schema/muting.ts
rename to packages/firefish-js/src/schema/muting.ts
diff --git a/packages/backend/src/models/schema/note-edit.ts b/packages/firefish-js/src/schema/note-edit.ts
similarity index 100%
rename from packages/backend/src/models/schema/note-edit.ts
rename to packages/firefish-js/src/schema/note-edit.ts
diff --git a/packages/backend/src/models/schema/note-favorite.ts b/packages/firefish-js/src/schema/note-favorite.ts
similarity index 100%
rename from packages/backend/src/models/schema/note-favorite.ts
rename to packages/firefish-js/src/schema/note-favorite.ts
diff --git a/packages/backend/src/models/schema/note-file.ts b/packages/firefish-js/src/schema/note-file.ts
similarity index 100%
rename from packages/backend/src/models/schema/note-file.ts
rename to packages/firefish-js/src/schema/note-file.ts
diff --git a/packages/backend/src/models/schema/note-reaction.ts b/packages/firefish-js/src/schema/note-reaction.ts
similarity index 100%
rename from packages/backend/src/models/schema/note-reaction.ts
rename to packages/firefish-js/src/schema/note-reaction.ts
diff --git a/packages/backend/src/models/schema/note.ts b/packages/firefish-js/src/schema/note.ts
similarity index 94%
rename from packages/backend/src/models/schema/note.ts
rename to packages/firefish-js/src/schema/note.ts
index 6064919960..73e85e6f0d 100644
--- a/packages/backend/src/models/schema/note.ts
+++ b/packages/firefish-js/src/schema/note.ts
@@ -1,4 +1,4 @@
-import { langmap } from "@/misc/langmap.js";
+import { langmap } from "../misc/langmap.js";
 
 export const packedNoteSchema = {
 	type: "object",
@@ -208,15 +208,5 @@ export const packedNoteSchema = {
 			optional: true,
 			nullable: true,
 		},
-		myRenoteCount: {
-			type: "number",
-			optional: true,
-			nullable: false,
-		},
-		quoteCount: {
-			type: "number",
-			optional: false,
-			nullable: false,
-		},
 	},
 } as const;
diff --git a/packages/backend/src/models/schema/notification.ts b/packages/firefish-js/src/schema/notification.ts
similarity index 96%
rename from packages/backend/src/models/schema/notification.ts
rename to packages/firefish-js/src/schema/notification.ts
index 97fd16339c..dec921a8a7 100644
--- a/packages/backend/src/models/schema/notification.ts
+++ b/packages/firefish-js/src/schema/notification.ts
@@ -1,4 +1,4 @@
-import { notificationTypes } from "@/types.js";
+import { notificationTypes } from "../consts.js";
 
 export const packedNotificationSchema = {
 	type: "object",
diff --git a/packages/backend/src/models/schema/page.ts b/packages/firefish-js/src/schema/page.ts
similarity index 100%
rename from packages/backend/src/models/schema/page.ts
rename to packages/firefish-js/src/schema/page.ts
diff --git a/packages/backend/src/models/schema/queue.ts b/packages/firefish-js/src/schema/queue.ts
similarity index 100%
rename from packages/backend/src/models/schema/queue.ts
rename to packages/firefish-js/src/schema/queue.ts
diff --git a/packages/backend/src/models/schema/renote-muting.ts b/packages/firefish-js/src/schema/renote-muting.ts
similarity index 100%
rename from packages/backend/src/models/schema/renote-muting.ts
rename to packages/firefish-js/src/schema/renote-muting.ts
diff --git a/packages/backend/src/models/schema/reply-muting.ts b/packages/firefish-js/src/schema/reply-muting.ts
similarity index 100%
rename from packages/backend/src/models/schema/reply-muting.ts
rename to packages/firefish-js/src/schema/reply-muting.ts
diff --git a/packages/backend/src/models/schema/user-group.ts b/packages/firefish-js/src/schema/user-group.ts
similarity index 100%
rename from packages/backend/src/models/schema/user-group.ts
rename to packages/firefish-js/src/schema/user-group.ts
diff --git a/packages/backend/src/models/schema/user-list.ts b/packages/firefish-js/src/schema/user-list.ts
similarity index 100%
rename from packages/backend/src/models/schema/user-list.ts
rename to packages/firefish-js/src/schema/user-list.ts
diff --git a/packages/backend/src/models/schema/user.ts b/packages/firefish-js/src/schema/user.ts
similarity index 100%
rename from packages/backend/src/models/schema/user.ts
rename to packages/firefish-js/src/schema/user.ts
diff --git a/packages/firefish-js/src/streaming.ts b/packages/firefish-js/src/streaming.ts
index 58491bc8c0..0fcd0c7498 100644
--- a/packages/firefish-js/src/streaming.ts
+++ b/packages/firefish-js/src/streaming.ts
@@ -1,6 +1,6 @@
 import { EventEmitter } from "eventemitter3";
 import ReconnectingWebsocket from "reconnecting";
-import type { BroadcastEvents, Channels } from "./streaming.types";
+import type { BroadcastEvents, Channels } from "./streaming.types.js";
 
 function autobind(instance: any): void {
 	const prototype = Object.getPrototypeOf(instance);
diff --git a/packages/firefish-js/src/streaming.types.ts b/packages/firefish-js/src/streaming.types.ts
index 5b81780271..ca4aa3396d 100644
--- a/packages/firefish-js/src/streaming.types.ts
+++ b/packages/firefish-js/src/streaming.types.ts
@@ -12,7 +12,7 @@ import type {
 	UserGroup,
 	UserLite,
 } from "./entities";
-import type { Connection } from "./streaming";
+import type { Connection } from "./streaming.js";
 
 type FIXME = any;
 
diff --git a/packages/firefish-js/src/type-utils.ts b/packages/firefish-js/src/type-utils.ts
index bf8297487b..92695709b5 100644
--- a/packages/firefish-js/src/type-utils.ts
+++ b/packages/firefish-js/src/type-utils.ts
@@ -1,7 +1,9 @@
-import type { Endpoints } from "./api.types";
+import type { Endpoints } from "./api.types.js";
 
-type PropertyOfType<Type, U> = {
+export type PropertyOfType<Type, U> = {
 	[K in keyof Type]: Type[K] extends U ? K : never;
 }[keyof Type];
 
 export type EndpointsOf<T> = PropertyOfType<Endpoints, { res: T }>;
+
+export type NonUndefinedAble<T> = T extends undefined ? never : T;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9b466c2336..56657373e1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -141,6 +141,9 @@ importers:
       file-type:
         specifier: 19.0.0
         version: 19.0.0
+      firefish-js:
+        specifier: workspace:*
+        version: link:../firefish-js
       fluent-ffmpeg:
         specifier: 2.1.2
         version: 2.1.2