diff --git a/keyserver/src/responders/redux-state-responders.js b/keyserver/src/responders/redux-state-responders.js
--- a/keyserver/src/responders/redux-state-responders.js
+++ b/keyserver/src/responders/redux-state-responders.js
@@ -8,6 +8,7 @@
 import { freshMessageStore } from 'lib/reducers/message-reducer.js';
 import { mostRecentlyReadThread } from 'lib/selectors/thread-selectors.js';
 import { mostRecentMessageTimestamp } from 'lib/shared/message-utils.js';
+import { getThreadHashesFromThreadInfos } from 'lib/shared/thread-store-utils.js';
 import {
   threadHasPermission,
   threadIsPending,
@@ -33,6 +34,7 @@
 } from 'lib/types/user-types.js';
 import { currentDateInTimeZone } from 'lib/utils/date-utils.js';
 import { ServerError } from 'lib/utils/errors.js';
+import { values } from 'lib/utils/objects.js';
 import { promiseAll } from 'lib/utils/promises.js';
 import type { URLInfo } from 'lib/utils/url-utils.js';
 import { tShape, ashoatKeyserverID } from 'lib/utils/validation-utils.js';
@@ -149,7 +151,13 @@
       threadInfoPromise,
       hasNotAcknowledgedPoliciesPromise,
     ]);
-    return { threadInfos: hasNotAcknowledgedPolicies ? {} : threadInfos };
+    if (hasNotAcknowledgedPolicies) {
+      return { threadInfos: {}, threadHashes: {} };
+    }
+    return {
+      threadInfos,
+      threadHashes: getThreadHashesFromThreadInfos(values(threadInfos), false),
+    };
   })();
   const messageStorePromise = (async () => {
     const [
diff --git a/lib/ops/thread-store-ops.js b/lib/ops/thread-store-ops.js
--- a/lib/ops/thread-store-ops.js
+++ b/lib/ops/thread-store-ops.js
@@ -1,6 +1,7 @@
 // @flow
 
 import { type BaseStoreOpsHandlers } from './base-ops.js';
+import { getThreadHashFromRawThreadInfo } from '../shared/thread-store-utils.js';
 import type {
   ClientDBThreadInfo,
   RawThreadInfo,
@@ -55,18 +56,29 @@
       return store;
     }
     let processedThreads = { ...store.threadInfos };
+    let processedHashes = { ...store.threadHashes };
     for (const operation of ops) {
       if (operation.type === 'replace') {
         processedThreads[operation.payload.id] = operation.payload.threadInfo;
+        processedHashes[operation.payload.id] = getThreadHashFromRawThreadInfo(
+          operation.payload.threadInfo,
+          true,
+        );
       } else if (operation.type === 'remove') {
         for (const id of operation.payload.ids) {
           delete processedThreads[id];
+          delete processedHashes[id];
         }
       } else if (operation.type === 'remove_all') {
         processedThreads = {};
+        processedHashes = {};
       }
     }
-    return { ...store, threadInfos: processedThreads };
+    return {
+      ...store,
+      threadInfos: processedThreads,
+      threadHashes: processedHashes,
+    };
   },
 
   convertOpsToClientDBOps(
diff --git a/lib/shared/thread-store-utils.js b/lib/shared/thread-store-utils.js
new file mode 100644
--- /dev/null
+++ b/lib/shared/thread-store-utils.js
@@ -0,0 +1,40 @@
+// @flow
+
+import type { RawThreadInfo } from '../types/thread-types.js';
+import { rawThreadInfoValidator } from '../types/thread-types.js';
+import { convertClientIDsToServerIDs } from '../utils/conversion-utils.js';
+import { hash } from '../utils/objects.js';
+import { ashoatKeyserverID } from '../utils/validation-utils.js';
+
+function getThreadHashFromRawThreadInfo(
+  threadInfo: RawThreadInfo,
+  shouldConvert: boolean,
+): number {
+  let threadInfoToHash = threadInfo;
+  if (shouldConvert) {
+    threadInfoToHash = convertClientIDsToServerIDs(
+      ashoatKeyserverID,
+      rawThreadInfoValidator,
+      threadInfoToHash,
+    );
+  }
+  return hash(threadInfoToHash);
+}
+
+function getThreadHashesFromThreadInfos(
+  threadInfos: $ReadOnlyArray<RawThreadInfo>,
+  shouldConvert: boolean,
+): { +[string]: number } {
+  const threadHashes: { [string]: number } = {};
+
+  for (const threadInfo of threadInfos) {
+    threadHashes[threadInfo.id] = getThreadHashFromRawThreadInfo(
+      threadInfo,
+      shouldConvert,
+    );
+  }
+
+  return threadHashes;
+}
+
+export { getThreadHashFromRawThreadInfo, getThreadHashesFromThreadInfos };
diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js
--- a/lib/types/thread-types.js
+++ b/lib/types/thread-types.js
@@ -215,10 +215,12 @@
 
 export type ThreadStore = {
   +threadInfos: { +[id: string]: RawThreadInfo },
+  +threadHashes: { +[id: string]: number },
 };
 export const threadStoreValidator: TInterface<ThreadStore> =
   tShape<ThreadStore>({
     threadInfos: t.dict(tID, rawThreadInfoValidator),
+    threadHashes: t.dict(tID, t.Number),
   });
 
 export type ClientDBThreadInfo = {
diff --git a/native/redux/default-state.js b/native/redux/default-state.js
--- a/native/redux/default-state.js
+++ b/native/redux/default-state.js
@@ -29,6 +29,7 @@
   },
   threadStore: {
     threadInfos: {},
+    threadHashes: {},
   },
   userStore: {
     userInfos: {},
diff --git a/native/redux/persist.js b/native/redux/persist.js
--- a/native/redux/persist.js
+++ b/native/redux/persist.js
@@ -30,6 +30,7 @@
 import { highestLocalIDSelector } from 'lib/selectors/local-id-selectors.js';
 import { createAsyncMigrate } from 'lib/shared/create-async-migrate.js';
 import { inconsistencyResponsesToReports } from 'lib/shared/report-utils.js';
+import { getThreadHashesFromThreadInfos } from 'lib/shared/thread-store-utils.js';
 import {
   getContainingThreadID,
   getCommunity,
@@ -58,7 +59,11 @@
   defaultConnectionInfo,
   type ConnectionInfo,
 } from 'lib/types/socket-types.js';
-import type { ClientDBThreadInfo } from 'lib/types/thread-types.js';
+import type {
+  ClientDBThreadInfo,
+  RawThreadInfos,
+  ThreadStore,
+} from 'lib/types/thread-types.js';
 import {
   translateClientDBMessageInfoToRawMessageInfo,
   translateRawMessageInfoToClientDBMessageInfo,
@@ -776,6 +781,20 @@
       },
     };
   },
+  [52]: async state => {
+    const clientDBThreadInfos = commCoreModule.getAllThreadsSync();
+    const rawThreadInfos = clientDBThreadInfos.map(
+      convertClientDBThreadInfoToRawThreadInfo,
+    );
+
+    return {
+      ...state,
+      threadStore: {
+        ...state.threadStore,
+        threadHashes: getThreadHashesFromThreadInfos(rawThreadInfos, true),
+      },
+    };
+  },
 };
 
 // After migration 31, we'll no longer want to persist `messageStore.messages`
@@ -891,6 +910,33 @@
   { whitelist: ['keyserverStore'] },
 );
 
+function transformAfterVersion<State, PersistState>(
+  version: number,
+  inbound: State => PersistState,
+) {
+  return (state, key, wholeState) => {
+    if (wholeState._persist?.version > version) {
+      return state;
+    }
+    return (inbound(state): any);
+  };
+}
+
+type PersistedThreadStore = $Diff<
+  ThreadStore,
+  { +threadInfos: RawThreadInfos },
+>;
+const threadStoreTransform: Transform = createTransform(
+  (state: ThreadStore): PersistedThreadStore => {
+    const { threadInfos, ...rest } = state;
+    return rest;
+  },
+  transformAfterVersion(51, (state: PersistedThreadStore): ThreadStore => {
+    return { ...state, threadInfos: {} };
+  }),
+  { whitelist: ['threadStore'] },
+);
+
 const persistConfig = {
   key: 'root',
   storage: AsyncStorage,
@@ -902,16 +948,16 @@
     'connectivity',
     'deviceOrientation',
     'frozen',
-    'threadStore',
     'storeLoaded',
     'connection',
   ],
   debug: __DEV__,
-  version: 51,
+  version: 52,
   transforms: [
     messageStoreMessagesBlocklistTransform,
     reportStoreTransform,
     keyserverStoreTransform,
+    threadStoreTransform,
   ],
   migrate: (createAsyncMigrate(migrations, { debug: __DEV__ }): any),
   timeout: ((__DEV__ ? 0 : undefined): number | void),
diff --git a/web/redux/default-state.js b/web/redux/default-state.js
--- a/web/redux/default-state.js
+++ b/web/redux/default-state.js
@@ -25,6 +25,7 @@
   },
   threadStore: {
     threadInfos: {},
+    threadHashes: {},
   },
   userStore: {
     userInfos: {},