diff --git a/keyserver/addons/rust-node-addon/Cargo.lock b/keyserver/addons/rust-node-addon/Cargo.lock
--- a/keyserver/addons/rust-node-addon/Cargo.lock
+++ b/keyserver/addons/rust-node-addon/Cargo.lock
@@ -2,6 +2,15 @@
 # It is not intended for manual editing.
 version = 3
 
+[[package]]
+name = "aho-corasick"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "anyhow"
 version = "1.0.69"
@@ -1013,6 +1022,8 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
 dependencies = [
+ "aho-corasick",
+ "memchr",
  "regex-syntax",
 ]
 
@@ -1065,6 +1076,7 @@
  "napi",
  "napi-build",
  "napi-derive",
+ "regex",
  "serde",
  "serde_json",
  "tokio",
diff --git a/keyserver/addons/rust-node-addon/Cargo.toml b/keyserver/addons/rust-node-addon/Cargo.toml
--- a/keyserver/addons/rust-node-addon/Cargo.toml
+++ b/keyserver/addons/rust-node-addon/Cargo.toml
@@ -27,6 +27,7 @@
 
 [build-dependencies]
 napi-build = "2.0.1"
+regex = "1"
 
 [profile.release]
 lto = true
diff --git a/keyserver/addons/rust-node-addon/build.rs b/keyserver/addons/rust-node-addon/build.rs
--- a/keyserver/addons/rust-node-addon/build.rs
+++ b/keyserver/addons/rust-node-addon/build.rs
@@ -1,5 +1,50 @@
 extern crate napi_build;
 
+use regex::Regex;
+
+use std::env;
+use std::fs;
+use std::path::Path;
+
 fn main() {
   napi_build::setup();
+
+  const VERSIONS_JS_PATH: &str = "../../../lib/facts/version.js";
+  println!("cargo:rerun-if-changed={}", VERSIONS_JS_PATH);
+  let js_path = Path::new(VERSIONS_JS_PATH);
+
+  let content =
+    fs::read_to_string(&js_path).expect("Failed to read version.js");
+
+  let version_line = content
+    .lines()
+    .find(|line| line.contains("webAndKeyserverCodeVersion"))
+    .expect("Failed to find webAndKeyserverCodeVersion line");
+
+  // Find a sequence in the input string that starts with
+  // 'webAndKeyserverCodeVersion', followed by any number of whitespace
+  // characters, an equals sign, any number of additional whitespace characters,
+  // a series of one or more digits (and capture these digits), and finally a
+  // semicolon.
+  let re = Regex::new(r"webAndKeyserverCodeVersion\s*=\s*(\d+);").unwrap();
+  let version: u64 = re
+    .captures(&version_line)
+    .and_then(|cap| cap.get(1))
+    .map_or_else(
+      || panic!("Failed to capture version number"),
+      |m| {
+        m.as_str()
+          .parse::<u64>()
+          .expect("Failed to parse version number")
+      },
+    );
+
+  let out_dir = env::var("OUT_DIR").unwrap();
+  let rust_path = Path::new(&out_dir).join("version.rs");
+
+  fs::write(
+    &rust_path,
+    format!("pub const CODE_VERSION: u64 = {};", version),
+  )
+  .expect("Failed to write version.rs");
 }
diff --git a/keyserver/addons/rust-node-addon/src/identity_client/mod.rs b/keyserver/addons/rust-node-addon/src/identity_client/mod.rs
--- a/keyserver/addons/rust-node-addon/src/identity_client/mod.rs
+++ b/keyserver/addons/rust-node-addon/src/identity_client/mod.rs
@@ -5,24 +5,32 @@
 pub mod remove_reserved_usernames;
 pub mod upload_one_time_keys;
 
-use grpc_clients::identity::authenticated::AuthLayer;
-use grpc_clients::identity::protos::unauthenticated as client_proto;
-use grpc_clients::identity::protos::authenticated::identity_client_service_client::IdentityClientServiceClient as AuthClient;
 use client_proto::identity_client_service_client::IdentityClientServiceClient;
 use client_proto::{
-  AddReservedUsernamesRequest, DeviceKeyUpload, IdentityKeyInfo, PreKey,
-  RegistrationFinishRequest, RegistrationStartRequest, DeviceType,
-  RemoveReservedUsernameRequest, InboundKeyInfo, UploadOneTimeKeysRequest
+  AddReservedUsernamesRequest, DeviceKeyUpload, DeviceType, IdentityKeyInfo,
+  InboundKeyInfo, PreKey, RegistrationFinishRequest, RegistrationStartRequest,
+  RemoveReservedUsernameRequest, UploadOneTimeKeysRequest,
 };
+use grpc_clients::identity::authenticated::ChainedInterceptedAuthClient;
+use grpc_clients::identity::protos::unauthenticated as client_proto;
+use grpc_clients::identity::shared::CodeVersionLayer;
 use lazy_static::lazy_static;
 use napi::bindgen_prelude::*;
 use serde::{Deserialize, Serialize};
-use tonic::codegen::InterceptedService;
 use std::env::var;
+use tonic::codegen::InterceptedService;
 use tonic::{transport::Channel, Request};
 use tracing::{self, info, instrument, warn, Level};
 use tracing_subscriber::EnvFilter;
 
+mod generated {
+  // We get the CODE_VERSION from this generated file
+  include!(concat!(env!("OUT_DIR"), "/version.rs"));
+}
+
+pub use generated::CODE_VERSION;
+pub const DEVICE_TYPE: &str = "keyserver";
+
 lazy_static! {
   static ref IDENTITY_SERVICE_CONFIG: IdentityServiceConfig = {
     let filter = EnvFilter::builder()
@@ -58,12 +66,15 @@
   }
 }
 
-async fn get_identity_client_service_channel(
-) -> Result<IdentityClientServiceClient<Channel>> {
+async fn get_identity_client_service_channel() -> Result<
+  IdentityClientServiceClient<InterceptedService<Channel, CodeVersionLayer>>,
+> {
   info!("Connecting to identity service");
 
   grpc_clients::identity::get_unauthenticated_client(
     &IDENTITY_SERVICE_CONFIG.identity_socket_addr,
+    CODE_VERSION,
+    DEVICE_TYPE.to_string(),
   )
   .await
   .map_err(|_| {
@@ -78,7 +89,7 @@
   user_id: String,
   device_id: String,
   access_token: String,
-) -> Result<AuthClient<InterceptedService<Channel, AuthLayer>>> {
+) -> Result<ChainedInterceptedAuthClient> {
   info!("Connecting to identity service");
 
   grpc_clients::identity::get_auth_client(
@@ -86,6 +97,8 @@
     user_id,
     device_id,
     access_token,
+    CODE_VERSION,
+    DEVICE_TYPE.to_string(),
   )
   .await
   .map_err(|_| {
@@ -167,3 +180,13 @@
   warn!("Received error: {}", error.message());
   Error::new(Status::GenericFailure, error.message())
 }
+
+#[cfg(test)]
+mod tests {
+  use super::CODE_VERSION;
+
+  #[test]
+  fn test_code_version_exists() {
+    assert!(CODE_VERSION > 0);
+  }
+}
diff --git a/keyserver/addons/rust-node-addon/src/identity_client/upload_one_time_keys.rs b/keyserver/addons/rust-node-addon/src/identity_client/upload_one_time_keys.rs
--- a/keyserver/addons/rust-node-addon/src/identity_client/upload_one_time_keys.rs
+++ b/keyserver/addons/rust-node-addon/src/identity_client/upload_one_time_keys.rs
@@ -23,7 +23,10 @@
   };
 
   debug!("Sending one time keys to Identity service");
-  let result = identity_client.upload_one_time_keys(upload_request).await;
+  identity_client
+    .upload_one_time_keys(upload_request)
+    .await
+    .map_err(handle_grpc_error)?;
 
   Ok(true)
 }
diff --git a/services/comm-services-lib/src/auth/service.rs b/services/comm-services-lib/src/auth/service.rs
--- a/services/comm-services-lib/src/auth/service.rs
+++ b/services/comm-services-lib/src/auth/service.rs
@@ -13,6 +13,12 @@
 const AWSCURRENT: &str = "AWSCURRENT";
 const AWSPREVIOUS: &str = "AWSPREVIOUS";
 
+// Identity service gRPC clients require a code version and device type.
+// We can supply some placeholder values for services for the time being, since
+// this metadata is only relevant for devices.
+const PLACEHOLDER_CODE_VERSION: u64 = 0;
+const DEVICE_TYPE: &str = "service";
+
 #[derive(
   Debug, derive_more::Display, derive_more::Error, derive_more::From,
 )]
@@ -81,6 +87,8 @@
           user_id,
           device_id,
           access_token,
+          PLACEHOLDER_CODE_VERSION,
+          DEVICE_TYPE.to_string(),
         )
         .await
         .map_err(AuthServiceError::from)
diff --git a/services/commtest/tests/grpc_client_test.rs b/services/commtest/tests/grpc_client_test.rs
--- a/services/commtest/tests/grpc_client_test.rs
+++ b/services/commtest/tests/grpc_client_test.rs
@@ -4,12 +4,16 @@
 async fn verify_access_token() {
   use grpc_clients::identity::unauthenticated::client::verify_user_access_token;
   let device_info = create_device(None).await;
+  let code_version = 100;
+  let device_type = "android";
 
   let token_valid = verify_user_access_token(
     "http://127.0.0.1:50054",
     &device_info.user_id,
     &device_info.device_id,
     &device_info.access_token,
+    code_version,
+    device_type.to_string(),
   )
   .await
   .expect("Failed to call identity's verify_user_access_token endpoint");
@@ -22,6 +26,8 @@
     &device_info.user_id,
     &device_info.device_id,
     "garbage",
+    code_version,
+    device_type.to_string(),
   )
   .await
   .expect("Failed to call identity's verify_user_access_token endpoint");
diff --git a/services/tunnelbroker/src/identity/mod.rs b/services/tunnelbroker/src/identity/mod.rs
--- a/services/tunnelbroker/src/identity/mod.rs
+++ b/services/tunnelbroker/src/identity/mod.rs
@@ -7,14 +7,24 @@
 use crate::config::CONFIG;
 use crate::error::Error;
 
+// Identity service gRPC clients require a code version and device type.
+// We can supply some placeholder values for services for the time being, since
+// this metadata is only relevant for devices.
+const PLACEHOLDER_CODE_VERSION: u64 = 0;
+const DEVICE_TYPE: &str = "service";
+
 /// Returns true if access token is valid
 pub async fn verify_user_access_token(
   user_id: &str,
   device_id: &str,
   access_token: &str,
 ) -> Result<bool, Error> {
-  let mut grpc_client =
-    get_unauthenticated_client(&CONFIG.identity_endpoint).await?;
+  let mut grpc_client = get_unauthenticated_client(
+    &CONFIG.identity_endpoint,
+    PLACEHOLDER_CODE_VERSION,
+    DEVICE_TYPE.to_string(),
+  )
+  .await?;
   let message = VerifyUserAccessTokenRequest {
     user_id: user_id.to_string(),
     signing_public_key: device_id.to_string(),
diff --git a/shared/grpc_clients/src/identity/authenticated.rs b/shared/grpc_clients/src/identity/authenticated.rs
--- a/shared/grpc_clients/src/identity/authenticated.rs
+++ b/shared/grpc_clients/src/identity/authenticated.rs
@@ -7,7 +7,8 @@
   Request, Status,
 };
 
-use crate::error::Error;
+use crate::identity::shared::{ChainedInterceptor, ToMetadataValueAscii};
+use crate::{error::Error, identity::shared::CodeVersionLayer};
 
 pub struct AuthLayer {
   user_id: String,
@@ -15,10 +16,6 @@
   access_token: String,
 }
 
-trait ToMetadataValueAscii {
-  fn parse_to_ascii(&self) -> Result<MetadataValue<Ascii>, Status>;
-}
-
 impl ToMetadataValueAscii for str {
   fn parse_to_ascii(&self) -> Result<MetadataValue<Ascii>, Status> {
     self.parse().map_err(|e: InvalidMetadataValue| {
@@ -40,21 +37,38 @@
     Ok(request)
   }
 }
+
+pub type ChainedInterceptedAuthClient = AuthClient<
+  InterceptedService<Channel, ChainedInterceptor<AuthLayer, CodeVersionLayer>>,
+>;
+
 pub async fn get_auth_client(
   url: &str,
   user_id: String,
   device_id: String,
   access_token: String,
-) -> Result<AuthClient<InterceptedService<Channel, AuthLayer>>, Error> {
+  code_version: u64,
+  device_type: String,
+) -> Result<ChainedInterceptedAuthClient, Error> {
   use crate::get_grpc_service_channel;
 
   let channel = get_grpc_service_channel(url).await?;
 
-  let interceptor = AuthLayer {
+  let auth_interceptor = AuthLayer {
     user_id,
     device_id,
     access_token,
   };
 
-  Ok(AuthClient::with_interceptor(channel, interceptor))
+  let version_interceptor = CodeVersionLayer {
+    version: code_version,
+    device_type,
+  };
+
+  let chained = ChainedInterceptor {
+    first: auth_interceptor,
+    second: version_interceptor,
+  };
+
+  Ok(AuthClient::with_interceptor(channel, chained))
 }
diff --git a/shared/grpc_clients/src/identity/mod.rs b/shared/grpc_clients/src/identity/mod.rs
--- a/shared/grpc_clients/src/identity/mod.rs
+++ b/shared/grpc_clients/src/identity/mod.rs
@@ -1,5 +1,6 @@
 pub mod authenticated;
 pub mod device;
+pub mod shared;
 pub mod unauthenticated;
 
 pub mod protos {
diff --git a/shared/grpc_clients/src/identity/shared.rs b/shared/grpc_clients/src/identity/shared.rs
new file mode 100644
--- /dev/null
+++ b/shared/grpc_clients/src/identity/shared.rs
@@ -0,0 +1,57 @@
+use tonic::{
+  metadata::{errors::InvalidMetadataValue, Ascii, MetadataValue},
+  service::Interceptor,
+  Request, Status,
+};
+
+pub struct CodeVersionLayer {
+  pub(crate) version: u64,
+  pub(crate) device_type: String,
+}
+
+impl Interceptor for CodeVersionLayer {
+  fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
+    let metadata = request.metadata_mut();
+    metadata.insert("code_version", self.version.parse_to_ascii()?);
+    metadata.insert("device_type", self.device_type.parse_to_ascii()?);
+
+    Ok(request)
+  }
+}
+
+pub trait ToMetadataValueAscii {
+  fn parse_to_ascii(&self) -> Result<MetadataValue<Ascii>, Status>;
+}
+
+impl ToMetadataValueAscii for u64 {
+  fn parse_to_ascii(&self) -> Result<MetadataValue<Ascii>, Status> {
+    let ascii_string = self.to_string();
+
+    ascii_string.parse().map_err(|e: InvalidMetadataValue| {
+      Status::invalid_argument(format!(
+        "Non-Ascii character present in metadata value: {}",
+        e
+      ))
+    })
+  }
+}
+
+pub struct ChainedInterceptor<A, B>
+where
+  A: Interceptor + Send + Sync + 'static,
+  B: Interceptor + Send + Sync + 'static,
+{
+  pub(crate) first: A,
+  pub(crate) second: B,
+}
+
+impl<A, B> Interceptor for ChainedInterceptor<A, B>
+where
+  A: Interceptor + Send + Sync + 'static,
+  B: Interceptor + Send + Sync + 'static,
+{
+  fn call(&mut self, request: Request<()>) -> Result<Request<()>, Status> {
+    let request = self.first.call(request)?;
+    self.second.call(request)
+  }
+}
diff --git a/shared/grpc_clients/src/identity/unauthenticated/client.rs b/shared/grpc_clients/src/identity/unauthenticated/client.rs
--- a/shared/grpc_clients/src/identity/unauthenticated/client.rs
+++ b/shared/grpc_clients/src/identity/unauthenticated/client.rs
@@ -12,8 +12,11 @@
   user_id: &str,
   device_id: &str,
   access_token: &str,
+  code_version: u64,
+  device_type: String,
 ) -> Result<bool, Error> {
-  let mut grpc_client = get_unauthenticated_client(identity_url).await?;
+  let mut grpc_client =
+    get_unauthenticated_client(identity_url, code_version, device_type).await?;
 
   let message = VerifyUserAccessTokenRequest {
     user_id: user_id.to_string(),
@@ -23,5 +26,5 @@
 
   let request = Request::new(message);
   let response = grpc_client.verify_user_access_token(request).await?;
-  return Ok(response.into_inner().token_valid);
+  Ok(response.into_inner().token_valid)
 }
diff --git a/shared/grpc_clients/src/identity/unauthenticated/mod.rs b/shared/grpc_clients/src/identity/unauthenticated/mod.rs
--- a/shared/grpc_clients/src/identity/unauthenticated/mod.rs
+++ b/shared/grpc_clients/src/identity/unauthenticated/mod.rs
@@ -1,13 +1,29 @@
 pub mod client;
 
+use tonic::codegen::InterceptedService;
 use tonic::transport::Channel;
 
-use super::protos::client::identity_client_service_client::IdentityClientServiceClient;
+use super::{
+  protos::client::identity_client_service_client::IdentityClientServiceClient,
+  shared::CodeVersionLayer,
+};
 use crate::error::Error;
 
 pub async fn get_unauthenticated_client(
   url: &str,
-) -> Result<IdentityClientServiceClient<Channel>, Error> {
+  code_version: u64,
+  device_type: String,
+) -> Result<
+  IdentityClientServiceClient<InterceptedService<Channel, CodeVersionLayer>>,
+  Error,
+> {
   let channel = crate::get_grpc_service_channel(url).await?;
-  Ok(IdentityClientServiceClient::new(channel))
+  let version_interceptor = CodeVersionLayer {
+    version: code_version,
+    device_type,
+  };
+  Ok(IdentityClientServiceClient::with_interceptor(
+    channel,
+    version_interceptor,
+  ))
 }
diff --git a/shared/grpc_clients/src/lib.rs b/shared/grpc_clients/src/lib.rs
--- a/shared/grpc_clients/src/lib.rs
+++ b/shared/grpc_clients/src/lib.rs
@@ -10,7 +10,7 @@
 use tonic::transport::{Certificate, Channel, ClientTlsConfig};
 use tracing::info;
 
-const CERT_PATHS: &'static [&'static str] = &[
+const CERT_PATHS: &[&str] = &[
   // MacOS and newer Ubuntu
   "/etc/ssl/cert.pem",
   // Common CA cert paths