diff --git a/index.d.ts b/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..38a0345e9a3f3ffd18ea6eb6cb25a0d2baf6eafc
--- /dev/null
+++ b/index.d.ts
@@ -0,0 +1,29 @@
+import { MinervaConfiguration } from '@/services/pluginsManager/pluginsManager';
+
+type Plugin = {
+  pluginName: string;
+  pluginVersion: string;
+  pluginUrl: string;
+};
+
+type RegisterPlugin = ({ pluginName, pluginVersion, pluginUrl }: Plugin) => {
+  element: HTMLDivElement;
+};
+
+declare global {
+  interface Window {
+    minerva: {
+      configuration?: MinervaConfiguration;
+      plugins: {
+        registerPlugin: RegisterPlugin;
+        setHashedPlugin({
+          pluginUrl,
+          pluginScript,
+        }: {
+          pluginUrl: string;
+          pluginScript: string;
+        }): void;
+      };
+    };
+  }
+}
diff --git a/package-lock.json b/package-lock.json
index dabf49dce6380c53f944390a58b1d5a32e3ba10b..ed31a761353363925aaa2032ae636640ad3e54f2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,7 @@
         "autoprefixer": "10.4.15",
         "axios": "^1.5.1",
         "axios-hooks": "^5.0.0",
+        "crypto-js": "^4.2.0",
         "downshift": "^8.2.3",
         "eslint-config-next": "13.4.19",
         "molart": "github:davidhoksza/MolArt",
@@ -43,6 +44,7 @@
         "@commitlint/config-conventional": "^17.7.0",
         "@testing-library/jest-dom": "^6.1.3",
         "@testing-library/react": "^14.0.0",
+        "@types/crypto-js": "^4.2.2",
         "@types/jest": "^29.5.5",
         "@types/react-redux": "^7.1.26",
         "@types/redux-mock-store": "^1.0.6",
@@ -2230,6 +2232,12 @@
         "@babel/types": "^7.20.7"
       }
     },
+    "node_modules/@types/crypto-js": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
+      "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
+      "dev": true
+    },
     "node_modules/@types/downloadjs": {
       "version": "1.4.6",
       "resolved": "https://registry.npmjs.org/@types/downloadjs/-/downloadjs-1.4.6.tgz",
@@ -4325,6 +4333,11 @@
         "node": ">= 8"
       }
     },
+    "node_modules/crypto-js": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+      "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
+    },
     "node_modules/css.escape": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
@@ -15572,6 +15585,12 @@
         "@babel/types": "^7.20.7"
       }
     },
+    "@types/crypto-js": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
+      "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
+      "dev": true
+    },
     "@types/downloadjs": {
       "version": "1.4.6",
       "resolved": "https://registry.npmjs.org/@types/downloadjs/-/downloadjs-1.4.6.tgz",
@@ -17098,6 +17117,11 @@
         "which": "^2.0.1"
       }
     },
+    "crypto-js": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+      "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
+    },
     "css.escape": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
diff --git a/package.json b/package.json
index 4fb3ac58884eefadf31d01b6ad6a9b331d450875..e36be1b84069f4e441b02d9d2be37fd16eb797d9 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
     "autoprefixer": "10.4.15",
     "axios": "^1.5.1",
     "axios-hooks": "^5.0.0",
+    "crypto-js": "^4.2.0",
     "downshift": "^8.2.3",
     "eslint-config-next": "13.4.19",
     "molart": "github:davidhoksza/MolArt",
@@ -57,6 +58,7 @@
     "@commitlint/config-conventional": "^17.7.0",
     "@testing-library/jest-dom": "^6.1.3",
     "@testing-library/react": "^14.0.0",
+    "@types/crypto-js": "^4.2.2",
     "@types/jest": "^29.5.5",
     "@types/react-redux": "^7.1.26",
     "@types/redux-mock-store": "^1.0.6",
diff --git a/pages/_app.tsx b/pages/_app.tsx
index d767f29ca9bff1e622a7e7d4fd8723c6ed9b3050..6aaa491fc0284e752eb320ecb1cf72d376013581 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -7,5 +7,4 @@ const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => (
     <Component {...pageProps} />
   </AppWrapper>
 );
-
 export default MyApp;
diff --git a/src/components/FunctionalArea/MapNavigation/MapNavigation.component.tsx b/src/components/FunctionalArea/MapNavigation/MapNavigation.component.tsx
index 8bb19a16424e70d0c3a0dba4efc6d302308c80d0..9dfc18639e19217d7fc8d00c5c6e190a45aa8c11 100644
--- a/src/components/FunctionalArea/MapNavigation/MapNavigation.component.tsx
+++ b/src/components/FunctionalArea/MapNavigation/MapNavigation.component.tsx
@@ -51,6 +51,9 @@ export const MapNavigation = (): JSX.Element => {
           )}
         </Button>
       ))}
+      <div className="fixed bottom-0 right-0 top-[104px] w-96 bg-white">
+        <div id="plugins" />
+      </div>
     </div>
   );
 };
diff --git a/src/components/Map/Drawer/Drawer.component.tsx b/src/components/Map/Drawer/Drawer.component.tsx
index 098c4f989ec233798af6de0addfbb28fca84b02e..4403fe9b6c719c971445feb36dafcd0d55f60329 100644
--- a/src/components/Map/Drawer/Drawer.component.tsx
+++ b/src/components/Map/Drawer/Drawer.component.tsx
@@ -9,6 +9,7 @@ import { OverlaysDrawer } from './OverlaysDrawer';
 import { BioEntityDrawer } from './BioEntityDrawer/BioEntityDrawer.component';
 import { ExportDrawer } from './ExportDrawer';
 import { ProjectInfoDrawer } from './ProjectInfoDrawer';
+import { PluginsDrawer } from './PluginDrawer';
 
 export const Drawer = (): JSX.Element => {
   const { isOpen, drawerName } = useAppSelector(drawerSelector);
@@ -28,6 +29,7 @@ export const Drawer = (): JSX.Element => {
       {isOpen && drawerName === 'bio-entity' && <BioEntityDrawer />}
       {isOpen && drawerName === 'project-info' && <ProjectInfoDrawer />}
       {isOpen && drawerName === 'export' && <ExportDrawer />}
+      {isOpen && drawerName === 'plugins' && <PluginsDrawer />}
     </div>
   );
 };
diff --git a/src/components/Map/Drawer/PluginDrawer/PluginsDrawer.component.tsx b/src/components/Map/Drawer/PluginDrawer/PluginsDrawer.component.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..bb01c303a137ab0e849e708734c3bbb6fa007cc0
--- /dev/null
+++ b/src/components/Map/Drawer/PluginDrawer/PluginsDrawer.component.tsx
@@ -0,0 +1,84 @@
+import { Button } from '@/shared/Button';
+import { DrawerHeading } from '@/shared/DrawerHeading';
+import { Input } from '@/shared/Input';
+import axios from 'axios';
+import React, { ChangeEvent, useState } from 'react';
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { activePluginsSelector } from '@/redux/plugins/plugins.selector';
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { removePlugin } from '@/redux/plugins/plugins.slice';
+
+export const PluginsDrawer = (): React.ReactNode => {
+  const [pluginUrl, setPluginUrl] = useState('');
+  const [isLoading, setIsLoading] = useState(false);
+  const activePlugins = useAppSelector(activePluginsSelector);
+  const dispatch = useAppDispatch();
+
+  const handleLoadPlugin = async (): Promise<void> => {
+    try {
+      setIsLoading(true);
+      const res = await axios(pluginUrl);
+      const pluginScript = res.data;
+      /* eslint-disable no-new-func */
+      const loadPlugin = new Function(pluginScript);
+      window.minerva.plugins.setHashedPlugin({
+        pluginUrl,
+        pluginScript,
+      });
+      loadPlugin();
+      setPluginUrl('');
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  const handleUnloadPlugin = (pluginId: string): void => {
+    dispatch(removePlugin({ pluginId }));
+  };
+
+  const handleChangePluginUrl = (event: ChangeEvent<HTMLInputElement>): void => {
+    setPluginUrl(event.target.value);
+  };
+
+  return (
+    <div data-testid="available-plugins-drawer" className="h-full max-h-full">
+      <DrawerHeading title="Available plugins" />
+      <div className="h-[calc(100%-93px)] max-h-[calc(100%-93px)] overflow-y-auto p-6">
+        <label>
+          URL:
+          <div className="relative mt-2.5">
+            <Input
+              className="h-10 rounded-r-md pr-[70px]"
+              value={pluginUrl}
+              onChange={handleChangePluginUrl}
+            />
+            <Button
+              className="absolute inset-y-0 right-0 w-[60px] justify-center text-xs font-medium text-primary-500 ring-primary-500 hover:ring-primary-500 disabled:text-primary-500 disabled:ring-primary-500"
+              variantStyles="ghost"
+              onClick={handleLoadPlugin}
+              disabled={isLoading || !pluginUrl}
+            >
+              Load
+            </Button>
+          </div>
+        </label>
+        <ul className="mt-8 flex w-full flex-col gap-y-8">
+          {activePlugins.map(plugin => (
+            <li key={plugin.hash} className="flex w-full items-center justify-between">
+              <span className="text-sm">
+                {plugin.name} ({plugin.version})
+              </span>
+              <Button
+                variantStyles="ghost"
+                onClick={() => handleUnloadPlugin(plugin.hash)}
+                className="h-10 w-[60px] justify-center text-xs font-medium text-primary-500 ring-primary-500 hover:ring-primary-500"
+              >
+                Unload
+              </Button>
+            </li>
+          ))}
+        </ul>
+      </div>
+    </div>
+  );
+};
diff --git a/src/components/Map/Drawer/PluginDrawer/index.ts b/src/components/Map/Drawer/PluginDrawer/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8858fdbe0d2b7c33f6ce041fbf382b887d4e4281
--- /dev/null
+++ b/src/components/Map/Drawer/PluginDrawer/index.ts
@@ -0,0 +1 @@
+export { PluginsDrawer } from './PluginsDrawer.component';
diff --git a/src/components/SPA/MinervaSPA.component.tsx b/src/components/SPA/MinervaSPA.component.tsx
index 5d0d77ae259b644fe129036bd5bdb759fb3b9b52..5762a59b846afa2583c0454ef3fcef59d036fddb 100644
--- a/src/components/SPA/MinervaSPA.component.tsx
+++ b/src/components/SPA/MinervaSPA.component.tsx
@@ -3,6 +3,8 @@ import { Map } from '@/components/Map';
 import { manrope } from '@/constants/font';
 import { useReduxBusQueryManager } from '@/utils/query-manager/useReduxBusQueryManager';
 import { twMerge } from 'tailwind-merge';
+import { useEffect } from 'react';
+import { PluginsManager } from '@/services/pluginsManager';
 import { useInitializeStore } from '../../utils/initialize/useInitializeStore';
 import { Modal } from '../FunctionalArea/Modal';
 import { ContextMenu } from '../FunctionalArea/ContextMenu';
@@ -12,6 +14,12 @@ export const MinervaSPA = (): JSX.Element => {
   useInitializeStore();
   useReduxBusQueryManager();
 
+  useEffect(() => {
+    const unsubscribe = PluginsManager.init();
+
+    return () => unsubscribe();
+  }, []);
+
   return (
     <div className={twMerge('relative', manrope.variable)}>
       <FunctionalArea />
diff --git a/src/models/pluginSchema.ts b/src/models/pluginSchema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0204b6f05d9e797c3cbad124eb063222bcc3e6f3
--- /dev/null
+++ b/src/models/pluginSchema.ts
@@ -0,0 +1,10 @@
+import { z } from 'zod';
+
+export const pluginSchema = z.object({
+  hash: z.string(),
+  name: z.string(),
+  version: z.string(),
+  isPublic: z.boolean(),
+  isDefault: z.boolean(),
+  urls: z.array(z.string()),
+});
diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts
index 212660e5ee66c695f389fd6c49b7168b6ec78ecb..1638e3328327a0791d63f174dea299d029ae0d3a 100644
--- a/src/redux/apiPath.ts
+++ b/src/redux/apiPath.ts
@@ -63,4 +63,6 @@ export const apiPath = {
   getSourceFile: (): string => `/projects/${PROJECT_ID}:downloadSource`,
   getMesh: (meshId: string): string => `mesh/${meshId}`,
   getTaxonomy: (taxonomyId: string): string => `taxonomy/${taxonomyId}`,
+  registerPluign: (): string => `plugins/`,
+  getPlugin: (pluginId: string): string => `plugins/${pluginId}/`,
 };
diff --git a/src/redux/plugins/overlays.mock.ts b/src/redux/plugins/overlays.mock.ts
new file mode 100644
index 0000000000000000000000000000000000000000..63cae1392973ceefc6234e54248670450dbb1137
--- /dev/null
+++ b/src/redux/plugins/overlays.mock.ts
@@ -0,0 +1,6 @@
+import { PluginsState } from './plugins.types';
+
+export const PLUGINS_INITIAL_STATE_MOCK: PluginsState = {
+  data: {},
+  pluginsId: [],
+};
diff --git a/src/redux/plugins/plugins.reducers.ts b/src/redux/plugins/plugins.reducers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6fa5bbf52332bf3b3ff5861261562fbe89d9a6e7
--- /dev/null
+++ b/src/redux/plugins/plugins.reducers.ts
@@ -0,0 +1,23 @@
+import type { ActionReducerMapBuilder } from '@reduxjs/toolkit';
+import type { PluginsState, RemovePluginAction } from './plugins.types';
+import { registerPlugin } from './plugins.thunk';
+
+export const removePluginReducer = (state: PluginsState, action: RemovePluginAction): void => {
+  const { pluginId } = action.payload;
+  state.pluginsId = state.pluginsId.filter(id => id !== pluginId);
+  delete state.data[pluginId];
+};
+
+export const registerPluginReducer = (builder: ActionReducerMapBuilder<PluginsState>): void => {
+  builder.addCase(registerPlugin.pending, (state, action) => {
+    const { hash } = action.meta.arg;
+    state.pluginsId.push(hash);
+  });
+  builder.addCase(registerPlugin.fulfilled, (state, action) => {
+    if (action.payload) {
+      const { hash } = action.meta.arg;
+
+      state.data[hash] = action.payload;
+    }
+  });
+};
diff --git a/src/redux/plugins/plugins.selector.ts b/src/redux/plugins/plugins.selector.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4dc1a677d88ec5010611811e8a031a318e22252f
--- /dev/null
+++ b/src/redux/plugins/plugins.selector.ts
@@ -0,0 +1,26 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { Plugin } from '@/types/models';
+import { rootSelector } from '../root/root.selectors';
+
+export const pluginsSelector = createSelector(rootSelector, state => state.plugins);
+
+export const activePluginsIdSelector = createSelector(pluginsSelector, state => state.pluginsId);
+
+export const pluginsDataSelector = createSelector(pluginsSelector, plugins => plugins.data);
+
+export const activePluginsSelector = createSelector(
+  pluginsDataSelector,
+  activePluginsIdSelector,
+  (data, pluginsId) => {
+    const result: Plugin[] = [];
+
+    pluginsId.forEach(pluginId => {
+      const element = data[pluginId];
+      if (element) {
+        result.push(element);
+      }
+    });
+
+    return result;
+  },
+);
diff --git a/src/redux/plugins/plugins.slice.ts b/src/redux/plugins/plugins.slice.ts
new file mode 100644
index 0000000000000000000000000000000000000000..be87d49638c587e2fafd19bc709daad157c72476
--- /dev/null
+++ b/src/redux/plugins/plugins.slice.ts
@@ -0,0 +1,23 @@
+import { createSlice } from '@reduxjs/toolkit';
+import type { PluginsState } from './plugins.types';
+import { registerPluginReducer, removePluginReducer } from './plugins.reducers';
+
+const initialState: PluginsState = {
+  pluginsId: [],
+  data: {},
+};
+
+export const pluginsSlice = createSlice({
+  name: 'plugins',
+  initialState,
+  reducers: {
+    removePlugin: removePluginReducer,
+  },
+  extraReducers: builder => {
+    registerPluginReducer(builder);
+  },
+});
+
+export const { removePlugin } = pluginsSlice.actions;
+
+export default pluginsSlice.reducer;
diff --git a/src/redux/plugins/plugins.thunk.ts b/src/redux/plugins/plugins.thunk.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1fca4a2f8c2de28b0b381623f043a87373c99173
--- /dev/null
+++ b/src/redux/plugins/plugins.thunk.ts
@@ -0,0 +1,76 @@
+/* eslint-disable no-await-in-loop */
+/* eslint-disable no-magic-numbers */
+import axios from 'axios';
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
+import { pluginSchema } from '@/models/pluginSchema';
+import type { Plugin } from '@/types/models';
+import { axiosInstance } from '@/services/api/utils/axiosInstance';
+import { apiPath } from '../apiPath';
+
+type RegisterPlugin = {
+  hash: string;
+  pluginUrl: string;
+  pluginName: string;
+  pluginVersion: string;
+  isPublic: boolean;
+};
+
+export const registerPlugin = createAsyncThunk(
+  'plugins/registerPlugin',
+  async ({
+    hash,
+    isPublic,
+    pluginName,
+    pluginUrl,
+    pluginVersion,
+  }: RegisterPlugin): Promise<Plugin | undefined> => {
+    const payload = {
+      hash,
+      url: pluginUrl,
+      name: pluginName,
+      version: pluginVersion,
+      isPublic: isPublic.toString(),
+    } as const;
+
+    const response = await axiosInstance.post<Plugin>(
+      apiPath.registerPluign(),
+      new URLSearchParams(payload),
+      {
+        withCredentials: true,
+      },
+    );
+
+    const isDataValid = validateDataUsingZodSchema(response.data, pluginSchema);
+
+    if (isDataValid) {
+      return response.data;
+    }
+
+    return undefined;
+  },
+);
+
+type GetInitPluginsProps = { pluginsId: string[] };
+
+export const getInitPlugins = createAsyncThunk<void, GetInitPluginsProps>(
+  'plugins/getInitPlugins',
+  async ({ pluginsId }): Promise<void> => {
+    /* eslint-disable no-restricted-syntax */
+    for (const pluginId of pluginsId) {
+      const res = await axiosInstance<Plugin>(apiPath.getPlugin(pluginId));
+
+      const isDataValid = validateDataUsingZodSchema(res.data, pluginSchema);
+
+      if (isDataValid) {
+        const { urls } = res.data;
+        const scriptRes = await axios(urls[0]);
+        const pluginScript = scriptRes.data;
+        window.minerva.plugins.setHashedPlugin({ pluginUrl: urls[0], pluginScript });
+        /* eslint-disable no-new-func */
+        const loadPlugin = new Function(pluginScript);
+        loadPlugin();
+      }
+    }
+  },
+);
diff --git a/src/redux/plugins/plugins.types.ts b/src/redux/plugins/plugins.types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..903fe01c45c8d564cf1ebaf3c286c8b159db3e9b
--- /dev/null
+++ b/src/redux/plugins/plugins.types.ts
@@ -0,0 +1,12 @@
+import { PayloadAction } from '@reduxjs/toolkit';
+import { Plugin } from '@/types/models';
+
+export type PluginsState = {
+  pluginsId: string[];
+  data: {
+    [pluginId: string]: Plugin | undefined;
+  };
+};
+
+export type RemovePluginPayload = { pluginId: string };
+export type RemovePluginAction = PayloadAction<RemovePluginPayload>;
diff --git a/src/redux/root/init.thunks.ts b/src/redux/root/init.thunks.ts
index c4ac19274381cdffa49913eff2a4e40ed79960e7..f6adbdc589aa5b1f696631b85fa2e0ac1bbb3889 100644
--- a/src/redux/root/init.thunks.ts
+++ b/src/redux/root/init.thunks.ts
@@ -20,6 +20,7 @@ import { getSessionValid } from '../user/user.thunks';
 import { getInitOverlays } from '../overlayBioEntity/overlayBioEntity.thunk';
 import { getConfiguration, getConfigurationOptions } from '../configuration/configuration.thunks';
 import { getStatisticsById } from '../statistics/statistics.thunks';
+import { getInitPlugins } from '../plugins/plugins.thunk';
 
 interface InitializeAppParams {
   queryData: QueryData;
@@ -31,6 +32,7 @@ export const fetchInitialAppData = createAsyncThunk<
   { dispatch: AppDispatch }
 >('appInit/fetchInitialAppData', async ({ queryData }, { dispatch }): Promise<void> => {
   /** Fetch all data required for rendering map */
+
   await Promise.all([
     dispatch(getConfigurationOptions()),
     dispatch(getProjectById(PROJECT_ID)),
@@ -70,4 +72,12 @@ export const fetchInitialAppData = createAsyncThunk<
   if (queryData.overlaysId) {
     dispatch(getInitOverlays({ overlaysId: queryData.overlaysId }));
   }
+
+  if (queryData.pluginsId) {
+    dispatch(
+      getInitPlugins({
+        pluginsId: queryData.pluginsId,
+      }),
+    );
+  }
 });
diff --git a/src/redux/root/query.selectors.ts b/src/redux/root/query.selectors.ts
index 3088b0dacb0ae0051fb11a60a658af927aff4667..3fbc3a8b939e7ff6aa95db3cf5b007aa3547fcbd 100644
--- a/src/redux/root/query.selectors.ts
+++ b/src/redux/root/query.selectors.ts
@@ -4,22 +4,26 @@ import { ZERO } from '@/constants/common';
 import { mapDataSelector } from '../map/map.selectors';
 import { perfectMatchSelector, searchValueSelector } from '../search/search.selectors';
 import { activeOverlaysIdSelector } from '../overlayBioEntity/overlayBioEntity.selector';
+import { activePluginsIdSelector } from '../plugins/plugins.selector';
 
 export const queryDataParamsSelector = createSelector(
   searchValueSelector,
   perfectMatchSelector,
   mapDataSelector,
   activeOverlaysIdSelector,
+  activePluginsIdSelector,
   (
     searchValue,
     perfectMatch,
     { modelId, backgroundId, position },
     activeOverlaysId,
+    activePluginsId,
   ): QueryDataParams => {
     const joinedSearchValue = searchValue.join(';');
     const shouldIncludeSearchValue = searchValue.length > ZERO && joinedSearchValue;
 
     const shouldIncludeOverlaysId = activeOverlaysId.length > ZERO;
+    const shouldIncludePluginsId = activePluginsId.length > ZERO;
 
     const queryDataParams: QueryDataParams = {
       perfectMatch,
@@ -28,6 +32,7 @@ export const queryDataParamsSelector = createSelector(
       ...position.last,
       ...(shouldIncludeSearchValue ? { searchValue: joinedSearchValue } : {}),
       ...(shouldIncludeOverlaysId ? { overlaysId: activeOverlaysId.join(',') } : {}),
+      ...(shouldIncludePluginsId ? { pluginsId: activePluginsId.join(',') } : {}),
     };
 
     return queryDataParams;
diff --git a/src/redux/root/root.fixtures.ts b/src/redux/root/root.fixtures.ts
index c236df284e59fbdba9e80a306213f18410e9cc67..e7b83da91015af644a20e7d39c949ec5580a5049 100644
--- a/src/redux/root/root.fixtures.ts
+++ b/src/redux/root/root.fixtures.ts
@@ -20,6 +20,7 @@ import { USER_INITIAL_STATE_MOCK } from '../user/user.mock';
 import { STATISTICS_STATE_INITIAL_MOCK } from '../statistics/statistics.mock';
 import { COMPARTMENT_PATHWAYS_INITIAL_STATE_MOCK } from '../compartmentPathways/compartmentPathways.mock';
 import { EXPORT_INITIAL_STATE_MOCK } from '../export/export.mock';
+import { PLUGINS_INITIAL_STATE_MOCK } from '../plugins/overlays.mock';
 
 export const INITIAL_STORE_STATE_MOCK: RootState = {
   search: SEARCH_STATE_INITIAL_MOCK,
@@ -43,4 +44,5 @@ export const INITIAL_STORE_STATE_MOCK: RootState = {
   statistics: STATISTICS_STATE_INITIAL_MOCK,
   compartmentPathways: COMPARTMENT_PATHWAYS_INITIAL_STATE_MOCK,
   export: EXPORT_INITIAL_STATE_MOCK,
+  plugins: PLUGINS_INITIAL_STATE_MOCK,
 };
diff --git a/src/redux/store.ts b/src/redux/store.ts
index a945a7518a65a15740f4922027fc4eb864b3d1cd..a515461172f3468e39668a4b5606a4df24528938 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -27,6 +27,7 @@ import { mapListenerMiddleware } from './map/middleware/map.middleware';
 import statisticsReducer from './statistics/statistics.slice';
 import compartmentPathwaysReducer from './compartmentPathways/compartmentPathways.slice';
 import exportReducer from './export/export.slice';
+import pluginsReducer from './plugins/plugins.slice';
 
 export const reducers = {
   search: searchReducer,
@@ -50,6 +51,7 @@ export const reducers = {
   statistics: statisticsReducer,
   compartmentPathways: compartmentPathwaysReducer,
   export: exportReducer,
+  plugins: pluginsReducer,
 };
 
 export const middlewares = [mapListenerMiddleware.middleware];
diff --git a/src/services/pluginsManager/index.ts b/src/services/pluginsManager/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c77cb1f59759be88679e29c12d76613f1f61be64
--- /dev/null
+++ b/src/services/pluginsManager/index.ts
@@ -0,0 +1 @@
+export { PluginsManager } from './pluginsManager';
diff --git a/src/services/pluginsManager/pluginsManager.ts b/src/services/pluginsManager/pluginsManager.ts
new file mode 100644
index 0000000000000000000000000000000000000000..620354deb5292401b119aef8d203249f61f9fde4
--- /dev/null
+++ b/src/services/pluginsManager/pluginsManager.ts
@@ -0,0 +1,59 @@
+import md5 from 'crypto-js/md5';
+import { store } from '@/redux/store';
+import { registerPlugin } from '@/redux/plugins/plugins.thunk';
+import { configurationMapper } from './pluginsManager.utils';
+import type { PluginsManagerType } from './pluginsManager.types';
+
+export const PluginsManager: PluginsManagerType = {
+  hashedPlugins: {},
+  setHashedPlugin({ pluginUrl, pluginScript }) {
+    const hash = md5(pluginScript).toString();
+
+    PluginsManager.hashedPlugins[pluginUrl] = hash;
+  },
+  init() {
+    window.minerva = {
+      plugins: {
+        registerPlugin: PluginsManager.registerPlugin,
+        setHashedPlugin: PluginsManager.setHashedPlugin,
+      },
+    };
+
+    const unsubscribe = store.subscribe(() => {
+      const configurationStore = store.getState().configuration.main.data;
+
+      if (configurationStore) {
+        const configuration = configurationMapper(configurationStore);
+
+        window.minerva = {
+          ...window.minerva,
+          configuration,
+        };
+      }
+    });
+
+    return unsubscribe;
+  },
+
+  registerPlugin({ pluginName, pluginVersion, pluginUrl }) {
+    const hash = PluginsManager.hashedPlugins[pluginUrl];
+
+    store.dispatch(
+      registerPlugin({
+        hash,
+        isPublic: false,
+        pluginName,
+        pluginUrl,
+        pluginVersion,
+      }),
+    );
+
+    const element = document.createElement('div');
+    const wrapper = document.querySelector('#plugins');
+    wrapper?.append(element);
+
+    return {
+      element,
+    };
+  },
+};
diff --git a/src/services/pluginsManager/pluginsManager.types.ts b/src/services/pluginsManager/pluginsManager.types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..00e247a7e7f3483875edd710a157df2ee740928d
--- /dev/null
+++ b/src/services/pluginsManager/pluginsManager.types.ts
@@ -0,0 +1,21 @@
+import { Unsubscribe } from '@reduxjs/toolkit';
+import { configurationMapper } from './pluginsManager.utils';
+
+export type RegisterPlugin = {
+  pluginName: string;
+  pluginVersion: string;
+  pluginUrl: string;
+};
+
+export type MinervaConfiguration = ReturnType<typeof configurationMapper>;
+
+export type PluginsManagerType = {
+  hashedPlugins: {
+    [url: string]: string;
+  };
+  setHashedPlugin({ pluginUrl, pluginScript }: { pluginUrl: string; pluginScript: string }): void;
+  init(): Unsubscribe;
+  registerPlugin({ pluginName, pluginVersion, pluginUrl }: RegisterPlugin): {
+    element: HTMLDivElement;
+  };
+};
diff --git a/src/services/pluginsManager/pluginsManager.utils.ts b/src/services/pluginsManager/pluginsManager.utils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dd7158a5d30ae742bbde630778dbcbb66e94a072
--- /dev/null
+++ b/src/services/pluginsManager/pluginsManager.utils.ts
@@ -0,0 +1,14 @@
+import { Configuration } from '@/types/models';
+
+export const configurationMapper = (data: Configuration): unknown => ({
+  annotators: data.annotators,
+  elementTypes: data.elementTypes,
+  miramiTypes: data.miriamTypes,
+  mapTypes: data.mapTypes,
+  modelConverters: data.modelFormats,
+  modificationStateTypes: data.modificationStateTypes,
+  options: data.options,
+  overlayTypes: data.overlayTypes,
+  privilegeTypes: data.privilegeTypes,
+  reactionTypes: data.reactionTypes,
+});
diff --git a/src/types/models.ts b/src/types/models.ts
index e48801f3cd773468ab419c4d233424833c4b3126..d46b347f7d6e3fa6c0f12b87a9949d3f94d82271 100644
--- a/src/types/models.ts
+++ b/src/types/models.ts
@@ -30,6 +30,7 @@ import {
   overviewImageLinkModel,
 } from '@/models/overviewImageLink';
 import { overviewImageView } from '@/models/overviewImageView';
+import { pluginSchema } from '@/models/pluginSchema';
 import { projectSchema } from '@/models/projectSchema';
 import { reactionSchema } from '@/models/reaction';
 import { reactionLineSchema } from '@/models/reactionLineSchema';
@@ -76,3 +77,4 @@ export type CompartmentPathway = z.infer<typeof compartmentPathwaySchema>;
 export type CompartmentPathwayDetails = z.infer<typeof compartmentPathwayDetailsSchema>;
 export type ExportNetwork = z.infer<typeof exportNetworkchema>;
 export type ExportElements = z.infer<typeof exportElementsSchema>;
+export type Plugin = z.infer<typeof pluginSchema>;
diff --git a/src/types/query.ts b/src/types/query.ts
index 98309123aeea5a80626fca86870beb56c6561ec3..be3453f011b515a134cf5aff62e1549ae31553c1 100644
--- a/src/types/query.ts
+++ b/src/types/query.ts
@@ -7,6 +7,7 @@ export interface QueryData {
   backgroundId?: number;
   initialPosition?: Partial<Point>;
   overlaysId?: number[];
+  pluginsId?: string[];
 }
 
 export interface QueryDataParams {
@@ -18,6 +19,7 @@ export interface QueryDataParams {
   y?: number;
   z?: number;
   overlaysId?: string;
+  pluginsId?: string;
 }
 
 export interface QueryDataRouterParams {
@@ -29,4 +31,5 @@ export interface QueryDataRouterParams {
   y?: string;
   z?: string;
   overlaysId?: string;
+  pluginsId?: string;
 }
diff --git a/src/utils/initialize/useInitializeStore.ts b/src/utils/initialize/useInitializeStore.ts
index 9722dd6173ea1fe82164f422f0b7d6dc106a22de..c648112775ac249fb74cc4ab3b439f803430297f 100644
--- a/src/utils/initialize/useInitializeStore.ts
+++ b/src/utils/initialize/useInitializeStore.ts
@@ -25,6 +25,7 @@ export const useInitializeStore = (): void => {
     if (isInitialized || !isQueryReady) {
       return;
     }
+
     dispatch(fetchInitialAppData({ queryData: parseQueryToTypes(query) }));
   }, [dispatch, isInitialized, query, isQueryReady, isInitDataLoadingFinished]);
 };
diff --git a/src/utils/parseQueryToTypes.ts b/src/utils/parseQueryToTypes.ts
index ee7440375a4834e13cf217b9af5fd714889ec56b..f04abadfedebada9e8058ffa3d4eae08b9ffc731 100644
--- a/src/utils/parseQueryToTypes.ts
+++ b/src/utils/parseQueryToTypes.ts
@@ -11,4 +11,5 @@ export const parseQueryToTypes = (query: QueryDataRouterParams): QueryData => ({
     z: Number(query.z) || undefined,
   },
   overlaysId: query.overlaysId?.split(',').map(Number),
+  pluginsId: query.pluginsId?.split(',').map(String),
 });
diff --git a/src/utils/query-manager/useReduxBusQueryManager.ts b/src/utils/query-manager/useReduxBusQueryManager.ts
index 80d277dd03a6954af2085dbc98fe7c75cf169663..b25ab80d963ee3b8e6fa12ba1d4bc3c9014f041b 100644
--- a/src/utils/query-manager/useReduxBusQueryManager.ts
+++ b/src/utils/query-manager/useReduxBusQueryManager.ts
@@ -10,11 +10,10 @@ export const useReduxBusQueryManager = (): void => {
   const isDataLoaded = useSelector(initDataAndMapLoadingFinished);
 
   const handleChangeQuery = useCallback(
-    () =>
-      router.replace(
+    () => {
+      return router.replace(
         {
           query: {
-            ...router.query,
             ...queryData,
           },
         },
@@ -22,7 +21,8 @@ export const useReduxBusQueryManager = (): void => {
         {
           shallow: true,
         },
-      ),
+      );
+    },
     // router is not an stable reference
     // eslint-disable-next-line react-hooks/exhaustive-deps
     [queryData],