diff --git a/services/identity/src/client_service.rs b/services/identity/src/client_service.rs
--- a/services/identity/src/client_service.rs
+++ b/services/identity/src/client_service.rs
@@ -26,7 +26,7 @@
   GenerateNonceResponse, OpaqueLoginFinishRequest, OpaqueLoginStartRequest,
   OpaqueLoginStartResponse, RegistrationFinishRequest, RegistrationStartRequest,
   RegistrationStartResponse, RemoveReservedUsernameRequest,
-  ReservedRegistrationStartRequest,
+  ReservedRegistrationStartRequest, RestoreUserRequest,
   SecondaryDeviceKeysUploadRequest, VerifyUserAccessTokenRequest,
   VerifyUserAccessTokenResponse, WalletAuthRequest, GetFarcasterUsersRequest,
   GetFarcasterUsersResponse
@@ -687,6 +687,14 @@
     Ok(Response::new(response))
   }
 
+  #[tracing::instrument(skip_all)]
+  async fn restore_user(
+    &self,
+    request: tonic::Request<RestoreUserRequest>,
+  ) -> Result<tonic::Response<AuthResponse>, tonic::Status> {
+    unimplemented!();
+  }
+
   #[tracing::instrument(skip_all)]
   async fn upload_keys_for_registered_device_and_log_in(
     &self,
diff --git a/services/identity/src/grpc_utils.rs b/services/identity/src/grpc_utils.rs
--- a/services/identity/src/grpc_utils.rs
+++ b/services/identity/src/grpc_utils.rs
@@ -14,7 +14,7 @@
     unauth::{
       DeviceKeyUpload, ExistingDeviceLoginRequest, OpaqueLoginStartRequest,
       RegistrationStartRequest, ReservedRegistrationStartRequest,
-      SecondaryDeviceKeysUploadRequest, WalletAuthRequest,
+      RestoreUserRequest, SecondaryDeviceKeysUploadRequest, WalletAuthRequest,
     },
   },
 };
@@ -171,6 +171,12 @@
   }
 }
 
+impl DeviceKeyUploadData for RestoreUserRequest {
+  fn device_key_upload(&self) -> Option<&DeviceKeyUpload> {
+    self.device_key_upload.as_ref()
+  }
+}
+
 pub trait DeviceKeyUploadActions {
   fn payload(&self) -> Result<String, Status>;
   fn payload_signature(&self) -> Result<String, Status>;
diff --git a/shared/protos/identity_unauth.proto b/shared/protos/identity_unauth.proto
--- a/shared/protos/identity_unauth.proto
+++ b/shared/protos/identity_unauth.proto
@@ -30,6 +30,9 @@
     returns (AuthResponse) {}
   rpc LogInExistingDevice(ExistingDeviceLoginRequest) returns (AuthResponse) {}
 
+  // Called by primary device to during backup restore protocol
+  rpc RestoreUser(RestoreUserRequest) returns (AuthResponse) {}
+
   /* Service actions */
 
   // Called by other services to verify a user's access token
@@ -230,6 +233,25 @@
   string initial_device_list = 5;
 }
 
+// Primary backup restore
+
+message RestoreUserRequest {
+  string user_id = 1;
+  optional string siwe_message = 2;
+  optional string siwe_signature = 3;
+  DeviceKeyUpload device_key_upload = 4;
+  // A stringified JSON object of the following format:
+  // {
+  //   "rawDeviceList": JSON.stringify({
+  //     "devices": [<primary_device_id: string>]
+  //     "timestamp": <UTC timestamp in milliseconds: int>,
+  //   }),
+  //   "curPrimarySignature": "base64-encoded new primary device signature",
+  //   "lastPrimarySignature": "base64-encoded old primary device signature"
+  // }
+  string device_list = 5;
+}
+
 // UploadKeysForRegisteredDeviceAndLogIn
 
 message SecondaryDeviceKeysUploadRequest {
diff --git a/web/protobufs/identity-unauth-structs.cjs b/web/protobufs/identity-unauth-structs.cjs
--- a/web/protobufs/identity-unauth-structs.cjs
+++ b/web/protobufs/identity-unauth-structs.cjs
@@ -45,6 +45,7 @@
 goog.exportSymbol('proto.identity.unauth.RegistrationStartResponse', null, global);
 goog.exportSymbol('proto.identity.unauth.RemoveReservedUsernameRequest', null, global);
 goog.exportSymbol('proto.identity.unauth.ReservedRegistrationStartRequest', null, global);
+goog.exportSymbol('proto.identity.unauth.RestoreUserRequest', null, global);
 goog.exportSymbol('proto.identity.unauth.SecondaryDeviceKeysUploadRequest', null, global);
 goog.exportSymbol('proto.identity.unauth.VerifyUserAccessTokenRequest', null, global);
 goog.exportSymbol('proto.identity.unauth.VerifyUserAccessTokenResponse', null, global);
@@ -322,6 +323,27 @@
    */
   proto.identity.unauth.WalletAuthRequest.displayName = 'proto.identity.unauth.WalletAuthRequest';
 }
+/**
+ * Generated by JsPbCodeGenerator.
+ * @param {Array=} opt_data Optional initial data array, typically from a
+ * server response, or constructed directly in Javascript. The array is used
+ * in place and becomes part of the constructed object. It is not cloned.
+ * If no data is provided, the constructed object will be empty, but still
+ * valid.
+ * @extends {jspb.Message}
+ * @constructor
+ */
+proto.identity.unauth.RestoreUserRequest = function(opt_data) {
+  jspb.Message.initialize(this, opt_data, 0, -1, null, null);
+};
+goog.inherits(proto.identity.unauth.RestoreUserRequest, jspb.Message);
+if (goog.DEBUG && !COMPILED) {
+  /**
+   * @public
+   * @override
+   */
+  proto.identity.unauth.RestoreUserRequest.displayName = 'proto.identity.unauth.RestoreUserRequest';
+}
 /**
  * Generated by JsPbCodeGenerator.
  * @param {Array=} opt_data Optional initial data array, typically from a
@@ -3522,6 +3544,313 @@
 
 
 
+if (jspb.Message.GENERATE_TO_OBJECT) {
+/**
+ * Creates an object representation of this proto.
+ * Field names that are reserved in JavaScript and will be renamed to pb_name.
+ * Optional fields that are not set will be set to undefined.
+ * To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
+ * For the list of reserved names please see:
+ *     net/proto2/compiler/js/internal/generator.cc#kKeyword.
+ * @param {boolean=} opt_includeInstance Deprecated. whether to include the
+ *     JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @return {!Object}
+ */
+proto.identity.unauth.RestoreUserRequest.prototype.toObject = function(opt_includeInstance) {
+  return proto.identity.unauth.RestoreUserRequest.toObject(opt_includeInstance, this);
+};
+
+
+/**
+ * Static version of the {@see toObject} method.
+ * @param {boolean|undefined} includeInstance Deprecated. Whether to include
+ *     the JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @param {!proto.identity.unauth.RestoreUserRequest} msg The msg instance to transform.
+ * @return {!Object}
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.identity.unauth.RestoreUserRequest.toObject = function(includeInstance, msg) {
+  var f, obj = {
+    userId: jspb.Message.getFieldWithDefault(msg, 1, ""),
+    siweMessage: jspb.Message.getFieldWithDefault(msg, 2, ""),
+    siweSignature: jspb.Message.getFieldWithDefault(msg, 3, ""),
+    deviceKeyUpload: (f = msg.getDeviceKeyUpload()) && proto.identity.unauth.DeviceKeyUpload.toObject(includeInstance, f),
+    deviceList: jspb.Message.getFieldWithDefault(msg, 5, "")
+  };
+
+  if (includeInstance) {
+    obj.$jspbMessageInstance = msg;
+  }
+  return obj;
+};
+}
+
+
+/**
+ * Deserializes binary data (in protobuf wire format).
+ * @param {jspb.ByteSource} bytes The bytes to deserialize.
+ * @return {!proto.identity.unauth.RestoreUserRequest}
+ */
+proto.identity.unauth.RestoreUserRequest.deserializeBinary = function(bytes) {
+  var reader = new jspb.BinaryReader(bytes);
+  var msg = new proto.identity.unauth.RestoreUserRequest;
+  return proto.identity.unauth.RestoreUserRequest.deserializeBinaryFromReader(msg, reader);
+};
+
+
+/**
+ * Deserializes binary data (in protobuf wire format) from the
+ * given reader into the given message object.
+ * @param {!proto.identity.unauth.RestoreUserRequest} msg The message object to deserialize into.
+ * @param {!jspb.BinaryReader} reader The BinaryReader to use.
+ * @return {!proto.identity.unauth.RestoreUserRequest}
+ */
+proto.identity.unauth.RestoreUserRequest.deserializeBinaryFromReader = function(msg, reader) {
+  while (reader.nextField()) {
+    if (reader.isEndGroup()) {
+      break;
+    }
+    var field = reader.getFieldNumber();
+    switch (field) {
+    case 1:
+      var value = /** @type {string} */ (reader.readString());
+      msg.setUserId(value);
+      break;
+    case 2:
+      var value = /** @type {string} */ (reader.readString());
+      msg.setSiweMessage(value);
+      break;
+    case 3:
+      var value = /** @type {string} */ (reader.readString());
+      msg.setSiweSignature(value);
+      break;
+    case 4:
+      var value = new proto.identity.unauth.DeviceKeyUpload;
+      reader.readMessage(value,proto.identity.unauth.DeviceKeyUpload.deserializeBinaryFromReader);
+      msg.setDeviceKeyUpload(value);
+      break;
+    case 5:
+      var value = /** @type {string} */ (reader.readString());
+      msg.setDeviceList(value);
+      break;
+    default:
+      reader.skipField();
+      break;
+    }
+  }
+  return msg;
+};
+
+
+/**
+ * Serializes the message to binary data (in protobuf wire format).
+ * @return {!Uint8Array}
+ */
+proto.identity.unauth.RestoreUserRequest.prototype.serializeBinary = function() {
+  var writer = new jspb.BinaryWriter();
+  proto.identity.unauth.RestoreUserRequest.serializeBinaryToWriter(this, writer);
+  return writer.getResultBuffer();
+};
+
+
+/**
+ * Serializes the given message to binary data (in protobuf wire
+ * format), writing to the given BinaryWriter.
+ * @param {!proto.identity.unauth.RestoreUserRequest} message
+ * @param {!jspb.BinaryWriter} writer
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.identity.unauth.RestoreUserRequest.serializeBinaryToWriter = function(message, writer) {
+  var f = undefined;
+  f = message.getUserId();
+  if (f.length > 0) {
+    writer.writeString(
+      1,
+      f
+    );
+  }
+  f = /** @type {string} */ (jspb.Message.getField(message, 2));
+  if (f != null) {
+    writer.writeString(
+      2,
+      f
+    );
+  }
+  f = /** @type {string} */ (jspb.Message.getField(message, 3));
+  if (f != null) {
+    writer.writeString(
+      3,
+      f
+    );
+  }
+  f = message.getDeviceKeyUpload();
+  if (f != null) {
+    writer.writeMessage(
+      4,
+      f,
+      proto.identity.unauth.DeviceKeyUpload.serializeBinaryToWriter
+    );
+  }
+  f = message.getDeviceList();
+  if (f.length > 0) {
+    writer.writeString(
+      5,
+      f
+    );
+  }
+};
+
+
+/**
+ * optional string user_id = 1;
+ * @return {string}
+ */
+proto.identity.unauth.RestoreUserRequest.prototype.getUserId = function() {
+  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
+};
+
+
+/**
+ * @param {string} value
+ * @return {!proto.identity.unauth.RestoreUserRequest} returns this
+ */
+proto.identity.unauth.RestoreUserRequest.prototype.setUserId = function(value) {
+  return jspb.Message.setProto3StringField(this, 1, value);
+};
+
+
+/**
+ * optional string siwe_message = 2;
+ * @return {string}
+ */
+proto.identity.unauth.RestoreUserRequest.prototype.getSiweMessage = function() {
+  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, ""));
+};
+
+
+/**
+ * @param {string} value
+ * @return {!proto.identity.unauth.RestoreUserRequest} returns this
+ */
+proto.identity.unauth.RestoreUserRequest.prototype.setSiweMessage = function(value) {
+  return jspb.Message.setField(this, 2, value);
+};
+
+
+/**
+ * Clears the field making it undefined.
+ * @return {!proto.identity.unauth.RestoreUserRequest} returns this
+ */
+proto.identity.unauth.RestoreUserRequest.prototype.clearSiweMessage = function() {
+  return jspb.Message.setField(this, 2, undefined);
+};
+
+
+/**
+ * Returns whether this field is set.
+ * @return {boolean}
+ */
+proto.identity.unauth.RestoreUserRequest.prototype.hasSiweMessage = function() {
+  return jspb.Message.getField(this, 2) != null;
+};
+
+
+/**
+ * optional string siwe_signature = 3;
+ * @return {string}
+ */
+proto.identity.unauth.RestoreUserRequest.prototype.getSiweSignature = function() {
+  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, ""));
+};
+
+
+/**
+ * @param {string} value
+ * @return {!proto.identity.unauth.RestoreUserRequest} returns this
+ */
+proto.identity.unauth.RestoreUserRequest.prototype.setSiweSignature = function(value) {
+  return jspb.Message.setField(this, 3, value);
+};
+
+
+/**
+ * Clears the field making it undefined.
+ * @return {!proto.identity.unauth.RestoreUserRequest} returns this
+ */
+proto.identity.unauth.RestoreUserRequest.prototype.clearSiweSignature = function() {
+  return jspb.Message.setField(this, 3, undefined);
+};
+
+
+/**
+ * Returns whether this field is set.
+ * @return {boolean}
+ */
+proto.identity.unauth.RestoreUserRequest.prototype.hasSiweSignature = function() {
+  return jspb.Message.getField(this, 3) != null;
+};
+
+
+/**
+ * optional DeviceKeyUpload device_key_upload = 4;
+ * @return {?proto.identity.unauth.DeviceKeyUpload}
+ */
+proto.identity.unauth.RestoreUserRequest.prototype.getDeviceKeyUpload = function() {
+  return /** @type{?proto.identity.unauth.DeviceKeyUpload} */ (
+    jspb.Message.getWrapperField(this, proto.identity.unauth.DeviceKeyUpload, 4));
+};
+
+
+/**
+ * @param {?proto.identity.unauth.DeviceKeyUpload|undefined} value
+ * @return {!proto.identity.unauth.RestoreUserRequest} returns this
+*/
+proto.identity.unauth.RestoreUserRequest.prototype.setDeviceKeyUpload = function(value) {
+  return jspb.Message.setWrapperField(this, 4, value);
+};
+
+
+/**
+ * Clears the message field making it undefined.
+ * @return {!proto.identity.unauth.RestoreUserRequest} returns this
+ */
+proto.identity.unauth.RestoreUserRequest.prototype.clearDeviceKeyUpload = function() {
+  return this.setDeviceKeyUpload(undefined);
+};
+
+
+/**
+ * Returns whether this field is set.
+ * @return {boolean}
+ */
+proto.identity.unauth.RestoreUserRequest.prototype.hasDeviceKeyUpload = function() {
+  return jspb.Message.getField(this, 4) != null;
+};
+
+
+/**
+ * optional string device_list = 5;
+ * @return {string}
+ */
+proto.identity.unauth.RestoreUserRequest.prototype.getDeviceList = function() {
+  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 5, ""));
+};
+
+
+/**
+ * @param {string} value
+ * @return {!proto.identity.unauth.RestoreUserRequest} returns this
+ */
+proto.identity.unauth.RestoreUserRequest.prototype.setDeviceList = function(value) {
+  return jspb.Message.setProto3StringField(this, 5, value);
+};
+
+
+
+
+
 if (jspb.Message.GENERATE_TO_OBJECT) {
 /**
  * Creates an object representation of this proto.
diff --git a/web/protobufs/identity-unauth-structs.cjs.flow b/web/protobufs/identity-unauth-structs.cjs.flow
--- a/web/protobufs/identity-unauth-structs.cjs.flow
+++ b/web/protobufs/identity-unauth-structs.cjs.flow
@@ -362,6 +362,44 @@
   initialDeviceList: string,
 };
 
+declare export class RestoreUserRequest extends Message {
+  getUserId(): string;
+  setUserId(value: string): RestoreUserRequest;
+
+  getSiweMessage(): string;
+  setSiweMessage(value: string): RestoreUserRequest;
+  hasSiweMessage(): boolean;
+  clearSiweMessage(): RestoreUserRequest;
+
+  getSiweSignature(): string;
+  setSiweSignature(value: string): RestoreUserRequest;
+  hasSiweSignature(): boolean;
+  clearSiweSignature(): RestoreUserRequest;
+
+  getDeviceKeyUpload(): DeviceKeyUpload | void;
+  setDeviceKeyUpload(value?: DeviceKeyUpload): RestoreUserRequest;
+  hasDeviceKeyUpload(): boolean;
+  clearDeviceKeyUpload(): RestoreUserRequest;
+
+  getDeviceList(): string;
+  setDeviceList(value: string): RestoreUserRequest;
+
+  serializeBinary(): Uint8Array;
+  toObject(includeInstance?: boolean): RestoreUserRequestObject;
+  static toObject(includeInstance: boolean, msg: RestoreUserRequest): RestoreUserRequestObject;
+  static serializeBinaryToWriter(message: RestoreUserRequest, writer: BinaryWriter): void;
+  static deserializeBinary(bytes: Uint8Array): RestoreUserRequest;
+  static deserializeBinaryFromReader(message: RestoreUserRequest, reader: BinaryReader): RestoreUserRequest;
+}
+
+export type RestoreUserRequestObject = {
+  userId: string,
+  siweMessage?: string,
+  siweSignature?: string,
+  deviceKeyUpload?: DeviceKeyUploadObject,
+  deviceList: string,
+}
+
 declare export class SecondaryDeviceKeysUploadRequest extends Message {
   getUserId(): string;
   setUserId(value: string): SecondaryDeviceKeysUploadRequest;
diff --git a/web/protobufs/identity-unauth.cjs b/web/protobufs/identity-unauth.cjs
--- a/web/protobufs/identity-unauth.cjs
+++ b/web/protobufs/identity-unauth.cjs
@@ -625,6 +625,67 @@
 };
 
 
+/**
+ * @const
+ * @type {!grpc.web.MethodDescriptor<
+ *   !proto.identity.unauth.RestoreUserRequest,
+ *   !proto.identity.unauth.AuthResponse>}
+ */
+const methodDescriptor_IdentityClientService_RestoreUser = new grpc.web.MethodDescriptor(
+  '/identity.unauth.IdentityClientService/RestoreUser',
+  grpc.web.MethodType.UNARY,
+  proto.identity.unauth.RestoreUserRequest,
+  proto.identity.unauth.AuthResponse,
+  /**
+   * @param {!proto.identity.unauth.RestoreUserRequest} request
+   * @return {!Uint8Array}
+   */
+  function(request) {
+    return request.serializeBinary();
+  },
+  proto.identity.unauth.AuthResponse.deserializeBinary
+);
+
+
+/**
+ * @param {!proto.identity.unauth.RestoreUserRequest} request The
+ *     request proto
+ * @param {?Object<string, string>} metadata User defined
+ *     call metadata
+ * @param {function(?grpc.web.RpcError, ?proto.identity.unauth.AuthResponse)}
+ *     callback The callback function(error, response)
+ * @return {!grpc.web.ClientReadableStream<!proto.identity.unauth.AuthResponse>|undefined}
+ *     The XHR Node Readable Stream
+ */
+proto.identity.unauth.IdentityClientServiceClient.prototype.restoreUser =
+    function(request, metadata, callback) {
+  return this.client_.rpcCall(this.hostname_ +
+      '/identity.unauth.IdentityClientService/RestoreUser',
+      request,
+      metadata || {},
+      methodDescriptor_IdentityClientService_RestoreUser,
+      callback);
+};
+
+
+/**
+ * @param {!proto.identity.unauth.RestoreUserRequest} request The
+ *     request proto
+ * @param {?Object<string, string>=} metadata User defined
+ *     call metadata
+ * @return {!Promise<!proto.identity.unauth.AuthResponse>}
+ *     Promise that resolves to the response
+ */
+proto.identity.unauth.IdentityClientServicePromiseClient.prototype.restoreUser =
+    function(request, metadata) {
+  return this.client_.unaryCall(this.hostname_ +
+      '/identity.unauth.IdentityClientService/RestoreUser',
+      request,
+      metadata || {},
+      methodDescriptor_IdentityClientService_RestoreUser);
+};
+
+
 /**
  * @const
  * @type {!grpc.web.MethodDescriptor<
@@ -1053,3 +1114,4 @@
 
 
 module.exports = proto.identity.unauth;
+
diff --git a/web/protobufs/identity-unauth.cjs.flow b/web/protobufs/identity-unauth.cjs.flow
--- a/web/protobufs/identity-unauth.cjs.flow
+++ b/web/protobufs/identity-unauth.cjs.flow
@@ -72,6 +72,13 @@
                response: identityStructs.AuthResponse) => void
   ): grpcWeb.ClientReadableStream<identityStructs.AuthResponse>;
 
+  restoreUser(
+    request: identityStructs.RestoreUserRequest,
+    metadata: grpcWeb.Metadata | void,
+    callback: (err: grpcWeb.RpcError,
+               response: identityStructs.AuthResponse) => void
+  ): grpcWeb.ClientReadableStream<identityStructs.AuthResponse>;
+
   verifyUserAccessToken(
     request: identityStructs.VerifyUserAccessTokenRequest,
     metadata: grpcWeb.Metadata | void,
@@ -172,6 +179,11 @@
     metadata?: grpcWeb.Metadata
   ): Promise<identityStructs.AuthResponse>;
 
+  restoreUser(
+    request: identityStructs.RestoreUserRequest,
+    metadata?: grpcWeb.Metadata
+  ): Promise<identityStructs.AuthResponse>;
+
   verifyUserAccessToken(
     request: identityStructs.VerifyUserAccessTokenRequest,
     metadata?: grpcWeb.Metadata