diff --git a/docs/plugins.md b/docs/plugins.md
new file mode 100644
index 0000000000000000000000000000000000000000..58af6267784516957b43981226f5e22bbd95820e
--- /dev/null
+++ b/docs/plugins.md
@@ -0,0 +1,82 @@
+# Plugin Integration with Minerva
+
+To seamlessly integrate your plugin with Minerva, follow these guidelines to ensure smooth registration, HTML structure creation, and interaction with Minerva.
+
+## Registering plugin with Minerva
+
+Your plugin should utilize the `window.minerva.plugins.registerPlugin` method for plugin registration. When the plugin is initialized, this method should be called inside plugin initialization method. The function `window.minerva.plugins.registerPlugin` takes an object as an argument:
+
+```ts
+{
+  pluginName: string;
+  pluginVersion: string;
+  pluginUrl: string;
+}
+```
+
+##### Usage example:
+
+```javascript
+window.minerva.plugins.registerPlugin({
+  pluginName: 'Your Plugin Name',
+  pluginVersion: '1.8.3',
+  pluginUrl: 'https://example.com/plugins/plugin.js',
+});
+```
+
+## Creating Plugin's HTML Structure
+
+The `window.minerva.plugins.registerPlugin` method returns object with `element` property which is a DOM element, allowing your plugin to append its HTML content to the DOM. Use this element to create and modify the HTML structure of your plugin.
+
+```
+// Plugin registration
+const { element } = window.minerva.plugins.registerPlugin({
+  pluginName: "Your Plugin Name",
+  pluginVersion: "1.0.0",
+  pluginUrl: "your-plugin-url",
+});
+
+// Modify plugin's HTML structure
+const yourContent = document.createElement('div');
+yourContent.textContent = "Your Plugin Content";
+element.appendChild(yourContent);
+```
+
+## Interacting with Minerva
+
+All interactions with Minerva should happen through the `window.minerva` object. This object includes:
+
+- configuration: includes information about available types of elements, reactions, miriam types, configuration options, map types and so on
+- methods will be added in the future
+
+## Example of plugin code before bundling:
+
+```javascript
+require('../css/styles.css');
+const $ = require('jquery');
+
+let pluginContainer;
+
+const createStructure = () => {
+  $(
+    `<div class="flex flex-col items-center p-2.5">
+      <h1 class="text-lg">My plugin ${minerva.configuration.overlayTypes[0].name}</h1>
+      <input class="mt-2.5 p-2.5 rounded-s font-semibold outline-none border border-[#cacaca] bg-[#f7f7f8]" value="https://minerva-dev.lcsb.uni.lu/minerva">
+    </div>`,
+  ).appendTo(pluginContainer);
+};
+
+function initPlugin() {
+  const { element } = window.minerva.plugins.registerPlugin({
+    pluginName: 'perfect-plugin',
+    pluginVersion: '9.9.9',
+    pluginUrl: 'https://example.com/plugins/perfect-plugin.js',
+  });
+
+  pluginContainer = element;
+
+  createStructure();
+}
+
+initPlugin();
+```
diff --git a/index.d.ts b/index.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..248f6f8ecb2d8aa13e21e2859fbfafacc263eba2
--- /dev/null
+++ b/index.d.ts
@@ -0,0 +1,27 @@
+import { MinervaConfiguration } from '@/services/pluginsManager/pluginsManager';
+
+type Plugin = {
+  pluginName: string;
+  pluginVersion: string;
+  pluginUrl: string;
+};
+
+type RegisterPlugin = ({ pluginName, pluginVersion, pluginUrl }: Plugin) => {
+  element: HTMLDivElement;
+};
+
+type HashPlugin = {
+  pluginUrl: string;
+  pluginScript: string;
+};
+
+declare global {
+  interface Window {
+    minerva: {
+      configuration?: MinervaConfiguration;
+      plugins: {
+        registerPlugin: RegisterPlugin;
+      };
+    };
+  }
+}
diff --git a/package-lock.json b/package-lock.json
index 8ac4a5d8aac76245502f5711c51d7396a93512f7..225d517d79a374d63e5b3f97ba7717d503763fde 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",
@@ -44,6 +45,7 @@
         "@testing-library/jest-dom": "^6.1.3",
         "@testing-library/react": "^14.0.0",
         "@testing-library/user-event": "^14.5.2",
+        "@types/crypto-js": "^4.2.2",
         "@types/jest": "^29.5.5",
         "@types/react-redux": "^7.1.26",
         "@types/redux-mock-store": "^1.0.6",
@@ -2244,6 +2246,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",
@@ -4339,6 +4347,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",
@@ -15593,6 +15606,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",
@@ -17119,6 +17138,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 cc4afb15ce32397ce7e992b86c1b14c1fe6fd3d8..0f1373dd7e5774c687cb662401a31c64e6d89de1 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",
     "@testing-library/user-event": "^14.5.2",
     "@types/jest": "^29.5.5",
     "@types/react-redux": "^7.1.26",
diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/AvailablePluginsDrawer.component.tsx b/src/components/Map/Drawer/AvailablePluginsDrawer/AvailablePluginsDrawer.component.tsx
index d1343d91bbf51f36f926c2af248d596790155f04..80af258edc647d357865948e2e52a9dc333dcaba 100644
--- a/src/components/Map/Drawer/AvailablePluginsDrawer/AvailablePluginsDrawer.component.tsx
+++ b/src/components/Map/Drawer/AvailablePluginsDrawer/AvailablePluginsDrawer.component.tsx
@@ -1,20 +1,16 @@
-import { publicPluginsListSelector } from '@/redux/plugins/plugins.selectors';
 import { DrawerHeading } from '@/shared/DrawerHeading';
-import { useSelector } from 'react-redux';
-import { LoadPlugin } from './LoadPlugin';
 import { LoadPluginFromUrl } from './LoadPluginFromUrl';
+import { PublicPlugins } from './PublicPlugins';
+import { PrivateActivePlugins } from './PrivateActivePlugins';
 
 export const AvailablePluginsDrawer = (): JSX.Element => {
-  const publicPlugins = useSelector(publicPluginsListSelector);
-
   return (
     <div className="h-full max-h-full" data-testid="available-plugins-drawer">
       <DrawerHeading title="Available plugins" />
       <div className="flex flex-col gap-6 p-6">
         <LoadPluginFromUrl />
-        {publicPlugins.map(plugin => (
-          <LoadPlugin key={plugin.hash} plugin={plugin} />
-        ))}
+        <PrivateActivePlugins />
+        <PublicPlugins />
       </div>
     </div>
   );
diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.test.tsx b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.test.tsx
index 92c659ac76bde94184d4324be245989f7c8f4dec..ba8ea94c6ced7739c43e902ac3ac60020959b57a 100644
--- a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.test.tsx
+++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.test.tsx
@@ -1,10 +1,36 @@
+/* eslint-disable no-magic-numbers */
 import { FIRST_ARRAY_ELEMENT } from '@/constants/common';
 import { PLUGINS_MOCK } from '@/models/mocks/pluginsMock';
 import { render, screen } from '@testing-library/react';
+import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
+import { InitialStoreState } from '@/utils/testing/getReduxStoreActionsListener';
+import { StoreType } from '@/redux/store';
+import { mockNetworkResponse } from '@/utils/mockNetworkResponse';
+import MockAdapter from 'axios-mock-adapter';
+import axios, { HttpStatusCode } from 'axios';
+import { apiPath } from '@/redux/apiPath';
+import { act } from 'react-dom/test-utils';
+import { PLUGINS_INITIAL_STATE_LIST_MOCK } from '@/redux/plugins/plugins.mock';
 import { LoadPlugin, Props } from './LoadPlugin.component';
 
-const renderComponent = ({ plugin }: Props): void => {
-  render(<LoadPlugin plugin={plugin} />);
+const mockedAxiosApiClient = mockNetworkResponse();
+const mockedAxiosClient = new MockAdapter(axios);
+
+const renderComponent = (
+  { plugin }: Props,
+  initialStore?: InitialStoreState,
+): { store: StoreType } => {
+  const { Wrapper, store } = getReduxWrapperWithStore(initialStore);
+  return (
+    render(
+      <Wrapper>
+        <LoadPlugin plugin={plugin} />
+      </Wrapper>,
+    ),
+    {
+      store,
+    }
+  );
 };
 
 describe('LoadPlugin - component', () => {
@@ -25,5 +51,54 @@ describe('LoadPlugin - component', () => {
       expect(loadButton.tagName).toBe('BUTTON');
       expect(loadButton).toBeInTheDocument();
     });
+    it('should change button label to unload if plugin is active', () => {
+      renderComponent(
+        { plugin },
+        {
+          plugins: {
+            activePlugins: {
+              data: {
+                [plugin.hash]: plugin,
+              },
+              pluginsId: [plugin.hash],
+            },
+            list: PLUGINS_INITIAL_STATE_LIST_MOCK,
+          },
+        },
+      );
+
+      expect(screen.queryByTestId('toggle-plugin')).toHaveTextContent('Unload');
+    });
+    it('should change button label to load if plugin is not active', () => {
+      renderComponent({ plugin });
+      expect(screen.queryByTestId('toggle-plugin')).toHaveTextContent('Load');
+    });
+    it('should unload plugin after click', async () => {
+      mockedAxiosApiClient.onPost(apiPath.registerPluign()).reply(HttpStatusCode.Ok, plugin);
+      mockedAxiosApiClient.onGet(apiPath.getPlugin(plugin.hash)).reply(HttpStatusCode.Ok, plugin);
+      mockedAxiosClient.onGet(plugin.urls[0]).reply(HttpStatusCode.Ok, '');
+      const { store } = renderComponent(
+        { plugin },
+        {
+          plugins: {
+            activePlugins: {
+              data: {
+                [plugin.hash]: plugin,
+              },
+              pluginsId: [plugin.hash],
+            },
+            list: PLUGINS_INITIAL_STATE_LIST_MOCK,
+          },
+        },
+      );
+
+      await act(() => {
+        screen.queryByTestId('toggle-plugin')?.click();
+      });
+
+      const { activePlugins } = store.getState().plugins;
+      expect(activePlugins.pluginsId).toEqual([]);
+      expect(activePlugins.data).toEqual({});
+    });
   });
 });
diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.tsx b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.tsx
index fee403ca18cb34195aaf42a559ba618eb68f821e..f129d79d0797827601fbff4068bec1b27e5fca2e 100644
--- a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.tsx
+++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/LoadPlugin.component.tsx
@@ -1,24 +1,29 @@
+/* eslint-disable no-magic-numbers */
 import { Button } from '@/shared/Button';
 import { MinervaPlugin } from '@/types/models';
+import { useLoadPlugin } from './hooks/useLoadPlugin';
 
 export interface Props {
   plugin: MinervaPlugin;
 }
 
 export const LoadPlugin = ({ plugin }: Props): JSX.Element => {
-  const handleLoadPlugin = (): void => {
-    // TODO: handleLoadPlugin
-  };
+  const { isPluginActive, togglePlugin, isPluginLoading } = useLoadPlugin({
+    hash: plugin.hash,
+    pluginUrl: plugin.urls[0],
+  });
 
   return (
-    <div className="flex w-full items-center justify-between">
+    <div className="flex w-full items-center justify-between text-sm">
       <span className="text-cetacean-blue">{plugin.name}</span>
       <Button
         variantStyles="secondary"
-        className="h-10 self-end rounded-e rounded-s"
-        onClick={handleLoadPlugin}
+        className="h-10 self-end rounded-e rounded-s text-xs font-medium"
+        onClick={togglePlugin}
+        data-testid="toggle-plugin"
+        disabled={isPluginLoading}
       >
-        Load
+        {isPluginActive ? 'Unload' : 'Load'}
       </Button>
     </div>
   );
diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.test.ts b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..04245fbf3a2191bdfc1344bee1c12d75137339c9
--- /dev/null
+++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.test.ts
@@ -0,0 +1,88 @@
+/* eslint-disable no-magic-numbers */
+import { renderHook, waitFor } from '@testing-library/react';
+import { act } from 'react-dom/test-utils';
+import axios, { HttpStatusCode } from 'axios';
+import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener';
+import { INITIAL_STORE_STATE_MOCK } from '@/redux/root/root.fixtures';
+import MockAdapter from 'axios-mock-adapter';
+import { PluginsManager } from '@/services/pluginsManager';
+import { pluginFixture } from '@/models/fixtures/pluginFixture';
+import { useLoadPlugin } from './useLoadPlugin';
+
+const mockedAxiosClient = new MockAdapter(axios);
+jest.mock('../../../../../../services/pluginsManager/pluginsManager');
+
+describe('useLoadPlugin', () => {
+  afterEach(() => {
+    jest.restoreAllMocks();
+  });
+  it('should unload plugin successfully', async () => {
+    const { Wrapper, store } = getReduxStoreWithActionsListener({
+      ...INITIAL_STORE_STATE_MOCK,
+      plugins: {
+        activePlugins: {
+          pluginsId: [pluginFixture.hash],
+          data: {
+            [pluginFixture.hash]: pluginFixture,
+          },
+        },
+        list: INITIAL_STORE_STATE_MOCK.plugins.list,
+      },
+    });
+
+    const {
+      result: {
+        current: { isPluginActive, isPluginLoading, togglePlugin },
+      },
+    } = renderHook(
+      () => useLoadPlugin({ hash: pluginFixture.hash, pluginUrl: pluginFixture.urls[0] }),
+      {
+        wrapper: Wrapper,
+      },
+    );
+
+    expect(isPluginActive).toBe(true);
+    expect(isPluginLoading).toBe(false);
+
+    act(() => {
+      togglePlugin();
+    });
+
+    const actions = store.getActions();
+    expect(actions[0]).toEqual({
+      payload: { pluginId: pluginFixture.hash },
+      type: 'plugins/removePlugin',
+    });
+  });
+  it('should load plugin successfully', async () => {
+    const hash = 'pluginHash';
+    const pluginUrl = 'http://example.com/plugin.js';
+    Math.max = jest.fn();
+    const { Wrapper } = getReduxStoreWithActionsListener(INITIAL_STORE_STATE_MOCK);
+    const pluginScript = `function init() {${Math.max(1, 2)}} init()`;
+
+    mockedAxiosClient.onGet(pluginUrl).reply(HttpStatusCode.Ok, pluginScript);
+
+    const {
+      result: {
+        current: { isPluginActive, isPluginLoading, togglePlugin },
+      },
+    } = renderHook(() => useLoadPlugin({ hash, pluginUrl }), {
+      wrapper: Wrapper,
+    });
+
+    expect(isPluginActive).toBe(false);
+    expect(isPluginLoading).toBe(false);
+
+    togglePlugin();
+
+    expect(Math.max).toHaveBeenCalledWith(1, 2);
+
+    await waitFor(() => {
+      expect(PluginsManager.setHashedPlugin).toHaveBeenCalledWith({
+        pluginScript,
+        pluginUrl,
+      });
+    });
+  });
+});
diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.ts b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d1b04f6549b8eb8b790302a7ff3ee31b45c822f7
--- /dev/null
+++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPlugin/hooks/useLoadPlugin.ts
@@ -0,0 +1,57 @@
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { isPluginActiveSelector, isPluginLoadingSelector } from '@/redux/plugins/plugins.selectors';
+import { removePlugin } from '@/redux/plugins/plugins.slice';
+import { PluginsManager } from '@/services/pluginsManager';
+import axios from 'axios';
+
+type UseLoadPluginReturnType = {
+  togglePlugin: () => void;
+  isPluginActive: boolean;
+  isPluginLoading: boolean;
+};
+
+type UseLoadPluginProps = {
+  hash: string;
+  pluginUrl: string;
+};
+
+export const useLoadPlugin = ({ hash, pluginUrl }: UseLoadPluginProps): UseLoadPluginReturnType => {
+  const isPluginActive = useAppSelector(state => isPluginActiveSelector(state, hash));
+  const isPluginLoading = useAppSelector(state => isPluginLoadingSelector(state, hash));
+
+  const dispatch = useAppDispatch();
+
+  const handleLoadPlugin = async (): Promise<void> => {
+    const response = await axios(pluginUrl);
+    const pluginScript = response.data;
+
+    /* eslint-disable no-new-func */
+    const loadPlugin = new Function(pluginScript);
+
+    PluginsManager.setHashedPlugin({
+      pluginUrl,
+      pluginScript,
+    });
+
+    loadPlugin();
+  };
+
+  const handleUnloadPlugin = (): void => {
+    dispatch(removePlugin({ pluginId: hash }));
+  };
+
+  const togglePlugin = (): void => {
+    if (isPluginActive) {
+      handleUnloadPlugin();
+    } else {
+      handleLoadPlugin();
+    }
+  };
+
+  return {
+    togglePlugin,
+    isPluginActive,
+    isPluginLoading,
+  };
+};
diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.test.tsx b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.test.tsx
index e1d83a8ba9fde4258d36ad1d5df8629ac583ce57..10d387fbba06a482aa6a79be309671efaabfd01c 100644
--- a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.test.tsx
+++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.test.tsx
@@ -1,11 +1,40 @@
-import { render, screen } from '@testing-library/react';
+/* eslint-disable no-magic-numbers */
+import { fireEvent, render, screen } from '@testing-library/react';
+import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
+import { InitialStoreState } from '@/utils/testing/getReduxStoreActionsListener';
+import { StoreType } from '@/redux/store';
+import { mockNetworkResponse } from '@/utils/mockNetworkResponse';
+import MockAdapter from 'axios-mock-adapter';
+import axios, { HttpStatusCode } from 'axios';
+import { apiPath } from '@/redux/apiPath';
+import { pluginFixture } from '@/models/fixtures/pluginFixture';
+import { act } from 'react-dom/test-utils';
+import { PLUGINS_INITIAL_STATE_LIST_MOCK } from '@/redux/plugins/plugins.mock';
 import { LoadPluginFromUrl } from './LoadPluginFromUrl.component';
 
-const renderComponent = (): void => {
-  render(<LoadPluginFromUrl />);
+const mockedAxiosApiClient = mockNetworkResponse();
+const mockedAxiosClient = new MockAdapter(axios);
+
+const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => {
+  const { Wrapper, store } = getReduxWrapperWithStore(initialStore);
+  return (
+    render(
+      <Wrapper>
+        <LoadPluginFromUrl />
+      </Wrapper>,
+    ),
+    {
+      store,
+    }
+  );
 };
 
 describe('LoadPluginFromUrl - component', () => {
+  global.URL.canParse = jest.fn();
+
+  afterEach(() => {
+    jest.restoreAllMocks();
+  });
   describe('when always', () => {
     it('renders plugin input label', () => {
       renderComponent();
@@ -28,5 +57,107 @@ describe('LoadPluginFromUrl - component', () => {
       expect(loadButton.tagName).toBe('BUTTON');
       expect(loadButton).toBeInTheDocument();
     });
+    it('should unload plugin after click', async () => {
+      mockedAxiosApiClient.onPost(apiPath.registerPluign()).reply(HttpStatusCode.Ok, pluginFixture);
+      mockedAxiosApiClient
+        .onGet(apiPath.getPlugin(pluginFixture.hash))
+        .reply(HttpStatusCode.Ok, pluginFixture);
+      mockedAxiosClient.onGet(pluginFixture.urls[0]).reply(HttpStatusCode.Ok, '');
+
+      renderComponent();
+      const input = screen.getByTestId('load-plugin-input-url');
+      expect(input).toBeVisible();
+
+      act(() => {
+        fireEvent.change(input, { target: { value: pluginFixture.urls[0] } });
+      });
+
+      expect(input).toHaveValue(pluginFixture.urls[0]);
+
+      const button = screen.queryByTestId('load-plugin-button');
+      expect(button).toBeVisible();
+
+      act(() => {
+        button?.click();
+      });
+
+      expect(button).toBeDisabled();
+    });
+    it('should not load plugin if it`s loaded already', async () => {
+      global.URL.canParse = jest.fn().mockReturnValue(true);
+
+      const plugin = {
+        ...pluginFixture,
+        urls: ['https://example.com/min.js'],
+      };
+      mockedAxiosClient.onGet(plugin.urls[0]).reply(HttpStatusCode.Ok, '');
+
+      const { store } = renderComponent({
+        plugins: {
+          activePlugins: {
+            pluginsId: [plugin.hash],
+            data: {
+              [plugin.hash]: plugin,
+            },
+          },
+          list: PLUGINS_INITIAL_STATE_LIST_MOCK,
+        },
+      });
+      const input = screen.getByTestId('load-plugin-input-url');
+      expect(input).toBeVisible();
+
+      act(() => {
+        fireEvent.change(input, { target: { value: plugin.urls[0] } });
+      });
+
+      expect(input).toHaveValue(plugin.urls[0]);
+
+      const button = screen.getByTestId('load-plugin-button');
+      expect(button).not.toBeDisabled();
+      await act(() => {
+        button.click();
+      });
+
+      const { activePlugins } = store.getState().plugins;
+
+      expect(activePlugins).toEqual({
+        pluginsId: [plugin.hash],
+        data: {
+          [plugin.hash]: plugin,
+        },
+      });
+
+      expect(input).toHaveValue('');
+    });
+    it('should disable url input if url is empty', async () => {
+      renderComponent();
+      const input = screen.getByTestId('load-plugin-input-url');
+      expect(input).toBeVisible();
+
+      act(() => {
+        fireEvent.change(input, { target: { value: '' } });
+      });
+
+      expect(input).toHaveValue('');
+
+      const button = screen.getByTestId('load-plugin-button');
+      expect(button).toBeDisabled();
+    });
+
+    it('should disable url input if url is not correct', async () => {
+      global.URL.canParse = jest.fn().mockReturnValue(false);
+      renderComponent();
+      const input = screen.getByTestId('load-plugin-input-url');
+      expect(input).toBeVisible();
+
+      act(() => {
+        fireEvent.change(input, { target: { value: 'abcd' } });
+      });
+
+      expect(input).toHaveValue('abcd');
+
+      const button = screen.getByTestId('load-plugin-button');
+      expect(button).toBeDisabled();
+    });
   });
 });
diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.tsx b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.tsx
index de486ad2bb19462bca3674a24f9a72a126d1f75c..f57224d048c4af8fd407c8c38cff19f111e8dccf 100644
--- a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.tsx
+++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/LoadPluginFromUrl.component.tsx
@@ -1,29 +1,28 @@
 import { Button } from '@/shared/Button';
-import { useState } from 'react';
+import { Input } from '@/shared/Input';
+import { useLoadPluginFromUrl } from './hooks/useLoadPluginFromUrl';
 
 export const LoadPluginFromUrl = (): JSX.Element => {
-  const [url, setUrl] = useState<string>('');
-
-  const handleLoadPlugin = (): void => {
-    // TODO: handleLoadPlugin
-  };
+  const { handleChangePluginUrl, handleLoadPlugin, isPending, pluginUrl } = useLoadPluginFromUrl();
 
   return (
     <div className="flex w-full">
       <label className="flex w-full flex-col gap-2 text-sm text-cetacean-blue">
         <span>URL:</span>
-        <input
-          className="h-10 w-full bg-cultured p-3"
+        <Input
+          className="h-10 w-full flex-none bg-cultured p-3"
           type="url"
-          value={url}
-          onChange={(e): void => setUrl(e.target.value)}
+          value={pluginUrl}
+          onChange={handleChangePluginUrl}
           data-testid="load-plugin-input-url"
         />
       </label>
       <Button
         variantStyles="secondary"
-        className="h-10 self-end rounded-e rounded-s"
+        className="h-10 self-end rounded-e rounded-s text-xs font-medium"
         onClick={handleLoadPlugin}
+        disabled={isPending}
+        data-testid="load-plugin-button"
       >
         Load
       </Button>
diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/hooks/useLoadPluginFromUrl.ts b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/hooks/useLoadPluginFromUrl.ts
new file mode 100644
index 0000000000000000000000000000000000000000..123e220ccc3e05c23d54bbcf62e8d9c607acb373
--- /dev/null
+++ b/src/components/Map/Drawer/AvailablePluginsDrawer/LoadPluginFromUrl/hooks/useLoadPluginFromUrl.ts
@@ -0,0 +1,57 @@
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { activePluginsDataSelector } from '@/redux/plugins/plugins.selectors';
+import { PluginsManager } from '@/services/pluginsManager';
+import axios from 'axios';
+import { ChangeEvent, useMemo, useState } from 'react';
+
+type UseLoadPluginReturnType = {
+  handleChangePluginUrl: (event: ChangeEvent<HTMLInputElement>) => void;
+  handleLoadPlugin: () => Promise<void>;
+  isPending: boolean;
+  pluginUrl: string;
+};
+
+export const useLoadPluginFromUrl = (): UseLoadPluginReturnType => {
+  const [pluginUrl, setPluginUrl] = useState('');
+  const [isLoading, setIsLoading] = useState(false);
+  const activePlugins = useAppSelector(activePluginsDataSelector);
+
+  const isPending = useMemo(
+    () => !pluginUrl || isLoading || !URL.canParse(pluginUrl),
+    [pluginUrl, isLoading],
+  );
+
+  const handleLoadPlugin = async (): Promise<void> => {
+    try {
+      setIsLoading(true);
+      const response = await axios(pluginUrl);
+      const pluginScript = response.data;
+
+      /* eslint-disable no-new-func */
+      const loadPlugin = new Function(pluginScript);
+
+      const hash = PluginsManager.setHashedPlugin({
+        pluginUrl,
+        pluginScript,
+      });
+
+      if (!(hash in activePlugins)) {
+        loadPlugin();
+      }
+
+      setPluginUrl('');
+    } finally {
+      setIsLoading(false);
+    }
+  };
+  const handleChangePluginUrl = (event: ChangeEvent<HTMLInputElement>): void => {
+    setPluginUrl(event.target.value);
+  };
+
+  return {
+    handleChangePluginUrl,
+    handleLoadPlugin,
+    isPending,
+    pluginUrl,
+  };
+};
diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/PrivateActivePlugins/PrivateActivePlugins.component.tsx b/src/components/Map/Drawer/AvailablePluginsDrawer/PrivateActivePlugins/PrivateActivePlugins.component.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..557b997413b41d2299194cfe1d08caa92907c45f
--- /dev/null
+++ b/src/components/Map/Drawer/AvailablePluginsDrawer/PrivateActivePlugins/PrivateActivePlugins.component.tsx
@@ -0,0 +1,8 @@
+import { privateActivePluginsSelector } from '@/redux/plugins/plugins.selectors';
+import { useSelector } from 'react-redux';
+import { LoadPlugin } from '../LoadPlugin';
+
+export const PrivateActivePlugins = (): React.ReactNode => {
+  const privateActivePlugins = useSelector(privateActivePluginsSelector);
+  return privateActivePlugins.map(plugin => <LoadPlugin key={plugin.hash} plugin={plugin} />);
+};
diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/PrivateActivePlugins/index.ts b/src/components/Map/Drawer/AvailablePluginsDrawer/PrivateActivePlugins/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4a8bc8b5fb91dbd12bfdff759888d4266870c8f5
--- /dev/null
+++ b/src/components/Map/Drawer/AvailablePluginsDrawer/PrivateActivePlugins/index.ts
@@ -0,0 +1 @@
+export { PrivateActivePlugins } from './PrivateActivePlugins.component';
diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/PublicPlugins/PublicPlugins.component.tsx b/src/components/Map/Drawer/AvailablePluginsDrawer/PublicPlugins/PublicPlugins.component.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..674b88756126ae4460e8b81d5dfd06a1047b86cf
--- /dev/null
+++ b/src/components/Map/Drawer/AvailablePluginsDrawer/PublicPlugins/PublicPlugins.component.tsx
@@ -0,0 +1,9 @@
+import { publicPluginsListSelector } from '@/redux/plugins/plugins.selectors';
+import React from 'react';
+import { useSelector } from 'react-redux';
+import { LoadPlugin } from '../LoadPlugin';
+
+export const PublicPlugins = (): React.ReactNode => {
+  const publicPlugins = useSelector(publicPluginsListSelector);
+  return publicPlugins.map(plugin => <LoadPlugin key={plugin.hash} plugin={plugin} />);
+};
diff --git a/src/components/Map/Drawer/AvailablePluginsDrawer/PublicPlugins/index.ts b/src/components/Map/Drawer/AvailablePluginsDrawer/PublicPlugins/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7c1fd8a7776423954e1da451bdf1ba757e14a1db
--- /dev/null
+++ b/src/components/Map/Drawer/AvailablePluginsDrawer/PublicPlugins/index.ts
@@ -0,0 +1 @@
+export { PublicPlugins } from './PublicPlugins.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/fixtures/pluginFixture.ts b/src/models/fixtures/pluginFixture.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9e1d83f8fe79ef99b8f6326fb1f392387752e884
--- /dev/null
+++ b/src/models/fixtures/pluginFixture.ts
@@ -0,0 +1,8 @@
+import { ZOD_SEED } from '@/constants';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { createFixture } from 'zod-fixture';
+import { pluginSchema } from '../pluginSchema';
+
+export const pluginFixture = createFixture(pluginSchema, {
+  seed: ZOD_SEED,
+});
diff --git a/src/models/pluginSchema.ts b/src/models/pluginSchema.ts
index 0204b6f05d9e797c3cbad124eb063222bcc3e6f3..2cfb0725fc31a652b5db6d5e38996002eb262af9 100644
--- a/src/models/pluginSchema.ts
+++ b/src/models/pluginSchema.ts
@@ -1,3 +1,4 @@
+/* eslint-disable no-magic-numbers */
 import { z } from 'zod';
 
 export const pluginSchema = z.object({
@@ -6,5 +7,5 @@ export const pluginSchema = z.object({
   version: z.string(),
   isPublic: z.boolean(),
   isDefault: z.boolean(),
-  urls: z.array(z.string()),
+  urls: z.array(z.string().min(1)),
 });
diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts
index 0c592bc7c5b8895dde55a87d0b40051491f7c7f6..5193c5e4865e5ad7d21c71896c3e666f27392ed9 100644
--- a/src/redux/apiPath.ts
+++ b/src/redux/apiPath.ts
@@ -63,5 +63,7 @@ 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}/`,
   getAllPlugins: (): string => `/plugins/`,
 };
diff --git a/src/redux/plugins/plugins.constants.ts b/src/redux/plugins/plugins.constants.ts
index 0aa77a3a906e4ecbfaf61cd980008a2a1ed7a397..c3365278b4f0cfc8eaedb8832ad14afbf2d3cad9 100644
--- a/src/redux/plugins/plugins.constants.ts
+++ b/src/redux/plugins/plugins.constants.ts
@@ -6,4 +6,8 @@ export const PLUGINS_INITIAL_STATE: PluginsState = {
     loading: 'idle',
     error: { name: '', message: '' },
   },
+  activePlugins: {
+    data: {},
+    pluginsId: [],
+  },
 };
diff --git a/src/redux/plugins/plugins.mock.ts b/src/redux/plugins/plugins.mock.ts
index 2322bed65b3f54bb96ceb55ec122d73f93650654..9b6b9c8f12621dfa722ed20e45d6aec0c69408ba 100644
--- a/src/redux/plugins/plugins.mock.ts
+++ b/src/redux/plugins/plugins.mock.ts
@@ -1,12 +1,18 @@
 import { DEFAULT_ERROR } from '@/constants/errors';
-import { PluginsList, PluginsState } from './plugins.types';
+import { ActivePlugins, PluginsList, PluginsState } from './plugins.types';
+
+export const PLUGINS_INITIAL_STATE_ACTIVE_PLUGINS_MOCK: ActivePlugins = {
+  data: {},
+  pluginsId: [],
+};
 
 export const PLUGINS_INITIAL_STATE_LIST_MOCK: PluginsList = {
-  data: undefined,
+  data: [],
   loading: 'idle',
   error: DEFAULT_ERROR,
 };
 
 export const PLUGINS_INITIAL_STATE_MOCK: PluginsState = {
   list: PLUGINS_INITIAL_STATE_LIST_MOCK,
+  activePlugins: PLUGINS_INITIAL_STATE_ACTIVE_PLUGINS_MOCK,
 };
diff --git a/src/redux/plugins/plugins.reducers.test.ts b/src/redux/plugins/plugins.reducers.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..edc59097d4d53fe239bace4d11948391b3857847
--- /dev/null
+++ b/src/redux/plugins/plugins.reducers.test.ts
@@ -0,0 +1,98 @@
+/* eslint-disable no-magic-numbers */
+import {
+  ToolkitStoreWithSingleSlice,
+  createStoreInstanceUsingSliceReducer,
+} from '@/utils/createStoreInstanceUsingSliceReducer';
+import { mockNetworkResponse } from '@/utils/mockNetworkResponse';
+import { HttpStatusCode } from 'axios';
+import { pluginFixture } from '@/models/fixtures/pluginFixture';
+import { apiPath } from '../apiPath';
+import { PluginsState } from './plugins.types';
+import pluginsReducer, { removePlugin } from './plugins.slice';
+import { registerPlugin } from './plugins.thunks';
+import { PLUGINS_INITIAL_STATE_MOCK } from './plugins.mock';
+
+const mockedAxiosClient = mockNetworkResponse();
+
+describe('plugins reducer', () => {
+  let store = {} as ToolkitStoreWithSingleSlice<PluginsState>;
+  beforeEach(() => {
+    store = createStoreInstanceUsingSliceReducer('plugins', pluginsReducer);
+  });
+
+  it('should match initial state', () => {
+    const action = { type: 'unknown' };
+
+    expect(pluginsReducer(undefined, action)).toEqual(PLUGINS_INITIAL_STATE_MOCK);
+  });
+  it('should remove overlay from store properly', () => {
+    const { type, payload } = store.dispatch(
+      removePlugin({
+        pluginId: 'hash1',
+      }),
+    );
+
+    expect(type).toBe('plugins/removePlugin');
+    expect(payload).toEqual({ pluginId: 'hash1' });
+  });
+  it('should update store after successful registerPlugin query', async () => {
+    mockedAxiosClient.onPost(apiPath.registerPluign()).reply(HttpStatusCode.Ok, pluginFixture);
+
+    const { type } = await store.dispatch(
+      registerPlugin({
+        hash: pluginFixture.hash,
+        isPublic: pluginFixture.isPublic,
+        pluginName: pluginFixture.name,
+        pluginUrl: pluginFixture.urls[0],
+        pluginVersion: pluginFixture.version,
+      }),
+    );
+
+    expect(type).toBe('plugins/registerPlugin/fulfilled');
+    const { data, pluginsId } = store.getState().plugins.activePlugins;
+
+    expect(data[pluginFixture.hash]).toEqual(pluginFixture);
+    expect(pluginsId).toContain(pluginFixture.hash);
+  });
+
+  it('should update store after failed registerPlugin query', async () => {
+    mockedAxiosClient.onPost(apiPath.registerPluign()).reply(HttpStatusCode.NotFound, undefined);
+
+    const { type, payload } = await store.dispatch(
+      registerPlugin({
+        hash: pluginFixture.hash,
+        isPublic: pluginFixture.isPublic,
+        pluginName: pluginFixture.name,
+        pluginUrl: pluginFixture.urls[0],
+        pluginVersion: pluginFixture.version,
+      }),
+    );
+
+    expect(type).toBe('plugins/registerPlugin/rejected');
+    expect(payload).toEqual(undefined);
+    const { data, pluginsId } = store.getState().plugins.activePlugins;
+
+    expect(data).toEqual({});
+
+    expect(pluginsId).not.toContain(pluginFixture.hash);
+  });
+
+  it('should update store on loading registerPlugin query', async () => {
+    mockedAxiosClient.onPost(apiPath.registerPluign()).reply(HttpStatusCode.NotFound, undefined);
+
+    store.dispatch(
+      registerPlugin({
+        hash: pluginFixture.hash,
+        isPublic: pluginFixture.isPublic,
+        pluginName: pluginFixture.name,
+        pluginUrl: pluginFixture.urls[0],
+        pluginVersion: pluginFixture.version,
+      }),
+    );
+
+    const { data, pluginsId } = store.getState().plugins.activePlugins;
+
+    expect(data).toEqual({});
+    expect(pluginsId).toContain(pluginFixture.hash);
+  });
+});
diff --git a/src/redux/plugins/plugins.reducers.ts b/src/redux/plugins/plugins.reducers.ts
index dd5f9fc6614702add5c78b11ccbf1811ad4bab65..f046459c4c9e88879b6d7b9ee88a755e7e29ddc1 100644
--- a/src/redux/plugins/plugins.reducers.ts
+++ b/src/redux/plugins/plugins.reducers.ts
@@ -1,6 +1,29 @@
-import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
-import { getAllPlugins } from './plugins.thunks';
-import { PluginsState } from './plugins.types';
+import type { ActionReducerMapBuilder } from '@reduxjs/toolkit';
+import type { PluginsState, RemovePluginAction } from './plugins.types';
+import { registerPlugin, getAllPlugins } from './plugins.thunks';
+
+export const removePluginReducer = (state: PluginsState, action: RemovePluginAction): void => {
+  const { pluginId } = action.payload;
+  state.activePlugins.pluginsId = state.activePlugins.pluginsId.filter(id => id !== pluginId);
+  delete state.activePlugins.data[pluginId];
+};
+
+export const registerPluginReducer = (builder: ActionReducerMapBuilder<PluginsState>): void => {
+  builder.addCase(registerPlugin.pending, (state, action) => {
+    const { hash } = action.meta.arg;
+    state.activePlugins.pluginsId.push(hash);
+  });
+  builder.addCase(registerPlugin.fulfilled, (state, action) => {
+    if (action.payload) {
+      const { hash } = action.meta.arg;
+
+      state.activePlugins.data[hash] = action.payload;
+    }
+  });
+  builder.addCase(registerPlugin.rejected, state => {
+    state.activePlugins.pluginsId = [];
+  });
+};
 
 export const getAllPluginsReducer = (builder: ActionReducerMapBuilder<PluginsState>): void => {
   builder.addCase(getAllPlugins.pending, state => {
diff --git a/src/redux/plugins/plugins.selectors.ts b/src/redux/plugins/plugins.selectors.ts
index 5ece3dd2e529230dd8f85c413703675ebd7de90d..1bf37c638ee768446fe26c5229f4e4a51e134f14 100644
--- a/src/redux/plugins/plugins.selectors.ts
+++ b/src/redux/plugins/plugins.selectors.ts
@@ -1,4 +1,5 @@
 import { createSelector } from '@reduxjs/toolkit';
+import { MinervaPlugin } from '@/types/models';
 import { rootSelector } from '../root/root.selectors';
 
 export const pluginsSelector = createSelector(rootSelector, state => state.plugins);
@@ -17,3 +18,50 @@ export const publicPluginsListSelector = createSelector(
     return (pluginsListData || []).filter(plugin => plugin.isPublic);
   },
 );
+
+export const activePluginsSelector = createSelector(pluginsSelector, state => state.activePlugins);
+
+export const activePluginsIdSelector = createSelector(
+  activePluginsSelector,
+  state => state.pluginsId,
+);
+
+export const activePluginsDataSelector = createSelector(
+  activePluginsSelector,
+  plugins => plugins.data,
+);
+
+export const allActivePluginsSelector = createSelector(
+  activePluginsDataSelector,
+  activePluginsIdSelector,
+  (data, pluginsId) => {
+    const result: MinervaPlugin[] = [];
+
+    pluginsId.forEach(pluginId => {
+      const element = data[pluginId];
+      if (element) {
+        result.push(element);
+      }
+    });
+
+    return result;
+  },
+);
+
+export const privateActivePluginsSelector = createSelector(
+  allActivePluginsSelector,
+  activePlugins => {
+    return (activePlugins || []).filter(plugin => !plugin.isPublic);
+  },
+);
+
+export const isPluginActiveSelector = createSelector(
+  [activePluginsIdSelector, (_, activePlugin: string): string => activePlugin],
+  (activePlugins, activePlugin) => activePlugins.includes(activePlugin),
+);
+
+export const isPluginLoadingSelector = createSelector(
+  [activePluginsSelector, (_, activePlugins: string): string => activePlugins],
+  ({ data, pluginsId }, pluginId) =>
+    pluginsId.includes(pluginId) && data[pluginId] && !Object.keys(data[pluginId]).length,
+);
diff --git a/src/redux/plugins/plugins.slice.ts b/src/redux/plugins/plugins.slice.ts
index 8ac4e01130e47b994104373c7cfa0c41ad20b8ac..aeb408420001e991c41e1a060c280880450af4af 100644
--- a/src/redux/plugins/plugins.slice.ts
+++ b/src/redux/plugins/plugins.slice.ts
@@ -1,14 +1,23 @@
 import { createSlice } from '@reduxjs/toolkit';
+import {
+  registerPluginReducer,
+  removePluginReducer,
+  getAllPluginsReducer,
+} from './plugins.reducers';
+
 import { PLUGINS_INITIAL_STATE } from './plugins.constants';
-import { getAllPluginsReducer } from './plugins.reducers';
 
-const pluginsState = createSlice({
+const pluginsSlice = createSlice({
   name: 'plugins',
   initialState: PLUGINS_INITIAL_STATE,
-  reducers: {},
+  reducers: {
+    removePlugin: removePluginReducer,
+  },
   extraReducers: builder => {
+    registerPluginReducer(builder);
     getAllPluginsReducer(builder);
   },
 });
 
-export default pluginsState.reducer;
+export const { removePlugin } = pluginsSlice.actions;
+export default pluginsSlice.reducer;
diff --git a/src/redux/plugins/plugins.thunks.test.ts b/src/redux/plugins/plugins.thunks.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7972222c206923167271607d9c4e844b56d55556
--- /dev/null
+++ b/src/redux/plugins/plugins.thunks.test.ts
@@ -0,0 +1,63 @@
+/* eslint-disable no-magic-numbers */
+import axios, { HttpStatusCode } from 'axios';
+import { mockNetworkResponse } from '@/utils/mockNetworkResponse';
+import MockAdapter from 'axios-mock-adapter';
+import { pluginFixture } from '@/models/fixtures/pluginFixture';
+import {
+  ToolkitStoreWithSingleSlice,
+  createStoreInstanceUsingSliceReducer,
+} from '@/utils/createStoreInstanceUsingSliceReducer';
+import { apiPath } from '../apiPath';
+import { PluginsState } from './plugins.types';
+import pluginsReducer from './plugins.slice';
+import { getInitPlugins } from './plugins.thunks';
+
+const mockedAxiosApiClient = mockNetworkResponse();
+const mockedAxiosClient = new MockAdapter(axios);
+
+describe('plugins - thunks', () => {
+  describe('getInitPlugins', () => {
+    let store = {} as ToolkitStoreWithSingleSlice<PluginsState>;
+    beforeEach(() => {
+      store = createStoreInstanceUsingSliceReducer('plugins', pluginsReducer);
+    });
+    const setHashedPluginMock = jest.fn();
+
+    beforeEach(() => {
+      setHashedPluginMock.mockClear();
+    });
+
+    it('should fetch and load initial plugins', async () => {
+      mockedAxiosApiClient.onPost(apiPath.registerPluign()).reply(HttpStatusCode.Ok, pluginFixture);
+      mockedAxiosApiClient
+        .onGet(apiPath.getPlugin(pluginFixture.hash))
+        .reply(HttpStatusCode.Ok, pluginFixture);
+      mockedAxiosClient.onGet(pluginFixture.urls[0]).reply(HttpStatusCode.Ok, '');
+
+      await store.dispatch(
+        getInitPlugins({
+          pluginsId: [pluginFixture.hash],
+          setHashedPlugin: setHashedPluginMock,
+        }),
+      );
+
+      expect(setHashedPluginMock).toHaveBeenCalledTimes(1);
+    });
+    it('should not load plugin if fetched plugin is not valid', async () => {
+      mockedAxiosApiClient.onPost(apiPath.registerPluign()).reply(HttpStatusCode.NotFound, {});
+      mockedAxiosApiClient
+        .onGet(apiPath.getPlugin(pluginFixture.hash))
+        .reply(HttpStatusCode.NotFound, {});
+      mockedAxiosClient.onGet(pluginFixture.urls[0]).reply(HttpStatusCode.NotFound, '');
+
+      await store.dispatch(
+        getInitPlugins({
+          pluginsId: [pluginFixture.hash],
+          setHashedPlugin: setHashedPluginMock,
+        }),
+      );
+
+      expect(setHashedPluginMock).not.toHaveBeenCalled();
+    });
+  });
+});
diff --git a/src/redux/plugins/plugins.thunks.ts b/src/redux/plugins/plugins.thunks.ts
index 732cfdcb4fcadb1b83a804bb51c8f581d2269f50..73c7341e07cfc2f9b93515487084fe26fade667d 100644
--- a/src/redux/plugins/plugins.thunks.ts
+++ b/src/redux/plugins/plugins.thunks.ts
@@ -1,11 +1,90 @@
+/* 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 { MinervaPlugin } from '@/types/models';
 import { axiosInstance } from '@/services/api/utils/axiosInstance';
-import { MinervaPlugin } from '@/types/models';
-import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
-import { createAsyncThunk } from '@reduxjs/toolkit';
 import { z } from 'zod';
 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<MinervaPlugin | undefined> => {
+    const payload = {
+      hash,
+      url: pluginUrl,
+      name: pluginName,
+      version: pluginVersion,
+      isPublic: isPublic.toString(),
+    } as const;
+
+    const response = await axiosInstance.post<MinervaPlugin>(
+      apiPath.registerPluign(),
+      new URLSearchParams(payload),
+      {
+        withCredentials: true,
+      },
+    );
+
+    const isDataValid = validateDataUsingZodSchema(response.data, pluginSchema);
+
+    if (isDataValid) {
+      return response.data;
+    }
+
+    return undefined;
+  },
+);
+
+type GetInitPluginsProps = {
+  pluginsId: string[];
+  setHashedPlugin: ({
+    pluginUrl,
+    pluginScript,
+  }: {
+    pluginUrl: string;
+    pluginScript: string;
+  }) => void;
+};
+
+export const getInitPlugins = createAsyncThunk<void, GetInitPluginsProps>(
+  'plugins/getInitPlugins',
+  async ({ pluginsId, setHashedPlugin }): Promise<void> => {
+    /* eslint-disable no-restricted-syntax, no-await-in-loop */
+    for (const pluginId of pluginsId) {
+      const res = await axiosInstance<MinervaPlugin>(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;
+        setHashedPlugin({ pluginUrl: urls[0], pluginScript });
+
+        /* eslint-disable no-new-func */
+        const loadPlugin = new Function(pluginScript);
+        loadPlugin();
+      }
+    }
+  },
+);
+
 export const getAllPlugins = createAsyncThunk(
   'plugins/getAllPlugins',
   async (): Promise<MinervaPlugin[]> => {
diff --git a/src/redux/plugins/plugins.types.ts b/src/redux/plugins/plugins.types.ts
index c7d7ae4c3ffdcf7c5477ddcb3bfa0408e7c2be77..2569f12845cc3394a7b8897e9133745a52cb9fa6 100644
--- a/src/redux/plugins/plugins.types.ts
+++ b/src/redux/plugins/plugins.types.ts
@@ -1,8 +1,20 @@
+import { PayloadAction } from '@reduxjs/toolkit';
+
 import { FetchDataState } from '@/types/fetchDataState';
 import { MinervaPlugin } from '@/types/models';
 
+export type RemovePluginPayload = { pluginId: string };
+export type RemovePluginAction = PayloadAction<RemovePluginPayload>;
+
 export type PluginsList = FetchDataState<MinervaPlugin[]>;
+export type ActivePlugins = {
+  pluginsId: string[];
+  data: {
+    [pluginId: string]: MinervaPlugin;
+  };
+};
 
 export type PluginsState = {
   list: PluginsList;
+  activePlugins: ActivePlugins;
 };
diff --git a/src/redux/root/init.thunks.ts b/src/redux/root/init.thunks.ts
index f6e14e6a928ae7e738fc3a267c10cd7592813379..557e87d96f6804aed9e108a81d9530a2cd93ccf2 100644
--- a/src/redux/root/init.thunks.ts
+++ b/src/redux/root/init.thunks.ts
@@ -1,8 +1,9 @@
-import { getDefaultSearchTab } from '@/components/FunctionalArea/TopBar/SearchBar/SearchBar.utils';
 import { PROJECT_ID } from '@/constants';
 import { openSearchDrawerWithSelectedTab } from '@/redux/drawer/drawer.slice';
 import { AppDispatch } from '@/redux/store';
 import { QueryData } from '@/types/query';
+import { getDefaultSearchTab } from '@/components/FunctionalArea/TopBar/SearchBar/SearchBar.utils';
+import { PluginsManager } from '@/services/pluginsManager';
 import { createAsyncThunk } from '@reduxjs/toolkit';
 import { getAllBackgroundsByProjectId } from '../backgrounds/backgrounds.thunks';
 import { getConfiguration, getConfigurationOptions } from '../configuration/configuration.thunks';
@@ -15,7 +16,7 @@ import {
 import { getModels } from '../models/models.thunks';
 import { getInitOverlays } from '../overlayBioEntity/overlayBioEntity.thunk';
 import { getAllPublicOverlaysByProjectId } from '../overlays/overlays.thunks';
-import { getAllPlugins } from '../plugins/plugins.thunks';
+import { getAllPlugins, getInitPlugins } from '../plugins/plugins.thunks';
 import { getProjectById } from '../project/project.thunks';
 import { setPerfectMatch } from '../search/search.slice';
 import { getSearchData } from '../search/search.thunks';
@@ -32,6 +33,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)),
@@ -74,4 +76,13 @@ export const fetchInitialAppData = createAsyncThunk<
   if (queryData.overlaysId) {
     dispatch(getInitOverlays({ overlaysId: queryData.overlaysId }));
   }
+
+  if (queryData.pluginsId) {
+    dispatch(
+      getInitPlugins({
+        pluginsId: queryData.pluginsId,
+        setHashedPlugin: PluginsManager.setHashedPlugin,
+      }),
+    );
+  }
 });
diff --git a/src/redux/root/query.selectors.ts b/src/redux/root/query.selectors.ts
index 3088b0dacb0ae0051fb11a60a658af927aff4667..b862ed429fdb885367f3f87755c47204903b08b7 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.selectors';
 
 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/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.test.ts b/src/services/pluginsManager/pluginsManager.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f9b55d2712e3cb23ec806e3440d373fd06bcd3d3
--- /dev/null
+++ b/src/services/pluginsManager/pluginsManager.test.ts
@@ -0,0 +1,75 @@
+/* eslint-disable no-magic-numbers */
+import { store } from '@/redux/store';
+import { configurationFixture } from '@/models/fixtures/configurationFixture';
+import { configurationMapper } from './pluginsManager.utils';
+import { PluginsManager } from './pluginsManager';
+
+jest.mock('../../redux/store');
+
+describe('PluginsManager', () => {
+  const originalWindow = { ...global.window };
+
+  beforeEach(() => {
+    global.window = { ...originalWindow };
+  });
+
+  afterEach(() => {
+    global.window = originalWindow;
+  });
+
+  afterEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('setHashedPlugin correctly computes hash and updates hashedPlugins', () => {
+    const pluginUrl = 'https://example.com/plugin.js';
+    const pluginScript = 'console.log("Hello, Plugin!");';
+
+    PluginsManager.setHashedPlugin({ pluginUrl, pluginScript });
+
+    expect(PluginsManager.hashedPlugins[pluginUrl]).toBe('edc7eeafccc9e1ab66f713298425947b');
+  });
+
+  it('init subscribes to store changes and updates minerva configuration', () => {
+    (store.getState as jest.Mock).mockReturnValueOnce({
+      configuration: { main: { data: configurationFixture } },
+    });
+
+    PluginsManager.init();
+
+    expect(store.subscribe).toHaveBeenCalled();
+
+    // Simulate store change
+    (store.subscribe as jest.Mock).mock.calls[0][0]();
+
+    expect(store.getState).toHaveBeenCalled();
+    expect(window.minerva.configuration).toEqual(configurationMapper(configurationFixture));
+  });
+  it('init does not update minerva configuration when configuration is undefined', () => {
+    (store.getState as jest.Mock).mockReturnValueOnce({
+      configuration: { main: { data: undefined } },
+    });
+
+    PluginsManager.init();
+
+    expect(store.subscribe).toHaveBeenCalled();
+
+    // Simulate store change
+    (store.subscribe as jest.Mock).mock.calls[0][0]();
+
+    expect(store.getState).toHaveBeenCalled();
+    expect(window.minerva.configuration).toBeUndefined();
+  });
+
+  it('registerPlugin dispatches action and returns element', () => {
+    const pluginName = 'TestPlugin';
+    const pluginVersion = '1.0.0';
+    const pluginUrl = 'https://example.com/test-plugin.js';
+
+    const result = PluginsManager.registerPlugin({ pluginName, pluginVersion, pluginUrl });
+
+    expect(store.dispatch).toHaveBeenCalled();
+
+    expect(result.element).toBeDefined();
+  });
+});
diff --git a/src/services/pluginsManager/pluginsManager.ts b/src/services/pluginsManager/pluginsManager.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a41a8ed5cad9d405bb07069425bf28f79b3112e5
--- /dev/null
+++ b/src/services/pluginsManager/pluginsManager.ts
@@ -0,0 +1,61 @@
+import md5 from 'crypto-js/md5';
+import { store } from '@/redux/store';
+import { registerPlugin } from '@/redux/plugins/plugins.thunks';
+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;
+
+    return hash;
+  },
+  init() {
+    window.minerva = {
+      plugins: {
+        registerPlugin: PluginsManager.registerPlugin,
+      },
+    };
+
+    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,
+      }),
+    );
+
+    // TODO: replace when plugins drawer is implemented
+    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..cedb9034b59df6bb3726b1f57e1d3109814be189
--- /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 }): string;
+  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/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..5a2a58772b7543ac5d79ced941ed15fb5b9e6834 100644
--- a/src/utils/query-manager/useReduxBusQueryManager.ts
+++ b/src/utils/query-manager/useReduxBusQueryManager.ts
@@ -11,10 +11,10 @@ export const useReduxBusQueryManager = (): void => {
 
   const handleChangeQuery = useCallback(
     () =>
+      // eslint-disable-next-line react-hooks/exhaustive-deps
       router.replace(
         {
           query: {
-            ...router.query,
             ...queryData,
           },
         },
@@ -23,7 +23,6 @@ export const useReduxBusQueryManager = (): void => {
           shallow: true,
         },
       ),
-    // router is not an stable reference
     // eslint-disable-next-line react-hooks/exhaustive-deps
     [queryData],
   );