diff --git a/keyserver/src/keyserver.js b/keyserver/src/keyserver.js
--- a/keyserver/src/keyserver.js
+++ b/keyserver/src/keyserver.js
@@ -30,6 +30,7 @@
   multimediaUploadResponder,
   uploadDownloadResponder,
 } from './uploads/uploads.js';
+import { verifyUserLoggedIn } from './user/login.js';
 import { initENSCache } from './utils/ens-cache.js';
 import {
   prefetchAllURLFacts,
@@ -58,6 +59,14 @@
       // in https://github.com/remy/nodemon/issues/751
       process.exit(2);
     }
+
+    // Allow login to be optional until staging environment is available
+    try {
+      await verifyUserLoggedIn();
+    } catch (e) {
+      console.warn('failed_identity_login');
+    }
+
     const cpuCount = os.cpus().length;
     for (let i = 0; i < cpuCount; i++) {
       cluster.fork();
diff --git a/keyserver/src/updaters/olm-account-updater.js b/keyserver/src/updaters/olm-account-updater.js
--- a/keyserver/src/updaters/olm-account-updater.js
+++ b/keyserver/src/updaters/olm-account-updater.js
@@ -13,7 +13,7 @@
 
 async function fetchCallUpdateOlmAccount<T>(
   olmAccountType: 'content' | 'notifications',
-  callback: (account: OlmAccount, picklingKey: string) => Promise<T>,
+  callback: (account: OlmAccount, picklingKey: string) => Promise<T> | T,
 ): Promise<T> {
   const isContent = olmAccountType === 'content';
   let retriesLeft = maxOlmAccountUpdateRetriesCount;
diff --git a/keyserver/src/user/login.js b/keyserver/src/user/login.js
new file mode 100644
--- /dev/null
+++ b/keyserver/src/user/login.js
@@ -0,0 +1,199 @@
+// @flow
+
+import type { Account as OlmAccount } from '@commapp/olm';
+import type { QueryResults } from 'mysql';
+import { getRustAPI } from 'rust-node-addon';
+
+import type { OLMOneTimeKeys } from 'lib/types/crypto-types';
+import { getCommConfig } from 'lib/utils/comm-config.js';
+import { ServerError } from 'lib/utils/errors.js';
+import { values } from 'lib/utils/objects.js';
+
+import { SQL, dbQuery } from '../database/database.js';
+import { getMessageForException } from '../responders/utils.js';
+import { fetchCallUpdateOlmAccount } from '../updaters/olm-account-updater.js';
+import { validateAccountPrekey } from '../utils/olm-utils.js';
+
+type UserCredentials = { +username: string, +password: string };
+type IdentityInfo = { +userId: string, +accessToken: string };
+
+const userIDMetadataKey = 'user_id';
+const accessTokenMetadataKey = 'access_token';
+
+export type AccountKeysSet = {
+  +identityKeys: string,
+  +prekey: string,
+  +prekeySignature: string,
+  +oneTimeKey: $ReadOnlyArray<string>,
+};
+
+function getOneTimeKeyValues(keyBlob: string): $ReadOnlyArray<string> {
+  const content: OLMOneTimeKeys = JSON.parse(keyBlob);
+  const keys: $ReadOnlyArray<string> = values(content.curve25519);
+  return keys;
+}
+
+function retrieveAccountKeysSet(account: OlmAccount): AccountKeysSet {
+  const identityKeys = account.identity_keys();
+
+  validateAccountPrekey(account);
+  const prekeyMap = JSON.parse(account.prekey()).curve25519;
+  const [prekey] = values(prekeyMap);
+  const prekeySignature = account.prekey_signature();
+
+  if (!prekeySignature || !prekey) {
+    throw new ServerError('invalid_prekey');
+  }
+
+  if (getOneTimeKeyValues(account.one_time_keys()).length < 10) {
+    account.generate_one_time_keys(10);
+  }
+
+  const oneTimeKey = getOneTimeKeyValues(account.one_time_keys());
+
+  return { identityKeys, oneTimeKey, prekey, prekeySignature };
+}
+
+// After register or login is successful
+function markKeysAsPublished(account: OlmAccount) {
+  account.mark_prekey_as_published();
+  account.mark_keys_as_published();
+}
+
+async function fetchIdentityInfo(): Promise<?IdentityInfo> {
+  const versionQuery = SQL`
+    SELECT data
+    FROM metadata
+    WHERE name IN (${userIDMetadataKey}, ${accessTokenMetadataKey})
+  `;
+
+  const [[userId, accessToken]] = await dbQuery(versionQuery);
+  if (!userId || !accessToken) {
+    return null;
+  }
+  return { userId, accessToken };
+}
+
+function saveIdentityInfo(userInfo: IdentityInfo): Promise<QueryResults> {
+  const updateQuery = SQL`
+    REPLACE INTO metadata (name, data)
+    VALUES (${userIDMetadataKey}, ${userInfo.userId}),
+      (${accessTokenMetadataKey}, ${userInfo.accessToken})
+  `;
+
+  return dbQuery(updateQuery);
+}
+
+async function verifyUserLoggedIn(): Promise<IdentityInfo> {
+  const result = await fetchIdentityInfo();
+
+  if (result) {
+    return result;
+  }
+
+  const identityInfo = await registerOrLogin();
+  await saveIdentityInfo(identityInfo);
+  return identityInfo;
+}
+
+async function registerOrLogin(): Promise<IdentityInfo> {
+  const rustAPIPromise = getRustAPI();
+
+  const userInfo = await getCommConfig<UserCredentials>({
+    folder: 'secrets',
+    name: 'user_credentials',
+  });
+
+  if (!userInfo) {
+    throw new ServerError('missing_user_credentials');
+  }
+
+  const {
+    identityKeys: notificationsIdentityKeys,
+    prekey: notificationsPrekey,
+    prekeySignature: notificationsPrekeySignature,
+    oneTimeKey: notificationsOneTimeKey,
+  } = await fetchCallUpdateOlmAccount('notifications', retrieveAccountKeysSet);
+
+  const contentAccountCallback = async (account: OlmAccount) => {
+    const {
+      identityKeys: contentIdentityKeys,
+      oneTimeKey,
+      prekey,
+      prekeySignature,
+    } = await retrieveAccountKeysSet(account);
+
+    const identityKeysBlob = {
+      primaryIdentityPublicKeys: JSON.parse(contentIdentityKeys),
+      notificationIdentityPublicKeys: JSON.parse(notificationsIdentityKeys),
+    };
+    const identityKeysBlobPayload = JSON.stringify(identityKeysBlob);
+    const signedIdentityKeysBlob = {
+      payload: identityKeysBlobPayload,
+      signature: account.sign(identityKeysBlobPayload),
+    };
+
+    return {
+      signedIdentityKeysBlob,
+      oneTimeKey,
+      prekey,
+      prekeySignature,
+    };
+  };
+
+  const [
+    rustAPI,
+    {
+      signedIdentityKeysBlob,
+      prekey: contentPrekey,
+      prekeySignature: contentPrekeySignature,
+      oneTimeKey: contentOneTimeKey,
+    },
+  ] = await Promise.all([
+    rustAPIPromise,
+    fetchCallUpdateOlmAccount('content', contentAccountCallback),
+  ]);
+
+  try {
+    const identity_info = await rustAPI.loginUser(
+      userInfo.username,
+      userInfo.password,
+      signedIdentityKeysBlob,
+      contentPrekey,
+      contentPrekeySignature,
+      notificationsPrekey,
+      notificationsPrekeySignature,
+      contentOneTimeKey,
+      notificationsOneTimeKey,
+    );
+    await Promise.all([
+      fetchCallUpdateOlmAccount('content', markKeysAsPublished),
+      fetchCallUpdateOlmAccount('notifications', markKeysAsPublished),
+    ]);
+    return identity_info;
+  } catch (e) {
+    try {
+      const identity_info = await rustAPI.registerUser(
+        userInfo.username,
+        userInfo.password,
+        signedIdentityKeysBlob,
+        contentPrekey,
+        contentPrekeySignature,
+        notificationsPrekey,
+        notificationsPrekeySignature,
+        contentOneTimeKey,
+        notificationsOneTimeKey,
+      );
+      await Promise.all([
+        fetchCallUpdateOlmAccount('content', markKeysAsPublished),
+        fetchCallUpdateOlmAccount('notifications', markKeysAsPublished),
+      ]);
+      return identity_info;
+    } catch (err) {
+      console.warn('Failed to register user: ' + getMessageForException(err));
+      throw new ServerError('identity_auth_failed');
+    }
+  }
+}
+
+export { verifyUserLoggedIn };
diff --git a/keyserver/src/utils/olm-utils.js b/keyserver/src/utils/olm-utils.js
--- a/keyserver/src/utils/olm-utils.js
+++ b/keyserver/src/utils/olm-utils.js
@@ -77,7 +77,7 @@
   return cachedOLMUtility;
 }
 
-async function validateAccountPrekey(account: OlmAccount): Promise<void> {
+function validateAccountPrekey(account: OlmAccount) {
   const currentDate = new Date();
   const lastPrekeyPublishDate = new Date(account.last_prekey_publish_time());