diff --git a/keyserver/src/scripts/validate-role-permissions.js b/keyserver/src/scripts/validate-role-permissions.js
new file mode 100644
--- /dev/null
+++ b/keyserver/src/scripts/validate-role-permissions.js
@@ -0,0 +1,162 @@
+// @flow
+
+import { getRolePermissionBlobs } from 'lib/permissions/thread-permissions.js';
+import {
+  configurableCommunityPermissions,
+  userSurfacedPermissions,
+  universalCommunityPermissions,
+} from 'lib/types/thread-permission-types.js';
+import { threadTypes } from 'lib/types/thread-types-enum.js';
+import { deepDiff, values } from 'lib/utils/objects.js';
+
+import { main } from './utils.js';
+import { SQL, dbQuery } from '../database/database.js';
+
+async function validateRolePermissions() {
+  // Get all roles for existing communities since custom roles are at a
+  // community-level rather than a thread-level.
+  const fetchRolesQuery = SQL`
+		SELECT r.id, r.name, r.permissions, r.thread, t.type, t.default_role
+		FROM roles r
+		INNER JOIN threads t
+			ON t.id = r.thread
+		WHERE t.type IN (${[
+      threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT,
+      threadTypes.COMMUNITY_ROOT,
+    ]})
+	`;
+  const [results] = await dbQuery(fetchRolesQuery);
+
+  for (const result of results) {
+    const roleID = result.id.toString();
+    const roleName = result.name;
+    const existingRolePermissions = JSON.parse(result.permissions);
+    const threadID = result.thread.toString();
+    const threadType = result.type;
+    const threadDefaultRole = result.default_role.toString();
+
+    // Get the 'expected permissions' set for the role. If the role is
+    // default (Members) or Admins, these permission blobs can be retrieved
+    // by calling getRolePermissionBlobs with the threadType. Otherwise, the
+    // role is a custom role and the expected permissions are the universal
+    // community permissions assuming the role has not been edited.
+    // The case of a role being edited is handled below.
+    const expectedPermissionBlobs = getRolePermissionBlobs(threadType);
+    let baseExpectedPermissionBlob;
+    if (roleID === threadDefaultRole) {
+      baseExpectedPermissionBlob = expectedPermissionBlobs.Members;
+    } else if (roleName === 'Admins') {
+      baseExpectedPermissionBlob = expectedPermissionBlobs.Admins;
+    } else if (roleName) {
+      baseExpectedPermissionBlob = Object.fromEntries(
+        universalCommunityPermissions.map(permission => [permission, true]),
+      );
+    } else {
+      baseExpectedPermissionBlob = {};
+    }
+    console.log('====================================');
+
+    // Ideally, this should never happen, but we'll skip over this in case.
+    if (!baseExpectedPermissionBlob) {
+      console.log(
+        `Skipping role ${roleName} with ID (${roleID}) in thread ${threadID}`,
+      );
+      continue;
+    }
+
+    // Deep diff seems to compare objects one-way (so deepDiff(a, b) !==
+    // deepDiff(b, a)). This means that if a key is not in `a` but not in `b`,
+    // the diff will not include that key. As a result, we need to compare both
+    // ways to ensure that we're not missing any permission discrepancies.
+    const expectedPermissionsToExistingPermissions = deepDiff(
+      baseExpectedPermissionBlob,
+      existingRolePermissions,
+    );
+    const existingPermissionsToExpectedPermissions = deepDiff(
+      existingRolePermissions,
+      baseExpectedPermissionBlob,
+    );
+
+    console.log(
+      `Validating: Role Name (${roleName}) | Role ID (${roleID}) | ` +
+        `Thread Type (${threadType}) | Thread ID (${threadID})\n`,
+    );
+    console.log(
+      `deepDiff(baseExpectedPermissionBlob, existingRolePermissions) = ${JSON.stringify(
+        expectedPermissionsToExistingPermissions,
+        null,
+        2,
+      )}\n`,
+    );
+    console.log(
+      `deepDiff(existingRolePermissions, baseExpectedPermissionBlob) = ${JSON.stringify(
+        existingPermissionsToExpectedPermissions,
+        null,
+        2,
+      )}\n`,
+    );
+
+    // Now, we want to see if the permission discrepancies are due to the user
+    // editing the role. To do this, we need to identify any permission
+    // discrepancies that could be linked to a specific user-surfaced
+    // permission. This could be useful in manually parsing through the
+    // script results to 'write off' discrepancies as user role edits.
+    const userSurfacedExpectedPermissionsToExistingPermissions = new Set();
+    const userSurfacedExistingPermissionsToExpectedPermissions = new Set();
+
+    for (const permission of values(userSurfacedPermissions)) {
+      const permissionSet = Array.from(
+        configurableCommunityPermissions[permission],
+      );
+      for (const p of permissionSet) {
+        if (expectedPermissionsToExistingPermissions[p] === true) {
+          userSurfacedExpectedPermissionsToExistingPermissions.add(permission);
+        }
+        if (existingPermissionsToExpectedPermissions[p] === true) {
+          userSurfacedExistingPermissionsToExpectedPermissions.add(permission);
+        }
+      }
+    }
+
+    const expectedPermissionsToExistingPermissionsValues = values(
+      expectedPermissionsToExistingPermissions,
+    );
+    const existingPermissionsToExpectedPermissionsValues = values(
+      existingPermissionsToExpectedPermissions,
+    );
+
+    if (
+      expectedPermissionsToExistingPermissionsValues.length > 0 ||
+      existingPermissionsToExpectedPermissionsValues.length > 0
+    ) {
+      console.log(
+        `Potential permission discrepancies for role ${roleName} that ` +
+          `could be linked back to user surfaced permissions (i.e. not an ` +
+          `actual discrepancy, but rather a user edited a role): \n`,
+      );
+
+      if (expectedPermissionsToExistingPermissionsValues.length > 0) {
+        console.log(
+          `userSurfacedExpectedPermissionsToExistingPermissions = ${JSON.stringify(
+            [...userSurfacedExpectedPermissionsToExistingPermissions],
+            null,
+            2,
+          )}\n`,
+        );
+      }
+      if (existingPermissionsToExpectedPermissionsValues.length > 0) {
+        console.log(
+          `userSurfacedExistingPermissionsToExpectedPermissions = ${JSON.stringify(
+            [...userSurfacedExistingPermissionsToExpectedPermissions],
+            null,
+            2,
+          )}`,
+        );
+      }
+    }
+  }
+
+  console.log('====================================');
+}
+
+main([validateRolePermissions]);