diff --git a/keyserver/src/responders/website-responders.js b/keyserver/src/responders/website-responders.js
--- a/keyserver/src/responders/website-responders.js
+++ b/keyserver/src/responders/website-responders.js
@@ -45,6 +45,7 @@
   +commQueryExecutorFilename: string,
   +opaqueURL: string,
   +backupClientFilename: string,
+  +webworkersOpaqueFilename: string,
 };
 let assetInfo: ?AssetInfo = null;
 async function getAssetInfo() {
@@ -61,6 +62,7 @@
       commQueryExecutorFilename: '',
       opaqueURL: 'http://localhost:8080/opaque-ke.wasm',
       backupClientFilename: '',
+      webworkersOpaqueFilename: '',
     };
     return assetInfo;
   }
@@ -86,6 +88,7 @@
       commQueryExecutorFilename: webworkersManifest['comm_query_executor.wasm'],
       opaqueURL: `compiled/${manifest['comm_opaque2_wasm_bg.wasm']}`,
       backupClientFilename: webworkersManifest['backup-client-wasm_bg.wasm'],
+      webworkersOpaqueFilename: webworkersManifest['comm_opaque2_wasm_bg.wasm'],
     };
     return assetInfo;
   } catch {
@@ -138,6 +141,7 @@
     opaqueURL,
     commQueryExecutorFilename,
     backupClientFilename,
+    webworkersOpaqueFilename,
   } = await assetInfoPromise;
 
   // prettier-ignore
@@ -190,6 +194,7 @@
           var olmFilename = "${olmFilename}";
           var commQueryExecutorFilename = "${commQueryExecutorFilename}";
           var backupClientFilename = "${backupClientFilename}";
+          var webworkersOpaqueFilename = "${webworkersOpaqueFilename}"
           var opaqueURL = "${opaqueURL}";
         </script>
         <script src="${jsURL}"></script>
diff --git a/web/shared-worker/utils/constants.js b/web/shared-worker/utils/constants.js
--- a/web/shared-worker/utils/constants.js
+++ b/web/shared-worker/utils/constants.js
@@ -15,6 +15,8 @@
 
 export const DEFAULT_OLM_FILENAME = 'olm.wasm';
 
+export const DEFAULT_WEBWORKERS_OPAQUE_FILENAME = 'comm_opaque2_wasm_bg.wasm';
+
 export const COMM_SQLITE_DATABASE_PATH = 'comm.sqlite';
 export const COMM_SQLITE_BACKUP_RESTORE_DATABASE_PATH =
   'comm_backup_restore.sqlite';
@@ -55,3 +57,13 @@
   const olmWasmFilename = olmFilename ? olmFilename : DEFAULT_OLM_FILENAME;
   return `${olmWasmDirPath}/${olmWasmFilename}`;
 }
+
+declare var webworkersOpaqueFilename: string;
+export function getOpaqueWasmPath(): string {
+  const origin = window.location.origin;
+  const opaqueWasmDirPath = `${origin}${baseURL}${WORKERS_MODULES_DIR_PATH}`;
+  const opaqueWasmFilename = webworkersOpaqueFilename
+    ? webworkersOpaqueFilename
+    : DEFAULT_WEBWORKERS_OPAQUE_FILENAME;
+  return `${opaqueWasmDirPath}/${opaqueWasmFilename}`;
+}
diff --git a/web/shared-worker/worker/identity-client.js b/web/shared-worker/worker/identity-client.js
new file mode 100644
--- /dev/null
+++ b/web/shared-worker/worker/identity-client.js
@@ -0,0 +1,36 @@
+// @flow
+
+import { getDeviceKeyUpload } from './worker-crypto.js';
+import { IdentityServiceClientWrapper } from '../../grpc/identity-service-client-wrapper.js';
+import { workerRequestMessageTypes } from '../../types/worker-types.js';
+import type {
+  WorkerResponseMessage,
+  WorkerRequestMessage,
+} from '../../types/worker-types.js';
+import type { EmscriptenModule } from '../types/module.js';
+import type { SQLiteQueryExecutor } from '../types/sqlite-query-executor.js';
+
+let identityClient: ?IdentityServiceClientWrapper = null;
+
+async function processAppIdentityClientRequest(
+  sqliteQueryExecutor: SQLiteQueryExecutor,
+  dbModule: EmscriptenModule,
+  message: WorkerRequestMessage,
+): Promise<?WorkerResponseMessage> {
+  if (
+    message.type === workerRequestMessageTypes.CREATE_IDENTITY_SERVICE_CLIENT
+  ) {
+    identityClient = new IdentityServiceClientWrapper(
+      message.platformDetails,
+      message.opaqueWasmPath,
+      message.authLayer,
+      async () => getDeviceKeyUpload(),
+    );
+  }
+}
+
+function getIdentityClient(): ?IdentityServiceClientWrapper {
+  return identityClient;
+}
+
+export { processAppIdentityClientRequest, getIdentityClient };
diff --git a/web/shared-worker/worker/shared-worker.js b/web/shared-worker/worker/shared-worker.js
--- a/web/shared-worker/worker/shared-worker.js
+++ b/web/shared-worker/worker/shared-worker.js
@@ -3,6 +3,7 @@
 import localforage from 'localforage';
 
 import { restoreBackup } from './backup.js';
+import { processAppIdentityClientRequest } from './identity-client.js';
 import {
   getClientStoreFromQueryExecutor,
   processDBStoreOperations,
@@ -32,6 +33,7 @@
   workerWriteRequests,
   workerOlmAPIRequests,
 } from '../../types/worker-types.js';
+import { workerIdentityClientRequests } from '../../types/worker-types.js';
 import { getDatabaseModule } from '../db-module.js';
 import {
   COMM_SQLITE_DATABASE_PATH,
@@ -233,7 +235,14 @@
 
   // write operations
   const isOlmAPIRequest = workerOlmAPIRequests.includes(message.type);
-  if (!workerWriteRequests.includes(message.type) && !isOlmAPIRequest) {
+  const isIdentityClientRequest = workerIdentityClientRequests.includes(
+    message.type,
+  );
+  if (
+    !workerWriteRequests.includes(message.type) &&
+    !isOlmAPIRequest &&
+    !isIdentityClientRequest
+  ) {
     throw new Error(`Request type ${message.type} not supported`);
   }
   if (!sqliteQueryExecutor || !dbModule) {
@@ -242,8 +251,15 @@
     );
   }
 
+  let result;
   if (isOlmAPIRequest) {
     await processAppOlmApiRequest(message);
+  } else if (isIdentityClientRequest) {
+    result = await processAppIdentityClientRequest(
+      sqliteQueryExecutor,
+      dbModule,
+      message,
+    );
   } else if (
     message.type === workerRequestMessageTypes.PROCESS_STORE_OPERATIONS
   ) {
@@ -278,7 +294,7 @@
     void persist();
   }
 
-  return undefined;
+  return result;
 }
 
 function connectHandler(event: SharedWorkerMessageEvent) {
diff --git a/web/shared-worker/worker/worker-crypto.js b/web/shared-worker/worker/worker-crypto.js
--- a/web/shared-worker/worker/worker-crypto.js
+++ b/web/shared-worker/worker/worker-crypto.js
@@ -3,7 +3,14 @@
 import olm from '@commapp/olm';
 import uuid from 'uuid';
 
-import type { CryptoStore, PickledOLMAccount } from 'lib/types/crypto-types.js';
+import type {
+  CryptoStore,
+  PickledOLMAccount,
+  IdentityKeysBlob,
+  SignedIdentityKeysBlob,
+} from 'lib/types/crypto-types.js';
+import type { IdentityDeviceKeyUpload } from 'lib/types/identity-service-types.js';
+import { retrieveAccountKeysSet } from 'lib/utils/olm-utils.js';
 
 import { getProcessingStoreOpsExceptionMessage } from './process-operations.js';
 import { getDBModule, getSQLiteQueryExecutor } from './worker-database.js';
@@ -166,4 +173,58 @@
   }
 }
 
-export { clearCryptoStore, processAppOlmApiRequest };
+function getSignedIdentityKeysBlob(): SignedIdentityKeysBlob {
+  if (!cryptoStore) {
+    throw new Error('Crypto account not initialized');
+  }
+
+  const { contentAccount, notificationAccount } = cryptoStore;
+
+  const identityKeysBlob: IdentityKeysBlob = {
+    notificationIdentityPublicKeys: JSON.parse(
+      notificationAccount.identity_keys(),
+    ),
+    primaryIdentityPublicKeys: JSON.parse(contentAccount.identity_keys()),
+  };
+
+  const payloadToBeSigned: string = JSON.stringify(identityKeysBlob);
+  const signedIdentityKeysBlob: SignedIdentityKeysBlob = {
+    payload: payloadToBeSigned,
+    signature: contentAccount.sign(payloadToBeSigned),
+  };
+
+  return signedIdentityKeysBlob;
+}
+
+function getDeviceKeyUpload(): IdentityDeviceKeyUpload {
+  if (!cryptoStore) {
+    throw new Error('Crypto account not initialized');
+  }
+  const { contentAccount, notificationAccount } = cryptoStore;
+
+  const signedIdentityKeysBlob = getSignedIdentityKeysBlob();
+
+  const primaryAccountKeysSet = retrieveAccountKeysSet(contentAccount);
+  const notificationAccountKeysSet =
+    retrieveAccountKeysSet(notificationAccount);
+
+  persistCryptoStore();
+
+  return {
+    keyPayload: signedIdentityKeysBlob.payload,
+    keyPayloadSignature: signedIdentityKeysBlob.signature,
+    contentPrekey: primaryAccountKeysSet.prekey,
+    contentPrekeySignature: primaryAccountKeysSet.prekeySignature,
+    notifPrekey: notificationAccountKeysSet.prekey,
+    notifPrekeySignature: notificationAccountKeysSet.prekeySignature,
+    contentOneTimeKeys: primaryAccountKeysSet.oneTimeKeys,
+    notifOneTimeKeys: notificationAccountKeysSet.oneTimeKeys,
+  };
+}
+
+export {
+  clearCryptoStore,
+  processAppOlmApiRequest,
+  getSignedIdentityKeysBlob,
+  getDeviceKeyUpload,
+};
diff --git a/web/types/worker-types.js b/web/types/worker-types.js
--- a/web/types/worker-types.js
+++ b/web/types/worker-types.js
@@ -2,6 +2,8 @@
 
 import type { AuthMetadata } from 'lib/shared/identity-client-context.js';
 import type { CryptoStore } from 'lib/types/crypto-types.js';
+import type { PlatformDetails } from 'lib/types/device-types.js';
+import type { IdentityServiceAuthLayer } from 'lib/types/identity-service-types.js';
 import type {
   ClientDBStore,
   ClientDBStoreOperations,
@@ -22,6 +24,7 @@
   CLEAR_SENSITIVE_DATA: 10,
   BACKUP_RESTORE: 11,
   INITIALIZE_CRYPTO_ACCOUNT: 12,
+  CREATE_IDENTITY_SERVICE_CLIENT: 13,
 });
 
 export const workerWriteRequests: $ReadOnlyArray<number> = [
@@ -37,6 +40,10 @@
   workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT,
 ];
 
+export const workerIdentityClientRequests: $ReadOnlyArray<number> = [
+  workerRequestMessageTypes.CREATE_IDENTITY_SERVICE_CLIENT,
+];
+
 export type PingWorkerRequestMessage = {
   +type: 0,
   +text: string,
@@ -106,6 +113,13 @@
   +initialCryptoStore?: CryptoStore,
 };
 
+export type CreateIdentityServiceClientRequestMessage = {
+  +type: 13,
+  +opaqueWasmPath: string,
+  +platformDetails: PlatformDetails,
+  +authLayer: ?IdentityServiceAuthLayer,
+};
+
 export type WorkerRequestMessage =
   | PingWorkerRequestMessage
   | InitWorkerRequestMessage
@@ -119,7 +133,8 @@
   | RemovePersistStorageItemRequestMessage
   | ClearSensitiveDataRequestMessage
   | BackupRestoreRequestMessage
-  | InitializeCryptoAccountRequestMessage;
+  | InitializeCryptoAccountRequestMessage
+  | CreateIdentityServiceClientRequestMessage;
 
 export type WorkerRequestProxyMessage = {
   +id: number,
diff --git a/web/webpack.config.cjs b/web/webpack.config.cjs
--- a/web/webpack.config.cjs
+++ b/web/webpack.config.cjs
@@ -160,6 +160,16 @@
       },
     ],
   }),
+  new CopyPlugin({
+    patterns: [
+      {
+        from:
+          'node_modules/@commapp/opaque-ke-wasm' +
+          '/pkg/comm_opaque2_wasm_bg.wasm',
+        to: path.join(__dirname, 'dist', 'webworkers'),
+      },
+    ],
+  }),
 ];
 
 const prodWebWorkersPlugins = [
@@ -202,6 +212,21 @@
       },
     ],
   }),
+  new CopyPlugin({
+    patterns: [
+      {
+        from:
+          'node_modules/@commapp/opaque-ke-wasm' +
+          '/pkg/comm_opaque2_wasm_bg.wasm',
+        to: path.join(
+          __dirname,
+          'dist',
+          'webworkers',
+          'opaque-ke.[contenthash:12].wasm',
+        ),
+      },
+    ],
+  }),
   new WebpackManifestPlugin({
     publicPath: '',
   }),