diff --git a/lib/shared/messages/message-specs.js b/lib/shared/messages/message-specs.js
--- a/lib/shared/messages/message-specs.js
+++ b/lib/shared/messages/message-specs.js
@@ -14,6 +14,7 @@
 import { leaveThreadMessageSpec } from './leave-thread-message-spec';
 import { type MessageSpec } from './message-spec';
 import { multimediaMessageSpec } from './multimedia-message-spec';
+import { reactionMessageSpec } from './reaction-message-spec';
 import { removeMembersMessageSpec } from './remove-members-message-spec';
 import { restoreEntryMessageSpec } from './restore-entry-message-spec';
 import { sidebarSourceMessageSpec } from './sidebar-source-message-spec';
@@ -43,4 +44,5 @@
   [messageTypes.UPDATE_RELATIONSHIP]: updateRelationshipMessageSpec,
   [messageTypes.SIDEBAR_SOURCE]: sidebarSourceMessageSpec,
   [messageTypes.CREATE_SIDEBAR]: createSidebarMessageSpec,
+  [messageTypes.REACTION]: reactionMessageSpec,
 });
diff --git a/lib/shared/messages/reaction-message-spec.js b/lib/shared/messages/reaction-message-spec.js
new file mode 100644
--- /dev/null
+++ b/lib/shared/messages/reaction-message-spec.js
@@ -0,0 +1,88 @@
+// @flow
+
+import invariant from 'invariant';
+
+import {
+  assertMessageType,
+  messageTypes,
+  type ClientDBMessageInfo,
+  type ReactionMessageData,
+  type RawReactionMessageInfo,
+  type ReactionMessageInfo,
+} from '../../types/message-types';
+import type { RelativeUserInfo } from '../../types/user-types';
+import { messagePreviewText, removeCreatorAsViewer } from '../message-utils';
+import type { MessageSpec, MessageTitleParam } from './message-spec';
+
+export const reactionMessageSpec: MessageSpec<
+  ReactionMessageData,
+  RawReactionMessageInfo,
+  ReactionMessageInfo,
+> = Object.freeze({
+  messageContentForClientDB(data: RawReactionMessageInfo): string {
+    return JSON.stringify({
+      targetMessageID: data.targetMessageID,
+      reaction: data.reaction,
+      action: data.action,
+    });
+  },
+
+  messageTitle({
+    messageInfo,
+    threadInfo,
+    viewerContext,
+  }: MessageTitleParam<ReactionMessageInfo>) {
+    let validMessageInfo: ReactionMessageInfo = (messageInfo: ReactionMessageInfo);
+    if (viewerContext === 'global_viewer') {
+      validMessageInfo = removeCreatorAsViewer(validMessageInfo);
+    }
+    return messagePreviewText(validMessageInfo, threadInfo);
+  },
+
+  rawMessageInfoFromClientDB(
+    clientDBMessageInfo: ClientDBMessageInfo,
+  ): RawReactionMessageInfo {
+    const messageType = assertMessageType(parseInt(clientDBMessageInfo.type));
+    invariant(
+      messageType === messageTypes.REACTION,
+      'message must be of type REACTION',
+    );
+    invariant(
+      clientDBMessageInfo.content !== undefined &&
+        clientDBMessageInfo.content !== null,
+      'content must be defined',
+    );
+    const content = JSON.parse(clientDBMessageInfo.content);
+
+    const rawReactionMessageInfo: RawReactionMessageInfo = {
+      type: messageTypes.REACTION,
+      id: clientDBMessageInfo.id,
+      threadID: clientDBMessageInfo.thread,
+      time: parseInt(clientDBMessageInfo.time),
+      creatorID: clientDBMessageInfo.user,
+      targetMessageID: content.targetMessageID,
+      reaction: content.reaction,
+      action: content.action,
+    };
+
+    return rawReactionMessageInfo;
+  },
+
+  createMessageInfo(
+    rawMessageInfo: RawReactionMessageInfo,
+    creator: RelativeUserInfo,
+  ): ReactionMessageInfo {
+    return {
+      type: messageTypes.REACTION,
+      id: rawMessageInfo.id,
+      threadID: rawMessageInfo.threadID,
+      creator,
+      time: rawMessageInfo.time,
+      targetMessageID: rawMessageInfo.targetMessageID,
+      reaction: rawMessageInfo.reaction,
+      action: rawMessageInfo.action,
+    };
+  },
+
+  generatesNotifs: false,
+});