diff --git a/lib/permissions/minimally-encoded-thread-permissions.js b/lib/permissions/minimally-encoded-thread-permissions.js
--- a/lib/permissions/minimally-encoded-thread-permissions.js
+++ b/lib/permissions/minimally-encoded-thread-permissions.js
@@ -1,23 +1,40 @@
 // @flow
 
 import invariant from 'invariant';
+import _isEqual from 'lodash/fp/isEqual.js';
+import _mapValues from 'lodash/fp/mapValues.js';
 
 import {
-  parseThreadPermissionString,
   constructThreadPermissionString,
+  parseThreadPermissionString,
 } from './prefixes.js';
+import { specialRoles } from './special-roles.js';
 import {
+  getAllThreadPermissions,
+  getRolePermissionBlobs,
+  makePermissionsBlob,
+  makePermissionsForChildrenBlob,
+} from './thread-permissions.js';
+import type { ThreadStoreOperation } from '../ops/thread-store-ops.js';
+import { getChildThreads } from '../selectors/thread-selectors.js';
+import type {
+  MinimallyEncodedThickMemberInfo,
+  ThickRawThreadInfo,
+} from '../types/minimally-encoded-thread-permissions-types.js';
+import {
+  assertThreadPermission,
+  assertThreadPermissionFilterPrefix,
+  assertThreadPermissionMembershipPrefix,
+  assertThreadPermissionPropagationPrefix,
   type ThreadPermission,
   type ThreadPermissionInfo,
+  threadPermissions,
+  type ThreadPermissionsBlob,
   type ThreadPermissionsInfo,
   type ThreadRolePermissionsBlob,
-  threadPermissions,
-  assertThreadPermission,
-  assertThreadPermissionPropagationPrefix,
-  assertThreadPermissionFilterPrefix,
-  assertThreadPermissionMembershipPrefix,
 } from '../types/thread-permission-types.js';
-import { entries, invertObjectToMap } from '../utils/objects.js';
+import type { RawThreadInfos } from '../types/thread-types.js';
+import { entries, invertObjectToMap, values } from '../utils/objects.js';
 import type { TRegex } from '../utils/validation-utils.js';
 import { tRegex } from '../utils/validation-utils.js';
 
@@ -246,6 +263,214 @@
     ]),
   );
 
+function updateRolesAndPermissions(
+  threads: RawThreadInfos,
+  rolePermissionsUpdater: ThreadRolePermissionsBlob => ThreadRolePermissionsBlob,
+): {
+  +operations: $ReadOnlyArray<ThreadStoreOperation>,
+} {
+  const updatedThreads = { ...threads };
+
+  const childThreads = getChildThreads(threads);
+  const threadChildrenIDs = _mapValues(
+    threadChildren => threadChildren.map(thread => thread.id),
+    childThreads,
+  );
+  function updateRoles(threadID: string) {
+    const threadInfo = updatedThreads[threadID];
+
+    const roles = { ...threadInfo.roles };
+    for (const roleID in roles) {
+      const role = roles[roleID];
+
+      const rolePermissionBlobs = getRolePermissionBlobs(threadInfo.type);
+      let updatedPermissionsBlob;
+      if (threadInfo.thick) {
+        // Each thick thread has exactly one role - for its members. We
+        // don't allow managing this role, which means we can simply get the
+        // computed permissions from the rolePermissionBlobs.
+        updatedPermissionsBlob = rolePermissionBlobs.Members;
+      } else if (
+        role.specialRole === specialRoles.ADMIN_ROLE &&
+        rolePermissionBlobs.Admins
+      ) {
+        // We don't allow managing the admin role, so in this case we can
+        // also use the result of getRolePermissionBlobs that should contain
+        // all the required changes.
+        updatedPermissionsBlob = rolePermissionBlobs.Admins;
+      } else {
+        // We allow admins to manage non-admin roles, which means we can't
+        // simply rely on the result of getRolePermissionBlobs - it doesn't
+        // contain any changes made by admins. In this case, we're running the
+        // rolePermissionsUpdater that updates the role accordingly. It is
+        // similar to what we do on the keyserver during a migration in
+        // addNewUserSurfacedPermission inside migration-config.js.
+        updatedPermissionsBlob = rolePermissionsUpdater(
+          decodeThreadRolePermissionsBitmaskArray(role.permissions),
+        );
+      }
+      const encodedUpdatedPermissions = threadRolePermissionsBlobToBitmaskArray(
+        updatedPermissionsBlob,
+      );
+      roles[roleID] = {
+        ...role,
+        permissions: encodedUpdatedPermissions,
+      };
+      updatedThreads[threadID] = {
+        ...threadInfo,
+        roles,
+      };
+    }
+  }
+
+  type MemberToThreadPermissions = {
+    [member: string]: ?ThreadPermissionsBlob,
+  };
+
+  function updateThickThreadMembers(
+    threadInfo: ThickRawThreadInfo,
+    memberToThreadPermissionsFromParent: ?MemberToThreadPermissions,
+  ): {
+    +members: $ReadOnlyArray<MinimallyEncodedThickMemberInfo>,
+    +memberToThreadPermissionsForChildren: MemberToThreadPermissions,
+  } {
+    const updatedMembers = [];
+    const memberToThreadPermissionsForChildren: MemberToThreadPermissions = {};
+    for (const member of threadInfo.members) {
+      const { id, role } = member;
+
+      const rolePermissions = role
+        ? decodeThreadRolePermissionsBitmaskArray(
+            threadInfo.roles[role].permissions,
+          )
+        : null;
+      const permissionsFromParent = memberToThreadPermissionsFromParent?.[id];
+
+      const computedPermissions = makePermissionsBlob(
+        rolePermissions,
+        permissionsFromParent,
+        threadInfo.id,
+        threadInfo.type,
+      );
+
+      updatedMembers.push({
+        ...member,
+        permissions: permissionsToBitmaskHex(
+          getAllThreadPermissions(computedPermissions, threadInfo.id),
+        ),
+      });
+
+      memberToThreadPermissionsForChildren[member.id] =
+        makePermissionsForChildrenBlob(computedPermissions);
+    }
+    return {
+      members: updatedMembers,
+      memberToThreadPermissionsForChildren,
+    };
+  }
+
+  function recursivelyUpdateThickThreadMemberPermissions(
+    threadID: string,
+    memberToThreadPermissionsFromParent: ?MemberToThreadPermissions,
+  ) {
+    const threadInfo = updatedThreads[threadID];
+    if (!threadInfo.thick) {
+      // We don't update members of thin threads because we aren't storing
+      // their permissions inside their member info. Member permissions are
+      // instead computed on the fly based on the roles. We're planning to
+      // use the same approach for thick threads in the future
+      // https://linear.app/comm/issue/ENG-9404/remove-permissions-from-members-in-thickrawthreadinfo
+      return;
+    }
+
+    const { members: updatedMembers, memberToThreadPermissionsForChildren } =
+      updateThickThreadMembers(threadInfo, memberToThreadPermissionsFromParent);
+    updatedThreads[threadID] = {
+      ...threadInfo,
+      members: updatedMembers,
+    };
+    for (const childID of threadChildrenIDs[threadID] ?? []) {
+      recursivelyUpdateThickThreadMemberPermissions(
+        childID,
+        memberToThreadPermissionsForChildren,
+      );
+    }
+  }
+
+  function recursivelyUpdateCurrentUserPermissions(
+    threadID: string,
+    permissionsFromParent: ?ThreadPermissionsBlob,
+  ) {
+    const threadInfo = updatedThreads[threadID];
+    const { currentUser, roles } = threadInfo;
+    const { role } = currentUser;
+
+    const rolePermissions = role
+      ? decodeThreadRolePermissionsBitmaskArray(roles[role].permissions)
+      : null;
+    const computedPermissions = makePermissionsBlob(
+      rolePermissions,
+      permissionsFromParent,
+      threadInfo.id,
+      threadInfo.type,
+    );
+
+    updatedThreads[threadID] = {
+      ...threadInfo,
+      currentUser: {
+        ...currentUser,
+        permissions: permissionsToBitmaskHex(
+          getAllThreadPermissions(computedPermissions, threadInfo.id),
+        ),
+      },
+    };
+
+    for (const childID of threadChildrenIDs[threadID] ?? []) {
+      recursivelyUpdateCurrentUserPermissions(
+        childID,
+        makePermissionsForChildrenBlob(computedPermissions),
+      );
+    }
+  }
+
+  for (const thread of values(threads)) {
+    if (!thread.parentThreadID) {
+      // We don't need to update these recursively because roles don't
+      // cascade from parents - modifying a parent thread role doesn't have
+      // any impact on children roles. This stays in contrast to how our
+      // permissions work - setting a parent role can affect which
+      // permissions are granted in the children threads, because we're
+      // propagating the permissions (based on a couple of strategies).
+      updateRoles(thread.id);
+    }
+  }
+  const rootThreadIDs = values(threads)
+    .filter(thread => !thread.parentThreadID)
+    .map(thread => thread.id);
+  rootThreadIDs.forEach(threadID =>
+    recursivelyUpdateThickThreadMemberPermissions(threadID, null),
+  );
+  rootThreadIDs.forEach(threadID =>
+    recursivelyUpdateCurrentUserPermissions(threadID, null),
+  );
+
+  const operations = values(updatedThreads)
+    .filter(
+      updatedThread => !_isEqual(updatedThread, threads[updatedThread.id]),
+    )
+    .map(thread => ({
+      type: 'replace',
+      payload: {
+        id: thread.id,
+        threadInfo: thread,
+      },
+    }));
+
+  return {
+    operations,
+  };
+}
+
 export {
   permissionsToBitmaskHex,
   threadPermissionsFromBitmaskHex,
@@ -254,6 +479,7 @@
   decodeRolePermissionBitmask,
   threadRolePermissionsBlobToBitmaskArray,
   decodeThreadRolePermissionsBitmaskArray,
+  updateRolesAndPermissions,
   tHexEncodedRolePermission,
   tHexEncodedPermissionsBitmask,
 };
diff --git a/lib/selectors/thread-selectors.js b/lib/selectors/thread-selectors.js
--- a/lib/selectors/thread-selectors.js
+++ b/lib/selectors/thread-selectors.js
@@ -180,28 +180,29 @@
   },
 );
 
+function getChildThreads<T: RawThreadInfo | ThreadInfo>(threadInfos: {
+  +[id: string]: T,
+}): { +[id: string]: Array<T> } {
+  const result: {
+    [string]: Array<T>,
+  } = {};
+  for (const id in threadInfos) {
+    const threadInfo = threadInfos[id];
+    const { parentThreadID } = threadInfo;
+    if (parentThreadID === null || parentThreadID === undefined) {
+      continue;
+    }
+    if (result[parentThreadID] === undefined) {
+      result[parentThreadID] = ([]: Array<T>);
+    }
+    result[parentThreadID].push(threadInfo);
+  }
+  return result;
+}
+
 const childThreadInfos: (state: BaseAppState<>) => {
   +[id: string]: $ReadOnlyArray<ThreadInfo>,
-} = createSelector(
-  threadInfoSelector,
-  (threadInfos: { +[id: string]: ThreadInfo }) => {
-    const result: {
-      [string]: ThreadInfo[],
-    } = {};
-    for (const id in threadInfos) {
-      const threadInfo = threadInfos[id];
-      const parentThreadID = threadInfo.parentThreadID;
-      if (parentThreadID === null || parentThreadID === undefined) {
-        continue;
-      }
-      if (result[parentThreadID] === undefined) {
-        result[parentThreadID] = ([]: ThreadInfo[]);
-      }
-      result[parentThreadID].push(threadInfo);
-    }
-    return result;
-  },
-);
+} = createSelector(threadInfoSelector, getChildThreads);
 
 const containedThreadInfos: (state: BaseAppState<>) => {
   +[id: string]: $ReadOnlyArray<ThreadInfo>,
@@ -587,4 +588,5 @@
   threadInfosSelectorForThreadType,
   thickRawThreadInfosSelector,
   unreadThickThreadIDsSelector,
+  getChildThreads,
 };
diff --git a/native/redux/persist-constants.js b/native/redux/persist-constants.js
--- a/native/redux/persist-constants.js
+++ b/native/redux/persist-constants.js
@@ -1,6 +1,6 @@
 // @flow
 
 const rootKey = 'root';
-const storeVersion = 87;
+const storeVersion = 88;
 
 export { rootKey, storeVersion };
diff --git a/native/redux/persist.js b/native/redux/persist.js
--- a/native/redux/persist.js
+++ b/native/redux/persist.js
@@ -2,6 +2,7 @@
 
 import AsyncStorage from '@react-native-async-storage/async-storage';
 import invariant from 'invariant';
+import _keyBy from 'lodash/fp/keyBy.js';
 import { Platform } from 'react-native';
 import Orientation from 'react-native-orientation-locker';
 import { createTransform } from 'redux-persist';
@@ -56,6 +57,7 @@
   convertUserInfosToReplaceUserOps,
   userStoreOpsHandlers,
 } from 'lib/ops/user-store-ops.js';
+import { updateRolesAndPermissions } from 'lib/permissions/minimally-encoded-thread-permissions.js';
 import { patchRawThreadInfosWithSpecialRole } from 'lib/permissions/special-roles.js';
 import { filterThreadIDsInFilterList } from 'lib/reducers/calendar-filters-reducer.js';
 import { highestLocalIDSelector } from 'lib/selectors/local-id-selectors.js';
@@ -103,6 +105,10 @@
 } from 'lib/types/report-types.js';
 import { defaultConnectionInfo } from 'lib/types/socket-types.js';
 import { defaultGlobalThemeInfo } from 'lib/types/theme-types.js';
+import {
+  userSurfacedPermissions,
+  type ThreadRolePermissionsBlob,
+} from 'lib/types/thread-permission-types.js';
 import type {
   ClientDBThreadInfo,
   LegacyRawThreadInfo,
@@ -126,9 +132,11 @@
   MigrationsManifest,
 } from 'lib/utils/migration-utils.js';
 import { entries } from 'lib/utils/objects.js';
+import { toggleUserSurfacedPermission } from 'lib/utils/role-utils.js';
 import {
   deprecatedConvertClientDBThreadInfoToRawThreadInfo,
   convertRawThreadInfoToClientDBThreadInfo,
+  convertClientDBThreadInfoToRawThreadInfo,
 } from 'lib/utils/thread-ops-utils.js';
 import { getUUID } from 'lib/utils/uuid.js';
 
@@ -1541,6 +1549,32 @@
       ops: {},
     };
   }: MigrationFunction<NavInfo, AppState>),
+  [88]: (async (state: AppState) => {
+    const clientDBThreadInfos = commCoreModule.getAllThreadsSync();
+    const rawThreadInfos = clientDBThreadInfos.map(
+      convertClientDBThreadInfoToRawThreadInfo,
+    );
+    const keyedRawThreadInfos = _keyBy('id')(rawThreadInfos);
+
+    // This function also results in setting DELETE_OWN_MESSAGES and
+    // DELETE_ALL_MESSAGES for admins. It is added automatically based on the
+    // result of getRolePermissionBlobs call.
+    const { operations } = updateRolesAndPermissions(
+      keyedRawThreadInfos,
+      (permissions: ThreadRolePermissionsBlob) =>
+        toggleUserSurfacedPermission(
+          permissions,
+          userSurfacedPermissions.DELETE_OWN_MESSAGES,
+        ),
+    );
+
+    return {
+      state,
+      ops: {
+        threadStoreOperations: operations,
+      },
+    };
+  }: MigrationFunction<NavInfo, AppState>),
 });
 
 // NOTE: renaming this object, and especially the `version` property
diff --git a/web/redux/persist-constants.js b/web/redux/persist-constants.js
--- a/web/redux/persist-constants.js
+++ b/web/redux/persist-constants.js
@@ -3,6 +3,6 @@
 const rootKey = 'root';
 const rootKeyPrefix = 'persist:';
 const completeRootKey = `${rootKeyPrefix}${rootKey}`;
-const storeVersion = 87;
+const storeVersion = 88;
 
 export { rootKey, rootKeyPrefix, completeRootKey, storeVersion };
diff --git a/web/redux/persist.js b/web/redux/persist.js
--- a/web/redux/persist.js
+++ b/web/redux/persist.js
@@ -12,15 +12,16 @@
   type ReplaceKeyserverOperation,
 } from 'lib/ops/keyserver-store-ops.js';
 import {
-  messageStoreOpsHandlers,
-  type ReplaceMessageStoreLocalMessageInfoOperation,
   type ClientDBMessageStoreOperation,
   type MessageStoreOperation,
+  messageStoreOpsHandlers,
+  type ReplaceMessageStoreLocalMessageInfoOperation,
 } from 'lib/ops/message-store-ops.js';
 import type {
   ClientDBThreadStoreOperation,
   ThreadStoreOperation,
 } from 'lib/ops/thread-store-ops.js';
+import { updateRolesAndPermissions } from 'lib/permissions/minimally-encoded-thread-permissions.js';
 import { patchRawThreadInfoWithSpecialRole } from 'lib/permissions/special-roles.js';
 import { createUpdateDBOpsForThreadStoreThreadInfos } from 'lib/shared/redux/client-db-utils.js';
 import { deprecatedUpdateRolesAndPermissions } from 'lib/shared/redux/deprecated-update-roles-and-permissions.js';
@@ -36,6 +37,8 @@
 import { defaultConnectionInfo } from 'lib/types/socket-types.js';
 import type { StoreOperations } from 'lib/types/store-ops-types.js';
 import { defaultGlobalThemeInfo } from 'lib/types/theme-types.js';
+import type { ThreadRolePermissionsBlob } from 'lib/types/thread-permission-types.js';
+import { userSurfacedPermissions } from 'lib/types/thread-permission-types.js';
 import type {
   ClientDBThreadInfo,
   RawThreadInfos,
@@ -45,14 +48,15 @@
 import { isDev } from 'lib/utils/dev-utils.js';
 import { stripMemberPermissionsFromRawThreadInfos } from 'lib/utils/member-info-utils.js';
 import {
-  generateIDSchemaMigrationOpsForDrafts,
   convertDraftStoreToNewIDSchema,
   createAsyncMigrate,
-  type StorageMigrationFunction,
+  generateIDSchemaMigrationOpsForDrafts,
   type MigrationFunction,
   type MigrationsManifest,
+  type StorageMigrationFunction,
 } from 'lib/utils/migration-utils.js';
 import { entries, values } from 'lib/utils/objects.js';
+import { toggleUserSurfacedPermission } from 'lib/utils/role-utils.js';
 import {
   convertClientDBThreadInfoToRawThreadInfo,
   convertRawThreadInfoToClientDBThreadInfo,
@@ -755,6 +759,58 @@
       ops: {},
     };
   }: MigrationFunction<WebNavInfo, AppState>),
+  [88]: (async (state: AppState) => {
+    const sharedWorker = await getCommSharedWorker();
+    const isDatabaseSupported = await sharedWorker.isSupported();
+
+    if (!isDatabaseSupported) {
+      return {
+        state,
+        ops: {},
+      };
+    }
+
+    const stores = await sharedWorker.schedule({
+      type: workerRequestMessageTypes.GET_CLIENT_STORE,
+    });
+
+    const clientDBThreadInfos: ?$ReadOnlyArray<ClientDBThreadInfo> =
+      stores?.store?.threads;
+
+    if (
+      clientDBThreadInfos === null ||
+      clientDBThreadInfos === undefined ||
+      clientDBThreadInfos.length === 0
+    ) {
+      return {
+        state,
+        ops: {},
+      };
+    }
+
+    const rawThreadInfos = clientDBThreadInfos.map(
+      convertClientDBThreadInfoToRawThreadInfo,
+    );
+
+    const keyedRawThreadInfos = _keyBy('id')(rawThreadInfos);
+    // This function also results in setting DELETE_OWN_MESSAGES and
+    // DELETE_ALL_MESSAGES for admins. It is added automatically based on the
+    // result of getRolePermissionBlobs call.
+    const { operations } = updateRolesAndPermissions(
+      keyedRawThreadInfos,
+      (permissions: ThreadRolePermissionsBlob) =>
+        toggleUserSurfacedPermission(
+          permissions,
+          userSurfacedPermissions.DELETE_OWN_MESSAGES,
+        ),
+    );
+    return {
+      state,
+      ops: {
+        threadStoreOperations: operations,
+      },
+    };
+  }: MigrationFunction<WebNavInfo, AppState>),
 };
 
 const persistConfig: PersistConfig = {