diff --git a/lib/shared/dm-ops/create-sidebar-spec.js b/lib/shared/dm-ops/create-sidebar-spec.js
new file mode 100644
--- /dev/null
+++ b/lib/shared/dm-ops/create-sidebar-spec.js
@@ -0,0 +1,106 @@
+// @flow
+
+import uuid from 'uuid';
+
+import { createThickRawThreadInfo } from './create-thread-spec.js';
+import type {
+  DMOperationSpec,
+  ProcessDMOperationUtilities,
+} from './dm-op-spec.js';
+import { isInvalidSidebarSource } from '../../shared/message-utils.js';
+import type { DMCreateSidebarOperation } from '../../types/dm-ops.js';
+import { messageTypes } from '../../types/message-types-enum.js';
+import {
+  type RawMessageInfo,
+  messageTruncationStatus,
+} from '../../types/message-types.js';
+import { threadTypes } from '../../types/thread-types-enum.js';
+import { updateTypes } from '../../types/update-types-enum.js';
+
+export const createSidebarSpec: DMOperationSpec<DMCreateSidebarOperation> =
+  Object.freeze({
+    processDMOperation: async (
+      dmOperation: DMCreateSidebarOperation,
+      viewerID: string,
+      utilities: ProcessDMOperationUtilities,
+    ) => {
+      const {
+        threadID,
+        creatorID,
+        time,
+        parentThreadID,
+        memberIDs,
+        sourceMessageID,
+        roleID,
+        newSidebarSourceMessageID,
+        newCreateSidebarMessageID,
+      } = dmOperation;
+      const allMemberIDs = [creatorID, ...memberIDs];
+
+      const rawThreadInfo = createThickRawThreadInfo({
+        threadID,
+        threadType: threadTypes.THICK_SIDEBAR,
+        creationTime: time,
+        parentThreadID,
+        allMemberIDs,
+        roleID,
+        creatorID,
+        viewerID,
+      });
+
+      rawThreadInfo.sourceMessageID = sourceMessageID;
+      rawThreadInfo.containingThreadID = parentThreadID;
+
+      const sourceMessage = await utilities.fetchMessage(sourceMessageID);
+      if (!sourceMessage) {
+        throw new Error(
+          `could not find sourceMessage ${sourceMessageID}... probably ` +
+            'joined thick thread ${parentThreadID} after its creation',
+        );
+      }
+      if (isInvalidSidebarSource(sourceMessage)) {
+        throw new Error(
+          `sourceMessage ${sourceMessageID} is an invalid sidebar source`,
+        );
+      }
+
+      const rawMessageInfos: Array<RawMessageInfo> = [
+        {
+          type: messageTypes.SIDEBAR_SOURCE,
+          id: newSidebarSourceMessageID,
+          threadID,
+          creatorID,
+          time,
+          sourceMessage,
+        },
+        {
+          type: messageTypes.CREATE_SIDEBAR,
+          id: newCreateSidebarMessageID,
+          threadID,
+          creatorID,
+          time: time + 1,
+          sourceMessageAuthorID: sourceMessage.creatorID,
+          initialThreadState: {
+            parentThreadID,
+            color: rawThreadInfo.color,
+            memberIDs: allMemberIDs,
+          },
+        },
+      ];
+
+      const threadJoinUpdateInfo = {
+        type: updateTypes.JOIN_THREAD,
+        id: uuid.v4(),
+        time,
+        threadInfo: rawThreadInfo,
+        rawMessageInfos,
+        truncationStatus: messageTruncationStatus.UNCHANGED,
+        rawEntryInfos: [],
+      };
+
+      return {
+        rawMessageInfos: [], // included in updateInfos below
+        updateInfos: [threadJoinUpdateInfo],
+      };
+    },
+  });
diff --git a/lib/shared/dm-ops/create-thread-spec.js b/lib/shared/dm-ops/create-thread-spec.js
--- a/lib/shared/dm-ops/create-thread-spec.js
+++ b/lib/shared/dm-ops/create-thread-spec.js
@@ -1,14 +1,160 @@
 // @flow
 
+import uuid from 'uuid';
+
 import type { DMOperationSpec } from './dm-op-spec.js';
+import { specialRoles } from '../../permissions/special-roles.js';
+import {
+  getAllThreadPermissions,
+  makePermissionsBlob,
+  getThickThreadRolePermissionsBlob,
+} from '../../permissions/thread-permissions.js';
+import { generatePendingThreadColor } from '../../shared/color-utils.js';
 import type { DMCreateThreadOperation } from '../../types/dm-ops.js';
+import { messageTypes } from '../../types/message-types-enum.js';
+import {
+  type RawMessageInfo,
+  messageTruncationStatus,
+} from '../../types/message-types.js';
+import {
+  type ThickRawThreadInfo,
+  type RoleInfo,
+  minimallyEncodeMemberInfo,
+  minimallyEncodeRoleInfo,
+  minimallyEncodeThreadCurrentUserInfo,
+} from '../../types/minimally-encoded-thread-permissions-types.js';
+import { defaultThreadSubscription } from '../../types/subscription-types.js';
+import type { ThickThreadType } from '../../types/thread-types-enum.js';
+import type { ThickMemberInfo } from '../../types/thread-types.js';
+import { updateTypes } from '../../types/update-types-enum.js';
+
+type CreateThickRawThreadInfoInput = {
+  +threadID: string,
+  +threadType: ThickThreadType,
+  +creationTime: number,
+  +parentThreadID?: ?string,
+  +allMemberIDs: $ReadOnlyArray<string>,
+  +roleID: string,
+  +creatorID: string,
+  +viewerID: string,
+};
+type MutableThickRawThreadInfo = { ...ThickRawThreadInfo };
+export function createThickRawThreadInfo(
+  input: CreateThickRawThreadInfoInput,
+): MutableThickRawThreadInfo {
+  const {
+    threadID,
+    threadType,
+    creationTime,
+    parentThreadID,
+    allMemberIDs,
+    roleID,
+    creatorID,
+    viewerID,
+  } = input;
+
+  const color = generatePendingThreadColor(allMemberIDs);
+
+  const rolePermissions = getThickThreadRolePermissionsBlob(threadType);
+  const membershipPermissions = getAllThreadPermissions(
+    makePermissionsBlob(rolePermissions, null, threadID, threadType),
+    threadID,
+  );
+  const role: RoleInfo = {
+    ...minimallyEncodeRoleInfo({
+      id: roleID,
+      name: 'Members',
+      permissions: rolePermissions,
+      isDefault: true,
+    }),
+    specialRole: specialRoles.DEFAULT_ROLE,
+  };
+
+  return {
+    thick: true,
+    minimallyEncoded: true,
+    id: threadID,
+    type: threadType,
+    color,
+    creationTime,
+    parentThreadID,
+    members: allMemberIDs.map(memberID =>
+      minimallyEncodeMemberInfo<ThickMemberInfo>({
+        id: memberID,
+        role: role.id,
+        permissions: membershipPermissions,
+        isSender: memberID === viewerID,
+        subscription: defaultThreadSubscription,
+      }),
+    ),
+    roles: {
+      [role.id]: role,
+    },
+    currentUser: minimallyEncodeThreadCurrentUserInfo({
+      role: role.id,
+      permissions: membershipPermissions,
+      subscription: defaultThreadSubscription,
+      unread: creatorID !== viewerID,
+    }),
+    repliesCount: 0,
+  };
+}
 
 export const createThreadSpec: DMOperationSpec<DMCreateThreadOperation> =
   Object.freeze({
-    processDMOperation: async () => {
+    processDMOperation: async (
+      dmOperation: DMCreateThreadOperation,
+      viewerID: string,
+    ) => {
+      const {
+        threadID,
+        creatorID,
+        time,
+        threadType,
+        memberIDs,
+        roleID,
+        newMessageID,
+      } = dmOperation;
+      const allMemberIDs = [creatorID, ...memberIDs];
+
+      const rawThreadInfo = createThickRawThreadInfo({
+        threadID,
+        threadType,
+        creationTime: time,
+        allMemberIDs,
+        roleID,
+        creatorID,
+        viewerID,
+      });
+
+      const rawMessageInfos: Array<RawMessageInfo> = [
+        {
+          type: messageTypes.CREATE_THREAD,
+          id: newMessageID,
+          threadID,
+          creatorID,
+          time,
+          initialThreadState: {
+            type: threadType,
+            color: rawThreadInfo.color,
+            memberIDs: allMemberIDs,
+          },
+        },
+      ];
+
+      const threadJoinUpdateInfo = {
+        type: updateTypes.JOIN_THREAD,
+        id: uuid.v4(),
+        time,
+        threadInfo: rawThreadInfo,
+        rawMessageInfos,
+        truncationStatus: messageTruncationStatus.UNCHANGED,
+        rawEntryInfos: [],
+      };
+
       return {
-        rawMessageInfos: [],
-        updateInfos: [],
+        rawMessageInfos: [], // included in updateInfos below
+        updateInfos: [threadJoinUpdateInfo],
       };
     },
   });
diff --git a/lib/shared/dm-ops/dm-op-specs.js b/lib/shared/dm-ops/dm-op-specs.js
--- a/lib/shared/dm-ops/dm-op-specs.js
+++ b/lib/shared/dm-ops/dm-op-specs.js
@@ -1,5 +1,6 @@
 // @flow
 
+import { createSidebarSpec } from './create-sidebar-spec.js';
 import { createThreadSpec } from './create-thread-spec.js';
 import type { DMOperationSpec } from './dm-op-spec.js';
 import { sendTextMessageSpec } from './send-text-message-spec.js';
@@ -9,5 +10,6 @@
   +[DMOperationType]: DMOperationSpec<any>,
 } = Object.freeze({
   [dmOperationTypes.CREATE_THREAD]: createThreadSpec,
+  [dmOperationTypes.CREATE_SIDEBAR]: createSidebarSpec,
   [dmOperationTypes.SEND_TEXT_MESSAGE]: sendTextMessageSpec,
 });
diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js
--- a/lib/types/dm-ops.js
+++ b/lib/types/dm-ops.js
@@ -4,13 +4,17 @@
 import t from 'tcomb';
 
 import type { RawMessageInfo } from './message-types.js';
-import { type ThickThreadType, thickThreadTypes } from './thread-types-enum.js';
+import {
+  type NonSidebarThickThreadType,
+  nonSidebarThickThreadTypes,
+} from './thread-types-enum.js';
 import type { ClientUpdateInfo } from './update-types.js';
 import { values } from '../utils/objects.js';
 import { tShape, tString, tUserID } from '../utils/validation-utils.js';
 
 export const dmOperationTypes = Object.freeze({
   CREATE_THREAD: 'create_thread',
+  CREATE_SIDEBAR: 'create_sidebar',
   SEND_TEXT_MESSAGE: 'send_text_message',
 });
 export type DMOperationType = $Values<typeof dmOperationTypes>;
@@ -20,10 +24,10 @@
   +threadID: string,
   +creatorID: string,
   +time: number,
-  +threadType: ThickThreadType,
-  +parentThreadID?: ?string,
+  +threadType: NonSidebarThickThreadType,
   +memberIDs: $ReadOnlyArray<string>,
-  +sourceMessageID?: ?string,
+  +roleID: string,
+  +newMessageID: string,
 };
 export const dmCreateThreadOperationValidator: TInterface<DMCreateThreadOperation> =
   tShape<DMCreateThreadOperation>({
@@ -31,10 +35,36 @@
     threadID: t.String,
     creatorID: tUserID,
     time: t.Number,
-    threadType: t.enums.of(values(thickThreadTypes)),
-    parentThreadID: t.maybe(t.String),
+    threadType: t.enums.of(values(nonSidebarThickThreadTypes)),
     memberIDs: t.list(tUserID),
-    sourceMessageID: t.maybe(t.String),
+    roleID: t.String,
+    newMessageID: t.String,
+  });
+
+export type DMCreateSidebarOperation = {
+  +type: 'create_sidebar',
+  +threadID: string,
+  +creatorID: string,
+  +time: number,
+  +parentThreadID: string,
+  +memberIDs: $ReadOnlyArray<string>,
+  +sourceMessageID: string,
+  +roleID: string,
+  +newSidebarSourceMessageID: string,
+  +newCreateSidebarMessageID: string,
+};
+export const dmCreateSidebarOperationValidator: TInterface<DMCreateSidebarOperation> =
+  tShape<DMCreateSidebarOperation>({
+    type: tString(dmOperationTypes.CREATE_SIDEBAR),
+    threadID: t.String,
+    creatorID: tUserID,
+    time: t.Number,
+    parentThreadID: t.String,
+    memberIDs: t.list(tUserID),
+    sourceMessageID: t.String,
+    roleID: t.String,
+    newSidebarSourceMessageID: t.String,
+    newCreateSidebarMessageID: t.String,
   });
 
 export type DMSendTextMessageOperation = {
@@ -55,7 +85,10 @@
     text: t.String,
   });
 
-export type DMOperation = DMCreateThreadOperation | DMSendTextMessageOperation;
+export type DMOperation =
+  | DMCreateThreadOperation
+  | DMCreateSidebarOperation
+  | DMSendTextMessageOperation;
 
 export type DMOperationResult = {
   rawMessageInfos: Array<RawMessageInfo>,
diff --git a/lib/types/thread-types-enum.js b/lib/types/thread-types-enum.js
--- a/lib/types/thread-types-enum.js
+++ b/lib/types/thread-types-enum.js
@@ -38,7 +38,7 @@
 });
 export type ThinThreadType = $Values<typeof thinThreadTypes>;
 
-export const thickThreadTypes = Object.freeze({
+export const nonSidebarThickThreadTypes = Object.freeze({
   // local "thick" thread (outside of community). no parent, can only have
   // sidebar children
   LOCAL: 13,
@@ -46,11 +46,25 @@
   PERSONAL: 14,
   // canonical thread for each single user
   PRIVATE: 15,
+});
+export type NonSidebarThickThreadType = $Values<
+  typeof nonSidebarThickThreadTypes,
+>;
+
+export const sidebarThickThreadTypes = Object.freeze({
   // has parent, not top-level (appears under parent in inbox), and visible to
   // all members of parent
   THICK_SIDEBAR: 16,
 });
-export type ThickThreadType = $Values<typeof thickThreadTypes>;
+export type SidebarThickThreadType = $Values<typeof sidebarThickThreadTypes>;
+
+export const thickThreadTypes = Object.freeze({
+  ...nonSidebarThickThreadTypes,
+  ...sidebarThickThreadTypes,
+});
+export type ThickThreadType =
+  | NonSidebarThickThreadType
+  | SidebarThickThreadType;
 
 export type ThreadType = ThinThreadType | ThickThreadType;