diff --git a/packages/cli/package.json b/packages/cli/package.json
index e93d01f70..de2deeb2c 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -47,7 +47,7 @@
     "@logto/core-kit": "workspace:*",
     "@logto/schemas": "workspace:*",
     "@logto/shared": "workspace:*",
-    "@silverhand/essentials": "2.4.1",
+    "@silverhand/essentials": "^2.4.1",
     "chalk": "^5.0.0",
     "decamelize": "^6.0.0",
     "dotenv": "^16.0.0",
diff --git a/packages/cloud/package.json b/packages/cloud/package.json
index 92cc56ae6..8b0a749aa 100644
--- a/packages/cloud/package.json
+++ b/packages/cloud/package.json
@@ -28,7 +28,7 @@
     "@logto/core-kit": "workspace:*",
     "@logto/schemas": "workspace:*",
     "@logto/shared": "workspace:*",
-    "@silverhand/essentials": "2.4.1",
+    "@silverhand/essentials": "^2.4.1",
     "@withtyped/postgres": "^0.8.1",
     "@withtyped/server": "^0.8.1",
     "accepts": "^1.3.8",
diff --git a/packages/console/package.json b/packages/console/package.json
index 31da40e7d..b6cec53ae 100644
--- a/packages/console/package.json
+++ b/packages/console/package.json
@@ -35,7 +35,7 @@
     "@parcel/transformer-svg-react": "2.8.3",
     "@silverhand/eslint-config": "2.0.1",
     "@silverhand/eslint-config-react": "2.0.1",
-    "@silverhand/essentials": "2.4.1",
+    "@silverhand/essentials": "^2.4.1",
     "@silverhand/ts-config": "2.0.3",
     "@silverhand/ts-config-react": "2.0.3",
     "@tsconfig/docusaurus": "^1.0.5",
diff --git a/packages/core/package.json b/packages/core/package.json
index 53268eeed..0b5ebed44 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -35,7 +35,7 @@
     "@logto/phrases-ui": "workspace:*",
     "@logto/schemas": "workspace:*",
     "@logto/shared": "workspace:*",
-    "@silverhand/essentials": "2.4.1",
+    "@silverhand/essentials": "^2.4.1",
     "aws-sdk": "^2.1329.0",
     "chalk": "^5.0.0",
     "clean-deep": "^3.4.0",
@@ -51,6 +51,7 @@
     "iconv-lite": "0.6.3",
     "jose": "^4.11.0",
     "js-yaml": "^4.1.0",
+    "keyv": "^4.5.2",
     "koa": "^2.13.1",
     "koa-body": "^5.0.0",
     "koa-compose": "^4.1.0",
@@ -63,6 +64,7 @@
     "lru-cache": "^7.14.1",
     "nanoid": "^4.0.0",
     "oidc-provider": "^8.0.0",
+    "p-memoize": "^7.1.1",
     "p-retry": "^5.1.2",
     "pg-protocol": "^1.6.0",
     "roarr": "^7.11.0",
diff --git a/packages/core/src/caches/well-known.ts b/packages/core/src/caches/well-known.ts
new file mode 100644
index 000000000..3e9901b37
--- /dev/null
+++ b/packages/core/src/caches/well-known.ts
@@ -0,0 +1,30 @@
+import Keyv from 'keyv';
+import type { AnyAsyncFunction } from 'p-memoize';
+import pMemoize from 'p-memoize';
+
+const cacheKeys = Object.freeze(['sie', 'sie-full', 'phrases', 'lng-tags'] as const);
+
+/** Well-known data type key for cache. */
+export type WellKnownCacheKey = (typeof cacheKeys)[number];
+
+// Not sure if we need guard value for `.has()` and `.get()`,
+// trust cache value for now.
+const wellKnownCache = new Keyv({ ttl: 300_000 /* 5 minutes */ });
+
+/**
+ * Use for centralized well-known data caching.
+ *
+ * WARN: You should store only well-known (public) data since it's a central cache.
+ */
+export const useWellKnownCache = <FunctionToMemoize extends AnyAsyncFunction>(
+  tenantId: string,
+  key: WellKnownCacheKey,
+  run: FunctionToMemoize
+) =>
+  pMemoize(run, {
+    cacheKey: () => `${tenantId}:${key}`,
+    cache: wellKnownCache,
+  });
+
+export const invalidateWellKnownCache = async (tenantId: string) =>
+  wellKnownCache.delete(cacheKeys.map((key) => `${tenantId}:${key}` as const));
diff --git a/packages/core/src/libraries/phrase.test.ts b/packages/core/src/libraries/phrase.test.ts
index f9faea10f..8ab9410dd 100644
--- a/packages/core/src/libraries/phrase.test.ts
+++ b/packages/core/src/libraries/phrase.test.ts
@@ -11,6 +11,7 @@ import {
   zhCnTag,
   zhHkTag,
 } from '#src/__mocks__/custom-phrase.js';
+import { invalidateWellKnownCache } from '#src/caches/well-known.js';
 import RequestError from '#src/errors/RequestError/index.js';
 import { MockQueries } from '#src/test-utils/tenant.js';
 
@@ -41,12 +42,15 @@ const findCustomPhraseByLanguageTag = jest.fn(async (languageTag: string) => {
   return mockCustomPhrase;
 });
 
+const tenantId = 'mock_id';
 const { createPhraseLibrary } = await import('#src/libraries/phrase.js');
 const { getPhrases } = createPhraseLibrary(
-  new MockQueries({ customPhrases: { findCustomPhraseByLanguageTag } })
+  new MockQueries({ customPhrases: { findCustomPhraseByLanguageTag } }),
+  tenantId
 );
 
-afterEach(() => {
+afterEach(async () => {
+  await invalidateWellKnownCache(tenantId);
   jest.clearAllMocks();
 });
 
diff --git a/packages/core/src/libraries/phrase.ts b/packages/core/src/libraries/phrase.ts
index bb6f163c5..05743c0ae 100644
--- a/packages/core/src/libraries/phrase.ts
+++ b/packages/core/src/libraries/phrase.ts
@@ -4,12 +4,16 @@ import type { CustomPhrase } from '@logto/schemas';
 import cleanDeep from 'clean-deep';
 import deepmerge from 'deepmerge';
 
+import { useWellKnownCache } from '#src/caches/well-known.js';
 import type Queries from '#src/tenants/Queries.js';
 
-export const createPhraseLibrary = (queries: Queries) => {
-  const { findCustomPhraseByLanguageTag } = queries.customPhrases;
+export const createPhraseLibrary = (queries: Queries, tenantId: string) => {
+  const { findCustomPhraseByLanguageTag, findAllCustomLanguageTags } = queries.customPhrases;
 
-  const getPhrases = async (supportedLanguage: string, customLanguages: string[]) => {
+  const _getPhrases = async (
+    supportedLanguage: string,
+    customLanguages: string[]
+  ): Promise<LocalePhrase> => {
     if (!isBuiltInLanguageTag(supportedLanguage)) {
       return deepmerge<LocalePhrase, CustomPhrase>(
         resource.en,
@@ -27,5 +31,18 @@ export const createPhraseLibrary = (queries: Queries) => {
     );
   };
 
-  return { getPhrases };
+  const getPhrases = useWellKnownCache(tenantId, 'phrases', _getPhrases);
+
+  const getAllCustomLanguageTags = useWellKnownCache(
+    tenantId,
+    'lng-tags',
+    findAllCustomLanguageTags
+  );
+
+  return {
+    /** NOTE: This function is cached by the first parameter. */
+    getPhrases,
+    /** NOTE: This function is cached. */
+    getAllCustomLanguageTags,
+  };
 };
diff --git a/packages/core/src/libraries/sign-in-experience/index.test.ts b/packages/core/src/libraries/sign-in-experience/index.test.ts
index b48305b2d..660e82e26 100644
--- a/packages/core/src/libraries/sign-in-experience/index.test.ts
+++ b/packages/core/src/libraries/sign-in-experience/index.test.ts
@@ -42,7 +42,7 @@ const getLogtoConnectors = jest.spyOn(connectorLibrary, 'getLogtoConnectors');
 
 const { createSignInExperienceLibrary } = await import('./index.js');
 const { validateLanguageInfo, removeUnavailableSocialConnectorTargets } =
-  createSignInExperienceLibrary(queries, connectorLibrary);
+  createSignInExperienceLibrary(queries, connectorLibrary, 'mock_id');
 
 beforeEach(() => {
   jest.clearAllMocks();
diff --git a/packages/core/src/libraries/sign-in-experience/index.ts b/packages/core/src/libraries/sign-in-experience/index.ts
index 5d055b63f..d9f14ab54 100644
--- a/packages/core/src/libraries/sign-in-experience/index.ts
+++ b/packages/core/src/libraries/sign-in-experience/index.ts
@@ -1,8 +1,11 @@
+import { connectorMetadataGuard } from '@logto/connector-kit';
 import { builtInLanguages } from '@logto/phrases-ui';
-import type { LanguageInfo, SignInExperience } from '@logto/schemas';
-import { ConnectorType } from '@logto/schemas';
+import type { ConnectorMetadata, LanguageInfo, SignInExperience } from '@logto/schemas';
+import { SignInExperiences, ConnectorType } from '@logto/schemas';
 import { deduplicate } from '@silverhand/essentials';
+import { z } from 'zod';
 
+import { useWellKnownCache } from '#src/caches/well-known.js';
 import RequestError from '#src/errors/RequestError/index.js';
 import type { ConnectorLibrary } from '#src/libraries/connector.js';
 import type Queries from '#src/tenants/Queries.js';
@@ -15,12 +18,12 @@ export type SignInExperienceLibrary = ReturnType<typeof createSignInExperienceLi
 
 export const createSignInExperienceLibrary = (
   queries: Queries,
-  connectorLibrary: ConnectorLibrary
+  { getLogtoConnectors }: ConnectorLibrary,
+  tenantId: string
 ) => {
   const {
     customPhrases: { findAllCustomLanguageTags },
     signInExperiences: { findDefaultSignInExperience, updateDefaultSignInExperience },
-    users: { hasActiveUsers },
   } = queries;
 
   const validateLanguageInfo = async (languageInfo: LanguageInfo) => {
@@ -36,7 +39,7 @@ export const createSignInExperienceLibrary = (
   };
 
   const removeUnavailableSocialConnectorTargets = async () => {
-    const connectors = await connectorLibrary.getLogtoConnectors();
+    const connectors = await getLogtoConnectors();
     const availableSocialConnectorTargets = deduplicate(
       connectors
         .filter(({ type }) => type === ConnectorType.Social)
@@ -52,11 +55,65 @@ export const createSignInExperienceLibrary = (
     });
   };
 
-  const getSignInExperience = async (): Promise<SignInExperience> => findDefaultSignInExperience();
+  const getSignInExperience = useWellKnownCache(tenantId, 'sie', findDefaultSignInExperience);
+
+  const _getFullSignInExperience = async (): Promise<FullSignInExperience> => {
+    const [signInExperience, logtoConnectors] = await Promise.all([
+      getSignInExperience(),
+      getLogtoConnectors(),
+    ]);
+
+    const forgotPassword = {
+      phone: logtoConnectors.some(({ type }) => type === ConnectorType.Sms),
+      email: logtoConnectors.some(({ type }) => type === ConnectorType.Email),
+    };
+
+    const socialConnectors = signInExperience.socialSignInConnectorTargets.reduce<
+      Array<ConnectorMetadata & { id: string }>
+    >((previous, connectorTarget) => {
+      const connectors = logtoConnectors.filter(
+        ({ metadata: { target } }) => target === connectorTarget
+      );
+
+      return [
+        ...previous,
+        ...connectors.map(({ metadata, dbEntry: { id } }) => ({ ...metadata, id })),
+      ];
+    }, []);
+
+    return {
+      ...signInExperience,
+      socialConnectors,
+      forgotPassword,
+    };
+  };
+
+  const getFullSignInExperience = useWellKnownCache(tenantId, 'sie-full', _getFullSignInExperience);
 
   return {
     validateLanguageInfo,
     removeUnavailableSocialConnectorTargets,
+    /** NOTE: This function is cached. */
     getSignInExperience,
+    /** NOTE: This function is cached. */
+    getFullSignInExperience,
   };
 };
+
+export type ForgotPassword = {
+  phone: boolean;
+  email: boolean;
+};
+
+export type ConnectorMetadataWithId = ConnectorMetadata & { id: string };
+
+export type FullSignInExperience = SignInExperience & {
+  socialConnectors: ConnectorMetadataWithId[];
+  forgotPassword: ForgotPassword;
+};
+
+export const guardFullSignInExperience: z.ZodType<FullSignInExperience> =
+  SignInExperiences.guard.extend({
+    socialConnectors: connectorMetadataGuard.extend({ id: z.string() }).array(),
+    forgotPassword: z.object({ phone: z.boolean(), email: z.boolean() }),
+  });
diff --git a/packages/core/src/queries/custom-phrase.ts b/packages/core/src/queries/custom-phrase.ts
index bb65c2fd6..369fbfddc 100644
--- a/packages/core/src/queries/custom-phrase.ts
+++ b/packages/core/src/queries/custom-phrase.ts
@@ -58,6 +58,10 @@ export const createCustomPhraseQueries = (pool: CommonQueryMethods) => {
   };
 
   return {
+    /**
+     * NOTE: Use `getAllCustomLanguageTags()` from phrase library
+     * if possible since that function leverages cache.
+     */
     findAllCustomLanguageTags,
     findAllCustomPhrases,
     findCustomPhraseByLanguageTag,
diff --git a/packages/core/src/queries/sign-in-experience.ts b/packages/core/src/queries/sign-in-experience.ts
index fe1a3f09d..1740342d9 100644
--- a/packages/core/src/queries/sign-in-experience.ts
+++ b/packages/core/src/queries/sign-in-experience.ts
@@ -16,5 +16,12 @@ export const createSignInExperienceQueries = (pool: CommonQueryMethods) => {
   const findDefaultSignInExperience = async () =>
     buildFindEntityByIdWithPool(pool)(SignInExperiences)(id);
 
-  return { updateDefaultSignInExperience, findDefaultSignInExperience };
+  return {
+    updateDefaultSignInExperience,
+    /**
+     * NOTE: Use `getSignInExperience()` from sign-in experience library
+     * if possible since that function leverages cache.
+     */
+    findDefaultSignInExperience,
+  };
 };
diff --git a/packages/core/src/routes-me/social.ts b/packages/core/src/routes-me/social.ts
index 58146a546..c543c4262 100644
--- a/packages/core/src/routes-me/social.ts
+++ b/packages/core/src/routes-me/social.ts
@@ -20,13 +20,11 @@ export default function socialRoutes<T extends AuthedMeRouter>(
   ...[router, tenant]: RouterInitArgs<T>
 ) {
   const {
-    libraries: {
-      connectors: { getLogtoConnectors, getLogtoConnectorById },
-    },
     queries: {
       users: { findUserById, updateUserById, deleteUserIdentity, hasUserWithIdentity },
       signInExperiences: { findDefaultSignInExperience },
     },
+    connectors: { getLogtoConnectors, getLogtoConnectorById },
   } = tenant;
 
   router.get('/social/connectors', async (ctx, next) => {
diff --git a/packages/core/src/routes/admin-user.test.ts b/packages/core/src/routes/admin-user.test.ts
index 9dca79d52..327ad7640 100644
--- a/packages/core/src/routes/admin-user.test.ts
+++ b/packages/core/src/routes/admin-user.test.ts
@@ -105,7 +105,9 @@ const usersLibraries = {
 const adminUserRoutes = await pickDefault(import('./admin-user.js'));
 
 describe('adminUserRoutes', () => {
-  const tenantContext = new MockTenant(undefined, mockedQueries, { users: usersLibraries });
+  const tenantContext = new MockTenant(undefined, mockedQueries, undefined, {
+    users: usersLibraries,
+  });
   const userRequest = createRequester({ authedRoutes: adminUserRoutes, tenantContext });
 
   afterEach(() => {
diff --git a/packages/core/src/routes/authn.test.ts b/packages/core/src/routes/authn.test.ts
index 6b1c0759c..8cf1a8931 100644
--- a/packages/core/src/routes/authn.test.ts
+++ b/packages/core/src/routes/authn.test.ts
@@ -62,6 +62,7 @@ const usersLibraries = {
 const tenantContext = new MockTenant(
   createMockProvider(jest.fn().mockResolvedValue(baseProviderMock)),
   undefined,
+  undefined,
   { users: usersLibraries, socials: socialsLibraries }
 );
 const { createRequester } = await import('#src/utils/test-utils.js');
diff --git a/packages/core/src/routes/connector.test.ts b/packages/core/src/routes/connector.test.ts
index 94a207b97..e3c93a8ac 100644
--- a/packages/core/src/routes/connector.test.ts
+++ b/packages/core/src/routes/connector.test.ts
@@ -76,24 +76,24 @@ const tenantContext = new MockTenant(
   undefined,
   { connectors: connectorQueries },
   {
-    signInExperiences: { removeUnavailableSocialConnectorTargets },
-    connectors: {
-      getLogtoConnectors,
-      getLogtoConnectorById: async (connectorId: string) => {
-        const connectors = await getLogtoConnectors();
-        const connector = connectors.find(({ dbEntry }) => dbEntry.id === connectorId);
-        assertThat(
-          connector,
-          new RequestError({
-            code: 'entity.not_found',
-            connectorId,
-            status: 404,
-          })
-        );
+    getLogtoConnectors,
+    getLogtoConnectorById: async (connectorId: string) => {
+      const connectors = await getLogtoConnectors();
+      const connector = connectors.find(({ dbEntry }) => dbEntry.id === connectorId);
+      assertThat(
+        connector,
+        new RequestError({
+          code: 'entity.not_found',
+          connectorId,
+          status: 404,
+        })
+      );
 
-        return connector;
-      },
+      return connector;
     },
+  },
+  {
+    signInExperiences: { removeUnavailableSocialConnectorTargets },
   }
 );
 
diff --git a/packages/core/src/routes/connector.ts b/packages/core/src/routes/connector.ts
index 4002c8c00..440551c9d 100644
--- a/packages/core/src/routes/connector.ts
+++ b/packages/core/src/routes/connector.ts
@@ -21,7 +21,7 @@ import type { AuthedRouter, RouterInitArgs } from './types.js';
 const generateConnectorId = buildIdGenerator(12);
 
 export default function connectorRoutes<T extends AuthedRouter>(
-  ...[router, { queries, libraries }]: RouterInitArgs<T>
+  ...[router, { queries, connectors, libraries }]: RouterInitArgs<T>
 ) {
   const {
     findConnectorById,
@@ -31,8 +31,8 @@ export default function connectorRoutes<T extends AuthedRouter>(
     insertConnector,
     updateConnector,
   } = queries.connectors;
+  const { getLogtoConnectorById, getLogtoConnectors } = connectors;
   const {
-    connectors: { getLogtoConnectorById, getLogtoConnectors },
     signInExperiences: { removeUnavailableSocialConnectorTargets },
   } = libraries;
 
diff --git a/packages/core/src/routes/connector.update.test.ts b/packages/core/src/routes/connector.update.test.ts
index c87c1b375..0da984db7 100644
--- a/packages/core/src/routes/connector.update.test.ts
+++ b/packages/core/src/routes/connector.update.test.ts
@@ -44,10 +44,10 @@ const tenantContext = new MockTenant(
   undefined,
   { connectors: { updateConnector } },
   {
-    connectors: {
-      getLogtoConnectors,
-      getLogtoConnectorById,
-    },
+    getLogtoConnectors,
+    getLogtoConnectorById,
+  },
+  {
     signInExperiences: {
       // eslint-disable-next-line @typescript-eslint/no-empty-function
       removeUnavailableSocialConnectorTargets: async () => {},
diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.test.ts b/packages/core/src/routes/interaction/actions/submit-interaction.test.ts
index 6ca2cb2da..f026a572d 100644
--- a/packages/core/src/routes/interaction/actions/submit-interaction.test.ts
+++ b/packages/core/src/routes/interaction/actions/submit-interaction.test.ts
@@ -56,7 +56,8 @@ describe('submit action', () => {
   const tenant = new MockTenant(
     undefined,
     { users: userQueries, signInExperiences: { updateDefaultSignInExperience: jest.fn() } },
-    { users: userLibraries, connectors: { getLogtoConnectorById } }
+    { getLogtoConnectorById },
+    { users: userLibraries }
   );
   const ctx = {
     ...createContextWithRouteParameters(),
diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.ts b/packages/core/src/routes/interaction/actions/submit-interaction.ts
index 1a81e4d08..4b3bed01f 100644
--- a/packages/core/src/routes/interaction/actions/submit-interaction.ts
+++ b/packages/core/src/routes/interaction/actions/submit-interaction.ts
@@ -149,7 +149,7 @@ const parseUserProfile = async (
 export default async function submitInteraction(
   interaction: VerifiedInteractionResult,
   ctx: WithInteractionDetailsContext,
-  { provider, libraries, queries }: TenantContext,
+  { provider, libraries, connectors, queries }: TenantContext,
   log?: LogEntry
 ) {
   const { hasActiveUsers, findUserById, updateUserById } = queries.users;
@@ -157,7 +157,6 @@ export default async function submitInteraction(
 
   const {
     users: { generateUserId, insertUser },
-    connectors,
   } = libraries;
   const { event, profile } = interaction;
 
diff --git a/packages/core/src/routes/interaction/index.test.ts b/packages/core/src/routes/interaction/index.test.ts
index 09dd93c5b..3aec98a74 100644
--- a/packages/core/src/routes/interaction/index.test.ts
+++ b/packages/core/src/routes/interaction/index.test.ts
@@ -101,21 +101,21 @@ const tenantContext = new MockTenant(
   createMockProvider(jest.fn().mockResolvedValue(baseProviderMock)),
   undefined,
   {
-    connectors: {
-      getLogtoConnectorById: async (connectorId: string) => {
-        const connector = await getLogtoConnectorByIdHelper(connectorId);
+    getLogtoConnectorById: async (connectorId: string) => {
+      const connector = await getLogtoConnectorByIdHelper(connectorId);
 
-        if (connector.type !== ConnectorType.Social) {
-          throw new RequestError({
-            code: 'entity.not_found',
-            status: 404,
-          });
-        }
+      if (connector.type !== ConnectorType.Social) {
+        throw new RequestError({
+          code: 'entity.not_found',
+          status: 404,
+        });
+      }
 
-        // @ts-expect-error
-        return connector as LogtoConnector;
-      },
+      // @ts-expect-error
+      return connector as LogtoConnector;
     },
+  },
+  {
     signInExperiences: {
       getSignInExperience: jest.fn().mockResolvedValue(mockSignInExperience),
     },
diff --git a/packages/core/src/routes/interaction/utils/find-user-by-identifier.test.ts b/packages/core/src/routes/interaction/utils/find-user-by-identifier.test.ts
index a2c8b4009..dbfcb1fe2 100644
--- a/packages/core/src/routes/interaction/utils/find-user-by-identifier.test.ts
+++ b/packages/core/src/routes/interaction/utils/find-user-by-identifier.test.ts
@@ -18,7 +18,7 @@ const tenantContext = new MockTenant(
   {
     users: queries,
   },
-  { connectors: { getLogtoConnectorById } }
+  { getLogtoConnectorById }
 );
 
 const findUserByIdentifier = await pickDefault(import('./find-user-by-identifier.js'));
diff --git a/packages/core/src/routes/interaction/utils/find-user-by-identifier.ts b/packages/core/src/routes/interaction/utils/find-user-by-identifier.ts
index d4ca483ac..efc523b3d 100644
--- a/packages/core/src/routes/interaction/utils/find-user-by-identifier.ts
+++ b/packages/core/src/routes/interaction/utils/find-user-by-identifier.ts
@@ -3,12 +3,12 @@ import type TenantContext from '#src/tenants/TenantContext.js';
 import type { UserIdentity } from '../types/index.js';
 
 export default async function findUserByIdentifier(
-  { queries, libraries }: TenantContext,
+  { queries, connectors }: TenantContext,
   identity: UserIdentity
 ) {
   const { findUserByEmail, findUserByUsername, findUserByPhone, findUserByIdentity } =
     queries.users;
-  const { getLogtoConnectorById } = libraries.connectors;
+  const { getLogtoConnectorById } = connectors;
 
   if ('username' in identity) {
     return findUserByUsername(identity.username);
diff --git a/packages/core/src/routes/interaction/utils/social-verification.test.ts b/packages/core/src/routes/interaction/utils/social-verification.test.ts
index 01508ceb8..19eba8280 100644
--- a/packages/core/src/routes/interaction/utils/social-verification.test.ts
+++ b/packages/core/src/routes/interaction/utils/social-verification.test.ts
@@ -11,7 +11,9 @@ const { mockEsm } = createMockUtils(jest);
 
 const getUserInfoByAuthCode = jest.fn().mockResolvedValue({ id: 'foo' });
 
-const tenant = new MockTenant(undefined, undefined, { socials: { getUserInfoByAuthCode } });
+const tenant = new MockTenant(undefined, undefined, undefined, {
+  socials: { getUserInfoByAuthCode },
+});
 
 mockEsm('#src/libraries/connector.js', () => ({
   getLogtoConnectorById: jest.fn().mockResolvedValue({
diff --git a/packages/core/src/routes/interaction/utils/social-verification.ts b/packages/core/src/routes/interaction/utils/social-verification.ts
index 10ee4d633..b88b7be23 100644
--- a/packages/core/src/routes/interaction/utils/social-verification.ts
+++ b/packages/core/src/routes/interaction/utils/social-verification.ts
@@ -14,12 +14,10 @@ import type { SocialAuthorizationUrlPayload } from '../types/index.js';
 
 export const createSocialAuthorizationUrl = async (
   ctx: WithLogContext,
-  { provider, libraries }: TenantContext,
+  { provider, connectors }: TenantContext,
   payload: SocialAuthorizationUrlPayload
 ) => {
-  const {
-    connectors: { getLogtoConnectorById },
-  } = libraries;
+  const { getLogtoConnectorById } = connectors;
 
   const { connectorId, state, redirectUri } = payload;
   assertThat(state && redirectUri, 'session.insufficient_info');
diff --git a/packages/core/src/routes/interaction/verifications/profile-verification.profile-registered.test.ts b/packages/core/src/routes/interaction/verifications/profile-verification.profile-registered.test.ts
index a13d88611..8917c333c 100644
--- a/packages/core/src/routes/interaction/verifications/profile-verification.profile-registered.test.ts
+++ b/packages/core/src/routes/interaction/verifications/profile-verification.profile-registered.test.ts
@@ -21,11 +21,7 @@ const getLogtoConnectorById = jest.fn().mockResolvedValue({
   metadata: { target: 'logto' },
 });
 
-const tenantContext = new MockTenant(
-  undefined,
-  { users: userQueries },
-  { connectors: { getLogtoConnectorById } }
-);
+const tenantContext = new MockTenant(undefined, { users: userQueries }, { getLogtoConnectorById });
 const verifyProfile = await pickDefault(import('./profile-verification.js'));
 
 const identifiers: Identifier[] = [
diff --git a/packages/core/src/routes/interaction/verifications/profile-verification.protected-identifier.test.ts b/packages/core/src/routes/interaction/verifications/profile-verification.protected-identifier.test.ts
index 420dc87b6..36b85cdb1 100644
--- a/packages/core/src/routes/interaction/verifications/profile-verification.protected-identifier.test.ts
+++ b/packages/core/src/routes/interaction/verifications/profile-verification.protected-identifier.test.ts
@@ -19,11 +19,9 @@ const tenantContext = new MockTenant(
     },
   },
   {
-    connectors: {
-      getLogtoConnectorById: jest.fn().mockResolvedValue({
-        metadata: { target: 'logto' },
-      }),
-    },
+    getLogtoConnectorById: jest.fn().mockResolvedValue({
+      metadata: { target: 'logto' },
+    }),
   }
 );
 const verifyProfile = await pickDefault(import('./profile-verification.js'));
diff --git a/packages/core/src/routes/interaction/verifications/profile-verification.ts b/packages/core/src/routes/interaction/verifications/profile-verification.ts
index fef88d309..066b40ce0 100644
--- a/packages/core/src/routes/interaction/verifications/profile-verification.ts
+++ b/packages/core/src/routes/interaction/verifications/profile-verification.ts
@@ -58,7 +58,7 @@ const verifyProfileIdentifiers = (
 };
 
 const verifyProfileNotRegisteredByOtherUserAccount = async (
-  { queries, libraries }: TenantContext,
+  { queries, connectors }: TenantContext,
   { username, email, phone, connectorId }: Profile,
   identifiers: Identifier[] = []
 ) => {
@@ -97,7 +97,7 @@ const verifyProfileNotRegisteredByOtherUserAccount = async (
   if (connectorId) {
     const {
       metadata: { target },
-    } = await libraries.connectors.getLogtoConnectorById(connectorId);
+    } = await connectors.getLogtoConnectorById(connectorId);
 
     const socialIdentifier = identifiers.find(
       (identifier): identifier is SocialIdentifier => identifier.key === 'social'
diff --git a/packages/core/src/routes/interaction/verifications/user-identity-verification.test.ts b/packages/core/src/routes/interaction/verifications/user-identity-verification.test.ts
index caa019c17..5f0fd92fc 100644
--- a/packages/core/src/routes/interaction/verifications/user-identity-verification.test.ts
+++ b/packages/core/src/routes/interaction/verifications/user-identity-verification.test.ts
@@ -11,11 +11,9 @@ const { mockEsmDefault } = createMockUtils(jest);
 
 const findUserByIdentifier = mockEsmDefault('../utils/find-user-by-identifier.js', () => jest.fn());
 
-const tenant = new MockTenant(
-  undefined,
-  {},
-  { socials: { findSocialRelatedUser: jest.fn().mockResolvedValue(null) } }
-);
+const tenant = new MockTenant(undefined, undefined, undefined, {
+  socials: { findSocialRelatedUser: jest.fn().mockResolvedValue(null) },
+});
 
 const verifyUserAccount = await pickDefault(import('./user-identity-verification.js'));
 
diff --git a/packages/core/src/routes/resource.test.ts b/packages/core/src/routes/resource.test.ts
index 9614c3292..a6ef70388 100644
--- a/packages/core/src/routes/resource.test.ts
+++ b/packages/core/src/routes/resource.test.ts
@@ -52,7 +52,7 @@ mockEsm('@logto/core-kit', () => ({
   buildIdGenerator: () => () => 'randomId',
 }));
 
-const tenantContext = new MockTenant(undefined, { scopes, resources }, libraries);
+const tenantContext = new MockTenant(undefined, { scopes, resources }, undefined, libraries);
 
 const resourceRoutes = await pickDefault(import('./resource.js'));
 
diff --git a/packages/core/src/routes/sign-in-experience/guard.test.ts b/packages/core/src/routes/sign-in-experience/guard.test.ts
index b5c29cf67..0ed434363 100644
--- a/packages/core/src/routes/sign-in-experience/guard.test.ts
+++ b/packages/core/src/routes/sign-in-experience/guard.test.ts
@@ -20,6 +20,7 @@ const tenantContext = new MockTenant(
       }),
     },
   },
+  undefined,
   {
     signInExperiences: {
       validateLanguageInfo,
diff --git a/packages/core/src/routes/sign-in-experience/index.test.ts b/packages/core/src/routes/sign-in-experience/index.test.ts
index cd6e076ae..4304594e4 100644
--- a/packages/core/src/routes/sign-in-experience/index.test.ts
+++ b/packages/core/src/routes/sign-in-experience/index.test.ts
@@ -1,6 +1,9 @@
 import type { SignInExperience, CreateSignInExperience } from '@logto/schemas';
 import { pickDefault, createMockUtils } from '@logto/shared/esm';
 
+import { MockTenant } from '#src/test-utils/tenant.js';
+import { createRequester } from '#src/utils/test-utils.js';
+
 import {
   mockFacebookConnector,
   mockGithubConnector,
@@ -17,8 +20,6 @@ import {
   mockPrivacyPolicyUrl,
   mockDemoSocialConnector,
 } from '#src/__mocks__/index.js';
-import { MockTenant } from '#src/test-utils/tenant.js';
-import { createRequester } from '#src/utils/test-utils.js';
 
 const { jest } = import.meta;
 const { mockEsmWithActual } = createMockUtils(jest);
@@ -56,15 +57,9 @@ const mockDeleteConnectorById = jest.fn();
 
 const tenantContext = new MockTenant(
   undefined,
-  {
-    signInExperiences,
-    customPhrases: { findAllCustomLanguageTags: async () => [] },
-    connectors: { deleteConnectorById: mockDeleteConnectorById },
-  },
-  {
-    signInExperiences: { validateLanguageInfo },
-    connectors: { getLogtoConnectors: mockGetLogtoConnectors },
-  }
+  { signInExperiences, customPhrases: { findAllCustomLanguageTags: async () => [] } },
+  { getLogtoConnectors: async () => logtoConnectors },
+  { signInExperiences: { validateLanguageInfo } }
 );
 
 const signInExperiencesRoutes = await pickDefault(import('./index.js'));
diff --git a/packages/core/src/routes/sign-in-experience/index.ts b/packages/core/src/routes/sign-in-experience/index.ts
index f260aeffa..b8c5b027d 100644
--- a/packages/core/src/routes/sign-in-experience/index.ts
+++ b/packages/core/src/routes/sign-in-experience/index.ts
@@ -8,14 +8,14 @@ import koaGuard from '#src/middleware/koa-guard.js';
 import type { AuthedRouter, RouterInitArgs } from '../types.js';
 
 export default function signInExperiencesRoutes<T extends AuthedRouter>(
-  ...[router, { queries, libraries }]: RouterInitArgs<T>
+  ...[router, { queries, libraries, connectors }]: RouterInitArgs<T>
 ) {
   const { findDefaultSignInExperience, updateDefaultSignInExperience } = queries.signInExperiences;
   const { deleteConnectorById } = queries.connectors;
   const {
     signInExperiences: { validateLanguageInfo },
-    connectors: { getLogtoConnectors },
   } = libraries;
+  const { getLogtoConnectors } = connectors;
 
   /**
    * As we only support single signInExperience settings for V1
diff --git a/packages/core/src/routes/verification-code.test.ts b/packages/core/src/routes/verification-code.test.ts
index c890903f7..0e9f4e517 100644
--- a/packages/core/src/routes/verification-code.test.ts
+++ b/packages/core/src/routes/verification-code.test.ts
@@ -23,13 +23,9 @@ const passcodeQueries = await mockEsmWithActual('#src/queries/passcode.js', () =
 const verificationCodeRoutes = await pickDefault(import('./verification-code.js'));
 
 describe('Generic verification code flow triggered by management API', () => {
-  const tenantContext = new MockTenant(
-    undefined,
-    { passcodes: passcodeQueries },
-    {
-      passcodes: passcodeLibraries,
-    }
-  );
+  const tenantContext = new MockTenant(undefined, { passcodes: passcodeQueries }, undefined, {
+    passcodes: passcodeLibraries,
+  });
   const verificationCodeRequest = createRequester({
     authedRoutes: verificationCodeRoutes,
     tenantContext,
diff --git a/packages/core/src/routes/well-known.phrases.content-language.test.ts b/packages/core/src/routes/well-known.phrases.content-language.test.ts
index f09b21f79..2e5282c18 100644
--- a/packages/core/src/routes/well-known.phrases.content-language.test.ts
+++ b/packages/core/src/routes/well-known.phrases.content-language.test.ts
@@ -4,6 +4,7 @@ import { pickDefault } from '@logto/shared/esm';
 
 import { trTrTag, zhCnTag, zhHkTag } from '#src/__mocks__/custom-phrase.js';
 import { mockSignInExperience } from '#src/__mocks__/index.js';
+import { invalidateWellKnownCache } from '#src/caches/well-known.js';
 import { MockTenant } from '#src/test-utils/tenant.js';
 import { createRequester } from '#src/utils/test-utils.js';
 
@@ -29,6 +30,7 @@ const tenantContext = new MockTenant(
     customPhrases: { findAllCustomLanguageTags: async () => [trTrTag, zhCnTag] },
     signInExperiences: { findDefaultSignInExperience },
   },
+  undefined,
   { phrases: { getPhrases: jest.fn().mockResolvedValue(en) } }
 );
 
@@ -39,7 +41,8 @@ const phraseRequest = createRequester({
   tenantContext,
 });
 
-afterEach(() => {
+afterEach(async () => {
+  await invalidateWellKnownCache(tenantContext.id);
   jest.clearAllMocks();
 });
 
diff --git a/packages/core/src/routes/well-known.phrases.test.ts b/packages/core/src/routes/well-known.phrases.test.ts
index eb43a0fc2..98cfa491d 100644
--- a/packages/core/src/routes/well-known.phrases.test.ts
+++ b/packages/core/src/routes/well-known.phrases.test.ts
@@ -4,6 +4,7 @@ import { pickDefault, createMockUtils } from '@logto/shared/esm';
 
 import { zhCnTag } from '#src/__mocks__/custom-phrase.js';
 import { mockSignInExperience } from '#src/__mocks__/index.js';
+import { invalidateWellKnownCache } from '#src/caches/well-known.js';
 import Queries from '#src/tenants/Queries.js';
 import { createMockProvider } from '#src/test-utils/oidc-provider.js';
 import { MockTenant } from '#src/test-utils/tenant.js';
@@ -46,6 +47,7 @@ const getPhrases = jest.fn(async () => zhCN);
 const tenantContext = new MockTenant(
   createMockProvider(),
   { customPhrases, signInExperiences: { findDefaultSignInExperience } },
+  undefined,
   { phrases: { getPhrases } }
 );
 
@@ -57,11 +59,12 @@ const phraseRequest = createRequester({
   tenantContext,
 });
 
-describe('when the application is not admin-console', () => {
-  afterEach(() => {
-    jest.clearAllMocks();
-  });
+afterEach(async () => {
+  await invalidateWellKnownCache(tenantContext.id);
+  jest.clearAllMocks();
+});
 
+describe('when the application is not admin-console', () => {
   it('should call findDefaultSignInExperience', async () => {
     await expect(phraseRequest.get('/.well-known/phrases')).resolves.toHaveProperty('status', 200);
     expect(findDefaultSignInExperience).toBeCalledTimes(1);
@@ -123,4 +126,16 @@ describe('when the application is not admin-console', () => {
     );
     expect(getPhrases).toBeCalledWith('fr', [customizedLanguage]);
   });
+
+  it('should use cache for continuous requests', async () => {
+    const [response1, response2, response3] = await Promise.all([
+      phraseRequest.get('/.well-known/phrases'),
+      phraseRequest.get('/.well-known/phrases'),
+      phraseRequest.get('/.well-known/phrases'),
+    ]);
+    expect(findDefaultSignInExperience).toHaveBeenCalledTimes(1);
+    expect(findAllCustomLanguageTags).toHaveBeenCalledTimes(1);
+    expect(response1.body).toStrictEqual(response2.body);
+    expect(response1.body).toStrictEqual(response3.body);
+  });
 });
diff --git a/packages/core/src/routes/well-known.test.ts b/packages/core/src/routes/well-known.test.ts
index 6cefa6861..1258dd477 100644
--- a/packages/core/src/routes/well-known.test.ts
+++ b/packages/core/src/routes/well-known.test.ts
@@ -10,6 +10,7 @@ import {
   mockWechatConnector,
   mockWechatNativeConnector,
 } from '#src/__mocks__/index.js';
+import { invalidateWellKnownCache } from '#src/caches/well-known.js';
 
 const { jest } = import.meta;
 const { mockEsm } = createMockUtils(jest);
@@ -32,34 +33,36 @@ const { createMockProvider } = await import('#src/test-utils/oidc-provider.js');
 const { MockTenant } = await import('#src/test-utils/tenant.js');
 const { createRequester } = await import('#src/utils/test-utils.js');
 
+const provider = createMockProvider();
+const getLogtoConnectors = jest.fn(async () => {
+  return [
+    mockAliyunDmConnector,
+    mockAliyunSmsConnector,
+    mockFacebookConnector,
+    mockGithubConnector,
+    mockGoogleConnector,
+    mockWechatConnector,
+    mockWechatNativeConnector,
+  ];
+});
+const tenantContext = new MockTenant(
+  provider,
+  {
+    signInExperiences: sieQueries,
+    users: { hasActiveUsers: jest.fn().mockResolvedValue(true) },
+  },
+  { getLogtoConnectors }
+);
+
 describe('GET /.well-known/sign-in-exp', () => {
-  afterEach(() => {
+  afterEach(async () => {
+    await invalidateWellKnownCache(tenantContext.id);
     jest.clearAllMocks();
   });
 
-  const provider = createMockProvider();
   const sessionRequest = createRequester({
     anonymousRoutes: wellKnownRoutes,
-    tenantContext: new MockTenant(
-      provider,
-      {
-        signInExperiences: sieQueries,
-        users: { hasActiveUsers: jest.fn().mockResolvedValue(true) },
-      },
-      {
-        connectors: {
-          getLogtoConnectors: jest.fn(async () => [
-            mockAliyunDmConnector,
-            mockAliyunSmsConnector,
-            mockFacebookConnector,
-            mockGithubConnector,
-            mockGoogleConnector,
-            mockWechatConnector,
-            mockWechatNativeConnector,
-          ]),
-        },
-      }
-    ),
+    tenantContext,
     middlewares: [
       async (ctx, next) => {
         ctx.addLogContext = jest.fn();
@@ -96,4 +99,16 @@ describe('GET /.well-known/sign-in-exp', () => {
       ],
     });
   });
+
+  it('should use cache for continuous requests', async () => {
+    const [response1, response2, response3] = await Promise.all([
+      sessionRequest.get('/.well-known/sign-in-exp'),
+      sessionRequest.get('/.well-known/sign-in-exp'),
+      sessionRequest.get('/.well-known/sign-in-exp'),
+    ]);
+    expect(findDefaultSignInExperience).toHaveBeenCalledTimes(1);
+    expect(getLogtoConnectors).toHaveBeenCalledTimes(1);
+    expect(response1.body).toStrictEqual(response2.body);
+    expect(response2.body).toStrictEqual(response3.body);
+  });
 });
diff --git a/packages/core/src/routes/well-known.ts b/packages/core/src/routes/well-known.ts
index 463355608..baaf12e25 100644
--- a/packages/core/src/routes/well-known.ts
+++ b/packages/core/src/routes/well-known.ts
@@ -1,27 +1,22 @@
-import type { ConnectorMetadata } from '@logto/connector-kit';
-import { ConnectorType } from '@logto/connector-kit';
 import { isBuiltInLanguageTag } from '@logto/phrases-ui';
 import { adminTenantId } from '@logto/schemas';
-import { object, string } from 'zod';
+import { conditionalArray } from '@silverhand/essentials';
+import { z } from 'zod';
 
 import { EnvSet, getTenantEndpoint } from '#src/env-set/index.js';
 import RequestError from '#src/errors/RequestError/index.js';
 import detectLanguage from '#src/i18n/detect-language.js';
+import { guardFullSignInExperience } from '#src/libraries/sign-in-experience/index.js';
 import koaGuard from '#src/middleware/koa-guard.js';
 
 import type { AnonymousRouter, RouterInitArgs } from './types.js';
 
 export default function wellKnownRoutes<T extends AnonymousRouter>(
-  ...[router, { queries, libraries, id }]: RouterInitArgs<T>
+  ...[router, { libraries, id }]: RouterInitArgs<T>
 ) {
   const {
-    customPhrases: { findAllCustomLanguageTags },
-    signInExperiences: { findDefaultSignInExperience },
-  } = queries;
-  const {
-    signInExperiences: { getSignInExperience },
-    connectors: { getLogtoConnectors },
-    phrases: { getPhrases },
+    signInExperiences: { getSignInExperience, getFullSignInExperience },
+    phrases: { getPhrases, getAllCustomLanguageTags },
   } = libraries;
 
   if (id === adminTenantId) {
@@ -38,45 +33,24 @@ export default function wellKnownRoutes<T extends AnonymousRouter>(
     });
   }
 
-  router.get('/.well-known/sign-in-exp', async (ctx, next) => {
-    const [signInExperience, logtoConnectors] = await Promise.all([
-      getSignInExperience(),
-      getLogtoConnectors(),
-    ]);
+  router.get(
+    '/.well-known/sign-in-exp',
+    koaGuard({ response: guardFullSignInExperience, status: 200 }),
+    async (ctx, next) => {
+      ctx.body = await getFullSignInExperience();
 
-    const forgotPassword = {
-      phone: logtoConnectors.some(({ type }) => type === ConnectorType.Sms),
-      email: logtoConnectors.some(({ type }) => type === ConnectorType.Email),
-    };
-
-    const socialConnectors = signInExperience.socialSignInConnectorTargets.reduce<
-      Array<ConnectorMetadata & { id: string }>
-    >((previous, connectorTarget) => {
-      const connectors = logtoConnectors.filter(
-        ({ metadata: { target } }) => target === connectorTarget
-      );
-
-      return [
-        ...previous,
-        ...connectors.map(({ metadata, dbEntry: { id } }) => ({ ...metadata, id })),
-      ];
-    }, []);
-
-    ctx.body = {
-      ...signInExperience,
-      socialConnectors,
-      forgotPassword,
-    };
-
-    return next();
-  });
+      return next();
+    }
+  );
 
   router.get(
     '/.well-known/phrases',
     koaGuard({
-      query: object({
-        lng: string().optional(),
+      query: z.object({
+        lng: z.string().optional(),
       }),
+      response: z.record(z.string().or(z.record(z.unknown()))),
+      status: 200,
     }),
     async (ctx, next) => {
       const {
@@ -85,12 +59,14 @@ export default function wellKnownRoutes<T extends AnonymousRouter>(
 
       const {
         languageInfo: { autoDetect, fallbackLanguage },
-      } = await findDefaultSignInExperience();
+      } = await getSignInExperience();
 
-      const targetLanguage = lng ? [lng] : [];
-      const detectedLanguages = autoDetect ? detectLanguage(ctx) : [];
-      const acceptableLanguages = [...targetLanguage, ...detectedLanguages, fallbackLanguage];
-      const customLanguages = await findAllCustomLanguageTags();
+      const acceptableLanguages = conditionalArray<string | string[]>(
+        lng,
+        autoDetect && detectLanguage(ctx),
+        fallbackLanguage
+      );
+      const customLanguages = await getAllCustomLanguageTags();
       const language =
         acceptableLanguages.find(
           (tag) => isBuiltInLanguageTag(tag) || customLanguages.includes(tag)
diff --git a/packages/core/src/tenants/Libraries.ts b/packages/core/src/tenants/Libraries.ts
index 9ddef88ab..88b1ee828 100644
--- a/packages/core/src/tenants/Libraries.ts
+++ b/packages/core/src/tenants/Libraries.ts
@@ -1,5 +1,5 @@
 import { createApplicationLibrary } from '#src/libraries/application.js';
-import { createConnectorLibrary } from '#src/libraries/connector.js';
+import type { ConnectorLibrary } from '#src/libraries/connector.js';
 import { createHookLibrary } from '#src/libraries/hook.js';
 import { createPasscodeLibrary } from '#src/libraries/passcode.js';
 import { createPhraseLibrary } from '#src/libraries/phrase.js';
@@ -12,10 +12,9 @@ import { createVerificationStatusLibrary } from '#src/libraries/verification-sta
 import type Queries from './Queries.js';
 
 export default class Libraries {
-  connectors = createConnectorLibrary(this.queries);
   users = createUserLibrary(this.queries);
-  signInExperiences = createSignInExperienceLibrary(this.queries, this.connectors);
-  phrases = createPhraseLibrary(this.queries);
+  signInExperiences = createSignInExperienceLibrary(this.queries, this.connectors, this.tenantId);
+  phrases = createPhraseLibrary(this.queries, this.tenantId);
   resources = createResourceLibrary(this.queries);
   hooks = createHookLibrary(this.queries);
   socials = createSocialLibrary(this.queries, this.connectors);
@@ -23,5 +22,10 @@ export default class Libraries {
   applications = createApplicationLibrary(this.queries);
   verificationStatuses = createVerificationStatusLibrary(this.queries);
 
-  constructor(private readonly queries: Queries) {}
+  constructor(
+    public readonly tenantId: string,
+    private readonly queries: Queries,
+    // Explicitly passing connector library to eliminate dependency issue
+    private readonly connectors: ConnectorLibrary
+  ) {}
 }
diff --git a/packages/core/src/tenants/Tenant.ts b/packages/core/src/tenants/Tenant.ts
index ed018c990..87cb7e8ee 100644
--- a/packages/core/src/tenants/Tenant.ts
+++ b/packages/core/src/tenants/Tenant.ts
@@ -8,6 +8,7 @@ import mount from 'koa-mount';
 import type Provider from 'oidc-provider';
 
 import { AdminApps, EnvSet, UserApps } from '#src/env-set/index.js';
+import { createConnectorLibrary } from '#src/libraries/connector.js';
 import koaConnectorErrorHandler from '#src/middleware/koa-connector-error-handler.js';
 import koaConsoleRedirectProxy from '#src/middleware/koa-console-redirect-proxy.js';
 import koaErrorHandler from '#src/middleware/koa-error-handler.js';
@@ -38,15 +39,18 @@ export default class Tenant implements TenantContext {
   #onRequestEmpty?: () => Promise<void>;
 
   public readonly provider: Provider;
-  public readonly queries: Queries;
-  public readonly libraries: Libraries;
   public readonly run: MiddlewareType;
 
   private readonly app: Koa;
 
-  private constructor(public readonly envSet: EnvSet, public readonly id: string) {
-    const queries = new Queries(envSet.pool);
-    const libraries = new Libraries(queries);
+  // eslint-disable-next-line max-params
+  private constructor(
+    public readonly envSet: EnvSet,
+    public readonly id: string,
+    public readonly queries = new Queries(envSet.pool),
+    public readonly connectors = createConnectorLibrary(queries),
+    public readonly libraries = new Libraries(id, queries, connectors)
+  ) {
     const isAdminTenant = id === adminTenantId;
     const mountedApps = [
       ...Object.values(UserApps),
@@ -54,8 +58,6 @@ export default class Tenant implements TenantContext {
     ];
 
     this.envSet = envSet;
-    this.queries = queries;
-    this.libraries = libraries;
 
     // Init app
     const app = new Koa();
@@ -76,6 +78,7 @@ export default class Tenant implements TenantContext {
       id,
       provider,
       queries,
+      connectors,
       libraries,
       envSet,
     };
diff --git a/packages/core/src/tenants/TenantContext.ts b/packages/core/src/tenants/TenantContext.ts
index 35d6c2bc6..e9fc4fab1 100644
--- a/packages/core/src/tenants/TenantContext.ts
+++ b/packages/core/src/tenants/TenantContext.ts
@@ -1,6 +1,7 @@
 import type Provider from 'oidc-provider';
 
 import type { EnvSet } from '#src/env-set/index.js';
+import type { ConnectorLibrary } from '#src/libraries/connector.js';
 
 import type Libraries from './Libraries.js';
 import type Queries from './Queries.js';
@@ -10,5 +11,6 @@ export default abstract class TenantContext {
   public abstract readonly envSet: EnvSet;
   public abstract readonly provider: Provider;
   public abstract readonly queries: Queries;
+  public abstract readonly connectors: ConnectorLibrary;
   public abstract readonly libraries: Libraries;
 }
diff --git a/packages/core/src/test-utils/tenant.ts b/packages/core/src/test-utils/tenant.ts
index af5881fb5..8ad4c144b 100644
--- a/packages/core/src/test-utils/tenant.ts
+++ b/packages/core/src/test-utils/tenant.ts
@@ -1,5 +1,7 @@
 import { createMockPool, createMockQueryResult } from 'slonik';
 
+import type { ConnectorLibrary } from '#src/libraries/connector.js';
+import { createConnectorLibrary } from '#src/libraries/connector.js';
 import Libraries from '#src/tenants/Libraries.js';
 import Queries from '#src/tenants/Queries.js';
 import type TenantContext from '#src/tenants/TenantContext.js';
@@ -46,15 +48,18 @@ export class MockTenant implements TenantContext {
   public id = 'mock_id';
   public envSet = mockEnvSet;
   public queries: Queries;
+  public connectors: ConnectorLibrary;
   public libraries: Libraries;
 
   constructor(
     public provider = createMockProvider(),
     queriesOverride?: Partial2<Queries>,
+    connectorsOverride?: Partial<ConnectorLibrary>,
     librariesOverride?: Partial2<Libraries>
   ) {
     this.queries = new MockQueries(queriesOverride);
-    this.libraries = new Libraries(this.queries);
+    this.connectors = { ...createConnectorLibrary(this.queries), ...connectorsOverride };
+    this.libraries = new Libraries(this.id, this.queries, this.connectors);
     this.setPartial('libraries', librariesOverride);
   }
 
diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json
index ca6540f16..6c61d8e15 100644
--- a/packages/integration-tests/package.json
+++ b/packages/integration-tests/package.json
@@ -28,7 +28,7 @@
     "@logto/schemas": "workspace:*",
     "@peculiar/webcrypto": "^1.3.3",
     "@silverhand/eslint-config": "2.0.1",
-    "@silverhand/essentials": "2.4.1",
+    "@silverhand/essentials": "^2.4.1",
     "@silverhand/ts-config": "2.0.3",
     "@types/expect-puppeteer": "^5.0.3",
     "@types/jest": "^29.4.0",
diff --git a/packages/phrases-ui/package.json b/packages/phrases-ui/package.json
index a49aba92b..576e6f479 100644
--- a/packages/phrases-ui/package.json
+++ b/packages/phrases-ui/package.json
@@ -34,7 +34,7 @@
   },
   "dependencies": {
     "@logto/language-kit": "workspace:*",
-    "@silverhand/essentials": "2.4.1",
+    "@silverhand/essentials": "^2.4.1",
     "zod": "^3.20.2"
   },
   "devDependencies": {
diff --git a/packages/phrases/package.json b/packages/phrases/package.json
index 745dd9dc0..a9e1c4db3 100644
--- a/packages/phrases/package.json
+++ b/packages/phrases/package.json
@@ -34,7 +34,7 @@
   },
   "dependencies": {
     "@logto/language-kit": "workspace:*",
-    "@silverhand/essentials": "2.4.1",
+    "@silverhand/essentials": "^2.4.1",
     "zod": "^3.20.2"
   },
   "devDependencies": {
diff --git a/packages/schemas/package.json b/packages/schemas/package.json
index 3b1180d79..0fd703b03 100644
--- a/packages/schemas/package.json
+++ b/packages/schemas/package.json
@@ -41,7 +41,7 @@
   },
   "devDependencies": {
     "@silverhand/eslint-config": "2.0.1",
-    "@silverhand/essentials": "2.4.1",
+    "@silverhand/essentials": "^2.4.1",
     "@silverhand/ts-config": "2.0.3",
     "@types/inquirer": "^9.0.0",
     "@types/jest": "^29.4.0",
diff --git a/packages/shared/package.json b/packages/shared/package.json
index 10779853a..ccf99c8d9 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -56,7 +56,7 @@
   "dependencies": {
     "@logto/core-kit": "workspace:*",
     "@logto/schemas": "workspace:*",
-    "@silverhand/essentials": "2.4.1",
+    "@silverhand/essentials": "^2.4.1",
     "chalk": "^5.0.0",
     "find-up": "^6.3.0",
     "nanoid": "^4.0.0",
diff --git a/packages/toolkit/connector-kit/package.json b/packages/toolkit/connector-kit/package.json
index acadffa46..c6702cb6b 100644
--- a/packages/toolkit/connector-kit/package.json
+++ b/packages/toolkit/connector-kit/package.json
@@ -33,7 +33,7 @@
   },
   "dependencies": {
     "@logto/language-kit": "workspace:*",
-    "@silverhand/essentials": "2.4.1"
+    "@silverhand/essentials": "^2.4.1"
   },
   "optionalDependencies": {
     "zod": "^3.20.2"
diff --git a/packages/toolkit/connector-kit/src/types.ts b/packages/toolkit/connector-kit/src/types.ts
index ab4f1773d..480c9f5a2 100644
--- a/packages/toolkit/connector-kit/src/types.ts
+++ b/packages/toolkit/connector-kit/src/types.ts
@@ -136,7 +136,7 @@ const connectorConfigFormItemGuard = z.discriminatedUnion('type', [
 
 export type ConnectorConfigFormItem = z.infer<typeof connectorConfigFormItemGuard>;
 
-const connectorMetadataGuard = z.object({
+export const connectorMetadataGuard = z.object({
   id: z.string(),
   target: z.string(),
   platform: z.nativeEnum(ConnectorPlatform).nullable(),
diff --git a/packages/toolkit/core-kit/package.json b/packages/toolkit/core-kit/package.json
index bcdd03e92..881b62525 100644
--- a/packages/toolkit/core-kit/package.json
+++ b/packages/toolkit/core-kit/package.json
@@ -50,7 +50,7 @@
     "@jest/types": "^29.0.3",
     "@silverhand/eslint-config": "2.0.1",
     "@silverhand/eslint-config-react": "2.0.1",
-    "@silverhand/essentials": "2.4.1",
+    "@silverhand/essentials": "^2.4.1",
     "@silverhand/ts-config": "2.0.3",
     "@types/color": "^3.0.3",
     "@types/jest": "^29.4.0",
diff --git a/packages/ui/package.json b/packages/ui/package.json
index c9ebcaf3b..885d2e222 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -33,7 +33,7 @@
     "@react-spring/web": "^9.6.1",
     "@silverhand/eslint-config": "2.0.1",
     "@silverhand/eslint-config-react": "2.0.1",
-    "@silverhand/essentials": "2.4.1",
+    "@silverhand/essentials": "^2.4.1",
     "@silverhand/jest-config": "1.2.2",
     "@silverhand/ts-config": "2.0.3",
     "@silverhand/ts-config-react": "2.0.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 11706ad28..9f57850b2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -32,7 +32,7 @@ importers:
       '@logto/schemas': workspace:*
       '@logto/shared': workspace:*
       '@silverhand/eslint-config': 2.0.1
-      '@silverhand/essentials': 2.4.1
+      '@silverhand/essentials': ^2.4.1
       '@silverhand/ts-config': 2.0.3
       '@types/inquirer': ^9.0.0
       '@types/jest': ^29.4.0
@@ -116,7 +116,7 @@ importers:
       '@logto/schemas': workspace:*
       '@logto/shared': workspace:*
       '@silverhand/eslint-config': 2.0.1
-      '@silverhand/essentials': 2.4.1
+      '@silverhand/essentials': ^2.4.1
       '@silverhand/jest-config': ^2.0.1
       '@silverhand/ts-config': 2.0.3
       '@types/accepts': ^1.3.5
@@ -196,7 +196,7 @@ importers:
       '@parcel/transformer-svg-react': 2.8.3
       '@silverhand/eslint-config': 2.0.1
       '@silverhand/eslint-config-react': 2.0.1
-      '@silverhand/essentials': 2.4.1
+      '@silverhand/essentials': ^2.4.1
       '@silverhand/ts-config': 2.0.3
       '@silverhand/ts-config-react': 2.0.3
       '@tsconfig/docusaurus': ^1.0.5
@@ -346,7 +346,7 @@ importers:
       '@logto/schemas': workspace:*
       '@logto/shared': workspace:*
       '@silverhand/eslint-config': 2.0.1
-      '@silverhand/essentials': 2.4.1
+      '@silverhand/essentials': ^2.4.1
       '@silverhand/ts-config': 2.0.3
       '@types/debug': ^4.1.7
       '@types/etag': ^1.8.1
@@ -385,6 +385,7 @@ importers:
       jest-matcher-specific-error: ^1.0.0
       jose: ^4.11.0
       js-yaml: ^4.1.0
+      keyv: ^4.5.2
       koa: ^2.13.1
       koa-body: ^5.0.0
       koa-compose: ^4.1.0
@@ -401,6 +402,7 @@ importers:
       nodemon: ^2.0.19
       oidc-provider: ^8.0.0
       openapi-types: ^12.0.0
+      p-memoize: ^7.1.1
       p-retry: ^5.1.2
       pg-protocol: ^1.6.0
       prettier: ^2.8.2
@@ -442,6 +444,7 @@ importers:
       iconv-lite: 0.6.3
       jose: 4.11.0
       js-yaml: 4.1.0
+      keyv: 4.5.2
       koa: 2.13.4
       koa-body: 5.0.0
       koa-compose: 4.1.0
@@ -454,6 +457,7 @@ importers:
       lru-cache: 7.14.1
       nanoid: 4.0.0
       oidc-provider: 8.0.0
+      p-memoize: 7.1.1
       p-retry: 5.1.2
       pg-protocol: 1.6.0
       roarr: 7.11.0
@@ -573,7 +577,7 @@ importers:
       '@logto/schemas': workspace:*
       '@peculiar/webcrypto': ^1.3.3
       '@silverhand/eslint-config': 2.0.1
-      '@silverhand/essentials': 2.4.1
+      '@silverhand/essentials': ^2.4.1
       '@silverhand/ts-config': 2.0.3
       '@types/expect-puppeteer': ^5.0.3
       '@types/jest': ^29.4.0
@@ -625,7 +629,7 @@ importers:
     specifiers:
       '@logto/language-kit': workspace:*
       '@silverhand/eslint-config': 2.0.1
-      '@silverhand/essentials': 2.4.1
+      '@silverhand/essentials': ^2.4.1
       '@silverhand/ts-config': 2.0.3
       eslint: ^8.34.0
       lint-staged: ^13.0.0
@@ -648,7 +652,7 @@ importers:
     specifiers:
       '@logto/language-kit': workspace:*
       '@silverhand/eslint-config': 2.0.1
-      '@silverhand/essentials': 2.4.1
+      '@silverhand/essentials': ^2.4.1
       '@silverhand/ts-config': 2.0.3
       buffer: ^5.7.1
       eslint: ^8.34.0
@@ -677,7 +681,7 @@ importers:
       '@logto/phrases': workspace:*
       '@logto/phrases-ui': workspace:*
       '@silverhand/eslint-config': 2.0.1
-      '@silverhand/essentials': 2.4.1
+      '@silverhand/essentials': ^2.4.1
       '@silverhand/ts-config': 2.0.3
       '@types/inquirer': ^9.0.0
       '@types/jest': ^29.4.0
@@ -730,7 +734,7 @@ importers:
       '@logto/core-kit': workspace:*
       '@logto/schemas': workspace:*
       '@silverhand/eslint-config': 2.0.1
-      '@silverhand/essentials': 2.4.1
+      '@silverhand/essentials': ^2.4.1
       '@silverhand/ts-config': 2.0.3
       '@types/jest': ^29.4.0
       '@types/node': ^18.11.18
@@ -767,7 +771,7 @@ importers:
     specifiers:
       '@logto/language-kit': workspace:*
       '@silverhand/eslint-config': 2.0.1
-      '@silverhand/essentials': 2.4.1
+      '@silverhand/essentials': ^2.4.1
       '@silverhand/ts-config': 2.0.3
       '@types/node': ^18.11.18
       eslint: ^8.34.0
@@ -797,7 +801,7 @@ importers:
       '@logto/language-kit': workspace:*
       '@silverhand/eslint-config': 2.0.1
       '@silverhand/eslint-config-react': 2.0.1
-      '@silverhand/essentials': 2.4.1
+      '@silverhand/essentials': ^2.4.1
       '@silverhand/ts-config': 2.0.3
       '@types/color': ^3.0.3
       '@types/jest': ^29.4.0
@@ -886,7 +890,7 @@ importers:
       '@react-spring/web': ^9.6.1
       '@silverhand/eslint-config': 2.0.1
       '@silverhand/eslint-config-react': 2.0.1
-      '@silverhand/essentials': 2.4.1
+      '@silverhand/essentials': ^2.4.1
       '@silverhand/jest-config': 1.2.2
       '@silverhand/ts-config': 2.0.3
       '@silverhand/ts-config-react': 2.0.3
@@ -10638,7 +10642,6 @@ packages:
   /mimic-fn/4.0.0:
     resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
     engines: {node: '>=12'}
-    dev: true
 
   /mimic-response/3.1.0:
     resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
@@ -11233,6 +11236,14 @@ packages:
       aggregate-error: 3.1.0
     dev: true
 
+  /p-memoize/7.1.1:
+    resolution: {integrity: sha512-DZ/bONJILHkQ721hSr/E9wMz5Am/OTJ9P6LhLFo2Tu+jL8044tgc9LwHO8g4PiaYePnlVVRAJcKmgy8J9MVFrA==}
+    engines: {node: '>=14.16'}
+    dependencies:
+      mimic-fn: 4.0.0
+      type-fest: 3.5.2
+    dev: false
+
   /p-retry/5.1.2:
     resolution: {integrity: sha512-couX95waDu98NfNZV+i/iLt+fdVxmI7CbrrdC2uDWfPdUAApyxT4wmDlyOtR5KtTDmkDO0zDScDjDou9YHhd9g==}
     engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}