diff --git a/lib/shared/messages/add-members-message-spec.js b/lib/shared/messages/add-members-message-spec.js
--- a/lib/shared/messages/add-members-message-spec.js
+++ b/lib/shared/messages/add-members-message-spec.js
@@ -30,11 +30,20 @@
 import { values } from '../../utils/objects.js';
 import { notifRobotextForMessageInfo } from '../notif-utils.js';
 
-export const addMembersMessageSpec: MessageSpec<
+type AddMembersMessageSpec = MessageSpec<
   AddMembersMessageData,
   RawAddMembersMessageInfo,
   AddMembersMessageInfo,
-> = Object.freeze({
+> & {
+  // We need to explicitly type this as non-optional so that
+  // it can be referenced from messageContentForClientDB below
+  +messageContentForServerDB: (
+    data: AddMembersMessageData | RawAddMembersMessageInfo,
+  ) => string,
+  ...
+};
+
+export const addMembersMessageSpec: AddMembersMessageSpec = Object.freeze({
   messageContentForServerDB(
     data: AddMembersMessageData | RawAddMembersMessageInfo,
   ): string {
@@ -42,7 +51,7 @@
   },
 
   messageContentForClientDB(data: RawAddMembersMessageInfo): string {
-    return this.messageContentForServerDB(data);
+    return addMembersMessageSpec.messageContentForServerDB(data);
   },
 
   rawMessageInfoFromServerDBRow(row: Object): RawAddMembersMessageInfo {
diff --git a/lib/shared/messages/change-role-message-spec.js b/lib/shared/messages/change-role-message-spec.js
--- a/lib/shared/messages/change-role-message-spec.js
+++ b/lib/shared/messages/change-role-message-spec.js
@@ -37,11 +37,20 @@
 import { notifRobotextForMessageInfo } from '../notif-utils.js';
 import { hasMinCodeVersion } from '../version-utils.js';
 
-export const changeRoleMessageSpec: MessageSpec<
+type ChangeRoleMessageSpec = MessageSpec<
   ChangeRoleMessageData,
   RawChangeRoleMessageInfo,
   ChangeRoleMessageInfo,
-> = Object.freeze({
+> & {
+  // We need to explicitly type this as non-optional so that
+  // it can be referenced from messageContentForClientDB below
+  +messageContentForServerDB: (
+    data: ChangeRoleMessageData | RawChangeRoleMessageInfo,
+  ) => string,
+  ...
+};
+
+export const changeRoleMessageSpec: ChangeRoleMessageSpec = Object.freeze({
   messageContentForServerDB(
     data: ChangeRoleMessageData | RawChangeRoleMessageInfo,
   ): string {
@@ -53,7 +62,7 @@
   },
 
   messageContentForClientDB(data: RawChangeRoleMessageInfo): string {
-    return this.messageContentForServerDB(data);
+    return changeRoleMessageSpec.messageContentForServerDB(data);
   },
 
   rawMessageInfoFromServerDBRow(row: Object): RawChangeRoleMessageInfo {
diff --git a/lib/shared/messages/change-settings-message-spec.js b/lib/shared/messages/change-settings-message-spec.js
--- a/lib/shared/messages/change-settings-message-spec.js
+++ b/lib/shared/messages/change-settings-message-spec.js
@@ -29,162 +29,172 @@
 import { notifRobotextForMessageInfo } from '../notif-utils.js';
 import { threadLabel } from '../thread-utils.js';
 
-export const changeSettingsMessageSpec: MessageSpec<
+type ChangeSettingsMessageSpec = MessageSpec<
   ChangeSettingsMessageData,
   RawChangeSettingsMessageInfo,
   ChangeSettingsMessageInfo,
-> = Object.freeze({
-  messageContentForServerDB(
+> & {
+  // We need to explicitly type this as non-optional so that
+  // it can be referenced from messageContentForClientDB below
+  +messageContentForServerDB: (
     data: ChangeSettingsMessageData | RawChangeSettingsMessageInfo,
-  ): string {
-    return JSON.stringify({
-      [data.field]: data.value,
-    });
-  },
-
-  messageContentForClientDB(data: RawChangeSettingsMessageInfo): string {
-    return this.messageContentForServerDB(data);
-  },
-
-  rawMessageInfoFromServerDBRow(row: Object): RawChangeSettingsMessageInfo {
-    const content = JSON.parse(row.content);
-    const field = Object.keys(content)[0];
-    return {
-      type: messageTypes.CHANGE_SETTINGS,
-      id: row.id.toString(),
-      threadID: row.threadID.toString(),
-      time: row.time,
-      creatorID: row.creatorID.toString(),
-      field,
-      value: content[field],
-    };
-  },
-
-  rawMessageInfoFromClientDB(
-    clientDBMessageInfo: ClientDBMessageInfo,
-  ): RawChangeSettingsMessageInfo {
-    invariant(
-      clientDBMessageInfo.content !== undefined &&
-        clientDBMessageInfo.content !== null,
-      'content must be defined for ChangeSettings',
-    );
-    const content = JSON.parse(clientDBMessageInfo.content);
-    const field = Object.keys(content)[0];
-    const rawChangeSettingsMessageInfo: RawChangeSettingsMessageInfo = {
-      type: messageTypes.CHANGE_SETTINGS,
-      id: clientDBMessageInfo.id,
-      threadID: clientDBMessageInfo.thread,
-      time: parseInt(clientDBMessageInfo.time),
-      creatorID: clientDBMessageInfo.user,
-      field,
-      value: content[field],
-    };
-    return rawChangeSettingsMessageInfo;
-  },
-
-  createMessageInfo(
-    rawMessageInfo: RawChangeSettingsMessageInfo,
-    creator: RelativeUserInfo,
-  ): ChangeSettingsMessageInfo {
-    return {
-      type: messageTypes.CHANGE_SETTINGS,
-      id: rawMessageInfo.id,
-      threadID: rawMessageInfo.threadID,
-      creator,
-      time: rawMessageInfo.time,
-      field: rawMessageInfo.field,
-      value: rawMessageInfo.value,
-    };
-  },
-
-  rawMessageInfoFromMessageData(
-    messageData: ChangeSettingsMessageData,
-    id: ?string,
-  ): RawChangeSettingsMessageInfo {
-    invariant(id, 'RawChangeSettingsMessageInfo needs id');
-    return { ...messageData, id };
-  },
-
-  robotext(
-    messageInfo: ChangeSettingsMessageInfo,
-    params: RobotextParams,
-  ): EntityText {
-    const creator = ET.user({ userInfo: messageInfo.creator });
-    const thread = ET.thread({
-      display: 'alwaysDisplayShortName',
-      threadID: messageInfo.threadID,
-      threadType: params.threadInfo?.type,
-      parentThreadID: params.threadInfo?.parentThreadID,
-      possessive: true,
-    });
-    if (
-      (messageInfo.field === 'name' || messageInfo.field === 'description') &&
-      messageInfo.value.toString() === ''
-    ) {
-      return ET`${creator} cleared ${thread} ${messageInfo.field}`;
-    }
-    if (messageInfo.field === 'avatar') {
-      return ET`${creator} updated ${thread} ${messageInfo.field}`;
-    }
-
-    let value;
-    if (
-      messageInfo.field === 'color' &&
-      messageInfo.value.toString().match(validHexColorRegex)
-    ) {
-      value = ET.color({ hex: `#${messageInfo.value}` });
-    } else if (messageInfo.field === 'type') {
+  ) => string,
+  ...
+};
+
+export const changeSettingsMessageSpec: ChangeSettingsMessageSpec =
+  Object.freeze({
+    messageContentForServerDB(
+      data: ChangeSettingsMessageData | RawChangeSettingsMessageInfo,
+    ): string {
+      return JSON.stringify({
+        [data.field]: data.value,
+      });
+    },
+
+    messageContentForClientDB(data: RawChangeSettingsMessageInfo): string {
+      return changeSettingsMessageSpec.messageContentForServerDB(data);
+    },
+
+    rawMessageInfoFromServerDBRow(row: Object): RawChangeSettingsMessageInfo {
+      const content = JSON.parse(row.content);
+      const field = Object.keys(content)[0];
+      return {
+        type: messageTypes.CHANGE_SETTINGS,
+        id: row.id.toString(),
+        threadID: row.threadID.toString(),
+        time: row.time,
+        creatorID: row.creatorID.toString(),
+        field,
+        value: content[field],
+      };
+    },
+
+    rawMessageInfoFromClientDB(
+      clientDBMessageInfo: ClientDBMessageInfo,
+    ): RawChangeSettingsMessageInfo {
       invariant(
-        typeof messageInfo.value === 'number',
-        'messageInfo.value should be number for thread type change ',
+        clientDBMessageInfo.content !== undefined &&
+          clientDBMessageInfo.content !== null,
+        'content must be defined for ChangeSettings',
       );
-      const newThreadType = assertThreadType(messageInfo.value);
-      value = threadLabel(newThreadType);
-    } else {
-      value = messageInfo.value.toString();
-    }
-    return ET`${creator} updated ${thread} ${messageInfo.field} to "${value}"`;
-  },
-
-  async notificationTexts(
-    messageInfos: $ReadOnlyArray<MessageInfo>,
-    threadInfo: ThreadInfo,
-    params: NotificationTextsParams,
-  ): Promise<NotifTexts> {
-    const mostRecentMessageInfo = messageInfos[0];
-    invariant(
-      mostRecentMessageInfo.type === messageTypes.CHANGE_SETTINGS,
-      'messageInfo should be messageTypes.CHANGE_SETTINGS!',
-    );
-    const { parentThreadInfo } = params;
-    const body = notifRobotextForMessageInfo(
-      mostRecentMessageInfo,
-      threadInfo,
-      parentThreadInfo,
-    );
-    return {
-      merged: body,
-      title: threadInfo.uiName,
-      body,
-    };
-  },
-
-  notificationCollapseKey(
-    rawMessageInfo: RawChangeSettingsMessageInfo,
-  ): string {
-    return joinResult(
-      rawMessageInfo.type,
-      rawMessageInfo.threadID,
-      rawMessageInfo.creatorID,
-      rawMessageInfo.field,
-    );
-  },
-
-  generatesNotifs: async () => pushTypes.NOTIF,
-
-  canBeSidebarSource: true,
-
-  canBePinned: false,
-
-  validator: rawChangeSettingsMessageInfoValidator,
-});
+      const content = JSON.parse(clientDBMessageInfo.content);
+      const field = Object.keys(content)[0];
+      const rawChangeSettingsMessageInfo: RawChangeSettingsMessageInfo = {
+        type: messageTypes.CHANGE_SETTINGS,
+        id: clientDBMessageInfo.id,
+        threadID: clientDBMessageInfo.thread,
+        time: parseInt(clientDBMessageInfo.time),
+        creatorID: clientDBMessageInfo.user,
+        field,
+        value: content[field],
+      };
+      return rawChangeSettingsMessageInfo;
+    },
+
+    createMessageInfo(
+      rawMessageInfo: RawChangeSettingsMessageInfo,
+      creator: RelativeUserInfo,
+    ): ChangeSettingsMessageInfo {
+      return {
+        type: messageTypes.CHANGE_SETTINGS,
+        id: rawMessageInfo.id,
+        threadID: rawMessageInfo.threadID,
+        creator,
+        time: rawMessageInfo.time,
+        field: rawMessageInfo.field,
+        value: rawMessageInfo.value,
+      };
+    },
+
+    rawMessageInfoFromMessageData(
+      messageData: ChangeSettingsMessageData,
+      id: ?string,
+    ): RawChangeSettingsMessageInfo {
+      invariant(id, 'RawChangeSettingsMessageInfo needs id');
+      return { ...messageData, id };
+    },
+
+    robotext(
+      messageInfo: ChangeSettingsMessageInfo,
+      params: RobotextParams,
+    ): EntityText {
+      const creator = ET.user({ userInfo: messageInfo.creator });
+      const thread = ET.thread({
+        display: 'alwaysDisplayShortName',
+        threadID: messageInfo.threadID,
+        threadType: params.threadInfo?.type,
+        parentThreadID: params.threadInfo?.parentThreadID,
+        possessive: true,
+      });
+      if (
+        (messageInfo.field === 'name' || messageInfo.field === 'description') &&
+        messageInfo.value.toString() === ''
+      ) {
+        return ET`${creator} cleared ${thread} ${messageInfo.field}`;
+      }
+      if (messageInfo.field === 'avatar') {
+        return ET`${creator} updated ${thread} ${messageInfo.field}`;
+      }
+
+      let value;
+      if (
+        messageInfo.field === 'color' &&
+        messageInfo.value.toString().match(validHexColorRegex)
+      ) {
+        value = ET.color({ hex: `#${messageInfo.value}` });
+      } else if (messageInfo.field === 'type') {
+        invariant(
+          typeof messageInfo.value === 'number',
+          'messageInfo.value should be number for thread type change ',
+        );
+        const newThreadType = assertThreadType(messageInfo.value);
+        value = threadLabel(newThreadType);
+      } else {
+        value = messageInfo.value.toString();
+      }
+      return ET`${creator} updated ${thread} ${messageInfo.field} to "${value}"`;
+    },
+
+    async notificationTexts(
+      messageInfos: $ReadOnlyArray<MessageInfo>,
+      threadInfo: ThreadInfo,
+      params: NotificationTextsParams,
+    ): Promise<NotifTexts> {
+      const mostRecentMessageInfo = messageInfos[0];
+      invariant(
+        mostRecentMessageInfo.type === messageTypes.CHANGE_SETTINGS,
+        'messageInfo should be messageTypes.CHANGE_SETTINGS!',
+      );
+      const { parentThreadInfo } = params;
+      const body = notifRobotextForMessageInfo(
+        mostRecentMessageInfo,
+        threadInfo,
+        parentThreadInfo,
+      );
+      return {
+        merged: body,
+        title: threadInfo.uiName,
+        body,
+      };
+    },
+
+    notificationCollapseKey(
+      rawMessageInfo: RawChangeSettingsMessageInfo,
+    ): string {
+      return joinResult(
+        rawMessageInfo.type,
+        rawMessageInfo.threadID,
+        rawMessageInfo.creatorID,
+        rawMessageInfo.field,
+      );
+    },
+
+    generatesNotifs: async () => pushTypes.NOTIF,
+
+    canBeSidebarSource: true,
+
+    canBePinned: false,
+
+    validator: rawChangeSettingsMessageInfoValidator,
+  });
diff --git a/lib/shared/messages/create-entry-message-spec.js b/lib/shared/messages/create-entry-message-spec.js
--- a/lib/shared/messages/create-entry-message-spec.js
+++ b/lib/shared/messages/create-entry-message-spec.js
@@ -22,11 +22,20 @@
 import { ET, type EntityText } from '../../utils/entity-text.js';
 import { notifTextsForEntryCreationOrEdit } from '../notif-utils.js';
 
-export const createEntryMessageSpec: MessageSpec<
+type CreateEntryMessageSpec = MessageSpec<
   CreateEntryMessageData,
   RawCreateEntryMessageInfo,
   CreateEntryMessageInfo,
-> = Object.freeze({
+> & {
+  // We need to explicitly type this as non-optional so that
+  // it can be referenced from messageContentForClientDB below
+  +messageContentForServerDB: (
+    data: CreateEntryMessageData | RawCreateEntryMessageInfo,
+  ) => string,
+  ...
+};
+
+export const createEntryMessageSpec: CreateEntryMessageSpec = Object.freeze({
   messageContentForServerDB(
     data: CreateEntryMessageData | RawCreateEntryMessageInfo,
   ): string {
@@ -38,7 +47,7 @@
   },
 
   messageContentForClientDB(data: RawCreateEntryMessageInfo): string {
-    return this.messageContentForServerDB(data);
+    return createEntryMessageSpec.messageContentForServerDB(data);
   },
 
   rawMessageInfoFromServerDBRow(row: Object): RawCreateEntryMessageInfo {
diff --git a/lib/shared/messages/create-sidebar-message-spec.js b/lib/shared/messages/create-sidebar-message-spec.js
--- a/lib/shared/messages/create-sidebar-message-spec.js
+++ b/lib/shared/messages/create-sidebar-message-spec.js
@@ -31,184 +31,199 @@
 } from '../../utils/entity-text.js';
 import { notifTextsForSidebarCreation } from '../notif-utils.js';
 
-export const createSidebarMessageSpec: MessageSpec<
+type CreateSidebarMessageSpec = MessageSpec<
   CreateSidebarMessageData,
   RawCreateSidebarMessageInfo,
   CreateSidebarMessageInfo,
-> = Object.freeze({
-  messageContentForServerDB(
+> & {
+  // We need to explicitly type this as non-optional so that
+  // it can be referenced from messageContentForClientDB below
+  +messageContentForServerDB: (
     data: CreateSidebarMessageData | RawCreateSidebarMessageInfo,
-  ): string {
-    return JSON.stringify({
-      ...data.initialThreadState,
-      sourceMessageAuthorID: data.sourceMessageAuthorID,
-    });
-  },
-
-  messageContentForClientDB(data: RawCreateSidebarMessageInfo): string {
-    return this.messageContentForServerDB(data);
-  },
-
-  rawMessageInfoFromServerDBRow(row: Object): RawCreateSidebarMessageInfo {
-    const { sourceMessageAuthorID, ...initialThreadState } = JSON.parse(
-      row.content,
-    );
-    return {
-      type: messageTypes.CREATE_SIDEBAR,
-      id: row.id.toString(),
-      threadID: row.threadID.toString(),
-      time: row.time,
-      creatorID: row.creatorID.toString(),
-      sourceMessageAuthorID,
-      initialThreadState,
-    };
-  },
-
-  rawMessageInfoFromClientDB(
-    clientDBMessageInfo: ClientDBMessageInfo,
-  ): RawCreateSidebarMessageInfo {
-    invariant(
-      clientDBMessageInfo.content !== undefined &&
-        clientDBMessageInfo.content !== null,
-      'content must be defined for CreateSidebar',
-    );
-
-    const { sourceMessageAuthorID, ...initialThreadState } = JSON.parse(
-      clientDBMessageInfo.content,
-    );
-    const rawCreateSidebarMessageInfo: RawCreateSidebarMessageInfo = {
-      type: messageTypes.CREATE_SIDEBAR,
-      id: clientDBMessageInfo.id,
-      threadID: clientDBMessageInfo.thread,
-      time: parseInt(clientDBMessageInfo.time),
-      creatorID: clientDBMessageInfo.user,
-      sourceMessageAuthorID: sourceMessageAuthorID,
-      initialThreadState: initialThreadState,
-    };
-    return rawCreateSidebarMessageInfo;
-  },
+  ) => string,
+  ...
+};
+
+export const createSidebarMessageSpec: CreateSidebarMessageSpec = Object.freeze(
+  {
+    messageContentForServerDB(
+      data: CreateSidebarMessageData | RawCreateSidebarMessageInfo,
+    ): string {
+      return JSON.stringify({
+        ...data.initialThreadState,
+        sourceMessageAuthorID: data.sourceMessageAuthorID,
+      });
+    },
+
+    messageContentForClientDB(data: RawCreateSidebarMessageInfo): string {
+      return createSidebarMessageSpec.messageContentForServerDB(data);
+    },
+
+    rawMessageInfoFromServerDBRow(row: Object): RawCreateSidebarMessageInfo {
+      const { sourceMessageAuthorID, ...initialThreadState } = JSON.parse(
+        row.content,
+      );
+      return {
+        type: messageTypes.CREATE_SIDEBAR,
+        id: row.id.toString(),
+        threadID: row.threadID.toString(),
+        time: row.time,
+        creatorID: row.creatorID.toString(),
+        sourceMessageAuthorID,
+        initialThreadState,
+      };
+    },
+
+    rawMessageInfoFromClientDB(
+      clientDBMessageInfo: ClientDBMessageInfo,
+    ): RawCreateSidebarMessageInfo {
+      invariant(
+        clientDBMessageInfo.content !== undefined &&
+          clientDBMessageInfo.content !== null,
+        'content must be defined for CreateSidebar',
+      );
 
-  createMessageInfo(
-    rawMessageInfo: RawCreateSidebarMessageInfo,
-    creator: RelativeUserInfo,
-    params: CreateMessageInfoParams,
-  ): ?CreateSidebarMessageInfo {
-    const { threadInfos } = params;
-    const parentThreadInfo =
-      threadInfos[rawMessageInfo.initialThreadState.parentThreadID];
-
-    const sourceMessageAuthor = params.createRelativeUserInfos([
-      rawMessageInfo.sourceMessageAuthorID,
-    ])[0];
-    if (!sourceMessageAuthor) {
-      return null;
-    }
-
-    return {
-      type: messageTypes.CREATE_SIDEBAR,
-      id: rawMessageInfo.id,
-      threadID: rawMessageInfo.threadID,
-      creator,
-      time: rawMessageInfo.time,
-      sourceMessageAuthor,
-      initialThreadState: {
-        name: rawMessageInfo.initialThreadState.name,
-        parentThreadInfo,
-        color: rawMessageInfo.initialThreadState.color,
-        otherMembers: params.createRelativeUserInfos(
-          rawMessageInfo.initialThreadState.memberIDs.filter(
-            (userID: string) => userID !== rawMessageInfo.creatorID,
+      const { sourceMessageAuthorID, ...initialThreadState } = JSON.parse(
+        clientDBMessageInfo.content,
+      );
+      const rawCreateSidebarMessageInfo: RawCreateSidebarMessageInfo = {
+        type: messageTypes.CREATE_SIDEBAR,
+        id: clientDBMessageInfo.id,
+        threadID: clientDBMessageInfo.thread,
+        time: parseInt(clientDBMessageInfo.time),
+        creatorID: clientDBMessageInfo.user,
+        sourceMessageAuthorID: sourceMessageAuthorID,
+        initialThreadState: initialThreadState,
+      };
+      return rawCreateSidebarMessageInfo;
+    },
+
+    createMessageInfo(
+      rawMessageInfo: RawCreateSidebarMessageInfo,
+      creator: RelativeUserInfo,
+      params: CreateMessageInfoParams,
+    ): ?CreateSidebarMessageInfo {
+      const { threadInfos } = params;
+      const parentThreadInfo =
+        threadInfos[rawMessageInfo.initialThreadState.parentThreadID];
+
+      const sourceMessageAuthor = params.createRelativeUserInfos([
+        rawMessageInfo.sourceMessageAuthorID,
+      ])[0];
+      if (!sourceMessageAuthor) {
+        return null;
+      }
+
+      return {
+        type: messageTypes.CREATE_SIDEBAR,
+        id: rawMessageInfo.id,
+        threadID: rawMessageInfo.threadID,
+        creator,
+        time: rawMessageInfo.time,
+        sourceMessageAuthor,
+        initialThreadState: {
+          name: rawMessageInfo.initialThreadState.name,
+          parentThreadInfo,
+          color: rawMessageInfo.initialThreadState.color,
+          otherMembers: params.createRelativeUserInfos(
+            rawMessageInfo.initialThreadState.memberIDs.filter(
+              (userID: string) => userID !== rawMessageInfo.creatorID,
+            ),
           ),
-        ),
-      },
-    };
-  },
-
-  rawMessageInfoFromMessageData(
-    messageData: CreateSidebarMessageData,
-    id: ?string,
-  ): RawCreateSidebarMessageInfo {
-    invariant(id, 'RawCreateSidebarMessageInfo needs id');
-    return { ...messageData, id };
-  },
-
-  robotext(
-    messageInfo: CreateSidebarMessageInfo,
-    params: RobotextParams,
-  ): EntityText {
-    let text = ET`started ${ET.thread({
-      display: 'alwaysDisplayShortName',
-      threadID: messageInfo.threadID,
-      threadType: params.threadInfo?.type,
-      parentThreadID: params.threadInfo?.parentThreadID,
-    })}`;
-    const users = messageInfo.initialThreadState.otherMembers.filter(
-      member => member.id !== messageInfo.sourceMessageAuthor.id,
-    );
-    if (users.length !== 0) {
-      const initialUsers = pluralizeEntityText(
-        users.map(user => ET`${ET.user({ userInfo: user })}`),
+        },
+      };
+    },
+
+    rawMessageInfoFromMessageData(
+      messageData: CreateSidebarMessageData,
+      id: ?string,
+    ): RawCreateSidebarMessageInfo {
+      invariant(id, 'RawCreateSidebarMessageInfo needs id');
+      return { ...messageData, id };
+    },
+
+    robotext(
+      messageInfo: CreateSidebarMessageInfo,
+      params: RobotextParams,
+    ): EntityText {
+      let text = ET`started ${ET.thread({
+        display: 'alwaysDisplayShortName',
+        threadID: messageInfo.threadID,
+        threadType: params.threadInfo?.type,
+        parentThreadID: params.threadInfo?.parentThreadID,
+      })}`;
+      const users = messageInfo.initialThreadState.otherMembers.filter(
+        member => member.id !== messageInfo.sourceMessageAuthor.id,
+      );
+      if (users.length !== 0) {
+        const initialUsers = pluralizeEntityText(
+          users.map(user => ET`${ET.user({ userInfo: user })}`),
+        );
+        text = ET`${text} and added ${initialUsers}`;
+      }
+      const creator = ET.user({ userInfo: messageInfo.creator });
+      return ET`${creator} ${text}`;
+    },
+
+    unshimMessageInfo(
+      unwrapped: RawCreateSidebarMessageInfo,
+    ): RawCreateSidebarMessageInfo {
+      return unwrapped;
+    },
+
+    async notificationTexts(
+      messageInfos: $ReadOnlyArray<MessageInfo>,
+      threadInfo: ThreadInfo,
+      params: NotificationTextsParams,
+    ): Promise<NotifTexts> {
+      const createSidebarMessageInfo = messageInfos[0];
+      invariant(
+        createSidebarMessageInfo.type === messageTypes.CREATE_SIDEBAR,
+        'first MessageInfo should be messageTypes.CREATE_SIDEBAR!',
       );
-      text = ET`${text} and added ${initialUsers}`;
-    }
-    const creator = ET.user({ userInfo: messageInfo.creator });
-    return ET`${creator} ${text}`;
-  },
-
-  unshimMessageInfo(
-    unwrapped: RawCreateSidebarMessageInfo,
-  ): RawCreateSidebarMessageInfo {
-    return unwrapped;
-  },
-
-  async notificationTexts(
-    messageInfos: $ReadOnlyArray<MessageInfo>,
-    threadInfo: ThreadInfo,
-    params: NotificationTextsParams,
-  ): Promise<NotifTexts> {
-    const createSidebarMessageInfo = messageInfos[0];
-    invariant(
-      createSidebarMessageInfo.type === messageTypes.CREATE_SIDEBAR,
-      'first MessageInfo should be messageTypes.CREATE_SIDEBAR!',
-    );
-
-    let sidebarSourceMessageInfo;
-    const secondMessageInfo = messageInfos[1];
-    if (
-      secondMessageInfo &&
-      secondMessageInfo.type === messageTypes.SIDEBAR_SOURCE
-    ) {
-      sidebarSourceMessageInfo = secondMessageInfo;
-    }
-
-    return notifTextsForSidebarCreation({
-      createSidebarMessageInfo,
-      sidebarSourceMessageInfo,
-      threadInfo,
-      params,
-    });
-  },
-
-  notificationCollapseKey(rawMessageInfo: RawCreateSidebarMessageInfo): string {
-    return joinResult(messageTypes.CREATE_SIDEBAR, rawMessageInfo.threadID);
-  },
-
-  generatesNotifs: async () => pushTypes.NOTIF,
-
-  userIDs(rawMessageInfo: RawCreateSidebarMessageInfo): $ReadOnlyArray<string> {
-    return rawMessageInfo.initialThreadState.memberIDs;
-  },
 
-  threadIDs(
-    rawMessageInfo: RawCreateSidebarMessageInfo,
-  ): $ReadOnlyArray<string> {
-    const { parentThreadID } = rawMessageInfo.initialThreadState;
-    return [parentThreadID];
+      let sidebarSourceMessageInfo;
+      const secondMessageInfo = messageInfos[1];
+      if (
+        secondMessageInfo &&
+        secondMessageInfo.type === messageTypes.SIDEBAR_SOURCE
+      ) {
+        sidebarSourceMessageInfo = secondMessageInfo;
+      }
+
+      return notifTextsForSidebarCreation({
+        createSidebarMessageInfo,
+        sidebarSourceMessageInfo,
+        threadInfo,
+        params,
+      });
+    },
+
+    notificationCollapseKey(
+      rawMessageInfo: RawCreateSidebarMessageInfo,
+    ): string {
+      return joinResult(messageTypes.CREATE_SIDEBAR, rawMessageInfo.threadID);
+    },
+
+    generatesNotifs: async () => pushTypes.NOTIF,
+
+    userIDs(
+      rawMessageInfo: RawCreateSidebarMessageInfo,
+    ): $ReadOnlyArray<string> {
+      return rawMessageInfo.initialThreadState.memberIDs;
+    },
+
+    threadIDs(
+      rawMessageInfo: RawCreateSidebarMessageInfo,
+    ): $ReadOnlyArray<string> {
+      const { parentThreadID } = rawMessageInfo.initialThreadState;
+      return [parentThreadID];
+    },
+
+    canBeSidebarSource: true,
+
+    canBePinned: false,
+
+    validator: rawCreateSidebarMessageInfoValidator,
   },
-
-  canBeSidebarSource: true,
-
-  canBePinned: false,
-
-  validator: rawCreateSidebarMessageInfoValidator,
-});
+);
diff --git a/lib/shared/messages/create-sub-thread-message-spec.js b/lib/shared/messages/create-sub-thread-message-spec.js
--- a/lib/shared/messages/create-sub-thread-message-spec.js
+++ b/lib/shared/messages/create-sub-thread-message-spec.js
@@ -29,143 +29,153 @@
 import { ET, type EntityText } from '../../utils/entity-text.js';
 import { notifTextsForSubthreadCreation } from '../notif-utils.js';
 
-export const createSubThreadMessageSpec: MessageSpec<
+type CreateSubThreadMessageSpec = MessageSpec<
   CreateSubthreadMessageData,
   RawCreateSubthreadMessageInfo,
   CreateSubthreadMessageInfo,
-> = Object.freeze({
-  messageContentForServerDB(
+> & {
+  // We need to explicitly type this as non-optional so that
+  // it can be referenced from messageContentForClientDB below
+  +messageContentForServerDB: (
     data: CreateSubthreadMessageData | RawCreateSubthreadMessageInfo,
-  ): string {
-    return data.childThreadID;
-  },
-
-  messageContentForClientDB(data: RawCreateSubthreadMessageInfo): string {
-    return this.messageContentForServerDB(data);
-  },
-
-  rawMessageInfoFromServerDBRow(row: Object): ?RawCreateSubthreadMessageInfo {
-    const subthreadPermissions = row.subthread_permissions;
-    if (!permissionLookup(subthreadPermissions, threadPermissions.KNOW_OF)) {
-      return null;
-    }
-    return {
-      type: messageTypes.CREATE_SUB_THREAD,
-      id: row.id.toString(),
-      threadID: row.threadID.toString(),
-      time: row.time,
-      creatorID: row.creatorID.toString(),
-      childThreadID: row.content,
-    };
-  },
-
-  rawMessageInfoFromClientDB(
-    clientDBMessageInfo: ClientDBMessageInfo,
-  ): RawCreateSubthreadMessageInfo {
-    const content = clientDBMessageInfo.content;
-    invariant(
-      content !== undefined && content !== null,
-      'content must be defined for CreateSubThread',
-    );
-    const rawCreateSubthreadMessageInfo: RawCreateSubthreadMessageInfo = {
-      type: messageTypes.CREATE_SUB_THREAD,
-      id: clientDBMessageInfo.id,
-      threadID: clientDBMessageInfo.thread,
-      time: parseInt(clientDBMessageInfo.time),
-      creatorID: clientDBMessageInfo.user,
-      childThreadID: content,
-    };
-    return rawCreateSubthreadMessageInfo;
-  },
-
-  createMessageInfo(
-    rawMessageInfo: RawCreateSubthreadMessageInfo,
-    creator: RelativeUserInfo,
-    params: CreateMessageInfoParams,
-  ): ?CreateSubthreadMessageInfo {
-    const { threadInfos } = params;
-    const childThreadInfo = threadInfos[rawMessageInfo.childThreadID];
-    if (!childThreadInfo) {
-      return null;
-    }
-    return {
-      type: messageTypes.CREATE_SUB_THREAD,
-      id: rawMessageInfo.id,
-      threadID: rawMessageInfo.threadID,
-      creator,
-      time: rawMessageInfo.time,
-      childThreadInfo,
-    };
-  },
-
-  rawMessageInfoFromMessageData(
-    messageData: CreateSubthreadMessageData,
-    id: ?string,
-  ): RawCreateSubthreadMessageInfo {
-    invariant(id, 'RawCreateSubthreadMessageInfo needs id');
-    return { ...messageData, id };
-  },
-
-  robotext(messageInfo: CreateSubthreadMessageInfo): EntityText {
-    const threadEntity = ET.thread({
-      display: 'shortName',
-      threadInfo: messageInfo.childThreadInfo,
-      subchannel: true,
-    });
-
-    let text;
-    if (messageInfo.childThreadInfo.name) {
-      const childNoun =
-        messageInfo.childThreadInfo.type === threadTypes.SIDEBAR
-          ? 'thread'
-          : 'subchannel';
-      text = ET`created a ${childNoun} named "${threadEntity}"`;
-    } else {
-      text = ET`created a ${threadEntity}`;
-    }
-
-    const creator = ET.user({ userInfo: messageInfo.creator });
-    return ET`${creator} ${text}`;
-  },
-
-  async notificationTexts(
-    messageInfos: $ReadOnlyArray<MessageInfo>,
-    threadInfo: ThreadInfo,
-  ): Promise<NotifTexts> {
-    const messageInfo = assertSingleMessageInfo(messageInfos);
-    invariant(
-      messageInfo.type === messageTypes.CREATE_SUB_THREAD,
-      'messageInfo should be messageTypes.CREATE_SUB_THREAD!',
-    );
-    return notifTextsForSubthreadCreation({
-      creator: messageInfo.creator,
-      threadType: messageInfo.childThreadInfo.type,
-      parentThreadInfo: threadInfo,
-      childThreadName: messageInfo.childThreadInfo.name,
-      childThreadUIName: messageInfo.childThreadInfo.uiName,
-    });
-  },
-
-  generatesNotifs: async (
-    rawMessageInfo: RawCreateSubthreadMessageInfo,
-    messageData: CreateSubthreadMessageData,
-    params: GeneratesNotifsParams,
-  ) => {
-    const { userNotMemberOfSubthreads } = params;
-    return userNotMemberOfSubthreads.has(rawMessageInfo.childThreadID)
-      ? pushTypes.NOTIF
-      : undefined;
-  },
-
-  threadIDs(
-    rawMessageInfo: RawCreateSubthreadMessageInfo,
-  ): $ReadOnlyArray<string> {
-    return [rawMessageInfo.childThreadID];
-  },
-
-  canBeSidebarSource: true,
-
-  canBePinned: false,
-
-  validator: rawCreateSubthreadMessageInfoValidator,
-});
+  ) => string,
+  ...
+};
+
+export const createSubThreadMessageSpec: CreateSubThreadMessageSpec =
+  Object.freeze({
+    messageContentForServerDB(
+      data: CreateSubthreadMessageData | RawCreateSubthreadMessageInfo,
+    ): string {
+      return data.childThreadID;
+    },
+
+    messageContentForClientDB(data: RawCreateSubthreadMessageInfo): string {
+      return createSubThreadMessageSpec.messageContentForServerDB(data);
+    },
+
+    rawMessageInfoFromServerDBRow(row: Object): ?RawCreateSubthreadMessageInfo {
+      const subthreadPermissions = row.subthread_permissions;
+      if (!permissionLookup(subthreadPermissions, threadPermissions.KNOW_OF)) {
+        return null;
+      }
+      return {
+        type: messageTypes.CREATE_SUB_THREAD,
+        id: row.id.toString(),
+        threadID: row.threadID.toString(),
+        time: row.time,
+        creatorID: row.creatorID.toString(),
+        childThreadID: row.content,
+      };
+    },
+
+    rawMessageInfoFromClientDB(
+      clientDBMessageInfo: ClientDBMessageInfo,
+    ): RawCreateSubthreadMessageInfo {
+      const content = clientDBMessageInfo.content;
+      invariant(
+        content !== undefined && content !== null,
+        'content must be defined for CreateSubThread',
+      );
+      const rawCreateSubthreadMessageInfo: RawCreateSubthreadMessageInfo = {
+        type: messageTypes.CREATE_SUB_THREAD,
+        id: clientDBMessageInfo.id,
+        threadID: clientDBMessageInfo.thread,
+        time: parseInt(clientDBMessageInfo.time),
+        creatorID: clientDBMessageInfo.user,
+        childThreadID: content,
+      };
+      return rawCreateSubthreadMessageInfo;
+    },
+
+    createMessageInfo(
+      rawMessageInfo: RawCreateSubthreadMessageInfo,
+      creator: RelativeUserInfo,
+      params: CreateMessageInfoParams,
+    ): ?CreateSubthreadMessageInfo {
+      const { threadInfos } = params;
+      const childThreadInfo = threadInfos[rawMessageInfo.childThreadID];
+      if (!childThreadInfo) {
+        return null;
+      }
+      return {
+        type: messageTypes.CREATE_SUB_THREAD,
+        id: rawMessageInfo.id,
+        threadID: rawMessageInfo.threadID,
+        creator,
+        time: rawMessageInfo.time,
+        childThreadInfo,
+      };
+    },
+
+    rawMessageInfoFromMessageData(
+      messageData: CreateSubthreadMessageData,
+      id: ?string,
+    ): RawCreateSubthreadMessageInfo {
+      invariant(id, 'RawCreateSubthreadMessageInfo needs id');
+      return { ...messageData, id };
+    },
+
+    robotext(messageInfo: CreateSubthreadMessageInfo): EntityText {
+      const threadEntity = ET.thread({
+        display: 'shortName',
+        threadInfo: messageInfo.childThreadInfo,
+        subchannel: true,
+      });
+
+      let text;
+      if (messageInfo.childThreadInfo.name) {
+        const childNoun =
+          messageInfo.childThreadInfo.type === threadTypes.SIDEBAR
+            ? 'thread'
+            : 'subchannel';
+        text = ET`created a ${childNoun} named "${threadEntity}"`;
+      } else {
+        text = ET`created a ${threadEntity}`;
+      }
+
+      const creator = ET.user({ userInfo: messageInfo.creator });
+      return ET`${creator} ${text}`;
+    },
+
+    async notificationTexts(
+      messageInfos: $ReadOnlyArray<MessageInfo>,
+      threadInfo: ThreadInfo,
+    ): Promise<NotifTexts> {
+      const messageInfo = assertSingleMessageInfo(messageInfos);
+      invariant(
+        messageInfo.type === messageTypes.CREATE_SUB_THREAD,
+        'messageInfo should be messageTypes.CREATE_SUB_THREAD!',
+      );
+      return notifTextsForSubthreadCreation({
+        creator: messageInfo.creator,
+        threadType: messageInfo.childThreadInfo.type,
+        parentThreadInfo: threadInfo,
+        childThreadName: messageInfo.childThreadInfo.name,
+        childThreadUIName: messageInfo.childThreadInfo.uiName,
+      });
+    },
+
+    generatesNotifs: async (
+      rawMessageInfo: RawCreateSubthreadMessageInfo,
+      messageData: CreateSubthreadMessageData,
+      params: GeneratesNotifsParams,
+    ) => {
+      const { userNotMemberOfSubthreads } = params;
+      return userNotMemberOfSubthreads.has(rawMessageInfo.childThreadID)
+        ? pushTypes.NOTIF
+        : undefined;
+    },
+
+    threadIDs(
+      rawMessageInfo: RawCreateSubthreadMessageInfo,
+    ): $ReadOnlyArray<string> {
+      return [rawMessageInfo.childThreadID];
+    },
+
+    canBeSidebarSource: true,
+
+    canBePinned: false,
+
+    validator: rawCreateSubthreadMessageInfoValidator,
+  });
diff --git a/lib/shared/messages/create-thread-message-spec.js b/lib/shared/messages/create-thread-message-spec.js
--- a/lib/shared/messages/create-thread-message-spec.js
+++ b/lib/shared/messages/create-thread-message-spec.js
@@ -31,11 +31,20 @@
 import { notifTextsForSubthreadCreation } from '../notif-utils.js';
 import { threadNoun } from '../thread-utils.js';
 
-export const createThreadMessageSpec: MessageSpec<
+type CreateThreadMessageSpec = MessageSpec<
   CreateThreadMessageData,
   RawCreateThreadMessageInfo,
   CreateThreadMessageInfo,
-> = Object.freeze({
+> & {
+  // We need to explicitly type this as non-optional so that
+  // it can be referenced from messageContentForClientDB below
+  +messageContentForServerDB: (
+    data: CreateThreadMessageData | RawCreateThreadMessageInfo,
+  ) => string,
+  ...
+};
+
+export const createThreadMessageSpec: CreateThreadMessageSpec = Object.freeze({
   messageContentForServerDB(
     data: CreateThreadMessageData | RawCreateThreadMessageInfo,
   ): string {
@@ -43,7 +52,7 @@
   },
 
   messageContentForClientDB(data: RawCreateThreadMessageInfo): string {
-    return this.messageContentForServerDB(data);
+    return createThreadMessageSpec.messageContentForServerDB(data);
   },
 
   rawMessageInfoFromServerDBRow(row: Object): RawCreateThreadMessageInfo {
diff --git a/lib/shared/messages/delete-entry-message-spec.js b/lib/shared/messages/delete-entry-message-spec.js
--- a/lib/shared/messages/delete-entry-message-spec.js
+++ b/lib/shared/messages/delete-entry-message-spec.js
@@ -21,11 +21,20 @@
 import { prettyDate } from '../../utils/date-utils.js';
 import { ET, type EntityText } from '../../utils/entity-text.js';
 
-export const deleteEntryMessageSpec: MessageSpec<
+type DeleteEntryMessageSpec = MessageSpec<
   DeleteEntryMessageData,
   RawDeleteEntryMessageInfo,
   DeleteEntryMessageInfo,
-> = Object.freeze({
+> & {
+  // We need to explicitly type this as non-optional so that
+  // it can be referenced from messageContentForClientDB below
+  +messageContentForServerDB: (
+    data: DeleteEntryMessageData | RawDeleteEntryMessageInfo,
+  ) => string,
+  ...
+};
+
+export const deleteEntryMessageSpec: DeleteEntryMessageSpec = Object.freeze({
   messageContentForServerDB(
     data: DeleteEntryMessageData | RawDeleteEntryMessageInfo,
   ): string {
@@ -37,7 +46,7 @@
   },
 
   messageContentForClientDB(data: RawDeleteEntryMessageInfo): string {
-    return this.messageContentForServerDB(data);
+    return deleteEntryMessageSpec.messageContentForServerDB(data);
   },
 
   rawMessageInfoFromServerDBRow(row: Object): RawDeleteEntryMessageInfo {
diff --git a/lib/shared/messages/edit-entry-message-spec.js b/lib/shared/messages/edit-entry-message-spec.js
--- a/lib/shared/messages/edit-entry-message-spec.js
+++ b/lib/shared/messages/edit-entry-message-spec.js
@@ -22,11 +22,20 @@
 import { ET, type EntityText } from '../../utils/entity-text.js';
 import { notifTextsForEntryCreationOrEdit } from '../notif-utils.js';
 
-export const editEntryMessageSpec: MessageSpec<
+type EditEntryMessageSpec = MessageSpec<
   EditEntryMessageData,
   RawEditEntryMessageInfo,
   EditEntryMessageInfo,
-> = Object.freeze({
+> & {
+  // We need to explicitly type this as non-optional so that
+  // it can be referenced from messageContentForClientDB below
+  +messageContentForServerDB: (
+    data: EditEntryMessageData | RawEditEntryMessageInfo,
+  ) => string,
+  ...
+};
+
+export const editEntryMessageSpec: EditEntryMessageSpec = Object.freeze({
   messageContentForServerDB(
     data: EditEntryMessageData | RawEditEntryMessageInfo,
   ): string {
@@ -38,7 +47,7 @@
   },
 
   messageContentForClientDB(data: RawEditEntryMessageInfo): string {
-    return this.messageContentForServerDB(data);
+    return editEntryMessageSpec.messageContentForServerDB(data);
   },
 
   rawMessageInfoFromServerDBRow(row: Object): RawEditEntryMessageInfo {
diff --git a/lib/shared/messages/multimedia-message-spec.js b/lib/shared/messages/multimedia-message-spec.js
--- a/lib/shared/messages/multimedia-message-spec.js
+++ b/lib/shared/messages/multimedia-message-spec.js
@@ -53,11 +53,24 @@
 import { threadIsGroupChat } from '../thread-utils.js';
 import { FUTURE_CODE_VERSION, hasMinCodeVersion } from '../version-utils.js';
 
-export const multimediaMessageSpec: MessageSpec<
+type MultimediaMessageSpec = MessageSpec<
   MediaMessageData | ImagesMessageData,
   RawMediaMessageInfo | RawImagesMessageInfo,
   MediaMessageInfo | ImagesMessageInfo,
-> = Object.freeze({
+> & {
+  // We need to explicitly type this as non-optional so that
+  // it can be referenced from messageContentForClientDB below
+  +messageContentForServerDB: (
+    data:
+      | MediaMessageData
+      | ImagesMessageData
+      | RawMediaMessageInfo
+      | RawImagesMessageInfo,
+  ) => string,
+  ...
+};
+
+export const multimediaMessageSpec: MultimediaMessageSpec = Object.freeze({
   messageContentForServerDB(
     data:
       | MediaMessageData
@@ -77,7 +90,7 @@
   messageContentForClientDB(
     data: RawMediaMessageInfo | RawImagesMessageInfo,
   ): string {
-    return this.messageContentForServerDB(data);
+    return multimediaMessageSpec.messageContentForServerDB(data);
   },
 
   rawMessageInfoFromClientDB(
diff --git a/lib/shared/messages/remove-members-message-spec.js b/lib/shared/messages/remove-members-message-spec.js
--- a/lib/shared/messages/remove-members-message-spec.js
+++ b/lib/shared/messages/remove-members-message-spec.js
@@ -30,145 +30,160 @@
 import { values } from '../../utils/objects.js';
 import { notifRobotextForMessageInfo } from '../notif-utils.js';
 
-export const removeMembersMessageSpec: MessageSpec<
+type RemoveMembersMessageSpec = MessageSpec<
   RemoveMembersMessageData,
   RawRemoveMembersMessageInfo,
   RemoveMembersMessageInfo,
-> = Object.freeze({
-  messageContentForServerDB(
+> & {
+  // We need to explicitly type this as non-optional so that
+  // it can be referenced from messageContentForClientDB below
+  +messageContentForServerDB: (
     data: RemoveMembersMessageData | RawRemoveMembersMessageInfo,
-  ): string {
-    return JSON.stringify(data.removedUserIDs);
-  },
-
-  messageContentForClientDB(data: RawRemoveMembersMessageInfo): string {
-    return this.messageContentForServerDB(data);
-  },
-
-  rawMessageInfoFromServerDBRow(row: Object): RawRemoveMembersMessageInfo {
-    return {
-      type: messageTypes.REMOVE_MEMBERS,
-      id: row.id.toString(),
-      threadID: row.threadID.toString(),
-      time: row.time,
-      creatorID: row.creatorID.toString(),
-      removedUserIDs: JSON.parse(row.content),
-    };
-  },
-
-  rawMessageInfoFromClientDB(
-    clientDBMessageInfo: ClientDBMessageInfo,
-  ): RawRemoveMembersMessageInfo {
-    const content = clientDBMessageInfo.content;
-    invariant(
-      content !== undefined && content !== null,
-      'content must be defined for RemoveMembers',
-    );
-    const rawRemoveMembersMessageInfo: RawRemoveMembersMessageInfo = {
-      type: messageTypes.REMOVE_MEMBERS,
-      id: clientDBMessageInfo.id,
-      threadID: clientDBMessageInfo.thread,
-      time: parseInt(clientDBMessageInfo.time),
-      creatorID: clientDBMessageInfo.user,
-      removedUserIDs: JSON.parse(content),
-    };
-    return rawRemoveMembersMessageInfo;
-  },
-
-  createMessageInfo(
-    rawMessageInfo: RawRemoveMembersMessageInfo,
-    creator: RelativeUserInfo,
-    params: CreateMessageInfoParams,
-  ): RemoveMembersMessageInfo {
-    const removedMembers = params.createRelativeUserInfos(
-      rawMessageInfo.removedUserIDs,
-    );
-    return {
-      type: messageTypes.REMOVE_MEMBERS,
-      id: rawMessageInfo.id,
-      threadID: rawMessageInfo.threadID,
-      creator,
-      time: rawMessageInfo.time,
-      removedMembers,
-    };
-  },
-
-  rawMessageInfoFromMessageData(
-    messageData: RemoveMembersMessageData,
-    id: ?string,
-  ): RawRemoveMembersMessageInfo {
-    invariant(id, 'RawRemoveMembersMessageInfo needs id');
-    return { ...messageData, id };
-  },
-
-  robotext(messageInfo: RemoveMembersMessageInfo): EntityText {
-    const users = messageInfo.removedMembers;
-    invariant(users.length !== 0, 'added who??');
-
-    const creator = ET.user({ userInfo: messageInfo.creator });
-    const removedUsers = pluralizeEntityText(
-      users.map(user => ET`${ET.user({ userInfo: user })}`),
-    );
+  ) => string,
+  ...
+};
+
+export const removeMembersMessageSpec: RemoveMembersMessageSpec = Object.freeze(
+  {
+    messageContentForServerDB(
+      data: RemoveMembersMessageData | RawRemoveMembersMessageInfo,
+    ): string {
+      return JSON.stringify(data.removedUserIDs);
+    },
+
+    messageContentForClientDB(data: RawRemoveMembersMessageInfo): string {
+      return removeMembersMessageSpec.messageContentForServerDB(data);
+    },
+
+    rawMessageInfoFromServerDBRow(row: Object): RawRemoveMembersMessageInfo {
+      return {
+        type: messageTypes.REMOVE_MEMBERS,
+        id: row.id.toString(),
+        threadID: row.threadID.toString(),
+        time: row.time,
+        creatorID: row.creatorID.toString(),
+        removedUserIDs: JSON.parse(row.content),
+      };
+    },
+
+    rawMessageInfoFromClientDB(
+      clientDBMessageInfo: ClientDBMessageInfo,
+    ): RawRemoveMembersMessageInfo {
+      const content = clientDBMessageInfo.content;
+      invariant(
+        content !== undefined && content !== null,
+        'content must be defined for RemoveMembers',
+      );
+      const rawRemoveMembersMessageInfo: RawRemoveMembersMessageInfo = {
+        type: messageTypes.REMOVE_MEMBERS,
+        id: clientDBMessageInfo.id,
+        threadID: clientDBMessageInfo.thread,
+        time: parseInt(clientDBMessageInfo.time),
+        creatorID: clientDBMessageInfo.user,
+        removedUserIDs: JSON.parse(content),
+      };
+      return rawRemoveMembersMessageInfo;
+    },
+
+    createMessageInfo(
+      rawMessageInfo: RawRemoveMembersMessageInfo,
+      creator: RelativeUserInfo,
+      params: CreateMessageInfoParams,
+    ): RemoveMembersMessageInfo {
+      const removedMembers = params.createRelativeUserInfos(
+        rawMessageInfo.removedUserIDs,
+      );
+      return {
+        type: messageTypes.REMOVE_MEMBERS,
+        id: rawMessageInfo.id,
+        threadID: rawMessageInfo.threadID,
+        creator,
+        time: rawMessageInfo.time,
+        removedMembers,
+      };
+    },
+
+    rawMessageInfoFromMessageData(
+      messageData: RemoveMembersMessageData,
+      id: ?string,
+    ): RawRemoveMembersMessageInfo {
+      invariant(id, 'RawRemoveMembersMessageInfo needs id');
+      return { ...messageData, id };
+    },
+
+    robotext(messageInfo: RemoveMembersMessageInfo): EntityText {
+      const users = messageInfo.removedMembers;
+      invariant(users.length !== 0, 'added who??');
+
+      const creator = ET.user({ userInfo: messageInfo.creator });
+      const removedUsers = pluralizeEntityText(
+        users.map(user => ET`${ET.user({ userInfo: user })}`),
+      );
 
-    return ET`${creator} removed ${removedUsers}`;
-  },
+      return ET`${creator} removed ${removedUsers}`;
+    },
+
+    async notificationTexts(
+      messageInfos: $ReadOnlyArray<MessageInfo>,
+      threadInfo: ThreadInfo,
+      params: NotificationTextsParams,
+    ): Promise<NotifTexts> {
+      const removedMembersObject: { [string]: RelativeUserInfo } = {};
+      for (const messageInfo of messageInfos) {
+        invariant(
+          messageInfo.type === messageTypes.REMOVE_MEMBERS,
+          'messageInfo should be messageTypes.REMOVE_MEMBERS!',
+        );
+        for (const member of messageInfo.removedMembers) {
+          removedMembersObject[member.id] = member;
+        }
+      }
+      const removedMembers = values(removedMembersObject);
 
-  async notificationTexts(
-    messageInfos: $ReadOnlyArray<MessageInfo>,
-    threadInfo: ThreadInfo,
-    params: NotificationTextsParams,
-  ): Promise<NotifTexts> {
-    const removedMembersObject: { [string]: RelativeUserInfo } = {};
-    for (const messageInfo of messageInfos) {
+      const mostRecentMessageInfo = messageInfos[0];
       invariant(
-        messageInfo.type === messageTypes.REMOVE_MEMBERS,
+        mostRecentMessageInfo.type === messageTypes.REMOVE_MEMBERS,
         'messageInfo should be messageTypes.REMOVE_MEMBERS!',
       );
-      for (const member of messageInfo.removedMembers) {
-        removedMembersObject[member.id] = member;
-      }
-    }
-    const removedMembers = values(removedMembersObject);
-
-    const mostRecentMessageInfo = messageInfos[0];
-    invariant(
-      mostRecentMessageInfo.type === messageTypes.REMOVE_MEMBERS,
-      'messageInfo should be messageTypes.REMOVE_MEMBERS!',
-    );
-    const mergedMessageInfo = { ...mostRecentMessageInfo, removedMembers };
-
-    const { parentThreadInfo } = params;
-    const robotext = notifRobotextForMessageInfo(
-      mergedMessageInfo,
-      threadInfo,
-      parentThreadInfo,
-    );
-    const merged = ET`${robotext} from ${ET.thread({
-      display: 'shortName',
-      threadInfo,
-    })}`;
-    return {
-      merged,
-      title: threadInfo.uiName,
-      body: robotext,
-    };
-  },
+      const mergedMessageInfo = { ...mostRecentMessageInfo, removedMembers };
 
-  notificationCollapseKey(rawMessageInfo: RawRemoveMembersMessageInfo): string {
-    return joinResult(
-      rawMessageInfo.type,
-      rawMessageInfo.threadID,
-      rawMessageInfo.creatorID,
-    );
-  },
+      const { parentThreadInfo } = params;
+      const robotext = notifRobotextForMessageInfo(
+        mergedMessageInfo,
+        threadInfo,
+        parentThreadInfo,
+      );
+      const merged = ET`${robotext} from ${ET.thread({
+        display: 'shortName',
+        threadInfo,
+      })}`;
+      return {
+        merged,
+        title: threadInfo.uiName,
+        body: robotext,
+      };
+    },
+
+    notificationCollapseKey(
+      rawMessageInfo: RawRemoveMembersMessageInfo,
+    ): string {
+      return joinResult(
+        rawMessageInfo.type,
+        rawMessageInfo.threadID,
+        rawMessageInfo.creatorID,
+      );
+    },
 
-  userIDs(rawMessageInfo: RawRemoveMembersMessageInfo): $ReadOnlyArray<string> {
-    return rawMessageInfo.removedUserIDs;
-  },
+    userIDs(
+      rawMessageInfo: RawRemoveMembersMessageInfo,
+    ): $ReadOnlyArray<string> {
+      return rawMessageInfo.removedUserIDs;
+    },
 
-  canBeSidebarSource: true,
+    canBeSidebarSource: true,
 
-  canBePinned: false,
+    canBePinned: false,
 
-  validator: rawRemoveMembersMessageInfoValidator,
-});
+    validator: rawRemoveMembersMessageInfoValidator,
+  },
+);
diff --git a/lib/shared/messages/restore-entry-message-spec.js b/lib/shared/messages/restore-entry-message-spec.js
--- a/lib/shared/messages/restore-entry-message-spec.js
+++ b/lib/shared/messages/restore-entry-message-spec.js
@@ -21,11 +21,20 @@
 import { prettyDate } from '../../utils/date-utils.js';
 import { ET, type EntityText } from '../../utils/entity-text.js';
 
-export const restoreEntryMessageSpec: MessageSpec<
+type RestoreEntryMessageSpec = MessageSpec<
   RestoreEntryMessageData,
   RawRestoreEntryMessageInfo,
   RestoreEntryMessageInfo,
-> = Object.freeze({
+> & {
+  // We need to explicitly type this as non-optional so that
+  // it can be referenced from messageContentForClientDB below
+  +messageContentForServerDB: (
+    data: RestoreEntryMessageData | RawRestoreEntryMessageInfo,
+  ) => string,
+  ...
+};
+
+export const restoreEntryMessageSpec: RestoreEntryMessageSpec = Object.freeze({
   messageContentForServerDB(
     data: RestoreEntryMessageData | RawRestoreEntryMessageInfo,
   ): string {
@@ -37,7 +46,7 @@
   },
 
   messageContentForClientDB(data: RawRestoreEntryMessageInfo): string {
-    return this.messageContentForServerDB(data);
+    return restoreEntryMessageSpec.messageContentForServerDB(data);
   },
 
   rawMessageInfoFromServerDBRow(row: Object): RawRestoreEntryMessageInfo {
diff --git a/lib/shared/messages/text-message-spec.js b/lib/shared/messages/text-message-spec.js
--- a/lib/shared/messages/text-message-spec.js
+++ b/lib/shared/messages/text-message-spec.js
@@ -89,11 +89,20 @@
   return result;
 };
 
-export const textMessageSpec: MessageSpec<
+type TextMessageSpec = MessageSpec<
   TextMessageData,
   RawTextMessageInfo,
   TextMessageInfo,
-> = Object.freeze({
+> & {
+  // We need to explicitly type this as non-optional so that
+  // it can be referenced from messageContentForClientDB below
+  +messageContentForServerDB: (
+    data: TextMessageData | RawTextMessageInfo,
+  ) => string,
+  ...
+};
+
+export const textMessageSpec: TextMessageSpec = Object.freeze({
   messageContentForServerDB(
     data: TextMessageData | RawTextMessageInfo,
   ): string {
@@ -101,7 +110,7 @@
   },
 
   messageContentForClientDB(data: RawTextMessageInfo): string {
-    return this.messageContentForServerDB(data);
+    return textMessageSpec.messageContentForServerDB(data);
   },
 
   messageTitle({ messageInfo, markdownRules }) {
diff --git a/lib/shared/messages/toggle-pin-message-spec.js b/lib/shared/messages/toggle-pin-message-spec.js
--- a/lib/shared/messages/toggle-pin-message-spec.js
+++ b/lib/shared/messages/toggle-pin-message-spec.js
@@ -23,11 +23,20 @@
 import { getPinnedContentFromMessage } from '../message-utils.js';
 import { hasMinCodeVersion } from '../version-utils.js';
 
-export const togglePinMessageSpec: MessageSpec<
+type TogglePinMessageSpec = MessageSpec<
   TogglePinMessageData,
   RawTogglePinMessageInfo,
   TogglePinMessageInfo,
-> = Object.freeze({
+> & {
+  // We need to explicitly type this as non-optional so that
+  // it can be referenced from messageContentForClientDB below
+  +messageContentForServerDB: (
+    data: TogglePinMessageData | RawTogglePinMessageInfo,
+  ) => string,
+  ...
+};
+
+export const togglePinMessageSpec: TogglePinMessageSpec = Object.freeze({
   messageContentForServerDB(
     data: TogglePinMessageData | RawTogglePinMessageInfo,
   ): string {
@@ -39,7 +48,7 @@
   },
 
   messageContentForClientDB(data: RawTogglePinMessageInfo): string {
-    return this.messageContentForServerDB(data);
+    return togglePinMessageSpec.messageContentForServerDB(data);
   },
 
   rawMessageInfoFromServerDBRow(
diff --git a/lib/shared/messages/update-relationship-message-spec.js b/lib/shared/messages/update-relationship-message-spec.js
--- a/lib/shared/messages/update-relationship-message-spec.js
+++ b/lib/shared/messages/update-relationship-message-spec.js
@@ -24,139 +24,154 @@
 import type { RelativeUserInfo } from '../../types/user-types.js';
 import { ET, type EntityText } from '../../utils/entity-text.js';
 
-export const updateRelationshipMessageSpec: MessageSpec<
+type UpdateRelationshipMessageSpec = MessageSpec<
   UpdateRelationshipMessageData,
   RawUpdateRelationshipMessageInfo,
   UpdateRelationshipMessageInfo,
-> = Object.freeze({
-  messageContentForServerDB(
+> & {
+  // We need to explicitly type this as non-optional so that
+  // it can be referenced from messageContentForClientDB below
+  +messageContentForServerDB: (
     data: UpdateRelationshipMessageData | RawUpdateRelationshipMessageInfo,
-  ): string {
-    return JSON.stringify({
-      operation: data.operation,
-      targetID: data.targetID,
-    });
-  },
-
-  messageContentForClientDB(data: RawUpdateRelationshipMessageInfo): string {
-    return this.messageContentForServerDB(data);
-  },
-
-  rawMessageInfoFromServerDBRow(row: Object): RawUpdateRelationshipMessageInfo {
-    const content = JSON.parse(row.content);
-    return {
-      type: messageTypes.UPDATE_RELATIONSHIP,
-      id: row.id.toString(),
-      threadID: row.threadID.toString(),
-      time: row.time,
-      creatorID: row.creatorID.toString(),
-      targetID: content.targetID,
-      operation: content.operation,
-    };
-  },
-
-  rawMessageInfoFromClientDB(
-    clientDBMessageInfo: ClientDBMessageInfo,
-  ): RawUpdateRelationshipMessageInfo {
-    invariant(
-      clientDBMessageInfo.content !== undefined &&
-        clientDBMessageInfo.content !== null,
-      'content must be defined for UpdateRelationship',
-    );
-    const content = JSON.parse(clientDBMessageInfo.content);
-    const rawUpdateRelationshipMessageInfo: RawUpdateRelationshipMessageInfo = {
-      type: messageTypes.UPDATE_RELATIONSHIP,
-      id: clientDBMessageInfo.id,
-      threadID: clientDBMessageInfo.thread,
-      time: parseInt(clientDBMessageInfo.time),
-      creatorID: clientDBMessageInfo.user,
-      targetID: content.targetID,
-      operation: content.operation,
-    };
-    return rawUpdateRelationshipMessageInfo;
-  },
-
-  createMessageInfo(
-    rawMessageInfo: RawUpdateRelationshipMessageInfo,
-    creator: RelativeUserInfo,
-    params: CreateMessageInfoParams,
-  ): ?UpdateRelationshipMessageInfo {
-    const target = params.createRelativeUserInfos([rawMessageInfo.targetID])[0];
-    if (!target) {
-      return null;
-    }
-    return {
-      type: messageTypes.UPDATE_RELATIONSHIP,
-      id: rawMessageInfo.id,
-      threadID: rawMessageInfo.threadID,
-      creator,
-      target,
-      time: rawMessageInfo.time,
-      operation: rawMessageInfo.operation,
-    };
-  },
-
-  rawMessageInfoFromMessageData(
-    messageData: UpdateRelationshipMessageData,
-    id: ?string,
-  ): RawUpdateRelationshipMessageInfo {
-    invariant(id, 'RawUpdateRelationshipMessageInfo needs id');
-    return { ...messageData, id };
-  },
-
-  // ESLint doesn't recognize that invariant always throws
-  // eslint-disable-next-line consistent-return
-  robotext(messageInfo: UpdateRelationshipMessageInfo): EntityText {
-    const creator = ET.user({ userInfo: messageInfo.creator });
-    if (messageInfo.operation === 'request_sent') {
-      const target = ET.user({ userInfo: messageInfo.target });
-      return ET`${creator} sent ${target} a friend request`;
-    } else if (messageInfo.operation === 'request_accepted') {
-      const targetPossessive = ET.user({
-        userInfo: messageInfo.target,
-        possessive: true,
+  ) => string,
+  ...
+};
+
+export const updateRelationshipMessageSpec: UpdateRelationshipMessageSpec =
+  Object.freeze({
+    messageContentForServerDB(
+      data: UpdateRelationshipMessageData | RawUpdateRelationshipMessageInfo,
+    ): string {
+      return JSON.stringify({
+        operation: data.operation,
+        targetID: data.targetID,
       });
-      return ET`${creator} accepted ${targetPossessive} friend request`;
-    }
-    invariant(
-      false,
-      `Invalid operation ${messageInfo.operation} ` +
-        `of message with type ${messageInfo.type}`,
-    );
-  },
-
-  unshimMessageInfo(
-    unwrapped: RawUpdateRelationshipMessageInfo,
-  ): RawUpdateRelationshipMessageInfo {
-    return unwrapped;
-  },
-
-  async notificationTexts(
-    messageInfos: $ReadOnlyArray<MessageInfo>,
-    threadInfo: ThreadInfo,
-  ): Promise<NotifTexts> {
-    const messageInfo = assertSingleMessageInfo(messageInfos);
-    const creator = ET.user({ userInfo: messageInfo.creator });
-    const prefix = ET`${creator}`;
-    const title = threadInfo.uiName;
-    const body =
-      messageInfo.operation === 'request_sent'
-        ? 'sent you a friend request'
-        : 'accepted your friend request';
-    const merged = ET`${prefix} ${body}`;
-    return {
-      merged,
-      body,
-      title,
-      prefix,
-    };
-  },
-
-  generatesNotifs: async () => pushTypes.NOTIF,
-
-  canBeSidebarSource: true,
-
-  canBePinned: false,
-
-  validator: rawUpdateRelationshipMessageInfoValidator,
-});
+    },
+
+    messageContentForClientDB(data: RawUpdateRelationshipMessageInfo): string {
+      return updateRelationshipMessageSpec.messageContentForServerDB(data);
+    },
+
+    rawMessageInfoFromServerDBRow(
+      row: Object,
+    ): RawUpdateRelationshipMessageInfo {
+      const content = JSON.parse(row.content);
+      return {
+        type: messageTypes.UPDATE_RELATIONSHIP,
+        id: row.id.toString(),
+        threadID: row.threadID.toString(),
+        time: row.time,
+        creatorID: row.creatorID.toString(),
+        targetID: content.targetID,
+        operation: content.operation,
+      };
+    },
+
+    rawMessageInfoFromClientDB(
+      clientDBMessageInfo: ClientDBMessageInfo,
+    ): RawUpdateRelationshipMessageInfo {
+      invariant(
+        clientDBMessageInfo.content !== undefined &&
+          clientDBMessageInfo.content !== null,
+        'content must be defined for UpdateRelationship',
+      );
+      const content = JSON.parse(clientDBMessageInfo.content);
+      const rawUpdateRelationshipMessageInfo: RawUpdateRelationshipMessageInfo =
+        {
+          type: messageTypes.UPDATE_RELATIONSHIP,
+          id: clientDBMessageInfo.id,
+          threadID: clientDBMessageInfo.thread,
+          time: parseInt(clientDBMessageInfo.time),
+          creatorID: clientDBMessageInfo.user,
+          targetID: content.targetID,
+          operation: content.operation,
+        };
+      return rawUpdateRelationshipMessageInfo;
+    },
+
+    createMessageInfo(
+      rawMessageInfo: RawUpdateRelationshipMessageInfo,
+      creator: RelativeUserInfo,
+      params: CreateMessageInfoParams,
+    ): ?UpdateRelationshipMessageInfo {
+      const target = params.createRelativeUserInfos([
+        rawMessageInfo.targetID,
+      ])[0];
+      if (!target) {
+        return null;
+      }
+      return {
+        type: messageTypes.UPDATE_RELATIONSHIP,
+        id: rawMessageInfo.id,
+        threadID: rawMessageInfo.threadID,
+        creator,
+        target,
+        time: rawMessageInfo.time,
+        operation: rawMessageInfo.operation,
+      };
+    },
+
+    rawMessageInfoFromMessageData(
+      messageData: UpdateRelationshipMessageData,
+      id: ?string,
+    ): RawUpdateRelationshipMessageInfo {
+      invariant(id, 'RawUpdateRelationshipMessageInfo needs id');
+      return { ...messageData, id };
+    },
+
+    // ESLint doesn't recognize that invariant always throws
+    // eslint-disable-next-line consistent-return
+    robotext(messageInfo: UpdateRelationshipMessageInfo): EntityText {
+      const creator = ET.user({ userInfo: messageInfo.creator });
+      if (messageInfo.operation === 'request_sent') {
+        const target = ET.user({ userInfo: messageInfo.target });
+        return ET`${creator} sent ${target} a friend request`;
+      } else if (messageInfo.operation === 'request_accepted') {
+        const targetPossessive = ET.user({
+          userInfo: messageInfo.target,
+          possessive: true,
+        });
+        return ET`${creator} accepted ${targetPossessive} friend request`;
+      }
+      invariant(
+        false,
+        `Invalid operation ${messageInfo.operation} ` +
+          `of message with type ${messageInfo.type}`,
+      );
+    },
+
+    unshimMessageInfo(
+      unwrapped: RawUpdateRelationshipMessageInfo,
+    ): RawUpdateRelationshipMessageInfo {
+      return unwrapped;
+    },
+
+    async notificationTexts(
+      messageInfos: $ReadOnlyArray<MessageInfo>,
+      threadInfo: ThreadInfo,
+    ): Promise<NotifTexts> {
+      const messageInfo = assertSingleMessageInfo(messageInfos);
+      const creator = ET.user({ userInfo: messageInfo.creator });
+      const prefix = ET`${creator}`;
+      const title = threadInfo.uiName;
+      const body =
+        messageInfo.operation === 'request_sent'
+          ? 'sent you a friend request'
+          : 'accepted your friend request';
+      const merged = ET`${prefix} ${body}`;
+      return {
+        merged,
+        body,
+        title,
+        prefix,
+      };
+    },
+
+    generatesNotifs: async () => pushTypes.NOTIF,
+
+    canBeSidebarSource: true,
+
+    canBePinned: false,
+
+    validator: rawUpdateRelationshipMessageInfoValidator,
+  });