diff --git a/keyserver/src/creators/message-creator.js b/keyserver/src/creators/message-creator.js
--- a/keyserver/src/creators/message-creator.js
+++ b/keyserver/src/creators/message-creator.js
@@ -39,7 +39,7 @@
 } from '../fetchers/message-fetchers.js';
 import { fetchOtherSessionsForViewer } from '../fetchers/session-fetchers.js';
 import { fetchServerThreadInfos } from '../fetchers/thread-fetchers.js';
-import type { Device } from '../push/send';
+import type { Device, PushUserInfo } from '../push/send.js';
 import { sendPushNotifs, sendRescindNotifs } from '../push/send.js';
 import { handleAsyncPromise } from '../responders/handlers.js';
 import type { Viewer } from '../session/viewer.js';
@@ -393,10 +393,12 @@
     }
   }
 
-  const messageInfosPerUser = {};
+  const messageInfosPerUser: {
+    [userID: string]: $ReadOnlyArray<RawMessageInfo>,
+  } = {};
   const latestMessagesPerUser: LatestMessagesPerUser = new Map();
-  const userPushInfoPromises = {};
-  const userRescindInfoPromises = {};
+  const userPushInfoPromises: { [string]: Promise<?PushUserInfo> } = {};
+  const userRescindInfoPromises: { [string]: Promise<?PushUserInfo> } = {};
 
   for (const pair of perUserInfo) {
     const [userID, preUserPushInfo] = pair;
@@ -431,7 +433,12 @@
     }
 
     const generateNotifUserInfoPromise = async (pushType: PushType) => {
-      const promises = [];
+      const promises: Array<
+        Promise<?{
+          +messageInfo: RawMessageInfo,
+          +messageData: MessageData,
+        }>,
+      > = [];
 
       for (const threadID of preUserPushInfo.notFocusedThreadIDs) {
         const messageIndices = threadsToMessageIndices.get(threadID);
diff --git a/keyserver/src/creators/role-creator.js b/keyserver/src/creators/role-creator.js
--- a/keyserver/src/creators/role-creator.js
+++ b/keyserver/src/creators/role-creator.js
@@ -46,7 +46,7 @@
 
   const time = Date.now();
   const newRows = [];
-  const namesToIDs = {};
+  const namesToIDs: { [string]: string } = {};
   for (const name in rolePermissions) {
     const id = ids.shift();
     namesToIDs[name] = id;
diff --git a/keyserver/src/creators/thread-creator.js b/keyserver/src/creators/thread-creator.js
--- a/keyserver/src/creators/thread-creator.js
+++ b/keyserver/src/creators/thread-creator.js
@@ -13,6 +13,7 @@
 import { getThreadTypeParentRequirement } from 'lib/shared/thread-utils.js';
 import type { Shape } from 'lib/types/core.js';
 import { messageTypes } from 'lib/types/message-types-enum.js';
+import type { RawMessageInfo, MessageData } from 'lib/types/message-types.js';
 import { threadPermissions } from 'lib/types/thread-permission-types.js';
 import {
   threadTypes,
@@ -22,6 +23,7 @@
   type ServerNewThreadRequest,
   type NewThreadResponse,
 } from 'lib/types/thread-types.js';
+import type { ServerUpdateInfo } from 'lib/types/update-types.js';
 import type { UserInfos } from 'lib/types/user-types.js';
 import { pushAll } from 'lib/utils/array.js';
 import { ServerError } from 'lib/utils/errors.js';
@@ -288,9 +290,9 @@
         ],
       };
 
-      let joinUpdateInfos = [];
+      let joinUpdateInfos: $ReadOnlyArray<ServerUpdateInfo> = [];
       let userInfos: UserInfos = {};
-      let newMessageInfos = [];
+      let newMessageInfos: $ReadOnlyArray<RawMessageInfo> = [];
       if (threadType !== threadTypes.PERSONAL) {
         const joinThreadResult = await joinThread(viewer, {
           threadID: existingThreadID,
@@ -395,7 +397,7 @@
   const initialMemberAndCreatorIDs = initialMemberIDs
     ? [...initialMemberIDs, viewer.userID]
     : [viewer.userID];
-  const messageDatas = [];
+  const messageDatas: Array<MessageData> = [];
   if (threadType !== threadTypes.SIDEBAR) {
     messageDatas.push({
       type: messageTypes.CREATE_THREAD,
diff --git a/keyserver/src/creators/update-creator.js b/keyserver/src/creators/update-creator.js
--- a/keyserver/src/creators/update-creator.js
+++ b/keyserver/src/creators/update-creator.js
@@ -16,10 +16,12 @@
 import {
   type CalendarQuery,
   defaultCalendarQuery,
+  type RawEntryInfo,
 } from 'lib/types/entry-types.js';
 import {
   defaultNumberPerThread,
   type MessageSelectionCriteria,
+  type RawMessageInfo,
 } from 'lib/types/message-types.js';
 import {
   type UpdateTarget,
@@ -500,7 +502,7 @@
 ): Promise<FetchUpdatesResult> {
   const { messageInfosResult, calendarResult, userInfosResult } = rawData;
 
-  const rawEntryInfosByThreadID = {};
+  const rawEntryInfosByThreadID: { [string]: Array<RawEntryInfo> } = {};
   for (const entryInfo of calendarResult?.rawEntryInfos ?? []) {
     if (!rawEntryInfosByThreadID[entryInfo.threadID]) {
       rawEntryInfosByThreadID[entryInfo.threadID] = [];
@@ -508,7 +510,7 @@
     rawEntryInfosByThreadID[entryInfo.threadID].push(entryInfo);
   }
 
-  const rawMessageInfosByThreadID = {};
+  const rawMessageInfosByThreadID: { [string]: Array<RawMessageInfo> } = {};
   for (const messageInfo of messageInfosResult?.rawMessageInfos ?? []) {
     if (!rawMessageInfosByThreadID[messageInfo.threadID]) {
       rawMessageInfosByThreadID[messageInfo.threadID] = [];
diff --git a/keyserver/src/fetchers/entry-fetchers.js b/keyserver/src/fetchers/entry-fetchers.js
--- a/keyserver/src/fetchers/entry-fetchers.js
+++ b/keyserver/src/fetchers/entry-fetchers.js
@@ -267,7 +267,7 @@
       !rawEntryInfoWithinCalendarQuery(rawEntryInfo, oldCalendarQuery),
   );
   let filteredRawEntryInfos = entryInfosNotInOldQuery;
-  let deletedEntryIDs = [];
+  let deletedEntryIDs: $ReadOnlyArray<string> = [];
   if (filterDeleted) {
     filteredRawEntryInfos = entryInfosNotInOldQuery.filter(
       rawEntryInfo => !rawEntryInfo.deleted,
diff --git a/keyserver/src/fetchers/message-fetchers.js b/keyserver/src/fetchers/message-fetchers.js
--- a/keyserver/src/fetchers/message-fetchers.js
+++ b/keyserver/src/fetchers/message-fetchers.js
@@ -30,6 +30,7 @@
   type FetchPinnedMessagesRequest,
   type FetchPinnedMessagesResult,
   type SearchMessagesResponse,
+  type MessageTruncationStatuses,
 } from 'lib/types/message-types.js';
 import { defaultNumberPerThread } from 'lib/types/message-types.js';
 import { threadPermissions } from 'lib/types/thread-permission-types.js';
@@ -72,8 +73,11 @@
   pushInfo: PushInfo,
 ): Promise<FetchCollapsableNotifsResult> {
   // First, we need to fetch any notifications that should be collapsed
-  const usersToCollapseKeysToInfo = {};
-  const usersToCollapsableNotifInfo = {};
+  const usersToCollapseKeysToInfo: {
+    [string]: { [string]: CollapsableNotifInfo },
+  } = {};
+  const usersToCollapsableNotifInfo: { [string]: Array<CollapsableNotifInfo> } =
+    {};
   for (const userID in pushInfo) {
     usersToCollapseKeysToInfo[userID] = {};
     usersToCollapsableNotifInfo[userID] = [];
@@ -82,7 +86,7 @@
       const messageData = pushInfo[userID].messageDatas[i];
       const collapseKey = getNotifCollapseKey(rawMessageInfo, messageData);
       if (!collapseKey) {
-        const collapsableNotifInfo = {
+        const collapsableNotifInfo: CollapsableNotifInfo = {
           collapseKey,
           existingMessageInfos: [],
           newMessageInfos: [rawMessageInfo],
@@ -91,11 +95,11 @@
         continue;
       }
       if (!usersToCollapseKeysToInfo[userID][collapseKey]) {
-        usersToCollapseKeysToInfo[userID][collapseKey] = {
+        usersToCollapseKeysToInfo[userID][collapseKey] = ({
           collapseKey,
           existingMessageInfos: [],
           newMessageInfos: [],
-        };
+        }: CollapsableNotifInfo);
       }
       usersToCollapseKeysToInfo[userID][collapseKey].newMessageInfos.push(
         rawMessageInfo,
@@ -309,7 +313,7 @@
 ): Promise<FetchMessageInfosResult> {
   const { sqlClause: selectionClause, timeFilterData } =
     parseMessageSelectionCriteria(viewer, criteria);
-  const truncationStatuses = {};
+  const truncationStatuses: MessageTruncationStatuses = {};
 
   const viewerID = viewer.id;
   const query = SQL`
@@ -522,7 +526,7 @@
   criteria: MessageSelectionCriteria,
   defaultTruncationStatus: MessageTruncationStatus,
 ) {
-  const truncationStatuses = {};
+  const truncationStatuses: MessageTruncationStatuses = {};
   if (criteria.threadCursors) {
     for (const threadID in criteria.threadCursors) {
       truncationStatuses[threadID] = defaultTruncationStatus;
@@ -605,7 +609,7 @@
   viewer: Viewer,
   rawMessageInfos: $ReadOnlyArray<RawMessageInfo>,
 ): FetchMessageInfosResult {
-  const truncationStatuses = {};
+  const truncationStatuses: MessageTruncationStatuses = {};
   for (const rawMessageInfo of rawMessageInfos) {
     truncationStatuses[rawMessageInfo.threadID] =
       messageTruncationStatus.UNCHANGED;
diff --git a/keyserver/src/fetchers/role-fetchers.js b/keyserver/src/fetchers/role-fetchers.js
--- a/keyserver/src/fetchers/role-fetchers.js
+++ b/keyserver/src/fetchers/role-fetchers.js
@@ -14,7 +14,7 @@
   `;
   const [result] = await dbQuery(query);
 
-  const roles = [];
+  const roles: Array<RoleInfo> = [];
   for (const row of result) {
     roles.push({
       id: row.id.toString(),
diff --git a/keyserver/src/fetchers/thread-fetchers.js b/keyserver/src/fetchers/thread-fetchers.js
--- a/keyserver/src/fetchers/thread-fetchers.js
+++ b/keyserver/src/fetchers/thread-fetchers.js
@@ -16,6 +16,7 @@
 import {
   type RawThreadInfos,
   type ServerThreadInfo,
+  type RawThreadInfo,
 } from 'lib/types/thread-types.js';
 import { ServerError } from 'lib/utils/errors.js';
 
@@ -277,7 +278,7 @@
     native: 285,
   });
 
-  const threadInfos = {};
+  const threadInfos: { [string]: RawThreadInfo } = {};
   for (const threadID in serverResult.threadInfos) {
     const serverThreadInfo = serverResult.threadInfos[threadID];
     const threadInfo = rawThreadInfoFromServerThreadInfo(
diff --git a/keyserver/src/fetchers/user-fetchers.js b/keyserver/src/fetchers/user-fetchers.js
--- a/keyserver/src/fetchers/user-fetchers.js
+++ b/keyserver/src/fetchers/user-fetchers.js
@@ -43,7 +43,7 @@
   `;
   const [result] = await dbQuery(query);
 
-  const userInfos = {};
+  const userInfos: { [id: string]: GlobalUserInfo } = {};
   for (const row of result) {
     const id = row.id.toString();
     const avatar: ?AvatarDBContent = row.avatar ? JSON.parse(row.avatar) : null;
diff --git a/keyserver/src/push/rescind.js b/keyserver/src/push/rescind.js
--- a/keyserver/src/push/rescind.js
+++ b/keyserver/src/push/rescind.js
@@ -1,6 +1,8 @@
 // @flow
 
 import apn from '@parse/node-apn';
+import type { ResponseFailure } from '@parse/node-apn';
+import type { FirebaseError } from 'firebase-admin';
 import invariant from 'invariant';
 
 import type { PlatformDetails } from 'lib/types/device-types.js';
@@ -19,7 +21,12 @@
   TargetedAndroidNotification,
   TargetedAPNsNotification,
 } from './types.js';
-import { apnPush, fcmPush } from './utils.js';
+import {
+  apnPush,
+  fcmPush,
+  type APNPushResult,
+  type FCMPushResult,
+} from './utils.js';
 import createIDs from '../creators/id-creator.js';
 import { dbQuery, SQL } from '../database/database.js';
 import type { SQLStatementType } from '../database/types.js';
@@ -33,7 +40,13 @@
   +deviceTokens: $ReadOnlyArray<string>,
 };
 
-type ParsedDeliveries = { +[id: string]: $ReadOnlyArray<ParsedDelivery> };
+type RescindDelivery = {
+  source: 'rescind',
+  rescindedID: string,
+  errors?:
+    | $ReadOnlyArray<FirebaseError | mixed>
+    | $ReadOnlyArray<ResponseFailure>,
+};
 
 async function rescindPushNotifs(
   notifCondition: SQLStatementType,
@@ -60,7 +73,7 @@
   const [fetchResult] = await dbQuery(fetchQuery);
 
   const allDeviceTokens = new Set();
-  const parsedDeliveries: ParsedDeliveries = {};
+  const parsedDeliveries: { [string]: $ReadOnlyArray<ParsedDelivery> } = {};
 
   for (const row of fetchResult) {
     const rawDelivery = JSON.parse(row.delivery);
@@ -101,7 +114,9 @@
   }
   const deviceTokenToCookieID = await getDeviceTokenToCookieID(allDeviceTokens);
 
-  const deliveryPromises = {};
+  const deliveryPromises: {
+    [string]: Promise<APNPushResult> | Promise<FCMPushResult>,
+  } = {};
   const notifInfo = {};
   const rescindedIDs = [];
 
@@ -193,9 +208,10 @@
   if (numRescinds > 0) {
     invariant(dbIDs, 'dbIDs should be set');
     for (const rescindedID in deliveryResults) {
-      const delivery = {};
-      delivery.source = 'rescind';
-      delivery.rescindedID = rescindedID;
+      const delivery: RescindDelivery = {
+        source: 'rescind',
+        rescindedID,
+      };
       const { errors } = deliveryResults[rescindedID];
       if (errors) {
         delivery.errors = errors;
diff --git a/keyserver/src/push/send.js b/keyserver/src/push/send.js
--- a/keyserver/src/push/send.js
+++ b/keyserver/src/push/send.js
@@ -91,7 +91,7 @@
   +stateVersion: ?number,
 };
 
-type PushUserInfo = {
+export type PushUserInfo = {
   +devices: Device[],
   // messageInfos and messageDatas have the same key
   +messageInfos: RawMessageInfo[],
@@ -639,7 +639,7 @@
     ]);
   }
 
-  const dbPromises = [];
+  const dbPromises: Array<Promise<mixed>> = [];
   if (allInvalidTokens.length > 0) {
     dbPromises.push(removeInvalidTokens(allInvalidTokens));
   }
@@ -829,11 +829,13 @@
       codeVersion,
       stateVersion,
     });
-    let innerMostArray = innerMap.get(versionKey);
-    if (!innerMostArray) {
-      innerMostArray = [];
-      innerMap.set(versionKey, innerMostArray);
+    let innerMostArrayTmp: ?Array<NotificationTargetDevice> =
+      innerMap.get(versionKey);
+    if (!innerMostArrayTmp) {
+      innerMostArrayTmp = [];
+      innerMap.set(versionKey, innerMostArrayTmp);
     }
+    const innerMostArray = innerMostArrayTmp;
 
     innerMostArray.push({
       cookieID: device.cookieID,
@@ -1327,7 +1329,7 @@
     };
   }
 
-  const deviceTokensToPayloadHash = {};
+  const deviceTokensToPayloadHash: { [string]: string } = {};
   for (const targetedNotification of targetedNotifications) {
     if (targetedNotification.encryptedPayloadHash) {
       deviceTokensToPayloadHash[targetedNotification.deviceToken] =
@@ -1527,7 +1529,7 @@
   }
 
   const time = Date.now();
-  const promises = [];
+  const promises: Array<Promise<mixed>> = [];
   for (const entry of userCookiePairsToInvalidDeviceTokens) {
     const [userCookiePair, deviceTokens] = entry;
     const [userID, cookieID] = userCookiePair.split('|');
diff --git a/keyserver/src/push/utils.js b/keyserver/src/push/utils.js
--- a/keyserver/src/push/utils.js
+++ b/keyserver/src/push/utils.js
@@ -43,7 +43,7 @@
 const wnsInvalidTokenErrorCodes = [404, 410];
 const wnsMaxNotificationPayloadByteSize = 5000;
 
-type APNPushResult =
+export type APNPushResult =
   | { +success: true }
   | {
       +errors: $ReadOnlyArray<ResponseFailure>,
diff --git a/keyserver/src/responders/landing-handler.js b/keyserver/src/responders/landing-handler.js
--- a/keyserver/src/responders/landing-handler.js
+++ b/keyserver/src/responders/landing-handler.js
@@ -57,11 +57,11 @@
   }
   if (process.env.NODE_ENV === 'development') {
     const fontURLs = await getDevFontURLs();
-    assetInfo = {
+    assetInfo = ({
       jsURL: 'http://localhost:8082/dev.build.js',
       fontURLs,
       cssInclude: '',
-    };
+    }: AssetInfo);
     return assetInfo;
   }
   try {
@@ -70,7 +70,7 @@
       'utf8',
     );
     const manifest = JSON.parse(manifestString);
-    assetInfo = {
+    assetInfo = ({
       jsURL: `compiled/${manifest['browser.js']}`,
       fontURLs: [googleFontsURL, iaDuoFontsURL],
       cssInclude: html`
@@ -80,7 +80,7 @@
           href="compiled/${manifest['browser.css']}"
         />
       `,
-    };
+    }: AssetInfo);
     return assetInfo;
   } catch {
     throw new Error(
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
@@ -17,7 +17,10 @@
 import { canUseDatabaseOnWeb } from 'lib/shared/web-database.js';
 import { entryStoreValidator } from 'lib/types/entry-types.js';
 import { defaultCalendarFilters } from 'lib/types/filter-types.js';
-import { inviteLinksStoreValidator } from 'lib/types/link-types.js';
+import {
+  inviteLinksStoreValidator,
+  type CommunityLinks,
+} from 'lib/types/link-types.js';
 import {
   defaultNumberPerThread,
   messageStoreValidator,
@@ -28,6 +31,7 @@
 import {
   currentUserInfoValidator,
   userInfosValidator,
+  type GlobalAccountUserInfo,
 } from 'lib/types/user-types.js';
 import { currentDateInTimeZone } from 'lib/utils/date-utils.js';
 import { ServerError } from 'lib/utils/errors.js';
@@ -108,11 +112,14 @@
       // Because of that we keep their userInfos inside the navInfo.
       if (urlInfo.selectedUserList) {
         const fetchedUserInfos = await fetchUserInfos(urlInfo.selectedUserList);
-        const userInfos = {};
+        const userInfos: { [string]: GlobalAccountUserInfo } = {};
         for (const userID in fetchedUserInfos) {
           const userInfo = fetchedUserInfos[userID];
           if (userInfo.username) {
-            userInfos[userID] = userInfo;
+            userInfos[userID] = {
+              ...userInfo,
+              username: userInfo.username,
+            };
           }
         }
         backupInfo = { userInfos, ...backupInfo };
@@ -312,7 +319,7 @@
 
   const inviteLinksStorePromise = (async () => {
     const primaryInviteLinks = await fetchPrimaryInviteLinks(viewer);
-    const links = {};
+    const links: { [string]: CommunityLinks } = {};
     for (const link of primaryInviteLinks) {
       if (link.primary) {
         links[link.communityID] = {
diff --git a/keyserver/src/responders/user-responders.js b/keyserver/src/responders/user-responders.js
--- a/keyserver/src/responders/user-responders.js
+++ b/keyserver/src/responders/user-responders.js
@@ -356,7 +356,7 @@
     }
   })();
 
-  const threadCursors = {};
+  const threadCursors: { [string]: null } = {};
   for (const watchedThreadID of request.watchedIDs) {
     threadCursors[watchedThreadID] = null;
   }
diff --git a/keyserver/src/scripts/generate-converter-from-validator.js b/keyserver/src/scripts/generate-converter-from-validator.js
--- a/keyserver/src/scripts/generate-converter-from-validator.js
+++ b/keyserver/src/scripts/generate-converter-from-validator.js
@@ -25,7 +25,7 @@
 function flattenInnerUnionValidators<T>(
   innerValidators: $ReadOnlyArray<TType<T>>,
 ): TInterface<{ +[string]: mixed }>[] {
-  let result = [];
+  let result: TInterface<{ +[string]: mixed }>[] = [];
   for (const innerValidator of innerValidators) {
     if (innerValidator.meta.kind === 'interface') {
       // In flow, union refinement only works if every variant has a key
diff --git a/keyserver/src/scripts/merge-users.js b/keyserver/src/scripts/merge-users.js
--- a/keyserver/src/scripts/merge-users.js
+++ b/keyserver/src/scripts/merge-users.js
@@ -15,6 +15,7 @@
 import {
   changeRole,
   commitMembershipChangeset,
+  type MembershipRow,
 } from '../updaters/thread-permission-updaters.js';
 import RelationshipChangeset from '../utils/relationship-changeset.js';
 
@@ -37,8 +38,8 @@
   toUserID: string,
   replaceUserInfo?: ReplaceUserInfo,
 ) {
-  let updateUserRowQuery = null;
-  let updateDatas = [];
+  let updateUserRowQuery: ?SQLStatementType = null;
+  let updateDatas: UpdateData[] = [];
   if (replaceUserInfo) {
     const replaceUserResult = await replaceUser(
       fromUserID,
@@ -122,7 +123,7 @@
       changeRole(threadID, [toUserID], role),
     ),
   );
-  const membershipRows = [];
+  const membershipRows: Array<MembershipRow> = [];
   const relationshipChangeset = new RelationshipChangeset();
   for (const currentChangeset of changesets) {
     const {
@@ -166,7 +167,7 @@
     throw new Error(`couldn't fetch fromUserID ${fromUserID}`);
   }
 
-  const changedFields = {};
+  const changedFields: { [string]: string } = {};
   if (replaceUserInfo.username) {
     changedFields.username = firstResult.username;
   }
@@ -178,7 +179,7 @@
     UPDATE users SET ${changedFields} WHERE id = ${toUserID}
   `;
 
-  const updateDatas = [];
+  const updateDatas: UpdateData[] = [];
   if (replaceUserInfo.username) {
     updateDatas.push({
       type: updateTypes.UPDATE_CURRENT_USER,
diff --git a/keyserver/src/search/users.js b/keyserver/src/search/users.js
--- a/keyserver/src/search/users.js
+++ b/keyserver/src/search/users.js
@@ -17,7 +17,7 @@
 
   const [result] = await dbQuery(sqlQuery);
 
-  const userInfos = [];
+  const userInfos: GlobalAccountUserInfo[] = [];
   for (const row of result) {
     userInfos.push({
       id: row.id.toString(),
diff --git a/keyserver/src/session/cookies.js b/keyserver/src/session/cookies.js
--- a/keyserver/src/session/cookies.js
+++ b/keyserver/src/session/cookies.js
@@ -417,7 +417,7 @@
     return result.viewer;
   }
 
-  let platformDetails = inputPlatformDetails;
+  let platformDetails: ?PlatformDetails = inputPlatformDetails;
   if (!platformDetails && result.type === 'invalidated') {
     platformDetails = result.platformDetails;
   }
@@ -633,7 +633,7 @@
   const time = Date.now();
   const { cookieID, cookieHash, cookiePassword } = viewer;
 
-  const updateObj = {};
+  const updateObj: { [string]: string | number } = {};
   updateObj.last_used = time;
   if (isBcryptHash(cookieHash)) {
     updateObj.hash = getCookieHash(cookiePassword);
diff --git a/keyserver/src/socket/socket.js b/keyserver/src/socket/socket.js
--- a/keyserver/src/socket/socket.js
+++ b/keyserver/src/socket/socket.js
@@ -457,7 +457,7 @@
     const { viewer } = this;
     invariant(viewer, 'should be set');
 
-    const responses = [];
+    const responses: Array<ServerServerSocketMessage> = [];
 
     const { sessionState, clientResponses } = message.payload;
     const {
@@ -474,7 +474,7 @@
       oldUpdatesCurrentAsOf,
     );
 
-    const threadCursors = {};
+    const threadCursors: { [string]: null } = {};
     for (const watchedThreadID of watchedIDs) {
       threadCursors[watchedThreadID] = null;
     }
diff --git a/keyserver/src/updaters/device-token-updaters.js b/keyserver/src/updaters/device-token-updaters.js
--- a/keyserver/src/updaters/device-token-updaters.js
+++ b/keyserver/src/updaters/device-token-updaters.js
@@ -21,7 +21,7 @@
     await clearDeviceToken(deviceToken);
   }
 
-  const setColumns = {};
+  const setColumns: { [string]: ?string } = {};
   setColumns.device_token = deviceToken;
   setColumns.platform = deviceType;
   if (update.platformDetails) {
diff --git a/keyserver/src/updaters/olm-session-updater.js b/keyserver/src/updaters/olm-session-updater.js
--- a/keyserver/src/updaters/olm-session-updater.js
+++ b/keyserver/src/updaters/olm-session-updater.js
@@ -47,7 +47,7 @@
     const [{ version, pickled_olm_session: pickledSession }] = olmSessionResult;
     const session = await unpickleOlmSession(pickledSession, picklingKey);
 
-    const encryptedMessages = {};
+    const encryptedMessages: { [string]: EncryptResult } = {};
     for (const messageName in messagesToEncrypt) {
       encryptedMessages[messageName] = session.encrypt(
         messagesToEncrypt[messageName],
diff --git a/keyserver/src/updaters/relationship-updaters.js b/keyserver/src/updaters/relationship-updaters.js
--- a/keyserver/src/updaters/relationship-updaters.js
+++ b/keyserver/src/updaters/relationship-updaters.js
@@ -41,7 +41,7 @@
   const uniqueUserIDs = [...new Set(request.userIDs)];
   const users = await fetchUserInfos(uniqueUserIDs);
 
-  let errors = {};
+  let errors: RelationshipErrors = {};
   const userIDs: string[] = [];
   for (const userID of uniqueUserIDs) {
     if (userID === viewer.userID || !users[userID].username) {
@@ -116,7 +116,9 @@
       }
     }
 
-    const promises = [updateUndirectedRelationships(undirectedInsertRows)];
+    const promises: Array<Promise<mixed>> = [
+      updateUndirectedRelationships(undirectedInsertRows),
+    ];
     if (directedInsertRows.length) {
       const directedInsertQuery = SQL`
         INSERT INTO relationships_directed (user1, user2, status)
@@ -211,7 +213,7 @@
   userPairs: $ReadOnlyArray<[string, string]>,
 ): UpdateData[] {
   const time = Date.now();
-  const updateDatas = [];
+  const updateDatas: Array<UpdateData> = [];
   for (const [user1, user2] of userPairs) {
     updateDatas.push({
       type: updateTypes.UPDATE_USER,
@@ -316,7 +318,7 @@
       `but we tried to do that for ${request.action}`,
   );
 
-  const threadIDPerUser = {};
+  const threadIDPerUser: { [string]: string } = {};
 
   const personalThreadsQuery = SQL`
     SELECT t.id AS threadID, m2.user AS user2
diff --git a/keyserver/src/updaters/role-updaters.js b/keyserver/src/updaters/role-updaters.js
--- a/keyserver/src/updaters/role-updaters.js
+++ b/keyserver/src/updaters/role-updaters.js
@@ -4,6 +4,7 @@
 import _isEqual from 'lodash/fp/isEqual.js';
 
 import { getRolePermissionBlobs } from 'lib/permissions/thread-permissions.js';
+import type { ThreadRolePermissionsBlob } from 'lib/types/thread-permission-types.js';
 import type { ThreadType } from 'lib/types/thread-types-enum.js';
 
 import createIDs from '../creators/id-creator.js';
@@ -18,8 +19,8 @@
 ): Promise<void> {
   const currentRoles = await fetchRoles(threadID);
 
-  const currentRolePermissions = {};
-  const currentRoleIDs = {};
+  const currentRolePermissions: { [string]: ThreadRolePermissionsBlob } = {};
+  const currentRoleIDs: { [string]: string } = {};
   for (const roleInfo of currentRoles) {
     currentRolePermissions[roleInfo.name] = roleInfo.permissions;
     currentRoleIDs[roleInfo.name] = roleInfo.id;
@@ -76,7 +77,7 @@
     promises.push(dbQuery(updateMembershipsQuery));
   }
 
-  const updatePermissions = {};
+  const updatePermissions: { [string]: ThreadRolePermissionsBlob } = {};
   for (const name in currentRoleIDs) {
     const currentPermissions = currentRolePermissions[name];
     const permissions = rolePermissions[name];
diff --git a/keyserver/src/updaters/session-updaters.js b/keyserver/src/updaters/session-updaters.js
--- a/keyserver/src/updaters/session-updaters.js
+++ b/keyserver/src/updaters/session-updaters.js
@@ -15,7 +15,7 @@
   viewer: Viewer,
   sessionUpdate: SessionUpdate,
 ): Promise<void> {
-  const sqlUpdate = {};
+  const sqlUpdate: { [string]: string | number } = {};
   if (sessionUpdate.query) {
     sqlUpdate.query = JSON.stringify(sessionUpdate.query);
   }
diff --git a/keyserver/src/updaters/thread-permission-updaters.js b/keyserver/src/updaters/thread-permission-updaters.js
--- a/keyserver/src/updaters/thread-permission-updaters.js
+++ b/keyserver/src/updaters/thread-permission-updaters.js
@@ -26,6 +26,7 @@
 import {
   type ServerUpdateInfo,
   type CreateUpdatesResult,
+  type UpdateData,
 } from 'lib/types/update-types.js';
 import { pushAll } from 'lib/utils/array.js';
 import { ServerError } from 'lib/utils/errors.js';
@@ -67,7 +68,7 @@
   +threadID: string,
   +oldRole: string,
 };
-type MembershipRow = MembershipRowToSave | MembershipRowToDelete;
+export type MembershipRow = MembershipRowToSave | MembershipRowToDelete;
 export type MembershipChangeset = {
   +membershipRows: MembershipRow[],
   +relationshipChangeset: RelationshipChangeset,
@@ -194,7 +195,7 @@
     relationshipChangeset.setAllRelationshipsExist(parentMemberIDs);
   }
 
-  const membershipRows = [];
+  const membershipRows: Array<MembershipRow> = [];
   const toUpdateDescendants = new Map();
   for (const userID of userIDs) {
     const existingMembership = existingMembershipInfo.get(userID);
@@ -416,7 +417,7 @@
 async function updateDescendantPermissions(
   initialChangedAncestor: ChangedAncestor,
 ): Promise<MembershipChangeset> {
-  const membershipRows = [];
+  const membershipRows: Array<MembershipRow> = [];
   const relationshipChangeset = new RelationshipChangeset();
 
   const initialDescendants = await fetchDescendantsForUpdate([
@@ -582,7 +583,20 @@
 ): Promise<DescendantInfo[]> {
   const threadIDs = ancestors.map(ancestor => ancestor.threadID);
 
-  const rows = [];
+  const rows: Array<{
+    +id: number,
+    +user: number,
+    +type: number,
+    +depth: number,
+    +parent_thread_id: number,
+    +containing_thread_id: number,
+    +role_permissions: string,
+    +permissions: string,
+    +permissions_for_children: string,
+    +role: number,
+    +permissions_from_parent: string | null,
+    +containing_role: ?number,
+  }> = [];
   while (threadIDs.length > 0) {
     const batch = threadIDs.splice(0, fetchDescendantsBatchSize);
     const query = SQL`
@@ -629,8 +643,11 @@
       curRolePermissions: JSON.parse(row.role_permissions),
       curPermissions: JSON.parse(row.permissions),
       curPermissionsForChildren: JSON.parse(row.permissions_for_children),
-      curPermissionsFromParent: JSON.parse(row.permissions_from_parent),
-      curMemberOfContainingThread: row.containing_role > 0,
+      curPermissionsFromParent: row.permissions_from_parent
+        ? JSON.parse(row.permissions_from_parent)
+        : null,
+      curMemberOfContainingThread:
+        !!row.containing_role && row.containing_role > 0,
     });
   }
 
@@ -643,9 +660,9 @@
         if (threadID !== parentThreadID && threadID !== containingThreadID) {
           continue;
         }
-        let user = users.get(userID);
+        let user: ?DescendantUserInfo = users.get(userID);
         if (!user) {
-          user = {};
+          user = ({}: DescendantUserInfo);
           users.set(userID, user);
         }
         if (threadID === parentThreadID) {
@@ -785,7 +802,7 @@
     relationshipChangeset.setAllRelationshipsExist(parentMemberIDs);
   }
 
-  const membershipRows = [];
+  const membershipRows: Array<MembershipRow> = [];
   const toUpdateDescendants = new Map();
   for (const [userID, membership] of membershipInfo) {
     const { rolePermissions: intendedRolePermissions, permissionsFromParent } =
@@ -1246,7 +1263,7 @@
   }
 
   const time = Date.now();
-  const updateDatas = [
+  const updateDatas: Array<UpdateData> = [
     {
       type: updateTypes.JOIN_THREAD,
       userID: viewer.userID,
@@ -1347,7 +1364,7 @@
     console.log(`recalculating permissions for threads with depth ${depth}`);
     while (threads.length > 0) {
       const batch = threads.splice(0, batchSize);
-      const membershipRows = [];
+      const membershipRows: Array<MembershipRow> = [];
       const relationshipChangeset = new RelationshipChangeset();
       await Promise.all(
         batch.map(async thread => {
diff --git a/keyserver/src/updaters/thread-updaters.js b/keyserver/src/updaters/thread-updaters.js
--- a/keyserver/src/updaters/thread-updaters.js
+++ b/keyserver/src/updaters/thread-updaters.js
@@ -12,6 +12,7 @@
 } from 'lib/shared/thread-utils.js';
 import type { Shape } from 'lib/types/core.js';
 import { messageTypes } from 'lib/types/message-types-enum.js';
+import type { RawMessageInfo, MessageData } from 'lib/types/message-types.js';
 import { threadPermissions } from 'lib/types/thread-permission-types.js';
 import { threadTypes } from 'lib/types/thread-types-enum.js';
 import {
@@ -39,6 +40,7 @@
   changeRole,
   recalculateThreadPermissions,
   commitMembershipChangeset,
+  type MembershipRow,
 } from './thread-permission-updaters.js';
 import createMessages from '../creators/message-creator.js';
 import { createUpdates } from '../creators/update-creator.js';
@@ -164,7 +166,7 @@
       : undefined,
   );
 
-  let newMessageInfos = [];
+  let newMessageInfos: Array<RawMessageInfo> = [];
   if (!silenceNewMessages) {
     const messageData = {
       type: messageTypes.CHANGE_ROLE,
@@ -343,8 +345,8 @@
     (options?.ignorePermissions && viewer.isScriptViewer) ?? false;
   const validationPromises = {};
 
-  const changedFields = {};
-  const sqlUpdate = {};
+  const changedFields: { [string]: string | number } = {};
+  const sqlUpdate: { [string]: ?string | number } = {};
   const untrimmedName = request.changes.name;
   if (untrimmedName !== undefined && untrimmedName !== null) {
     const name = firstLine(untrimmedName);
@@ -727,7 +729,7 @@
   const { addMembersChangeset, recalculatePermissionsChangeset } =
     await promiseAll(intermediatePromises);
 
-  const membershipRows = [];
+  const membershipRows: Array<MembershipRow> = [];
   const relationshipChangeset = new RelationshipChangeset();
   if (recalculatePermissionsChangeset) {
     const {
@@ -768,10 +770,10 @@
     updateMembershipsLastMessage: silenceMessages,
   });
 
-  let newMessageInfos = [];
+  let newMessageInfos: Array<RawMessageInfo> = [];
   if (!silenceMessages) {
     const time = Date.now();
-    const messageDatas = [];
+    const messageDatas: Array<MessageData> = [];
     for (const fieldName in changedFields) {
       const newValue = changedFields[fieldName];
       messageDatas.push({
diff --git a/keyserver/src/updaters/user-subscription-updaters.js b/keyserver/src/updaters/user-subscription-updaters.js
--- a/keyserver/src/updaters/user-subscription-updaters.js
+++ b/keyserver/src/updaters/user-subscription-updaters.js
@@ -29,7 +29,7 @@
     throw new ServerError('not_member');
   }
 
-  const promises = [];
+  const promises: Array<Promise<mixed>> = [];
 
   const newSubscription = {
     ...threadInfo.currentUser.subscription,
diff --git a/keyserver/src/utils/validation-utils.test.js b/keyserver/src/utils/validation-utils.test.js
--- a/keyserver/src/utils/validation-utils.test.js
+++ b/keyserver/src/utils/validation-utils.test.js
@@ -49,7 +49,7 @@
   it('should redact password dict key', () => {
     const validator = tShape({ passwords: t.dict(tPassword, t.Bool) });
     const object = { passwords: { password1: true, password2: false } };
-    const redacted = { passwords: {} };
+    const redacted: { +passwords: { [string]: mixed } } = { passwords: {} };
     redacted.passwords[redactedString] = false;
     expect(sanitizeInput(validator, object)).toStrictEqual(redacted);
   });