From 6b03989c2eefa9e2a3e5733934666c8f5aa0ff83 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Wed, 3 Jul 2024 23:48:32 +0900
Subject: [PATCH] refactor (client): move instance data to IndexDB (close
 #10939)

---
 .../client/src/components/MkAutocomplete.vue  |  4 +-
 packages/client/src/components/MkDonation.vue |  3 +-
 .../client/src/components/MkEmojiPicker.vue   |  7 ++--
 .../src/components/MkForgotPassword.vue       |  8 ++--
 .../src/components/MkInstanceTicker.vue       |  5 ++-
 .../client/src/components/MkMediaCaption.vue  |  6 +--
 .../client/src/components/MkNotification.vue  |  7 ++--
 .../src/components/MkNotificationFolded.vue   | 11 ++---
 packages/client/src/components/MkPostForm.vue |  4 +-
 .../MkPushNotificationAllowButton.vue         | 14 +++----
 packages/client/src/components/MkSignup.vue   |  4 +-
 .../client/src/components/MkStarButton.vue    | 11 ++---
 .../src/components/MkStarButtonNoEmoji.vue    | 14 ++++---
 .../src/components/MkTutorialDialog.vue       | 33 ++++++++-------
 .../client/src/components/global/MkAd.vue     |  8 ++--
 .../client/src/components/global/MkEmoji.vue  |  6 +--
 .../src/components/note/MkNoteTranslation.vue |  4 +-
 packages/client/src/init.ts                   | 41 ++++++++-----------
 packages/client/src/instance.ts               | 34 +++++++++------
 packages/client/src/pages/about-firefish.vue  |  5 ++-
 packages/client/src/pages/about.emojis.vue    | 10 +----
 packages/client/src/pages/about.vue           |  5 ++-
 .../client/src/pages/admin/bot-protection.vue | 18 ++++----
 .../client/src/pages/admin/email-settings.vue | 24 ++++++-----
 .../client/src/pages/admin/experiments.vue    |  4 +-
 packages/client/src/pages/admin/hashtags.vue  |  6 +--
 packages/client/src/pages/admin/index.vue     | 14 +++----
 .../client/src/pages/admin/instance-block.vue |  4 +-
 .../client/src/pages/admin/object-storage.vue | 30 +++++++-------
 .../client/src/pages/admin/other-settings.vue |  4 +-
 .../client/src/pages/admin/proxy-account.vue  |  8 ++--
 packages/client/src/pages/admin/security.vue  | 22 +++++-----
 packages/client/src/pages/admin/settings.vue  |  4 +-
 packages/client/src/pages/gallery/edit.vue    |  8 ++--
 packages/client/src/pages/mfm-cheat-sheet.vue |  6 ++-
 packages/client/src/pages/settings/index.vue  |  9 ++--
 packages/client/src/pages/settings/theme.vue  |  7 ++--
 packages/client/src/pages/timeline.vue        | 19 +++++----
 packages/client/src/pages/user-info.vue       |  6 ++-
 .../client/src/pages/welcome.entrance.a.vue   | 12 +++---
 packages/client/src/scripts/helpMenu.ts       |  3 +-
 packages/client/src/ui/_common_/navbar.vue    | 19 ++++++---
 packages/client/src/ui/deck/tl-column.vue     | 15 ++++---
 packages/client/src/ui/visitor/a.vue          | 21 ++--------
 packages/client/src/ui/visitor/b.vue          | 28 +------------
 packages/client/src/ui/visitor/header.vue     | 15 +------
 packages/client/src/ui/visitor/kanban.vue     |  6 +--
 packages/client/src/widgets/server-info.vue   |  4 +-
 .../src/widgets/server-metric/index.vue       |  7 ++--
 packages/firefish-js/src/entities.ts          |  1 +
 50 files changed, 280 insertions(+), 288 deletions(-)

diff --git a/packages/client/src/components/MkAutocomplete.vue b/packages/client/src/components/MkAutocomplete.vue
index 4e890cc03e..b6bc6b65ba 100644
--- a/packages/client/src/components/MkAutocomplete.vue
+++ b/packages/client/src/components/MkAutocomplete.vue
@@ -100,7 +100,7 @@ import * as os from "@/os";
 import { MFM_TAGS } from "@/scripts/mfm-tags";
 import { defaultStore } from "@/store";
 import { addSkinTone, emojilist } from "@/scripts/emojilist";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import { i18n } from "@/i18n";
 
 interface EmojiDef {
@@ -141,7 +141,7 @@ for (const x of lib) {
 emjdb.sort((a, b) => a.name.length - b.name.length);
 
 // #region Construct Emoji DB
-const customEmojis = instance.emojis;
+const customEmojis = getInstanceInfo().emojis;
 const emojiDefinitions: EmojiDef[] = [];
 
 for (const x of customEmojis) {
diff --git a/packages/client/src/components/MkDonation.vue b/packages/client/src/components/MkDonation.vue
index ad9df629f4..a799fef8fc 100644
--- a/packages/client/src/components/MkDonation.vue
+++ b/packages/client/src/components/MkDonation.vue
@@ -64,7 +64,7 @@ import MkButton from "@/components/MkButton.vue";
 import { host } from "@/config";
 import { i18n } from "@/i18n";
 import * as os from "@/os";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import icon from "@/scripts/icon";
 
 const show = ref(false);
@@ -73,6 +73,7 @@ const emit = defineEmits<{
 	(ev: "closed"): void;
 }>();
 
+const instance = getInstanceInfo();
 const hostname =
 	instance.name?.length && instance.name?.length < 38 ? instance.name : host;
 
diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue
index c731fe9b0e..5bf3d5d829 100644
--- a/packages/client/src/components/MkEmojiPicker.vue
+++ b/packages/client/src/components/MkEmojiPicker.vue
@@ -164,7 +164,7 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, onMounted, ref, watch } from "vue";
+import { onMounted, ref, watch } from "vue";
 import type { entities } from "firefish-js";
 import { FocusTrap } from "focus-trap-vue";
 import XSection from "@/components/MkEmojiPicker.section.vue";
@@ -175,7 +175,7 @@ import Ripple from "@/components/MkRipple.vue";
 import * as os from "@/os";
 import { isTouchUsing } from "@/scripts/touch";
 import { deviceKind } from "@/scripts/device-kind";
-import { emojiCategories, instance } from "@/instance";
+import { emojiCategories, getInstanceInfo } from "@/instance";
 import { i18n } from "@/i18n";
 import { defaultStore } from "@/store";
 import icon from "@/scripts/icon";
@@ -235,7 +235,7 @@ const size = reactionPickerSize;
 const width = reactionPickerWidth;
 const height = reactionPickerHeight;
 const customEmojiCategories = emojiCategories;
-const customEmojis = instance.emojis;
+const customEmojis = getInstanceInfo().emojis;
 const q = ref<string | null>(null);
 const searchResultCustom = ref<entities.CustomEmoji[]>([]);
 const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
@@ -459,7 +459,6 @@ async function paste(event: ClipboardEvent) {
 }
 
 function done(query?: string | null): boolean {
-	// biome-ignore lint/style/noParameterAssign: assign it intentially
 	if (query == null) query = q.value;
 	if (query == null || typeof query !== "string") return false;
 
diff --git a/packages/client/src/components/MkForgotPassword.vue b/packages/client/src/components/MkForgotPassword.vue
index 2ca1a87e87..7191e332d0 100644
--- a/packages/client/src/components/MkForgotPassword.vue
+++ b/packages/client/src/components/MkForgotPassword.vue
@@ -9,7 +9,7 @@
 		<template #header>{{ i18n.ts.forgotPassword }}</template>
 
 		<form
-			v-if="instance.enableEmail"
+			v-if="enableEmail"
 			class="bafeceda"
 			@submit.prevent="onSubmit"
 		>
@@ -68,7 +68,7 @@ import XModalWindow from "@/components/MkModalWindow.vue";
 import MkButton from "@/components/MkButton.vue";
 import MkInput from "@/components/form/input.vue";
 import * as os from "@/os";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import { i18n } from "@/i18n";
 
 const emit = defineEmits<{
@@ -82,6 +82,8 @@ const username = ref("");
 const email = ref("");
 const processing = ref(false);
 
+const { enableEmail } = getInstanceInfo();
+
 async function onSubmit() {
 	processing.value = true;
 	await os.apiWithDialog("request-reset-password", {
@@ -89,7 +91,7 @@ async function onSubmit() {
 		email: email.value,
 	});
 	emit("done");
-	dialog.value!.close();
+	dialog.value?.close();
 }
 </script>
 
diff --git a/packages/client/src/components/MkInstanceTicker.vue b/packages/client/src/components/MkInstanceTicker.vue
index 3ac06d4fe3..a2a6585395 100644
--- a/packages/client/src/components/MkInstanceTicker.vue
+++ b/packages/client/src/components/MkInstanceTicker.vue
@@ -19,7 +19,7 @@ import { ref } from "vue";
 
 import type { entities } from "firefish-js";
 import { instanceName, version } from "@/config";
-import { instance as Instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
 
 const props = defineProps<{
@@ -28,9 +28,10 @@ const props = defineProps<{
 
 const ticker = ref<HTMLElement | null>(null);
 
+// FIXME: the following assumption is not necessarily correct
 // if no instance data is given, this is for the local instance
 const instance = props.instance ?? {
-	faviconUrl: Instance.iconUrl || "/favicon.ico",
+	faviconUrl: getInstanceInfo().iconUrl || "/favicon.ico",
 	name: instanceName,
 	themeColor: (
 		document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement
diff --git a/packages/client/src/components/MkMediaCaption.vue b/packages/client/src/components/MkMediaCaption.vue
index b09024adb7..24cff479bc 100644
--- a/packages/client/src/components/MkMediaCaption.vue
+++ b/packages/client/src/components/MkMediaCaption.vue
@@ -76,7 +76,7 @@ import MkButton from "@/components/MkButton.vue";
 import bytes from "@/filters/bytes";
 import number from "@/filters/number";
 import { i18n } from "@/i18n";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 
 const props = withDefaults(
 	defineProps<{
@@ -109,14 +109,14 @@ const modal = ref<InstanceType<typeof MkModal> | null>(null);
 const inputValue = ref(props.input.default ? props.input.default : null);
 
 const remainingLength = computed(() => {
-	const maxCaptionLength = instance.maxCaptionTextLength ?? 512;
+	const maxCaptionLength = getInstanceInfo().maxCaptionTextLength ?? 512;
 	if (typeof inputValue.value !== "string") return maxCaptionLength;
 	return maxCaptionLength - length(inputValue.value);
 });
 
 function done(canceled: boolean, result?: string | null) {
 	emit("done", { canceled, result });
-	modal.value!.close();
+	modal.value?.close();
 }
 
 async function ok() {
diff --git a/packages/client/src/components/MkNotification.vue b/packages/client/src/components/MkNotification.vue
index e3f8259def..3158ea43ab 100644
--- a/packages/client/src/components/MkNotification.vue
+++ b/packages/client/src/components/MkNotification.vue
@@ -285,7 +285,7 @@ import * as os from "@/os";
 import { useStream } from "@/stream";
 import { useTooltip } from "@/scripts/use-tooltip";
 import { defaultStore } from "@/store";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import icon from "@/scripts/icon";
 
 const props = withDefaults(
@@ -309,8 +309,9 @@ const hideFollowButton = defaultStore.state.hideFollowButtons;
 const showEmojiReactions =
 	defaultStore.state.enableEmojiReactions ||
 	defaultStore.state.showEmojisInReactionNotifications;
-const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReaction)
-	? instance.defaultReaction
+const realDefaultReaction = getInstanceInfo().defaultReaction;
+const defaultReaction = ["⭐", "👍", "❤️"].includes(realDefaultReaction)
+	? realDefaultReaction
 	: "⭐";
 
 let readObserver: IntersectionObserver | undefined;
diff --git a/packages/client/src/components/MkNotificationFolded.vue b/packages/client/src/components/MkNotificationFolded.vue
index c7a2730cd3..91c6f5d902 100644
--- a/packages/client/src/components/MkNotificationFolded.vue
+++ b/packages/client/src/components/MkNotificationFolded.vue
@@ -88,7 +88,7 @@ import * as os from "@/os";
 import { useStream } from "@/stream";
 import { useTooltip } from "@/scripts/use-tooltip";
 import { defaultStore } from "@/store";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import icon from "@/scripts/icon";
 import type {
 	NotificationFolded,
@@ -116,8 +116,9 @@ const reactionRef = ref<InstanceType<typeof XReactionIcon> | null>(null);
 const showEmojiReactions =
 	defaultStore.state.enableEmojiReactions ||
 	defaultStore.state.showEmojisInReactionNotifications;
-const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReaction)
-	? instance.defaultReaction
+const realDefaultReaction = getInstanceInfo().defaultReaction;
+const defaultReaction = ["⭐", "👍", "❤️"].includes(realDefaultReaction)
+	? realDefaultReaction
 	: "⭐";
 
 const users = computed(() => props.notification.users.slice(0, 5));
@@ -178,7 +179,7 @@ useTooltip(reactionRef, (showing) => {
 				? n.reaction.replace(/^:(\w+):$/, ":$1@.:")
 				: n.reaction,
 			emojis: n.note.emojis,
-			targetElement: reactionRef.value!.$el,
+			targetElement: reactionRef.value?.$el,
 		},
 		{},
 		"closed",
@@ -203,7 +204,7 @@ onMounted(() => {
 	readObserver.observe(elRef.value!);
 
 	connection = stream.useChannel("main");
-	connection.on("readAllNotifications", () => readObserver!.disconnect());
+	connection.on("readAllNotifications", () => readObserver?.disconnect());
 });
 
 onUnmounted(() => {
diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue
index ce1119ed82..148450e389 100644
--- a/packages/client/src/components/MkPostForm.vue
+++ b/packages/client/src/components/MkPostForm.vue
@@ -322,7 +322,7 @@ import { selectFiles } from "@/scripts/select-file";
 import { defaultStore, notePostInterruptors, postFormActions } from "@/store";
 import MkInfo from "@/components/MkInfo.vue";
 import { i18n } from "@/i18n";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import { getAccounts, openAccountMenu as openAccountMenu_ } from "@/account";
 import { me } from "@/me";
 import { uploadFile } from "@/scripts/upload";
@@ -497,7 +497,7 @@ const textLength = computed((): number => {
 });
 
 const maxTextLength = computed((): number => {
-	return instance ? instance.maxNoteTextLength : 3000;
+	return getInstanceInfo().maxNoteTextLength ?? 3000;
 });
 
 const canPost = computed((): boolean => {
diff --git a/packages/client/src/components/MkPushNotificationAllowButton.vue b/packages/client/src/components/MkPushNotificationAllowButton.vue
index 1d1be0fb05..217f8688b3 100644
--- a/packages/client/src/components/MkPushNotificationAllowButton.vue
+++ b/packages/client/src/components/MkPushNotificationAllowButton.vue
@@ -58,7 +58,7 @@ import { ref } from "vue";
 import { getAccounts } from "@/account";
 import { isSignedIn, me } from "@/me";
 import MkButton from "@/components/MkButton.vue";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import { api, apiWithDialog, promiseDialog } from "@/os";
 import { i18n } from "@/i18n";
 
@@ -76,6 +76,7 @@ defineProps<{
 	showOnlyToRegister?: boolean;
 }>();
 
+const { swPublickey } = getInstanceInfo();
 // ServiceWorker registration
 const registration = ref<ServiceWorkerRegistration | undefined>();
 // If this browser supports push notification
@@ -94,14 +95,14 @@ const pushRegistrationInServer = ref<
 >();
 
 function subscribe() {
-	if (!registration.value || !supported.value || !instance.swPublickey) return;
+	if (!registration.value || !supported.value || !swPublickey) return;
 
 	// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
 	return promiseDialog(
 		registration.value.pushManager
 			.subscribe({
 				userVisibleOnly: true,
-				applicationServerKey: urlBase64ToUint8Array(instance.swPublickey),
+				applicationServerKey: urlBase64ToUint8Array(swPublickey),
 			})
 			.then(
 				async (subscription) => {
@@ -186,12 +187,7 @@ if (navigator.serviceWorker == null) {
 		pushSubscription.value =
 			await registration.value.pushManager.getSubscription();
 
-		if (
-			instance.swPublickey &&
-			"PushManager" in window &&
-			isSignedIn(me) &&
-			me.token
-		) {
+		if (swPublickey && "PushManager" in window && isSignedIn(me) && me.token) {
 			supported.value = true;
 
 			if (pushSubscription.value) {
diff --git a/packages/client/src/components/MkSignup.vue b/packages/client/src/components/MkSignup.vue
index c23bea58a2..283c8aafb6 100644
--- a/packages/client/src/components/MkSignup.vue
+++ b/packages/client/src/components/MkSignup.vue
@@ -288,7 +288,7 @@ import MkCaptcha from "@/components/MkCaptcha.vue";
 import * as config from "@/config";
 import * as os from "@/os";
 import { signIn } from "@/account";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
 
@@ -306,6 +306,8 @@ const emit = defineEmits<{
 	(ev: "signupEmailPending"): void;
 }>();
 
+const instance = getInstanceInfo();
+
 const host = toUnicode(config.host);
 
 const hcaptcha = ref();
diff --git a/packages/client/src/components/MkStarButton.vue b/packages/client/src/components/MkStarButton.vue
index 2ee4514482..e6c4a9b3fc 100644
--- a/packages/client/src/components/MkStarButton.vue
+++ b/packages/client/src/components/MkStarButton.vue
@@ -30,11 +30,11 @@
 			</g>
 		</svg>
 		<i
-			v-else-if="instance.defaultReaction === '👍'"
+			v-else-if="defaultReaction === '👍'"
 			:class="icon('ph-thumbs-up')"
 		></i>
 		<i
-			v-else-if="instance.defaultReaction === '❤️'"
+			v-else-if="defaultReaction === '❤️'"
 			:class="icon('ph-heart')"
 		></i>
 		<i v-else :class="icon('ph-star')"></i>
@@ -48,19 +48,20 @@ import { pleaseLogin } from "@/scripts/please-login";
 import * as os from "@/os";
 import { defaultStore } from "@/store";
 import { i18n } from "@/i18n";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import icon from "@/scripts/icon";
 
 const props = defineProps<{
 	note: entities.Note;
 }>();
 
+const { defaultReaction } = getInstanceInfo();
+
 function star(ev?: MouseEvent): void {
 	pleaseLogin();
 	os.api("notes/reactions/create", {
 		noteId: props.note.id,
-		reaction:
-			defaultStore.state.woozyMode === true ? "🥴" : instance.defaultReaction,
+		reaction: defaultStore.state.woozyMode === true ? "🥴" : defaultReaction,
 	});
 	const el =
 		ev && ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
diff --git a/packages/client/src/components/MkStarButtonNoEmoji.vue b/packages/client/src/components/MkStarButtonNoEmoji.vue
index dc441db3f2..349e8cb6bf 100644
--- a/packages/client/src/components/MkStarButtonNoEmoji.vue
+++ b/packages/client/src/components/MkStarButtonNoEmoji.vue
@@ -9,23 +9,23 @@
 	>
 		<span v-if="!reacted">
 			<i
-				v-if="instance.defaultReaction === '👍'"
+				v-if="defaultReaction === '👍'"
 				:class="icon('ph-thumbs-up')"
 			></i>
 			<i
-				v-else-if="instance.defaultReaction === '❤️'"
+				v-else-if="defaultReaction === '❤️'"
 				:class="icon('ph-heart')"
 			></i>
 			<i v-else :class="icon('ph-star')"></i>
 		</span>
 		<span v-else>
 			<i
-				v-if="instance.defaultReaction === '👍'"
+				v-if="defaultReaction === '👍'"
 				class="ph-thumbs-up ph-lg ph-fill"
 				:class="$style.yellow"
 			></i>
 			<i
-				v-else-if="instance.defaultReaction === '❤️'"
+				v-else-if="defaultReaction === '❤️'"
 				class="ph-heart ph-lg ph-fill"
 				:class="$style.red"
 			></i>
@@ -45,7 +45,7 @@ import XDetails from "@/components/MkUsersTooltip.vue";
 import { pleaseLogin } from "@/scripts/please-login";
 import * as os from "@/os";
 import { i18n } from "@/i18n";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import { useTooltip } from "@/scripts/use-tooltip";
 import icon from "@/scripts/icon";
 
@@ -57,13 +57,15 @@ const props = defineProps<{
 
 const buttonRef = ref<HTMLElement>();
 
+const { defaultReaction } = getInstanceInfo();
+
 function toggleStar(ev?: MouseEvent): void {
 	pleaseLogin();
 
 	if (!props.reacted) {
 		os.api("notes/reactions/create", {
 			noteId: props.note.id,
-			reaction: instance.defaultReaction,
+			reaction: defaultReaction,
 		});
 		const el =
 			ev && ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
diff --git a/packages/client/src/components/MkTutorialDialog.vue b/packages/client/src/components/MkTutorialDialog.vue
index 9616c24802..fd7dbe6d4c 100644
--- a/packages/client/src/components/MkTutorialDialog.vue
+++ b/packages/client/src/components/MkTutorialDialog.vue
@@ -212,14 +212,19 @@ import FormSwitch from "@/components/form/switch.vue";
 import { defaultStore } from "@/store";
 import { i18n } from "@/i18n";
 import { isModerator } from "@/me";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import icon from "@/scripts/icon";
 
-const isLocalTimelineAvailable = !instance.disableLocalTimeline || isModerator;
+const {
+	disableLocalTimeline,
+	disableRecommendedTimeline,
+	disableGlobalTimeline,
+} = getInstanceInfo();
+
+const isLocalTimelineAvailable = !disableLocalTimeline || isModerator;
 const isRecommendedTimelineAvailable =
-	!instance.disableRecommendedTimeline || isModerator;
-const isGlobalTimelineAvailable =
-	!instance.disableGlobalTimeline || isModerator;
+	!disableRecommendedTimeline || isModerator;
+const isGlobalTimelineAvailable = !disableGlobalTimeline || isModerator;
 
 const timelines = ["home"];
 
@@ -252,13 +257,13 @@ const tutorial = computed({
 	},
 });
 
-const autoplayMfm = computed(
-	defaultStore.makeGetterSetter(
-		"animatedMfm",
-		(v) => !v,
-		(v) => !v,
-	),
-);
+// const autoplayMfm = computed(
+// 	defaultStore.makeGetterSetter(
+// 		"animatedMfm",
+// 		(v) => !v,
+// 		(v) => !v,
+// 	),
+// );
 const reduceAnimation = computed(
 	defaultStore.makeGetterSetter(
 		"animation",
@@ -267,9 +272,9 @@ const reduceAnimation = computed(
 	),
 );
 
-function close(res) {
+function close(_res) {
 	tutorial.value = -1;
-	dialog.value.close();
+	dialog.value?.close();
 }
 </script>
 
diff --git a/packages/client/src/components/global/MkAd.vue b/packages/client/src/components/global/MkAd.vue
index 96be6d8b15..ea37295f5d 100644
--- a/packages/client/src/components/global/MkAd.vue
+++ b/packages/client/src/components/global/MkAd.vue
@@ -40,7 +40,7 @@
 
 <script lang="ts" setup>
 import { ref } from "vue";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import { host } from "@/config";
 import MkButton from "@/components/MkButton.vue";
 import { defaultStore } from "@/store";
@@ -48,7 +48,9 @@ import * as os from "@/os";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
 
-type Ad = (typeof instance)["ads"][number];
+// TODO?: rename to community banner
+const instanceAds = getInstanceInfo().ads;
+type Ad = (typeof instanceAds)[number];
 
 const props = defineProps<{
 	prefer: string[];
@@ -65,7 +67,7 @@ const choseAd = (): Ad | Ad[] | null => {
 		return props.specify;
 	}
 
-	const allAds = instance.ads.map((ad) =>
+	const allAds = instanceAds.map((ad) =>
 		defaultStore.state.mutedAds.includes(ad.id)
 			? {
 					...ad,
diff --git a/packages/client/src/components/global/MkEmoji.vue b/packages/client/src/components/global/MkEmoji.vue
index 57380ba6f4..ea47363c55 100644
--- a/packages/client/src/components/global/MkEmoji.vue
+++ b/packages/client/src/components/global/MkEmoji.vue
@@ -26,7 +26,7 @@ import type { entities } from "firefish-js";
 import { getStaticImageUrl } from "@/scripts/get-static-image-url";
 import { char2filePath } from "@/scripts/twemoji-base";
 import { defaultStore } from "@/store";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 
 const props = defineProps<{
 	emoji: string;
@@ -41,7 +41,7 @@ const char = computed(() => (isCustom.value ? null : props.emoji));
 const useOsNativeEmojis = computed(
 	() => defaultStore.state.useOsNativeEmojis && !props.isReaction,
 );
-const ce = computed(() => props.customEmojis ?? instance.emojis ?? []);
+const ce = computed(() => props.customEmojis ?? getInstanceInfo().emojis ?? []);
 const customEmoji = computed(() =>
 	isCustom.value
 		? ce.value.find(
@@ -55,7 +55,7 @@ const url = computed(() => {
 	} else {
 		return defaultStore.state.disableShowingAnimatedImages
 			? getStaticImageUrl(customEmoji.value!.url)
-			: customEmoji.value!.url;
+			: customEmoji.value?.url;
 	}
 });
 const alt = computed(() =>
diff --git a/packages/client/src/components/note/MkNoteTranslation.vue b/packages/client/src/components/note/MkNoteTranslation.vue
index 8b07c9f903..ec544d4203 100644
--- a/packages/client/src/components/note/MkNoteTranslation.vue
+++ b/packages/client/src/components/note/MkNoteTranslation.vue
@@ -27,7 +27,7 @@ import { me } from "@/me";
 import type { NoteTranslation, NoteType } from "@/types/note";
 import { computed, ref, watch } from "vue";
 import * as os from "@/os";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 
 const props = defineProps<{
 	note: NoteType;
@@ -39,7 +39,7 @@ const translating = ref<boolean>();
 const hasError = ref<boolean>();
 const canTranslate = computed(
 	() =>
-		instance.translatorAvailable &&
+		getInstanceInfo().translatorAvailable &&
 		translation.value == null &&
 		translating.value !== true,
 );
diff --git a/packages/client/src/init.ts b/packages/client/src/init.ts
index c3fff47e9a..9ae1db8496 100644
--- a/packages/client/src/init.ts
+++ b/packages/client/src/init.ts
@@ -13,13 +13,13 @@ import "@phosphor-icons/web/fill";
 import "@phosphor-icons/web/light";
 import "@phosphor-icons/web/regular";
 
-// #region account indexedDB migration
-
+// #region IndexDB migrations
 const accounts = localStorage.getItem("accounts");
 if (accounts) {
 	set("accounts", JSON.parse(accounts));
 	localStorage.removeItem("accounts");
 }
+localStorage.removeItem("instance");
 // #endregion
 
 import {
@@ -37,7 +37,7 @@ import components from "@/components";
 import { lang, ui, version } from "@/config";
 import directives from "@/directives";
 import { i18n } from "@/i18n";
-import { fetchInstance, instance } from "@/instance";
+import { getInstanceInfo, initializeInstanceCache } from "@/instance";
 import { isSignedIn, me } from "@/me";
 import { alert, api, confirm, popup, post, toast } from "@/os";
 import { deviceKind } from "@/scripts/device-kind";
@@ -68,6 +68,7 @@ function checkForSplash() {
 }
 
 (async () => {
+	await initializeInstanceCache();
 	console.info(`Firefish v${version}`);
 
 	if (_DEV_) {
@@ -177,14 +178,10 @@ function checkForSplash() {
 	}
 	// #endregion
 
-	const fetchInstanceMetaPromise = fetchInstance();
+	localStorage.setItem("v", getInstanceInfo().version);
 
-	fetchInstanceMetaPromise.then(() => {
-		localStorage.setItem("v", instance.version);
-
-		// Init service worker
-		initializeSw();
-	});
+	// Init service worker
+	initializeSw();
 
 	const app = createApp(
 		window.location.search === "?zen"
@@ -341,21 +338,15 @@ function checkForSplash() {
 	};
 	// #endregion
 
-	fetchInstanceMetaPromise.then(() => {
-		if (defaultStore.state.themeInitial) {
-			if (instance.defaultLightTheme != null)
-				ColdDeviceStorage.set(
-					"lightTheme",
-					JSON.parse(instance.defaultLightTheme),
-				);
-			if (instance.defaultDarkTheme != null)
-				ColdDeviceStorage.set(
-					"darkTheme",
-					JSON.parse(instance.defaultDarkTheme),
-				);
-			defaultStore.set("themeInitial", false);
-		}
-	});
+	const { defaultLightTheme, defaultDarkTheme } = getInstanceInfo();
+
+	if (defaultStore.state.themeInitial) {
+		if (defaultLightTheme != null)
+			ColdDeviceStorage.set("lightTheme", JSON.parse(defaultLightTheme));
+		if (defaultDarkTheme != null)
+			ColdDeviceStorage.set("darkTheme", JSON.parse(defaultDarkTheme));
+		defaultStore.set("themeInitial", false);
+	}
 
 	watch(
 		defaultStore.reactiveState.useBlurEffectForModal,
diff --git a/packages/client/src/instance.ts b/packages/client/src/instance.ts
index d24b3ca356..f4ea4d431e 100644
--- a/packages/client/src/instance.ts
+++ b/packages/client/src/instance.ts
@@ -1,30 +1,40 @@
 import type { entities } from "firefish-js";
-import { computed, reactive } from "vue";
+import { computed } from "vue";
 import { api } from "./os";
+import { set, get } from "idb-keyval";
 
 // TODO: 他のタブと永続化されたstateを同期
 
-const instanceData = localStorage.getItem("instance");
-// TODO: instanceをリアクティブにするかは再考の余地あり
+// TODO: get("instance") requires top-level await
+let instance: entities.DetailedInstanceMetadata;
 
-export const instance: entities.DetailedInstanceMetadata = reactive(
-	instanceData
-		? JSON.parse(instanceData)
-		: {
-				// TODO: set default values
-			},
-);
+export function getInstanceInfo(): entities.DetailedInstanceMetadata {
+	return instance;
+}
 
-export async function fetchInstance() {
+export async function initializeInstanceCache(): Promise<void> {
+	// Is the data stored in IndexDB?
+	const fromIdb = await get<string>("instance");
+	if (fromIdb != null) {
+		instance = JSON.parse(fromIdb);
+	}
+	// Call API
+	updateInstanceCache();
+}
+
+export async function updateInstanceCache(): Promise<void> {
 	const meta = await api("meta", {
 		detail: true,
 	});
 
+	// TODO: set default values
+	instance = {} as entities.DetailedInstanceMetadata;
+
 	for (const [k, v] of Object.entries(meta)) {
 		instance[k] = v;
 	}
 
-	localStorage.setItem("instance", JSON.stringify(instance));
+	set("instance", JSON.stringify(instance));
 }
 
 export const emojiCategories = computed(() => {
diff --git a/packages/client/src/pages/about-firefish.vue b/packages/client/src/pages/about-firefish.vue
index 4ddd508fbb..b0b21ab375 100644
--- a/packages/client/src/pages/about-firefish.vue
+++ b/packages/client/src/pages/about-firefish.vue
@@ -35,7 +35,7 @@
 							><MkEmoji
 								class="emoji"
 								:emoji="emoji.emoji"
-								:custom-emojis="instance.emojis"
+								:custom-emojis="instanceEmojis"
 								:is-reaction="false"
 								:normal="true"
 								:no-style="true"
@@ -96,12 +96,13 @@ import { defaultReactions, defaultStore } from "@/store";
 import * as os from "@/os";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 
 let easterEggReady = false;
 const easterEggEmojis = ref([]);
 const easterEggEngine = ref(null);
 const containerEl = ref<HTMLElement>();
+const instanceEmojis = getInstanceInfo().emojis;
 
 function iconLoaded() {
 	const emojis =
diff --git a/packages/client/src/pages/about.emojis.vue b/packages/client/src/pages/about.emojis.vue
index b94c415caf..ec7b9528c6 100644
--- a/packages/client/src/pages/about.emojis.vue
+++ b/packages/client/src/pages/about.emojis.vue
@@ -6,12 +6,6 @@
 					><i :class="icon('ph-magnifying-glass')"></i
 				></template>
 			</MkInput>
-
-			<!-- たくさんあると邪魔
-		<div class="tags">
-			<span class="tag _button" v-for="tag in tags" :class="{ active: selectedTags.has(tag) }" @click="toggleTag(tag)">{{ tag }}</span>
-		</div>
-		-->
 		</div>
 
 		<MkFolder v-if="searchEmojis" class="emojis">
@@ -54,7 +48,7 @@ import MkInput from "@/components/form/input.vue";
 import MkSelect from "@/components/form/select.vue";
 import MkFolder from "@/components/MkFolder.vue";
 import MkTab from "@/components/MkTab.vue";
-import { emojiCategories, emojiTags, instance } from "@/instance";
+import { emojiCategories, emojiTags, getInstanceInfo } from "@/instance";
 import { i18n } from "@/i18n";
 import iconify from "@/scripts/icon";
 
@@ -72,7 +66,7 @@ export default defineComponent({
 		return {
 			q: "",
 			customEmojiCategories: emojiCategories,
-			customEmojis: instance.emojis,
+			customEmojis: getInstanceInfo().emojis,
 			tags: emojiTags,
 			selectedTags: new Set(),
 			searchEmojis: null,
diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue
index f757d3a1a5..3102f31c9c 100644
--- a/packages/client/src/pages/about.vue
+++ b/packages/client/src/pages/about.vue
@@ -192,7 +192,7 @@ import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import { deviceKind } from "@/scripts/device-kind";
 import { isModerator } from "@/me";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import { defaultStore } from "@/store";
 import icon from "@/scripts/icon";
 import "swiper/scss";
@@ -207,6 +207,7 @@ withDefaults(
 	},
 );
 
+const instance = getInstanceInfo();
 const stats = ref(null);
 const instanceIcon = ref<HTMLImageElement>();
 let iconClicks = 0;
@@ -307,7 +308,7 @@ function onSlideChange() {
 }
 
 function syncSlide(index: number) {
-	swiperRef!.slideTo(index);
+	swiperRef?.slideTo(index);
 }
 </script>
 
diff --git a/packages/client/src/pages/admin/bot-protection.vue b/packages/client/src/pages/admin/bot-protection.vue
index 766fdc0088..a1515a5f3a 100644
--- a/packages/client/src/pages/admin/bot-protection.vue
+++ b/packages/client/src/pages/admin/bot-protection.vue
@@ -81,7 +81,7 @@ import FormButton from "@/components/MkButton.vue";
 import FormSuspense from "@/components/form/suspense.vue";
 import FormSlot from "@/components/form/slot.vue";
 import * as os from "@/os";
-import { fetchInstance } from "@/instance";
+import { updateInstanceCache } from "@/instance";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
 
@@ -89,7 +89,7 @@ const MkCaptcha = defineAsyncComponent(
 	() => import("@/components/MkCaptcha.vue"),
 );
 
-const provider = ref<any>(null);
+const provider = ref<string | null>(null);
 const hcaptchaSiteKey = ref<string | null>(null);
 const hcaptchaSecretKey = ref<string | null>(null);
 const recaptchaSiteKey = ref<string | null>(null);
@@ -97,14 +97,14 @@ const recaptchaSecretKey = ref<string | null>(null);
 
 async function init() {
 	const meta = await os.api("admin/meta");
-	hcaptchaSiteKey.value = meta.hcaptchaSiteKey;
-	hcaptchaSecretKey.value = meta.hcaptchaSecretKey;
-	recaptchaSiteKey.value = meta.recaptchaSiteKey;
-	recaptchaSecretKey.value = meta.recaptchaSecretKey;
+	hcaptchaSiteKey.value = meta?.hcaptchaSiteKey;
+	hcaptchaSecretKey.value = meta?.hcaptchaSecretKey;
+	recaptchaSiteKey.value = meta?.recaptchaSiteKey;
+	recaptchaSecretKey.value = meta?.recaptchaSecretKey;
 
-	provider.value = meta.enableHcaptcha
+	provider.value = meta?.enableHcaptcha
 		? "hcaptcha"
-		: meta.enableRecaptcha
+		: meta?.enableRecaptcha
 			? "recaptcha"
 			: null;
 }
@@ -118,7 +118,7 @@ function save() {
 		recaptchaSiteKey: recaptchaSiteKey.value,
 		recaptchaSecretKey: recaptchaSecretKey.value,
 	}).then(() => {
-		fetchInstance();
+		updateInstanceCache();
 	});
 }
 </script>
diff --git a/packages/client/src/pages/admin/email-settings.vue b/packages/client/src/pages/admin/email-settings.vue
index 52dadc15b9..fc4dbced53 100644
--- a/packages/client/src/pages/admin/email-settings.vue
+++ b/packages/client/src/pages/admin/email-settings.vue
@@ -100,13 +100,13 @@ import FormSuspense from "@/components/form/suspense.vue";
 import FormSplit from "@/components/form/split.vue";
 import FormSection from "@/components/form/section.vue";
 import * as os from "@/os";
-import { fetchInstance, instance } from "@/instance";
+import { updateInstanceCache, getInstanceInfo } from "@/instance";
 import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
 
 const enableEmail = ref(false);
-const email: any = ref(null);
+const email = ref<string | null>(null);
 const smtpSecure = ref(false);
 const smtpHost = ref("");
 const smtpPort = ref(0);
@@ -115,20 +115,22 @@ const smtpPass = ref("");
 
 async function init() {
 	const meta = await os.api("admin/meta");
-	enableEmail.value = meta.enableEmail;
-	email.value = meta.email;
-	smtpSecure.value = meta.smtpSecure;
-	smtpHost.value = meta.smtpHost;
-	smtpPort.value = meta.smtpPort;
-	smtpUser.value = meta.smtpUser;
-	smtpPass.value = meta.smtpPass;
+	enableEmail.value = meta?.enableEmail;
+	email.value = meta?.email;
+	smtpSecure.value = meta?.smtpSecure;
+	smtpHost.value = meta?.smtpHost;
+	smtpPort.value = meta?.smtpPort;
+	smtpUser.value = meta?.smtpUser;
+	smtpPass.value = meta?.smtpPass;
 }
 
+const { maintainerEmail } = getInstanceInfo();
+
 async function testEmail() {
 	const { canceled, result: destination } = await os.inputText({
 		title: i18n.ts.destination,
 		type: "email",
-		placeholder: instance.maintainerEmail,
+		placeholder: maintainerEmail,
 	});
 	if (canceled) return;
 	os.apiWithDialog("admin/send-email", {
@@ -148,7 +150,7 @@ function save() {
 		smtpUser: smtpUser.value,
 		smtpPass: smtpPass.value,
 	}).then(() => {
-		fetchInstance();
+		updateInstanceCache();
 	});
 }
 
diff --git a/packages/client/src/pages/admin/experiments.vue b/packages/client/src/pages/admin/experiments.vue
index 2740d9de66..3ab0a288ca 100644
--- a/packages/client/src/pages/admin/experiments.vue
+++ b/packages/client/src/pages/admin/experiments.vue
@@ -33,7 +33,7 @@ import MkStickyContainer from "@/components/global/MkStickyContainer.vue";
 import FormSuspense from "@/components/form/suspense.vue";
 import FormSwitch from "@/components/form/switch.vue";
 import * as os from "@/os";
-import { fetchInstance } from "@/instance";
+import { updateInstanceCache } from "@/instance";
 import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
@@ -62,7 +62,7 @@ function save() {
 		},
 	};
 	os.apiWithDialog("admin/update-meta", experiments).then(() => {
-		fetchInstance();
+		updateInstanceCache();
 	});
 }
 
diff --git a/packages/client/src/pages/admin/hashtags.vue b/packages/client/src/pages/admin/hashtags.vue
index 6ec2efb804..fac32e495f 100644
--- a/packages/client/src/pages/admin/hashtags.vue
+++ b/packages/client/src/pages/admin/hashtags.vue
@@ -32,7 +32,7 @@ import FormButton from "@/components/MkButton.vue";
 import FormTextarea from "@/components/form/textarea.vue";
 import FormSuspense from "@/components/form/suspense.vue";
 import * as os from "@/os";
-import { fetchInstance } from "@/instance";
+import { updateInstanceCache } from "@/instance";
 import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
@@ -41,14 +41,14 @@ const hiddenTags = ref("");
 
 async function init() {
 	const meta = await os.api("admin/meta");
-	hiddenTags.value = meta.hiddenTags.join("\n");
+	hiddenTags.value = meta?.hiddenTags.join("\n");
 }
 
 function save() {
 	os.apiWithDialog("admin/update-meta", {
 		hiddenTags: hiddenTags.value.split("\n").map((h: string) => h.trim()) || [],
 	}).then(() => {
-		fetchInstance();
+		updateInstanceCache();
 	});
 }
 
diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue
index 7586e7952c..fa40df4827 100644
--- a/packages/client/src/pages/admin/index.vue
+++ b/packages/client/src/pages/admin/index.vue
@@ -71,9 +71,9 @@ import {
 import { i18n } from "@/i18n";
 import MkSuperMenu from "@/components/MkSuperMenu.vue";
 import MkInfo from "@/components/MkInfo.vue";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import { version } from "@/config";
-import { isAdmin, me } from "@/me";
+import { isAdmin } from "@/me";
 import * as os from "@/os";
 import { lookupUser } from "@/scripts/lookup-user";
 import { lookupFile } from "@/scripts/lookup-file";
@@ -98,6 +98,8 @@ const indexInfo = {
 
 provide("shouldOmitHeaderTitle", false);
 
+const instance = getInstanceInfo();
+
 const INFO = ref(indexInfo);
 const childInfo = ref(null);
 const narrow = ref(false);
@@ -110,7 +112,7 @@ const noBotProtection =
 const noEmailServer = !instance.enableEmail;
 const thereIsUnresolvedAbuseReport = ref(false);
 const updateAvailable = ref(false);
-const currentPage = computed(() => router.currentRef.value.child);
+const currentPage = computed(() => router.currentRef.value?.child);
 
 os.api("admin/abuse-user-reports", {
 	state: "unresolved",
@@ -312,7 +314,7 @@ onUnmounted(() => {
 
 watch(router.currentRef, (to) => {
 	if (
-		to.route.path === "/admin" &&
+		to?.route.path === "/admin" &&
 		to.child?.route.name == null &&
 		!narrow.value
 	) {
@@ -405,10 +407,6 @@ const lookup = (ev) => {
 	);
 };
 
-const headerActions = computed(() => []);
-
-const headerTabs = computed(() => []);
-
 definePageMetadata(INFO.value);
 
 defineExpose({
diff --git a/packages/client/src/pages/admin/instance-block.vue b/packages/client/src/pages/admin/instance-block.vue
index 80d9046c88..10c1abd707 100644
--- a/packages/client/src/pages/admin/instance-block.vue
+++ b/packages/client/src/pages/admin/instance-block.vue
@@ -50,7 +50,7 @@ import FormTextarea from "@/components/form/textarea.vue";
 import FormSuspense from "@/components/form/suspense.vue";
 import MkTab from "@/components/MkTab.vue";
 import * as os from "@/os";
-import { fetchInstance } from "@/instance";
+import { updateInstanceCache } from "@/instance";
 import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
@@ -72,7 +72,7 @@ function save() {
 		blockedHosts: blockedHosts.value.split("\n").map((h) => h.trim()) || [],
 		silencedHosts: silencedHosts.value.split("\n").map((h) => h.trim()) || [],
 	}).then(() => {
-		fetchInstance();
+		updateInstanceCache();
 	});
 }
 
diff --git a/packages/client/src/pages/admin/object-storage.vue b/packages/client/src/pages/admin/object-storage.vue
index 55532c6125..cad245ef01 100644
--- a/packages/client/src/pages/admin/object-storage.vue
+++ b/packages/client/src/pages/admin/object-storage.vue
@@ -156,7 +156,7 @@ import FormInput from "@/components/form/input.vue";
 import FormSuspense from "@/components/form/suspense.vue";
 import FormSplit from "@/components/form/split.vue";
 import * as os from "@/os";
-import { fetchInstance } from "@/instance";
+import { updateInstanceCache } from "@/instance";
 import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
@@ -177,19 +177,19 @@ const objectStorageS3ForcePathStyle = ref(true);
 
 async function init() {
 	const meta = await os.api("admin/meta");
-	useObjectStorage.value = meta.useObjectStorage;
-	objectStorageBaseUrl.value = meta.objectStorageBaseUrl;
-	objectStorageBucket.value = meta.objectStorageBucket;
-	objectStoragePrefix.value = meta.objectStoragePrefix;
-	objectStorageEndpoint.value = meta.objectStorageEndpoint;
-	objectStorageRegion.value = meta.objectStorageRegion;
-	objectStoragePort.value = meta.objectStoragePort;
-	objectStorageAccessKey.value = meta.objectStorageAccessKey;
-	objectStorageSecretKey.value = meta.objectStorageSecretKey;
-	objectStorageUseSSL.value = meta.objectStorageUseSSL;
-	objectStorageUseProxy.value = meta.objectStorageUseProxy;
-	objectStorageSetPublicRead.value = meta.objectStorageSetPublicRead;
-	objectStorageS3ForcePathStyle.value = meta.objectStorageS3ForcePathStyle;
+	useObjectStorage.value = meta?.useObjectStorage;
+	objectStorageBaseUrl.value = meta?.objectStorageBaseUrl;
+	objectStorageBucket.value = meta?.objectStorageBucket;
+	objectStoragePrefix.value = meta?.objectStoragePrefix;
+	objectStorageEndpoint.value = meta?.objectStorageEndpoint;
+	objectStorageRegion.value = meta?.objectStorageRegion;
+	objectStoragePort.value = meta?.objectStoragePort;
+	objectStorageAccessKey.value = meta?.objectStorageAccessKey;
+	objectStorageSecretKey.value = meta?.objectStorageSecretKey;
+	objectStorageUseSSL.value = meta?.objectStorageUseSSL;
+	objectStorageUseProxy.value = meta?.objectStorageUseProxy;
+	objectStorageSetPublicRead.value = meta?.objectStorageSetPublicRead;
+	objectStorageS3ForcePathStyle.value = meta?.objectStorageS3ForcePathStyle;
 }
 
 function save() {
@@ -208,7 +208,7 @@ function save() {
 		objectStorageSetPublicRead: objectStorageSetPublicRead.value,
 		objectStorageS3ForcePathStyle: objectStorageS3ForcePathStyle.value,
 	}).then(() => {
-		fetchInstance();
+		updateInstanceCache();
 	});
 }
 
diff --git a/packages/client/src/pages/admin/other-settings.vue b/packages/client/src/pages/admin/other-settings.vue
index 9d844a72a6..72ecc316ed 100644
--- a/packages/client/src/pages/admin/other-settings.vue
+++ b/packages/client/src/pages/admin/other-settings.vue
@@ -17,7 +17,7 @@ import { computed } from "vue";
 
 import FormSuspense from "@/components/form/suspense.vue";
 import * as os from "@/os";
-import { fetchInstance } from "@/instance";
+import { updateInstanceCache } from "@/instance";
 import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
@@ -28,7 +28,7 @@ async function init() {
 
 function save() {
 	os.apiWithDialog("admin/update-meta").then(() => {
-		fetchInstance();
+		updateInstanceCache();
 	});
 }
 
diff --git a/packages/client/src/pages/admin/proxy-account.vue b/packages/client/src/pages/admin/proxy-account.vue
index 4d0be071c4..7e407f7b07 100644
--- a/packages/client/src/pages/admin/proxy-account.vue
+++ b/packages/client/src/pages/admin/proxy-account.vue
@@ -42,7 +42,7 @@ import MkButton from "@/components/MkButton.vue";
 import MkInfo from "@/components/MkInfo.vue";
 import FormSuspense from "@/components/form/suspense.vue";
 import * as os from "@/os";
-import { fetchInstance } from "@/instance";
+import { updateInstanceCache } from "@/instance";
 import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
@@ -52,7 +52,7 @@ const proxyAccountId = ref<any>(null);
 
 async function init() {
 	const meta = await os.api("admin/meta");
-	proxyAccountId.value = meta.proxyAccountId;
+	proxyAccountId.value = meta?.proxyAccountId;
 	if (proxyAccountId.value) {
 		proxyAccount.value = await os.api("users/show", {
 			userId: proxyAccountId.value,
@@ -72,7 +72,7 @@ function save() {
 	os.apiWithDialog("admin/update-meta", {
 		proxyAccountId: proxyAccountId.value,
 	}).then(() => {
-		fetchInstance();
+		updateInstanceCache();
 	});
 }
 
@@ -80,7 +80,7 @@ function del() {
 	os.apiWithDialog("admin/update-meta", {
 		proxyAccountId: null,
 	}).then(() => {
-		fetchInstance();
+		updateInstanceCache();
 	});
 }
 
diff --git a/packages/client/src/pages/admin/security.vue b/packages/client/src/pages/admin/security.vue
index 8cb02c0d82..3872145e75 100644
--- a/packages/client/src/pages/admin/security.vue
+++ b/packages/client/src/pages/admin/security.vue
@@ -151,7 +151,7 @@ import FormInput from "@/components/form/input.vue";
 import FormTextarea from "@/components/form/textarea.vue";
 import FormButton from "@/components/MkButton.vue";
 import * as os from "@/os";
-import { fetchInstance } from "@/instance";
+import { updateInstanceCache } from "@/instance";
 import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
@@ -168,15 +168,15 @@ const allowedHosts = ref("");
 
 async function init() {
 	const meta = await os.api("admin/meta");
-	summalyProxy.value = meta.summalyProxy;
-	enableHcaptcha.value = meta.enableHcaptcha;
-	enableRecaptcha.value = meta.enableRecaptcha;
-	enableIpLogging.value = meta.enableIpLogging;
-	enableActiveEmailValidation.value = meta.enableActiveEmailValidation;
+	summalyProxy.value = meta?.summalyProxy;
+	enableHcaptcha.value = meta?.enableHcaptcha;
+	enableRecaptcha.value = meta?.enableRecaptcha;
+	enableIpLogging.value = meta?.enableIpLogging;
+	enableActiveEmailValidation.value = meta?.enableActiveEmailValidation;
 
-	secureMode.value = meta.secureMode;
-	privateMode.value = meta.privateMode;
-	allowedHosts.value = meta.allowedHosts.join("\n");
+	secureMode.value = meta?.secureMode;
+	privateMode.value = meta?.privateMode;
+	allowedHosts.value = meta?.allowedHosts.join("\n");
 }
 
 function save() {
@@ -185,7 +185,7 @@ function save() {
 		enableIpLogging: enableIpLogging.value,
 		enableActiveEmailValidation: enableActiveEmailValidation.value,
 	}).then(() => {
-		fetchInstance();
+		updateInstanceCache();
 	});
 }
 
@@ -195,7 +195,7 @@ function saveInstance() {
 		privateMode: privateMode.value,
 		allowedHosts: allowedHosts.value.split("\n"),
 	}).then(() => {
-		fetchInstance();
+		updateInstanceCache();
 	});
 }
 
diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue
index 1c2188daac..03e5720152 100644
--- a/packages/client/src/pages/admin/settings.vue
+++ b/packages/client/src/pages/admin/settings.vue
@@ -484,7 +484,7 @@ import FormSplit from "@/components/form/split.vue";
 import FormSuspense from "@/components/form/suspense.vue";
 import MkRadios from "@/components/form/radios.vue";
 import * as os from "@/os";
-import { fetchInstance } from "@/instance";
+import { updateInstanceCache } from "@/instance";
 import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
@@ -660,7 +660,7 @@ function save() {
 		enableServerMachineStats: enableServerMachineStats.value,
 		enableIdenticonGeneration: enableIdenticonGeneration.value,
 	}).then(() => {
-		fetchInstance();
+		updateInstanceCache();
 	});
 }
 
diff --git a/packages/client/src/pages/gallery/edit.vue b/packages/client/src/pages/gallery/edit.vue
index 7c3a4137a8..549de975bd 100644
--- a/packages/client/src/pages/gallery/edit.vue
+++ b/packages/client/src/pages/gallery/edit.vue
@@ -41,7 +41,7 @@
 				</div>
 
 				<FormSwitch
-					v-if="!instance.markLocalFilesNsfwByDefault"
+					v-if="!markLocalFilesNsfwByDefault"
 					v-model="isSensitive"
 					>{{ i18n.ts.markAsSensitive }}</FormSwitch
 				>
@@ -75,7 +75,7 @@ import { selectFiles } from "@/scripts/select-file";
 import * as os from "@/os";
 import { useRouter } from "@/router";
 import { definePageMetadata } from "@/scripts/page-metadata";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
 
@@ -91,6 +91,8 @@ const description = ref(null);
 const title = ref(null);
 const isSensitive = ref(false);
 
+const { markLocalFilesNsfwByDefault } = getInstanceInfo();
+
 function selectFile(evt) {
 	selectFiles(evt.currentTarget ?? evt.target, null).then((selected) => {
 		files.value = files.value.concat(selected);
@@ -118,7 +120,7 @@ async function save() {
 			fileIds: files.value.map((file) => file.id),
 			isSensitive: isSensitive.value,
 		});
-		router.push(`/gallery/${created.id}`);
+		router.push(`/gallery/${created?.id}`);
 	}
 }
 
diff --git a/packages/client/src/pages/mfm-cheat-sheet.vue b/packages/client/src/pages/mfm-cheat-sheet.vue
index a0335e0bab..8f5d214043 100644
--- a/packages/client/src/pages/mfm-cheat-sheet.vue
+++ b/packages/client/src/pages/mfm-cheat-sheet.vue
@@ -447,18 +447,20 @@ import { ref } from "vue";
 import MkTextarea from "@/components/form/textarea.vue";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import { i18n } from "@/i18n";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import icon from "@/scripts/icon";
 
 defineProps<{
 	popup?: boolean;
 }>();
 
+const sampleEmoji = getInstanceInfo().emojis.slice(0, 1);
+
 const preview_mention = ref("@example");
 const preview_hashtag = ref("#test");
 const preview_link = ref(`[${i18n.ts._mfm.dummy}](https://firefish.dev)`);
 const preview_emoji = ref(
-	instance.emojis.length ? `:${instance.emojis[0].name}:` : ":emojiname:",
+	sampleEmoji.length > 0 ? `:${sampleEmoji[0].name}:` : ":emojiname:",
 );
 const preview_bold = ref(`**${i18n.ts._mfm.dummy}**`);
 const preview_small = ref(
diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue
index 4954219ecb..96e538898a 100644
--- a/packages/client/src/pages/settings/index.vue
+++ b/packages/client/src/pages/settings/index.vue
@@ -48,7 +48,7 @@ import MkSuperMenu from "@/components/MkSuperMenu.vue";
 import { signOut } from "@/account";
 import { me } from "@/me";
 import { unisonReload } from "@/scripts/unison-reload";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import { useRouter } from "@/router";
 import {
 	definePageMetadata,
@@ -71,7 +71,7 @@ const router = useRouter();
 const narrow = ref(false);
 const NARROW_THRESHOLD = 600;
 
-const currentPage = computed(() => router.currentRef.value.child);
+const currentPage = computed(() => router.currentRef.value?.child);
 
 const ro = new ResizeObserver((entries, observer) => {
 	if (entries.length === 0) return;
@@ -282,7 +282,7 @@ onUnmounted(() => {
 
 watch(router.currentRef, (to) => {
 	if (
-		to.route.name === "settings" &&
+		to?.route.name === "settings" &&
 		to.child?.route.name == null &&
 		!narrow.value
 	) {
@@ -291,7 +291,8 @@ watch(router.currentRef, (to) => {
 });
 
 const emailNotConfigured = computed(
-	() => instance.enableEmail && (me.email == null || !me.emailVerified),
+	() =>
+		getInstanceInfo().enableEmail && (me?.email == null || !me.emailVerified),
 );
 
 provideMetadataReceiver((info) => {
diff --git a/packages/client/src/pages/settings/theme.vue b/packages/client/src/pages/settings/theme.vue
index edb2931fb9..8416d1df5e 100644
--- a/packages/client/src/pages/settings/theme.vue
+++ b/packages/client/src/pages/settings/theme.vue
@@ -157,7 +157,7 @@ import { selectFile } from "@/scripts/select-file";
 import { isDeviceDarkmode } from "@/scripts/is-device-darkmode";
 import { ColdDeviceStorage, defaultStore } from "@/store";
 import { i18n } from "@/i18n";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import { uniqueBy } from "@/scripts/array";
 import { fetchThemes, getThemes } from "@/theme-store";
 import { definePageMetadata } from "@/scripts/page-metadata";
@@ -165,9 +165,10 @@ import icon from "@/scripts/icon";
 
 const installedThemes = ref(getThemes());
 const builtinThemes = getBuiltinThemesRef();
+const { defaultDarkTheme, defaultLightTheme } = getInstanceInfo();
 
 const instanceDarkTheme = computed(() =>
-	instance.defaultDarkTheme ? JSON5.parse(instance.defaultDarkTheme) : null,
+	defaultDarkTheme ? JSON5.parse(defaultDarkTheme) : null,
 );
 const installedDarkThemes = computed(() =>
 	installedThemes.value.filter((t) => t.base === "dark" || t.kind === "dark"),
@@ -176,7 +177,7 @@ const builtinDarkThemes = computed(() =>
 	builtinThemes.value.filter((t) => t.base === "dark" || t.kind === "dark"),
 );
 const instanceLightTheme = computed(() =>
-	instance.defaultLightTheme ? JSON5.parse(instance.defaultLightTheme) : null,
+	defaultLightTheme ? JSON5.parse(defaultLightTheme) : null,
 );
 const installedLightThemes = computed(() =>
 	installedThemes.value.filter((t) => t.base === "light" || t.kind === "light"),
diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue
index 31434338b2..55ddd67085 100644
--- a/packages/client/src/pages/timeline.vue
+++ b/packages/client/src/pages/timeline.vue
@@ -74,7 +74,7 @@ import XPostForm from "@/components/MkPostForm.vue";
 import * as os from "@/os";
 import { defaultStore } from "@/store";
 import { i18n } from "@/i18n";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import { isModerator, isSignedIn, me } from "@/me";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import { deviceKind } from "@/scripts/device-kind";
@@ -86,17 +86,22 @@ if (isSignedIn(me) && defaultStore.reactiveState.tutorial.value !== -1) {
 	os.popup(XTutorial, {}, {}, "closed");
 }
 
+const {
+	disableLocalTimeline,
+	enableGuestTimeline,
+	disableRecommendedTimeline,
+	disableGlobalTimeline,
+} = getInstanceInfo();
+
 const isHomeTimelineAvailable = isSignedIn(me);
 const isLocalTimelineAvailable =
-	(!instance.disableLocalTimeline &&
-		(isSignedIn(me) || instance.enableGuestTimeline)) ||
+	(!disableLocalTimeline && (isSignedIn(me) || enableGuestTimeline)) ||
 	isModerator;
 const isSocialTimelineAvailable = isLocalTimelineAvailable && isSignedIn(me);
 const isRecommendedTimelineAvailable =
-	!instance.disableRecommendedTimeline && isSignedIn(me);
+	!disableRecommendedTimeline && isSignedIn(me);
 const isGlobalTimelineAvailable =
-	(!instance.disableGlobalTimeline &&
-		(isSignedIn(me) || instance.enableGuestTimeline)) ||
+	(!disableGlobalTimeline && (isSignedIn(me) || enableGuestTimeline)) ||
 	isModerator;
 const keymap = {
 	t: focus,
@@ -201,7 +206,7 @@ function saveSrc(
 }
 
 function focus(): void {
-	tlComponent.value.focus();
+	tlComponent.value?.focus();
 }
 
 const headerActions = computed(() =>
diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue
index 90fab34eb1..271add14bf 100644
--- a/packages/client/src/pages/user-info.vue
+++ b/packages/client/src/pages/user-info.vue
@@ -316,7 +316,7 @@
 							type="number"
 							:placeholder="
 								i18n.t('defaultValueIs', {
-									value: instance.driveCapacityPerLocalUserMb,
+									value: defaultDriveCapacity,
 								})
 							"
 							@update:model-value="applyDriveCapacityOverride"
@@ -364,7 +364,7 @@ import { userPage } from "@/filters/user";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import { i18n } from "@/i18n";
 import { isAdmin, isModerator } from "@/me";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import icon from "@/scripts/icon";
 
 const props = defineProps<{
@@ -391,6 +391,8 @@ const filesPagination = {
 	})),
 };
 
+const defaultDriveCapacity = getInstanceInfo().driveCapacityPerLocalUserMb;
+
 function createFetcher() {
 	if (isModerator) {
 		return () =>
diff --git a/packages/client/src/pages/welcome.entrance.a.vue b/packages/client/src/pages/welcome.entrance.a.vue
index 985fce79f5..30dd62f072 100644
--- a/packages/client/src/pages/welcome.entrance.a.vue
+++ b/packages/client/src/pages/welcome.entrance.a.vue
@@ -17,8 +17,8 @@
 			<div class="main">
 				<img
 					:src="
-						instance.faviconUrl ||
-						instance.iconUrl ||
+						faviconUrl ||
+						iconUrl ||
 						'/favicon.ico'
 					"
 					alt=""
@@ -105,7 +105,7 @@ import MkButton from "@/components/MkButton.vue";
 import MkFeaturedPhotos from "@/components/MkFeaturedPhotos.vue";
 import { instanceName } from "@/config";
 import * as os from "@/os";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import { i18n } from "@/i18n";
 import { defaultReactions } from "@/store";
 import icon from "@/scripts/icon";
@@ -116,6 +116,8 @@ const tags = ref();
 const onlineUsersCount = ref();
 const instances = ref();
 
+const { faviconUrl, iconUrl } = getInstanceInfo();
+
 os.api("meta", { detail: true }).then((_meta) => {
 	meta.value = _meta;
 });
@@ -181,12 +183,12 @@ function showMenu(ev) {
 					os.pageWindow("/about-firefish");
 				},
 			},
-			instance.tosUrl
+			getInstanceInfo.tosUrl
 				? {
 						text: i18n.ts.tos,
 						icon: `${icon("ph-scroll")}`,
 						action: () => {
-							window.open(instance.tosUrl, "_blank");
+							window.open(getInstanceInfo.tosUrl, "_blank");
 						},
 					}
 				: null,
diff --git a/packages/client/src/scripts/helpMenu.ts b/packages/client/src/scripts/helpMenu.ts
index 151ed1269c..5a5c08b297 100644
--- a/packages/client/src/scripts/helpMenu.ts
+++ b/packages/client/src/scripts/helpMenu.ts
@@ -1,7 +1,7 @@
 import XTutorial from "@/components/MkTutorialDialog.vue";
 import XCheatSheet from "@/components/MkCheatSheetDialog.vue";
 import { defaultStore } from "@/store";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import { host } from "@/config";
 import * as os from "@/os";
 import { i18n } from "@/i18n";
@@ -9,6 +9,7 @@ import icon from "@/scripts/icon";
 import type { MenuItem } from "@/types/menu";
 
 const instanceSpecificItems: MenuItem[] = [];
+const instance = getInstanceInfo();
 
 if (instance.tosUrl != null) {
 	instanceSpecificItems.push({
diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue
index 70a2740774..80c3d7479d 100644
--- a/packages/client/src/ui/_common_/navbar.vue
+++ b/packages/client/src/ui/_common_/navbar.vue
@@ -159,7 +159,7 @@ import { isAdmin, isModerator, me } from "@/me";
 import { openHelpMenu_ } from "@/scripts/helpMenu";
 import { defaultStore } from "@/store";
 import { i18n } from "@/i18n";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import { version } from "@/config";
 import icon from "@/scripts/icon";
 
@@ -189,13 +189,20 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
 	calcViewState();
 });
 
+const {
+	maintainerName,
+	maintainerEmail,
+	disableRegistration,
+	enableHcaptcha,
+	enableRecaptcha,
+	enableEmail,
+} = getInstanceInfo();
+
 const noMaintainerInformation =
-	isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
+	isEmpty(maintainerName) || isEmpty(maintainerEmail);
 const noBotProtection =
-	!instance.disableRegistration &&
-	!instance.enableHcaptcha &&
-	!instance.enableRecaptcha;
-const noEmailServer = !instance.enableEmail;
+	!disableRegistration && !enableHcaptcha && !enableRecaptcha;
+const noEmailServer = !enableEmail;
 const thereIsUnresolvedAbuseReport = ref(false);
 const updateAvailable = ref(false);
 
diff --git a/packages/client/src/ui/deck/tl-column.vue b/packages/client/src/ui/deck/tl-column.vue
index 1a4707988b..082fbc2031 100644
--- a/packages/client/src/ui/deck/tl-column.vue
+++ b/packages/client/src/ui/deck/tl-column.vue
@@ -51,7 +51,7 @@ import { removeColumn, updateColumn } from "./deck-store";
 import XTimeline from "@/components/MkTimeline.vue";
 import * as os from "@/os";
 import { isModerator, isSignedIn, me } from "@/me";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
 
@@ -69,18 +69,23 @@ const disabled = ref(false);
 const indicated = ref(false);
 const columnActive = ref(true);
 
+const {
+	disableLocalTimeline,
+	disableRecommendedTimeline,
+	disableGlobalTimeline,
+} = getInstanceInfo();
+
 onMounted(() => {
 	if (props.column.tl == null) {
 		setType();
 	} else if (isSignedIn(me)) {
 		disabled.value =
 			!isModerator &&
-			((instance.disableLocalTimeline &&
+			((disableLocalTimeline &&
 				["local", "social"].includes(props.column.tl)) ||
-				(instance.disableRecommendedTimeline &&
+				(disableRecommendedTimeline &&
 					["recommended"].includes(props.column.tl)) ||
-				(instance.disableGlobalTimeline &&
-					["global"].includes(props.column.tl)));
+				(disableGlobalTimeline && ["global"].includes(props.column.tl)));
 	}
 });
 
diff --git a/packages/client/src/ui/visitor/a.vue b/packages/client/src/ui/visitor/a.vue
index 107c4014cf..955f4de742 100644
--- a/packages/client/src/ui/visitor/a.vue
+++ b/packages/client/src/ui/visitor/a.vue
@@ -3,7 +3,7 @@
 		<div
 			v-if="mainRouter.currentRoute?.name === 'index'"
 			class="banner"
-			:style="{ backgroundImage: `url(${instance.bannerUrl})` }"
+			:style="{ backgroundImage: `url(${bannerUrl})` }"
 		>
 			<div>
 				<h1 v-if="meta">
@@ -32,7 +32,7 @@
 		<div
 			v-else
 			class="banner-mini"
-			:style="{ backgroundImage: `url(${instance.bannerUrl})` }"
+			:style="{ backgroundImage: `url(${bannerUrl})` }"
 		>
 			<div>
 				<h1 v-if="meta">
@@ -86,7 +86,7 @@ import MkButton from "@/components/MkButton.vue";
 import { ColdDeviceStorage, defaultStore } from "@/store";
 import { mainRouter } from "@/router";
 import { i18n } from "@/i18n";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 
 const DESKTOP_THRESHOLD = 1100;
 
@@ -111,6 +111,7 @@ export default defineComponent({
 			mainRouter,
 			isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
 			i18n,
+			bannerUrl: getInstanceInfo().bannerUrl,
 		};
 	},
 
@@ -122,7 +123,6 @@ export default defineComponent({
 					defaultStore.set("darkMode", !defaultStore.state.darkMode);
 				},
 				s: search,
-				"h|/": this.help,
 			};
 		},
 	},
@@ -148,13 +148,6 @@ export default defineComponent({
 	},
 
 	methods: {
-		// @ThatOneCalculator: Are these methods even used?
-		// I can't find references to them anywhere else in the code...
-
-		// setParallax(el) {
-		// 	new simpleParallax(el);
-		// },
-
 		changePage(page) {
 			if (page == null) return;
 
@@ -166,12 +159,6 @@ export default defineComponent({
 		top() {
 			window.scroll({ top: 0, behavior: "smooth" });
 		},
-
-		help() {
-			// TODO(thatonecalculator): popup with keybinds
-			// window.open('https://misskey-hub.net/docs/keyboard-shortcut.md', '_blank');
-			console.log("d = dark/light mode, s = search, p = post :3");
-		},
 	},
 });
 </script>
diff --git a/packages/client/src/ui/visitor/b.vue b/packages/client/src/ui/visitor/b.vue
index 68e3f9d1bf..fb8bfe5aaf 100644
--- a/packages/client/src/ui/visitor/b.vue
+++ b/packages/client/src/ui/visitor/b.vue
@@ -88,10 +88,9 @@ import XKanban from "./kanban.vue";
 import { host, instanceName } from "@/config";
 import { search } from "@/scripts/search";
 import * as os from "@/os";
-import { instance } from "@/instance";
 import XSigninDialog from "@/components/MkSigninDialog.vue";
 import XSignupDialog from "@/components/MkSignupDialog.vue";
-import { ColdDeviceStorage, defaultStore } from "@/store";
+import { defaultStore } from "@/store";
 import { mainRouter } from "@/router";
 import type { PageMetadata } from "@/scripts/page-metadata";
 import { provideMetadataReceiver } from "@/scripts/page-metadata";
@@ -110,30 +109,7 @@ provideMetadataReceiver((info) => {
 	}
 });
 
-const announcements = {
-	endpoint: "announcements",
-	limit: 10,
-};
-const isTimelineAvailable =
-	!instance.disableLocalTimeline ||
-	!instance.disableRecommendedTimeline ||
-	!instance.disableGlobalTimeline;
-const showMenu = ref(false);
-const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
-const narrow = ref(window.innerWidth < 1280);
-const meta = ref();
-
-const keymap = computed(() => {
-	return {
-		d: () => {
-			if (ColdDeviceStorage.get("syncDeviceDarkMode")) return;
-			defaultStore.set("darkMode", !defaultStore.state.darkMode);
-		},
-		s: search,
-	};
-});
-
-const root = computed(() => mainRouter.currentRoute.value.name === "index");
+const root = computed(() => mainRouter.currentRoute.value?.name === "index");
 
 os.api("meta", { detail: true }).then((res) => {
 	meta.value = res;
diff --git a/packages/client/src/ui/visitor/header.vue b/packages/client/src/ui/visitor/header.vue
index 5458efb656..542a18a2b5 100644
--- a/packages/client/src/ui/visitor/header.vue
+++ b/packages/client/src/ui/visitor/header.vue
@@ -6,14 +6,6 @@
 					><i :class="icon('ph-house icon')"></i
 					>{{ i18n.ts.home }}</MkA
 				>
-				<!-- <MkA
-					v-if="isTimelineAvailable"
-					to="/timeline"
-					class="link"
-					active-class="active"
-					><i :class="icon('ph-chats-circle icon')"></i
-					>{{ i18n.ts.timeline }}</MkA
-				> -->
 				<MkA to="/explore" class="link" active-class="active"
 					><i :class="icon('ph-compass icon')"></i
 					>{{ i18n.ts.explore }}</MkA
@@ -109,7 +101,6 @@ import { defineComponent } from "vue";
 import XSigninDialog from "@/components/MkSigninDialog.vue";
 import XSignupDialog from "@/components/MkSignupDialog.vue";
 import * as os from "@/os";
-import { instance } from "@/instance";
 import { search } from "@/scripts/search";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
@@ -126,11 +117,6 @@ export default defineComponent({
 			narrow: null,
 			showMenu: false,
 			i18n,
-			isTimelineAvailable:
-				!instance.disableLocalTimeline ||
-				!instance.disableRecommendedTimeline ||
-				!instance.disableGlobalTimeline,
-			icon,
 		};
 	},
 
@@ -162,6 +148,7 @@ export default defineComponent({
 		},
 
 		search,
+		icon,
 	},
 });
 </script>
diff --git a/packages/client/src/ui/visitor/kanban.vue b/packages/client/src/ui/visitor/kanban.vue
index 3b523e2110..e22a5733ef 100644
--- a/packages/client/src/ui/visitor/kanban.vue
+++ b/packages/client/src/ui/visitor/kanban.vue
@@ -4,7 +4,7 @@
 		:style="{
 			backgroundImage: transparent
 				? 'none'
-				: `url(${instance.backgroundImageUrl})`,
+				: `url(${backgroundImageUrl})`,
 		}"
 	>
 		<div class="back" :class="{ transparent }"></div>
@@ -91,7 +91,7 @@ import XSigninDialog from "@/components/MkSigninDialog.vue";
 import XSignupDialog from "@/components/MkSignupDialog.vue";
 import MkButton from "@/components/MkButton.vue";
 import { i18n } from "@/i18n";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 
 export default defineComponent({
 	components: {
@@ -129,7 +129,7 @@ export default defineComponent({
 				limit: 10,
 			},
 			i18n,
-			instance,
+			backgroundImageUrl: getInstanceInfo().backgroundImageUrl,
 		};
 	},
 
diff --git a/packages/client/src/widgets/server-info.vue b/packages/client/src/widgets/server-info.vue
index ffb42df847..317837664f 100644
--- a/packages/client/src/widgets/server-info.vue
+++ b/packages/client/src/widgets/server-info.vue
@@ -34,7 +34,7 @@ import type { Widget, WidgetComponentExpose } from "./widget";
 import { useWidgetPropsManager } from "./widget";
 import type { GetFormResultType } from "@/scripts/form";
 import { host } from "@/config";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 
 const name = "serverInfo";
 
@@ -47,6 +47,8 @@ const emit = defineEmits<{ (ev: "updateProps", props: WidgetProps) }>();
 
 const { configure } = useWidgetPropsManager(name, widgetPropsDef, props, emit);
 
+const instance = getInstanceInfo();
+
 defineExpose<WidgetComponentExpose>({
 	name,
 	configure,
diff --git a/packages/client/src/widgets/server-metric/index.vue b/packages/client/src/widgets/server-metric/index.vue
index 6513b961a8..c5cc549940 100644
--- a/packages/client/src/widgets/server-metric/index.vue
+++ b/packages/client/src/widgets/server-metric/index.vue
@@ -12,13 +12,13 @@
 				<i :class="icon('ph-sort-ascending')"></i></button
 		></template>
 
-		<div v-if="!instance.enableServerMachineStats" class="mkw-serverMetric">
+		<div v-if="!enableServerMachineStats" class="mkw-serverMetric">
 			<h3 style="text-align: center; color: var(--error)">
 				{{ i18n.ts.disabled }}
 			</h3>
 		</div>
 		<div
-			v-else-if="meta && instance.enableServerMachineStats"
+			v-else-if="meta && enableServerMachineStats"
 			class="mkw-serverMetric"
 		>
 			<XCpuMemory
@@ -62,7 +62,7 @@ import type { GetFormResultType } from "@/scripts/form";
 import * as os from "@/os";
 import { useStream } from "@/stream";
 import { i18n } from "@/i18n";
-import { instance } from "@/instance";
+import { getInstanceInfo } from "@/instance";
 import icon from "@/scripts/icon";
 
 const name = "serverMetric";
@@ -96,6 +96,7 @@ const { widgetProps, configure, save } = useWidgetPropsManager(
 );
 
 const meta = ref(null);
+const { enableServerMachineStats } = getInstanceInfo();
 
 os.apiGet("server-info", {}).then((res) => {
 	meta.value = res;
diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index bcbb305243..7da946ba95 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -405,6 +405,7 @@ export type DetailedInstanceMetadata = LiteInstanceMetadata & {
 	emailRequiredForSignup: boolean;
 	mascotImageUrl: string;
 	bannerUrl: string;
+	backgroundImageUrl: string;
 	errorImageUrl: string;
 	iconUrl: string | null;
 	maxCaptionTextLength: number;