diff --git a/.buildkite/eslint_flow_jest.yml b/.buildkite/eslint_flow_jest.yml
--- a/.buildkite/eslint_flow_jest.yml
+++ b/.buildkite/eslint_flow_jest.yml
@@ -2,6 +2,8 @@
   - label: ':eslint: :jest: ESLint & Flow & Jest'
     command:
       - '(pkill flow || true)'
+      - 'curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y'
+      - '. /root/.cargo/env'
       - 'yarn cleaninstall --frozen-lockfile --skip-optional --network-timeout 180000'
       - 'yarn eslint --max-warnings=0 & yarn workspace lib flow & yarn workspace web flow & yarn workspace landing flow & yarn workspace native flow & yarn workspace keyserver flow'
       - 'yarn workspace lib test && yarn workspace keyserver test'
diff --git a/.buildkite/ios.yml b/.buildkite/ios.yml
--- a/.buildkite/ios.yml
+++ b/.buildkite/ios.yml
@@ -2,7 +2,11 @@
   - label: ':ios: iOS Build'
     command:
       - 'pod repo update && yarn workspace native clean-ios'
-      - 'yarn cleaninstall --frozen-lockfile --skip-optional --network-timeout 180000'
+      - 'curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y'
+      - 'source /Users/comm/.cargo/env'
+      - 'yarn cleaninstall --frozen-lockfile --skip-optional'
       - 'cd native/ios && xcodebuild -workspace Comm.xcworkspace -scheme Comm -destination generic/platform=iOS -allowProvisioningUpdates'
+    env:
+      PROTOC: "/opt/homebrew/bin/protoc"
     agents:
       - 'mac=true'
diff --git a/keyserver/Dockerfile b/keyserver/Dockerfile
--- a/keyserver/Dockerfile
+++ b/keyserver/Dockerfile
@@ -70,7 +70,16 @@
 RUN mkdir /home/comm/backups
 
 #-------------------------------------------------------------------------------
-# STEP 4: SET UP NVM
+# STEP 4: SET UP CARGO (RUST PACKAGE MANAGER)
+# We use Cargo to build pre-compiled Node.js addons in Rust
+#-------------------------------------------------------------------------------
+
+# Install Rust and add Cargo's bin directory to the $PATH environment variable
+RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
+ENV PATH /home/comm/.cargo/bin:$PATH
+
+#-------------------------------------------------------------------------------
+# STEP 5: SET UP NVM
 # We use nvm to make sure we're running the right Node version
 #-------------------------------------------------------------------------------
 
@@ -86,7 +95,7 @@
 RUN cd keyserver && . bash/source-nvm.sh
 
 #-------------------------------------------------------------------------------
-# STEP 5: YARN CLEANINSTALL
+# STEP 6: YARN CLEANINSTALL
 # We run yarn cleaninstall before copying most of the files in for build caching
 #-------------------------------------------------------------------------------
 
@@ -98,6 +107,13 @@
 COPY --chown=comm native/package.json native/.flowconfig native/
 COPY --chown=comm landing/package.json landing/.flowconfig landing/
 COPY --chown=comm desktop/package.json desktop/
+COPY --chown=comm keyserver/addons/opaque-ke-napi/package.json \
+  keyserver/addons/opaque-ke-napi/
+
+# Create empty Rust library and copy in Cargo.toml file
+RUN cargo init keyserver/addons/opaque-ke-napi --lib
+COPY --chown=comm keyserver/addons/opaque-ke-napi/Cargo.toml \
+  keyserver/addons/opaque-ke-napi/
 
 # Copy in files needed for patch-package
 COPY --chown=comm patches patches/
@@ -106,7 +122,7 @@
 RUN yarn cleaninstall
 
 #-------------------------------------------------------------------------------
-# STEP 6: WEBPACK BUILD
+# STEP 7: WEBPACK BUILD
 # We do this first so Docker doesn't rebuild when only keyserver files change
 #-------------------------------------------------------------------------------
 
@@ -118,14 +134,21 @@
 RUN yarn workspace web prod
 
 #-------------------------------------------------------------------------------
-# STEP 7: COPY IN SOURCE FILES
+# STEP 8: COPY IN SOURCE FILES
 # We run this later so the above layers are cached if only source files change
 #-------------------------------------------------------------------------------
 
 COPY --chown=comm . .
 
 #-------------------------------------------------------------------------------
-# STEP 8: RUN BUILD SCRIPTS
+# STEP 9: BUILD NODE ADDON
+# Now that source files have been copied in, build the opaque-ke-napi addon
+#-------------------------------------------------------------------------------
+
+RUN yarn workspace opaque-ke-napi build
+
+#-------------------------------------------------------------------------------
+# STEP 10: RUN BUILD SCRIPTS
 # We need to populate keyserver/dist, among other things
 #-------------------------------------------------------------------------------
 
@@ -133,7 +156,7 @@
 RUN yarn workspace keyserver prod-build
 
 #-------------------------------------------------------------------------------
-# STEP 9: RUN THE SERVER
+# STEP 11: RUN THE SERVER
 # Actually run the Node.js keyserver using nvm
 #-------------------------------------------------------------------------------
 
diff --git a/keyserver/addons/opaque-ke-napi/.gitignore b/keyserver/addons/opaque-ke-napi/.gitignore
new file mode 100644
--- /dev/null
+++ b/keyserver/addons/opaque-ke-napi/.gitignore
@@ -0,0 +1,3 @@
+Cargo.lock
+target
+napi
diff --git a/keyserver/addons/opaque-ke-napi/Cargo.toml b/keyserver/addons/opaque-ke-napi/Cargo.toml
new file mode 100644
--- /dev/null
+++ b/keyserver/addons/opaque-ke-napi/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+edition = "2021"
+name = "opaque-ke-napi"
+version = "0.1.0"
+license = "BSD-3-Clause"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
+napi = { version = "2.10.1", default-features = false, features = ["napi4"] }
+napi-derive = { version = "2.9.1", default-features = false }
+
+[build-dependencies]
+napi-build = "2.0.1"
+
+[profile.release]
+lto = true
diff --git a/keyserver/addons/opaque-ke-napi/build.rs b/keyserver/addons/opaque-ke-napi/build.rs
new file mode 100644
--- /dev/null
+++ b/keyserver/addons/opaque-ke-napi/build.rs
@@ -0,0 +1,5 @@
+extern crate napi_build;
+
+fn main() {
+  napi_build::setup();
+}
diff --git a/keyserver/addons/opaque-ke-napi/index.js b/keyserver/addons/opaque-ke-napi/index.js
new file mode 100644
--- /dev/null
+++ b/keyserver/addons/opaque-ke-napi/index.js
@@ -0,0 +1,35 @@
+// @flow
+
+const { platform, arch } = process;
+
+type RustAPI = {
+  +sum: (a: number, b: number) => number,
+};
+
+async function getRustAPI(): Promise<RustAPI> {
+  let nativeBinding = null;
+  if (platform === 'darwin' && arch === 'x64') {
+    // $FlowFixMe
+    nativeBinding = await import('./napi/opaque-ke-napi.darwin-x64.node');
+  } else if (platform === 'darwin' && arch === 'arm64') {
+    // $FlowFixMe
+    nativeBinding = await import('./napi/opaque-ke-napi.darwin-arm64.node');
+  } else if (platform === 'linux' && arch === 'x64') {
+    // $FlowFixMe
+    nativeBinding = await import('./napi/opaque-ke-napi.linux-x64-gnu.node');
+  } else if (platform === 'linux' && arch === 'arm64') {
+    // $FlowFixMe
+    nativeBinding = await import('./napi/opaque-ke-napi.linux-arm64-gnu.node');
+  } else {
+    throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`);
+  }
+
+  if (!nativeBinding) {
+    throw new Error('Failed to load native binding');
+  }
+
+  const { sum } = nativeBinding.default;
+  return { sum };
+}
+
+export { getRustAPI };
diff --git a/keyserver/addons/opaque-ke-napi/package.json b/keyserver/addons/opaque-ke-napi/package.json
new file mode 100644
--- /dev/null
+++ b/keyserver/addons/opaque-ke-napi/package.json
@@ -0,0 +1,33 @@
+{
+  "name": "opaque-ke-napi",
+  "version": "0.0.1",
+  "main": "index.js",
+  "type": "module",
+  "napi": {
+    "name": "opaque-ke-napi",
+    "triples": {
+      "defaults": false,
+      "additional": [
+        "x86_64-apple-darwin",
+        "aarch64-apple-darwin",
+        "x86_64-unknown-linux-gnu",
+        "aarch64-unknown-linux-gnu"
+      ]
+    }
+  },
+  "license": "BSD-3-Clause",
+  "devDependencies": {
+    "@napi-rs/cli": "^2.13.0"
+  },
+  "engines": {
+    "node": ">= 16"
+  },
+  "scripts": {
+    "artifacts": "napi artifacts",
+    "build": "napi build --platform napi --release",
+    "build:debug": "napi build --platform napi",
+    "version": "napi version",
+    "postinstall": "yarn build",
+    "clean": "rm -rf target/ && rm -rf napi/ && rm -rf node_modules/"
+  }
+}
diff --git a/keyserver/addons/opaque-ke-napi/src/lib.rs b/keyserver/addons/opaque-ke-napi/src/lib.rs
new file mode 100644
--- /dev/null
+++ b/keyserver/addons/opaque-ke-napi/src/lib.rs
@@ -0,0 +1,9 @@
+#![deny(clippy::all)]
+
+#[macro_use]
+extern crate napi_derive;
+
+#[napi]
+pub fn sum(a: i32, b: i32) -> i32 {
+  a + b
+}
diff --git a/keyserver/loader.mjs b/keyserver/loader.mjs
--- a/keyserver/loader.mjs
+++ b/keyserver/loader.mjs
@@ -1,16 +1,21 @@
 // @flow
 
-const localPackages = ['landing', 'lib', 'web'];
+const localPackages = {
+  landing: 'landing',
+  lib: 'lib',
+  web: 'web',
+  ['opaque-ke-napi']: 'keyserver/addons/opaque-ke-napi',
+};
 
 async function resolve(specifier, context, nextResolve) {
   const defaultResult = await nextResolve(specifier, context);
 
-  // Special hack to use Babel-transpiled lib and web
-  if (localPackages.some(pkg => specifier.startsWith(`${pkg}/`))) {
-    const url = defaultResult.url.replace(
-      specifier,
-      `keyserver/dist/${specifier}`,
-    );
+  for (const pkg in localPackages) {
+    if (specifier !== pkg && !specifier.startsWith(`${pkg}/`)) {
+      continue;
+    }
+    const path = localPackages[pkg];
+    const url = defaultResult.url.replace(path, `keyserver/dist/${pkg}`);
     return { url };
   }
 
diff --git a/keyserver/package.json b/keyserver/package.json
--- a/keyserver/package.json
+++ b/keyserver/package.json
@@ -8,7 +8,7 @@
   "scripts": {
     "clean": "rm -rf dist/ && rm -rf node_modules/ && mkdir dist",
     "babel-build": ". bash/source-nvm.sh && yarn --silent babel src/ --out-dir dist/ --config-file ./babel.config.cjs --verbose --ignore 'src/landing/flow-typed','src/landing/node_modules','src/landing/package.json','src/lib/flow-typed','src/lib/node_modules','src/lib/package.json','src/web/flow-typed','src/web/node_modules','src/web/package.json','src/web/dist','src/web/webpack.config.js','src/web/account-bar.react.js','src/web/app.react.js','src/web/calendar','src/web/chat','src/web/flow','src/web/loading-indicator.react.js','src/web/modals','src/web/root.js','src/web/router-history.js','src/web/script.js','src/web/selectors/chat-selectors.js','src/web/selectors/entry-selectors.js','src/web/splash','src/web/vector-utils.js','src/web/vectors.react.js'",
-    "rsync": "rsync -rLpmuv --exclude '*/package.json' --exclude '*/node_modules/*' --include '*.json' --include '*.cjs' --exclude '*.*' src/ dist/",
+    "rsync": "rsync -rLpmuv --exclude '*/package.json' --exclude '*/node_modules/*' --include '*.json' --include '*.cjs' --include '*.node' --exclude '*.*' src/ dist/",
     "prod-build": "yarn babel-build && yarn rsync && yarn update-geoip",
     "update-geoip": "yarn script dist/scripts/update-geoip.js",
     "prod": "node --trace-warnings --experimental-json-modules --loader=./loader.mjs --experimental-specifier-resolution=node dist/keyserver",
@@ -62,6 +62,7 @@
     "mysql2": "^2.3.3",
     "node-schedule": "^2.1.0",
     "nodemailer": "^6.6.1",
+    "opaque-ke-napi": "0.0.1",
     "react": "17.0.2",
     "react-dom": "17.0.2",
     "react-html-email": "^3.0.0",
diff --git a/keyserver/src/opaque-ke-napi b/keyserver/src/opaque-ke-napi
new file mode 120000
--- /dev/null
+++ b/keyserver/src/opaque-ke-napi
@@ -0,0 +1 @@
+../addons/opaque-ke-napi
\ No newline at end of file
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -8,10 +8,10 @@
     "keyserver",
     "landing",
     "desktop",
-    "keyserver/addons/opaque-ke-node"
+    "keyserver/addons/opaque-ke-napi"
   ],
   "scripts": {
-    "clean": "yarn workspace lib clean && yarn workspace web clean && yarn workspace native clean && yarn workspace keyserver clean && yarn workspace landing clean && yarn workspace desktop clean && rm -rf node_modules/",
+    "clean": "yarn workspace lib clean && yarn workspace web clean && yarn workspace native clean && yarn workspace keyserver clean && yarn workspace landing clean && yarn workspace desktop clean && yarn workspace opaque-ke-napi clean && rm -rf node_modules/",
     "cleaninstall": "(killall flow || pkill flow || true) && yarn clean && yarn",
     "eslint": "eslint .",
     "eslint:fix": "eslint --fix .",
diff --git a/yarn.lock b/yarn.lock
--- a/yarn.lock
+++ b/yarn.lock
@@ -3106,6 +3106,11 @@
   resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz#af577b477c683fad17c619a78208cede06f9605c"
   integrity sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q==
 
+"@napi-rs/cli@^2.13.0":
+  version "2.13.0"
+  resolved "https://registry.yarnpkg.com/@napi-rs/cli/-/cli-2.13.0.tgz#227f1b63edc6fb9364317e4719884b4451d147b6"
+  integrity sha512-8U6TLh2PYXM2SX7HnpRBCqlPU48M9tRe0TLlb4qgUx61bt6PT6Qtdeho3e0ila70fnrbqCA6dnJWrbgbJIopcQ==
+
 "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents":
   version "2.1.8-no-fsevents"
   resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.tgz#da7c3996b8e6e19ebd14d82eaced2313e7769f9b"
@@ -6764,11 +6769,6 @@
   dependencies:
     rsvp "^4.8.4"
 
-cargo-cp-artifact@^0.1:
-  version "0.1.7"
-  resolved "https://registry.yarnpkg.com/cargo-cp-artifact/-/cargo-cp-artifact-0.1.7.tgz#1181b9d6e71f00f17c068c05e3cd1b0864783341"
-  integrity sha512-pxEV9p1on8vu3BOKstVisF9TwMyGKCBRvzaVpQHuU2sLULCKrn3MJWx/4XlNzmG6xNCTPf78DJ7WCGgr2mOzjg==
-
 chainsaw@~0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98"