diff --git a/keyserver/src/responders/website-responders.js b/keyserver/src/responders/website-responders.js
--- a/keyserver/src/responders/website-responders.js
+++ b/keyserver/src/responders/website-responders.js
@@ -4,10 +4,12 @@
 import { detect as detectBrowser } from 'detect-browser';
 import type { $Response, $Request } from 'express';
 import fs from 'fs';
+import _isEqual from 'lodash/fp/isEqual.js';
 import _keyBy from 'lodash/fp/keyBy.js';
 import * as React from 'react';
 // eslint-disable-next-line import/extensions
 import ReactDOMServer from 'react-dom/server';
+import t from 'tcomb';
 import { promisify } from 'util';
 
 import { baseLegalPolicies } from 'lib/facts/policies.js';
@@ -23,17 +25,28 @@
   createPendingThread,
 } from 'lib/shared/thread-utils.js';
 import { defaultWebEnabledApps } from 'lib/types/enabled-apps.js';
+import { entryStoreValidator } from 'lib/types/entry-types.js';
 import { defaultCalendarFilters } from 'lib/types/filter-types.js';
-import { defaultNumberPerThread } from 'lib/types/message-types.js';
+import {
+  defaultNumberPerThread,
+  messageStoreValidator,
+} from 'lib/types/message-types.js';
 import { defaultEnabledReports } from 'lib/types/report-types.js';
 import { defaultConnectionInfo } from 'lib/types/socket-types.js';
 import { threadPermissions } from 'lib/types/thread-permission-types.js';
 import { threadTypes } from 'lib/types/thread-types-enum.js';
+import { threadStoreValidator } from 'lib/types/thread-types.js';
+import {
+  currentUserInfoValidator,
+  userInfosValidator,
+} from 'lib/types/user-types.js';
 import { currentDateInTimeZone } from 'lib/utils/date-utils.js';
 import { ServerError } from 'lib/utils/errors.js';
 import { promiseAll } from 'lib/utils/promises.js';
 import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js';
+import { tBool, tNumber, tShape, tString } from 'lib/utils/validation-utils.js';
 import getTitle from 'web/title/getTitle.js';
+import { navInfoValidator } from 'web/types/nav-types.js';
 import { navInfoFromURL } from 'web/url-utils.js';
 
 import { fetchEntryInfos } from '../fetchers/entry-fetchers.js';
@@ -49,6 +62,7 @@
 import { Viewer } from '../session/viewer.js';
 import { streamJSON, waitForStream } from '../utils/json-stream.js';
 import { getAppURLFactsFromRequestURL } from '../utils/urls.js';
+import { validateOutput } from '../utils/validation-utils.js';
 
 const { renderToNodeStream } = ReactDOMServer;
 
@@ -141,6 +155,86 @@
   }
 }
 
+const initialReduxStateValidator = tShape({
+  navInfo: navInfoValidator,
+  deviceID: t.Nil,
+  currentUserInfo: currentUserInfoValidator,
+  draftStore: t.irreducible('default draftStore', _isEqual({ drafts: {} })),
+  sessionID: t.maybe(t.String),
+  entryStore: entryStoreValidator,
+  threadStore: threadStoreValidator,
+  userStore: tShape({
+    userInfos: userInfosValidator,
+    inconsistencyReports: t.irreducible(
+      'default inconsistencyReports',
+      _isEqual([]),
+    ),
+  }),
+  messageStore: messageStoreValidator,
+  updatesCurrentAsOf: t.Number,
+  loadingStatuses: t.irreducible('default loadingStatuses', _isEqual({})),
+  calendarFilters: t.irreducible(
+    'defaultCalendarFilters',
+    _isEqual(defaultCalendarFilters),
+  ),
+  urlPrefix: tString(''),
+  windowDimensions: t.irreducible(
+    'default windowDimensions',
+    _isEqual({ width: 0, height: 0 }),
+  ),
+  baseHref: t.String,
+  notifPermissionAlertInfo: t.irreducible(
+    'default notifPermissionAlertInfo',
+    _isEqual(defaultNotifPermissionAlertInfo),
+  ),
+  connection: tShape({
+    status: tString('connecting'),
+    queuedActivityUpdates: t.irreducible(
+      'default queuedActivityUpdates',
+      _isEqual([]),
+    ),
+    actualizedCalendarQuery: tShape({
+      startDate: t.String,
+      endDate: t.String,
+      filters: t.irreducible(
+        'default filters',
+        _isEqual(defaultCalendarFilters),
+      ),
+    }),
+    lateResponses: t.irreducible('default lateResponses', _isEqual([])),
+    showDisconnectedBar: tBool(false),
+  }),
+  watchedThreadIDs: t.irreducible('default watchedThreadIDs', _isEqual([])),
+  lifecycleState: tString('active'),
+  enabledApps: t.irreducible(
+    'defaultWebEnabledApps',
+    _isEqual(defaultWebEnabledApps),
+  ),
+  reportStore: t.irreducible(
+    'default reportStore',
+    _isEqual({
+      enabledReports: defaultEnabledReports,
+      queuedReports: [],
+    }),
+  ),
+  nextLocalID: tNumber(0),
+  cookie: t.Nil,
+  deviceToken: t.Nil,
+  dataLoaded: t.Boolean,
+  windowActive: tBool(true),
+  userPolicies: t.irreducible('default userPolicies', _isEqual({})),
+  cryptoStore: t.irreducible(
+    'default cryptoStore',
+    _isEqual({
+      primaryIdentityKeys: null,
+      notificationIdentityKeys: null,
+    }),
+  ),
+  pushApiPublicKey: t.maybe(t.String),
+  _persist: t.Nil,
+  commServicesAccessToken: t.Nil,
+});
+
 async function websiteResponder(
   viewer: Viewer,
   req: $Request,
@@ -432,7 +526,12 @@
     _persist: null,
     commServicesAccessToken: null,
   });
-  const jsonStream = streamJSON(res, initialReduxState);
+  const validatedInitialReduxState = validateOutput(
+    viewer,
+    initialReduxStateValidator,
+    initialReduxState,
+  );
+  const jsonStream = streamJSON(res, validatedInitialReduxState);
 
   await waitForStream(jsonStream);
   res.end(html`
diff --git a/lib/types/entry-types.js b/lib/types/entry-types.js
--- a/lib/types/entry-types.js
+++ b/lib/types/entry-types.js
@@ -61,6 +61,11 @@
   +daysToEntries: { +[day: string]: string[] },
   +lastUserInteractionCalendar: number,
 };
+export const entryStoreValidator: TInterface<EntryStore> = tShape<EntryStore>({
+  entryInfos: t.dict(tID, rawEntryInfoValidator),
+  daysToEntries: t.dict(t.String, t.list(tID)),
+  lastUserInteractionCalendar: t.Number,
+});
 
 export type CalendarQuery = {
   +startDate: string,
diff --git a/lib/types/message-types.js b/lib/types/message-types.js
--- a/lib/types/message-types.js
+++ b/lib/types/message-types.js
@@ -390,6 +390,13 @@
   lastNavigatedTo: number, // millisecond timestamp
   lastPruned: number, // millisecond timestamp
 };
+const threadMessageInfoValidator: TInterface<ThreadMessageInfo> =
+  tShape<ThreadMessageInfo>({
+    messageIDs: t.list(tID),
+    startReached: t.Boolean,
+    lastNavigatedTo: t.Number,
+    lastPruned: t.Number,
+  });
 
 // Tracks client-local information about a message that hasn't been assigned an
 // ID by the server yet. As soon as the client gets an ack from the server for
@@ -397,10 +404,18 @@
 export type LocalMessageInfo = {
   +sendFailed?: boolean,
 };
+const localMessageInfoValidator: TInterface<LocalMessageInfo> =
+  tShape<LocalMessageInfo>({
+    sendFailed: t.maybe(t.Boolean),
+  });
 
 export type MessageStoreThreads = {
   +[threadID: string]: ThreadMessageInfo,
 };
+const messageStoreThreadsValidator: TDict<MessageStoreThreads> = t.dict(
+  tID,
+  threadMessageInfoValidator,
+);
 
 export type MessageStore = {
   +messages: { +[id: string]: RawMessageInfo },
@@ -408,6 +423,13 @@
   +local: { +[id: string]: LocalMessageInfo },
   +currentAsOf: number,
 };
+export const messageStoreValidator: TInterface<MessageStore> =
+  tShape<MessageStore>({
+    messages: t.dict(tID, rawMessageInfoValidator),
+    threads: messageStoreThreadsValidator,
+    local: t.dict(t.String, localMessageInfoValidator),
+    currentAsOf: t.Number,
+  });
 
 // MessageStore messages ops
 export type RemoveMessageOperation = {
diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js
--- a/lib/types/thread-types.js
+++ b/lib/types/thread-types.js
@@ -28,7 +28,10 @@
 import { type ThreadType, threadTypeValidator } from './thread-types-enum.js';
 import type { ClientUpdateInfo, ServerUpdateInfo } from './update-types.js';
 import type { UserInfo, UserInfos } from './user-types.js';
-import { type ThreadEntity } from '../utils/entity-text.js';
+import {
+  type ThreadEntity,
+  threadEntityValidator,
+} from '../utils/entity-text.js';
 import { tID, tShape } from '../utils/validation-utils.js';
 
 export type MemberInfo = {
@@ -49,6 +52,11 @@
   +username: ?string,
   +isViewer: boolean,
 };
+const relativeMemberInfoValidator = tShape<RelativeMemberInfo>({
+  ...memberInfoValidator.meta.props,
+  username: t.maybe(t.String),
+  isViewer: t.Boolean,
+});
 
 export type RoleInfo = {
   +id: string,
@@ -133,6 +141,25 @@
   +repliesCount: number,
   +pinnedCount?: number,
 };
+export const threadInfoValidator: TInterface<ThreadInfo> = tShape<ThreadInfo>({
+  id: tID,
+  type: threadTypeValidator,
+  name: t.maybe(t.String),
+  uiName: t.union([t.String, threadEntityValidator]),
+  avatar: t.maybe(clientAvatarValidator),
+  description: t.maybe(t.String),
+  color: t.String,
+  creationTime: t.Number,
+  parentThreadID: t.maybe(tID),
+  containingThreadID: t.maybe(tID),
+  community: t.maybe(tID),
+  members: t.list(relativeMemberInfoValidator),
+  roles: t.dict(tID, roleInfoValidator),
+  currentUser: threadCurrentUserInfoValidator,
+  sourceMessageID: t.maybe(tID),
+  repliesCount: t.Number,
+  pinnedCount: t.maybe(t.Number),
+});
 
 export type ResolvedThreadInfo = {
   +id: string,
@@ -185,6 +212,10 @@
 export type ThreadStore = {
   +threadInfos: { +[id: string]: RawThreadInfo },
 };
+export const threadStoreValidator: TInterface<ThreadStore> =
+  tShape<ThreadStore>({
+    threadInfos: t.dict(tID, rawThreadInfoValidator),
+  });
 
 export type RemoveThreadOperation = {
   +type: 'remove',
diff --git a/lib/utils/entity-text.js b/lib/utils/entity-text.js
--- a/lib/utils/entity-text.js
+++ b/lib/utils/entity-text.js
@@ -2,12 +2,18 @@
 
 import invariant from 'invariant';
 import * as React from 'react';
+import t, { type TInterface, type TUnion } from 'tcomb';
 
 import type { GetENSNames } from './ens-helpers.js';
+import { tID, tShape, tString } from './validation-utils.js';
 import { useENSNames } from '../hooks/ens-cache.js';
 import { threadNoun } from '../shared/thread-utils.js';
 import { stringForUser } from '../shared/user-utils.js';
-import { type ThreadType, threadTypes } from '../types/thread-types-enum.js';
+import {
+  type ThreadType,
+  threadTypes,
+  threadTypeValidator,
+} from '../types/thread-types-enum.js';
 import { type RawThreadInfo, type ThreadInfo } from '../types/thread-types.js';
 import { basePluralize } from '../utils/text-utils.js';
 
@@ -18,6 +24,13 @@
   +isViewer?: ?boolean,
   +possessive?: ?boolean, // eg. `user's` instead of `user`
 };
+export const userEntityValidator: TInterface<UserEntity> = tShape<UserEntity>({
+  type: tString('user'),
+  id: t.String,
+  username: t.maybe(t.String),
+  isViewer: t.maybe(t.Boolean),
+  possessive: t.maybe(t.Boolean),
+});
 
 // Comments explain how thread name will appear from user4's perspective
 export type ThreadEntity =
@@ -48,6 +61,28 @@
       +possessive?: ?boolean, // eg. `this thread's` instead of `this thread`
     };
 
+export const threadEntityValidator: TUnion<ThreadEntity> = t.union([
+  tShape({
+    type: tString('thread'),
+    id: tID,
+    name: t.maybe(t.String),
+    display: tString('uiName'),
+    uiName: t.union([t.list(userEntityValidator), t.String]),
+    ifJustViewer: t.maybe(t.enums.of(['just_you_string', 'viewer_username'])),
+  }),
+  tShape({
+    type: tString('thread'),
+    id: tID,
+    name: t.maybe(t.String),
+    display: tString('shortName'),
+    threadType: t.maybe(threadTypeValidator),
+    parentThreadID: t.maybe(tID),
+    alwaysDisplayShortName: t.maybe(t.Boolean),
+    subchannel: t.maybe(t.Boolean),
+    possessive: t.maybe(t.Boolean),
+  }),
+]);
+
 type ColorEntity = {
   +type: 'color',
   +hex: string,
diff --git a/web/types/nav-types.js b/web/types/nav-types.js
--- a/web/types/nav-types.js
+++ b/web/types/nav-types.js
@@ -1,13 +1,25 @@
 // @flow
+import t from 'tcomb';
+import type { TInterface } from 'tcomb';
 
-import type { BaseNavInfo } from 'lib/types/nav-types.js';
-import type { ThreadInfo } from 'lib/types/thread-types.js';
+import { type BaseNavInfo } from 'lib/types/nav-types.js';
+import {
+  type ThreadInfo,
+  threadInfoValidator,
+} from 'lib/types/thread-types.js';
+import { tID, tShape } from 'lib/utils/validation-utils.js';
 
 export type NavigationTab = 'calendar' | 'chat' | 'settings';
+const navigationTabValidator = t.enums.of(['calendar', 'chat', 'settings']);
 
 export type NavigationSettingsSection = 'account' | 'danger-zone';
+const navigationSettingsSectionValidator = t.enums.of([
+  'account',
+  'danger-zone',
+]);
 
 export type NavigationChatMode = 'view' | 'create';
+const navigationChatModeValidator = t.enums.of(['view', 'create']);
 
 export type NavInfo = {
   ...$Exact<BaseNavInfo>,
@@ -18,3 +30,14 @@
   +selectedUserList?: $ReadOnlyArray<string>,
   +chatMode?: NavigationChatMode,
 };
+
+export const navInfoValidator: TInterface<NavInfo> = tShape<$Exact<NavInfo>>({
+  startDate: t.String,
+  endDate: t.String,
+  tab: navigationTabValidator,
+  activeChatThreadID: t.maybe(tID),
+  pendingThread: t.maybe(threadInfoValidator),
+  settingsSection: t.maybe(navigationSettingsSectionValidator),
+  selectedUserList: t.maybe(t.list(t.String)),
+  chatMode: t.maybe(navigationChatModeValidator),
+});