diff --git a/lib/facts/identity-search.js b/lib/facts/identity-search.js
new file mode 100644
--- /dev/null
+++ b/lib/facts/identity-search.js
@@ -0,0 +1,9 @@
+// @flow
+
+import { isDev } from '../utils/dev-utils.js';
+
+const identitySearchURL: string = isDev
+  ? 'wss://identity.staging.commtechnologies.org:51004'
+  : 'wss://identity.commtechnologies.org:51004';
+
+export { identitySearchURL };
diff --git a/lib/identity-search/identity-search-context.js b/lib/identity-search/identity-search-context.js
new file mode 100644
--- /dev/null
+++ b/lib/identity-search/identity-search-context.js
@@ -0,0 +1,186 @@
+// @flow
+
+import invariant from 'invariant';
+import * as React from 'react';
+
+import { identitySearchURL } from '../facts/identity-search.js';
+import { identitySearchHeartbeatTimeout } from '../shared/timeouts.js';
+import type { IdentitySearchAuthMessage } from '../types/identity-search/auth-message-types.js';
+import {
+  type IdentitySearchMessageToClient,
+  identitySearchMessageToClientTypes,
+  identitySearchMessageToServerTypes,
+  identitySearchMessageToClientValidator,
+} from '../types/identity-search/messages.js';
+import type { Heartbeat } from '../types/websocket/heartbeat-types.js';
+import { useGetIdentitySearchAuthMessage } from '../utils/identity-search-utils.js';
+
+export type IdentitySearchSocketListener = (
+  message: IdentitySearchMessageToClient,
+) => mixed;
+
+type IdentitySearchContextType = {
+  +addListener: (listener: IdentitySearchSocketListener) => void,
+  +removeListener: (listener: IdentitySearchSocketListener) => void,
+  +connected: boolean,
+};
+
+const IdentitySearchContext: React.Context<?IdentitySearchContextType> =
+  React.createContext<?IdentitySearchContextType>();
+
+type Props = {
+  +children: React.Node,
+};
+
+function IdentitySearchProvider(props: Props): React.Node {
+  const { children } = props;
+  const [connected, setConnected] = React.useState(false);
+  const listeners = React.useRef<Set<IdentitySearchSocketListener>>(new Set());
+  const getIdentitySearchAuthMessage = useGetIdentitySearchAuthMessage();
+  const [identitySearchAuthMessage, setIdentitySearchAuthMessage] =
+    React.useState<?IdentitySearchAuthMessage>(null);
+  const socket = React.useRef<?WebSocket>(null);
+  const heartbeatTimeoutID = React.useRef<?TimeoutID>();
+
+  const stopHeartbeatTimeout = React.useCallback(() => {
+    if (heartbeatTimeoutID.current) {
+      clearTimeout(heartbeatTimeoutID.current);
+      heartbeatTimeoutID.current = null;
+    }
+  }, []);
+
+  const resetHeartbeatTimeout = React.useCallback(() => {
+    stopHeartbeatTimeout();
+    heartbeatTimeoutID.current = setTimeout(() => {
+      socket.current?.close();
+      setConnected(false);
+    }, identitySearchHeartbeatTimeout);
+  }, [stopHeartbeatTimeout]);
+
+  React.useEffect(() => {
+    void (async () => {
+      const newAuthMessage = await getIdentitySearchAuthMessage();
+      setIdentitySearchAuthMessage(newAuthMessage);
+    })();
+  }, [getIdentitySearchAuthMessage]);
+
+  React.useEffect(() => {
+    if (connected || !identitySearchAuthMessage) {
+      return;
+    }
+
+    const identitySearchSocket = new WebSocket(identitySearchURL);
+
+    identitySearchSocket.onopen = () => {
+      identitySearchSocket.send(JSON.stringify(identitySearchAuthMessage));
+    };
+
+    identitySearchSocket.onclose = () => {
+      setConnected(false);
+    };
+
+    identitySearchSocket.onerror = e => {
+      setConnected(false);
+      console.log('Identity Search socket error', e.message);
+    };
+
+    identitySearchSocket.onmessage = (event: MessageEvent) => {
+      if (typeof event.data !== 'string') {
+        console.log('socket received a non-string message');
+        return;
+      }
+      let rawMessage;
+      try {
+        rawMessage = JSON.parse(event.data);
+      } catch (e) {
+        console.log('error while parsing Identity Search message:', e.message);
+        return;
+      }
+
+      if (!identitySearchMessageToClientValidator.is(rawMessage)) {
+        console.log('invalid Identity Search message');
+        return;
+      }
+
+      const message: IdentitySearchMessageToClient = rawMessage;
+
+      resetHeartbeatTimeout();
+
+      for (const listener of listeners.current) {
+        listener(message);
+      }
+
+      if (
+        message.type ===
+        identitySearchMessageToClientTypes.CONNECTION_INITIALIZATION_RESPONSE
+      ) {
+        if (message.status.type === 'Success' && !connected) {
+          setConnected(true);
+        } else if (message.status.type === 'Success' && connected) {
+          console.log(
+            'received ConnectionInitializationResponse with status:',
+            'Success for already connected socket',
+          );
+        } else {
+          setConnected(false);
+          console.log(
+            'creating session with Identity Search error:',
+            message.status.data,
+          );
+        }
+      } else if (
+        message.type === identitySearchMessageToClientTypes.HEARTBEAT
+      ) {
+        const heartbeat: Heartbeat = {
+          type: identitySearchMessageToServerTypes.HEARTBEAT,
+        };
+        socket.current?.send(JSON.stringify(heartbeat));
+      }
+    };
+
+    socket.current = identitySearchSocket;
+  }, [
+    connected,
+    identitySearchAuthMessage,
+    resetHeartbeatTimeout,
+    stopHeartbeatTimeout,
+  ]);
+
+  const addListener = React.useCallback(
+    (listener: IdentitySearchSocketListener) => {
+      listeners.current.add(listener);
+    },
+    [],
+  );
+
+  const removeListener = React.useCallback(
+    (listener: IdentitySearchSocketListener) => {
+      listeners.current.delete(listener);
+    },
+    [],
+  );
+
+  const value: IdentitySearchContextType = React.useMemo(
+    () => ({
+      connected,
+      addListener,
+      removeListener,
+    }),
+    [connected, addListener, removeListener],
+  );
+
+  return (
+    <IdentitySearchContext.Provider value={value}>
+      {children}
+    </IdentitySearchContext.Provider>
+  );
+}
+
+function useIdentitySearch(): IdentitySearchContextType {
+  const context = React.useContext(IdentitySearchContext);
+  invariant(context, 'IdentitySearchContext not found');
+
+  return context;
+}
+
+export { IdentitySearchProvider, useIdentitySearch };
diff --git a/lib/shared/timeouts.js b/lib/shared/timeouts.js
--- a/lib/shared/timeouts.js
+++ b/lib/shared/timeouts.js
@@ -36,3 +36,7 @@
 // Time after which the client consider the Tunnelbroker connection
 // as unhealthy and chooses to close the socket.
 export const tunnelbrokerHeartbeatTimeout = 9000; // in milliseconds
+
+// Time after which the client consider the Identity Search connection
+// as unhealthy and chooses to close the socket.
+export const identitySearchHeartbeatTimeout = 9000; // in milliseconds
diff --git a/lib/utils/identity-search-utils.js b/lib/utils/identity-search-utils.js
new file mode 100644
--- /dev/null
+++ b/lib/utils/identity-search-utils.js
@@ -0,0 +1,36 @@
+// @flow
+
+import invariant from 'invariant';
+import * as React from 'react';
+
+import { IdentityClientContext } from '../shared/identity-client-context.js';
+import { type IdentitySearchAuthMessage } from '../types/identity-search/auth-message-types.js';
+
+export function useGetIdentitySearchAuthMessage(): () => Promise<?IdentitySearchAuthMessage> {
+  const identityContext = React.useContext(IdentityClientContext);
+  invariant(identityContext, 'Identity context should be set');
+  const getAuthMetadata = identityContext.getAuthMetadata;
+
+  return React.useCallback(async () => {
+    if (!getAuthMetadata) {
+      return null;
+    }
+
+    const authMetadata = await getAuthMetadata();
+
+    if (
+      !authMetadata.userID ||
+      !authMetadata.deviceID ||
+      !authMetadata.accessToken
+    ) {
+      throw new Error('Auth metadata is incomplete');
+    }
+
+    return {
+      type: 'IdentitySearchAuthMessage',
+      userID: authMetadata?.userID,
+      deviceID: authMetadata?.deviceID,
+      accessToken: authMetadata?.accessToken,
+    };
+  }, [getAuthMetadata]);
+}