diff --git a/backend/src/commandTypes.ts b/backend/src/commandTypes.ts
index 3c3cb1cb..e36ce831 100644
--- a/backend/src/commandTypes.ts
+++ b/backend/src/commandTypes.ts
@@ -17,6 +17,7 @@ import { createTypeHelper } from "knub-command-manager";
 import { getChannelIdFromMessageId } from "./data/getChannelIdFromMessageId";
 import { MessageTarget, resolveMessageTarget } from "./utils/resolveMessageTarget";
 import { inputPatternToRegExp } from "./validatorUtils";
+import { isValidTimezone } from "./utils/isValidTimezone";
 
 export const commandTypes = {
   ...baseTypeConverters,
@@ -93,6 +94,14 @@ export const commandTypes = {
       throw new TypeConversionError(`Could not parse RegExp: \`${disableInlineCode(e.message)}\``);
     }
   },
+
+  timezone(value: string) {
+    if (!isValidTimezone(value)) {
+      throw new TypeConversionError(`Invalid timezone: ${disableInlineCode(value)}`);
+    }
+
+    return value;
+  },
 };
 
 export const commandTypeHelpers = {
@@ -105,4 +114,5 @@ export const commandTypeHelpers = {
   messageTarget: createTypeHelper<Promise<MessageTarget>>(commandTypes.messageTarget),
   anyId: createTypeHelper<Promise<string>>(commandTypes.anyId),
   regex: createTypeHelper<RegExp>(commandTypes.regex),
+  timezone: createTypeHelper<string>(commandTypes.timezone),
 };
diff --git a/backend/src/data/ApiLogins.ts b/backend/src/data/ApiLogins.ts
index 62572c78..22ab35e8 100644
--- a/backend/src/data/ApiLogins.ts
+++ b/backend/src/data/ApiLogins.ts
@@ -5,7 +5,7 @@ import crypto from "crypto";
 import moment from "moment-timezone";
 // tslint:disable-next-line:no-submodule-imports
 import uuidv4 from "uuid/v4";
-import { DBDateFormat } from "../utils/dateFormats";
+import { DBDateFormat } from "../utils";
 
 export class ApiLogins extends BaseRepository {
   private apiLogins: Repository<ApiLogin>;
diff --git a/backend/src/data/ApiUserInfo.ts b/backend/src/data/ApiUserInfo.ts
index 37e96ebb..09d9df75 100644
--- a/backend/src/data/ApiUserInfo.ts
+++ b/backend/src/data/ApiUserInfo.ts
@@ -3,7 +3,7 @@ import { ApiUserInfo as ApiUserInfoEntity, ApiUserInfoData } from "./entities/Ap
 import { BaseRepository } from "./BaseRepository";
 import { connection } from "./db";
 import moment from "moment-timezone";
-import { DBDateFormat } from "../utils/dateFormats";
+import { DBDateFormat } from "../utils";
 
 export class ApiUserInfo extends BaseRepository {
   private apiUserInfo: Repository<ApiUserInfoEntity>;
diff --git a/backend/src/data/GuildCases.ts b/backend/src/data/GuildCases.ts
index dc2696b6..37396c3f 100644
--- a/backend/src/data/GuildCases.ts
+++ b/backend/src/data/GuildCases.ts
@@ -2,11 +2,10 @@ import { Case } from "./entities/Case";
 import { CaseNote } from "./entities/CaseNote";
 import { BaseGuildRepository } from "./BaseGuildRepository";
 import { getRepository, In, Repository } from "typeorm";
-import { disableLinkPreviews } from "../utils";
+import { DBDateFormat, disableLinkPreviews } from "../utils";
 import { CaseTypes } from "./CaseTypes";
 import moment = require("moment-timezone");
 import { connection } from "./db";
-import { DBDateFormat } from "../utils/dateFormats";
 
 const CASE_SUMMARY_REASON_MAX_LENGTH = 300;
 
diff --git a/backend/src/data/GuildMemberTimezones.ts b/backend/src/data/GuildMemberTimezones.ts
new file mode 100644
index 00000000..df4633aa
--- /dev/null
+++ b/backend/src/data/GuildMemberTimezones.ts
@@ -0,0 +1,48 @@
+import { BaseGuildRepository } from "./BaseGuildRepository";
+import { MemberTimezone } from "./entities/MemberTimezone";
+import { getRepository, Repository } from "typeorm/index";
+import { connection } from "./db";
+
+export class GuildMemberTimezones extends BaseGuildRepository {
+  protected memberTimezones: Repository<MemberTimezone>;
+
+  constructor(guildId: string) {
+    super(guildId);
+    this.memberTimezones = getRepository(MemberTimezone);
+  }
+
+  get(memberId: string) {
+    return this.memberTimezones.findOne({
+      guild_id: this.guildId,
+      member_id: memberId,
+    });
+  }
+
+  async set(memberId, timezone: string) {
+    await connection.transaction(async entityManager => {
+      const repo = entityManager.getRepository(MemberTimezone);
+      const existingRow = await repo.findOne({
+        guild_id: this.guildId,
+        member_id: memberId,
+      });
+
+      if (existingRow) {
+        await repo.update(
+          {
+            guild_id: this.guildId,
+            member_id: memberId,
+          },
+          {
+            timezone,
+          },
+        );
+      } else {
+        await repo.insert({
+          guild_id: this.guildId,
+          member_id: memberId,
+          timezone,
+        });
+      }
+    });
+  }
+}
diff --git a/backend/src/data/cleanup/configs.ts b/backend/src/data/cleanup/configs.ts
index c0c6b8f0..04c76044 100644
--- a/backend/src/data/cleanup/configs.ts
+++ b/backend/src/data/cleanup/configs.ts
@@ -2,7 +2,7 @@ import { connection } from "../db";
 import { getRepository, In } from "typeorm";
 import { Config } from "../entities/Config";
 import moment from "moment-timezone";
-import { DBDateFormat } from "../../utils/dateFormats";
+import { DBDateFormat } from "../../utils";
 
 const CLEAN_PER_LOOP = 50;
 
diff --git a/backend/src/data/cleanup/messages.ts b/backend/src/data/cleanup/messages.ts
index 4eb3e961..1b9f8427 100644
--- a/backend/src/data/cleanup/messages.ts
+++ b/backend/src/data/cleanup/messages.ts
@@ -1,9 +1,8 @@
-import { DAYS, MINUTES } from "../../utils";
+import { DAYS, DBDateFormat, MINUTES } from "../../utils";
 import { getRepository, In } from "typeorm";
 import { SavedMessage } from "../entities/SavedMessage";
 import moment from "moment-timezone";
 import { connection } from "../db";
-import { DBDateFormat } from "../../utils/dateFormats";
 
 /**
  * How long message edits, deletions, etc. will include the original message content.
diff --git a/backend/src/data/cleanup/nicknames.ts b/backend/src/data/cleanup/nicknames.ts
index b62bb287..e48b2670 100644
--- a/backend/src/data/cleanup/nicknames.ts
+++ b/backend/src/data/cleanup/nicknames.ts
@@ -1,9 +1,8 @@
 import { getRepository, In } from "typeorm";
 import moment from "moment-timezone";
 import { NicknameHistoryEntry } from "../entities/NicknameHistoryEntry";
-import { DAYS } from "../../utils";
+import { DAYS, DBDateFormat } from "../../utils";
 import { connection } from "../db";
-import { DBDateFormat } from "../../utils/dateFormats";
 
 export const NICKNAME_RETENTION_PERIOD = 30 * DAYS;
 const CLEAN_PER_LOOP = 500;
diff --git a/backend/src/data/cleanup/usernames.ts b/backend/src/data/cleanup/usernames.ts
index b583eb2d..6bcca3d2 100644
--- a/backend/src/data/cleanup/usernames.ts
+++ b/backend/src/data/cleanup/usernames.ts
@@ -1,9 +1,8 @@
 import { getRepository, In } from "typeorm";
 import moment from "moment-timezone";
 import { UsernameHistoryEntry } from "../entities/UsernameHistoryEntry";
-import { DAYS } from "../../utils";
+import { DAYS, DBDateFormat } from "../../utils";
 import { connection } from "../db";
-import { DBDateFormat } from "../../utils/dateFormats";
 
 export const USERNAME_RETENTION_PERIOD = 30 * DAYS;
 const CLEAN_PER_LOOP = 500;
diff --git a/backend/src/data/entities/MemberTimezone.ts b/backend/src/data/entities/MemberTimezone.ts
new file mode 100644
index 00000000..c05158d1
--- /dev/null
+++ b/backend/src/data/entities/MemberTimezone.ts
@@ -0,0 +1,14 @@
+import { Column, Entity, PrimaryColumn } from "typeorm";
+
+@Entity("member_timezones")
+export class MemberTimezone {
+  @Column()
+  @PrimaryColumn()
+  guild_id: string;
+
+  @Column()
+  @PrimaryColumn()
+  member_id: string;
+
+  @Column() timezone: string;
+}
diff --git a/backend/src/migrations/1597109357201-CreateMemberTimezonesTable.ts b/backend/src/migrations/1597109357201-CreateMemberTimezonesTable.ts
new file mode 100644
index 00000000..df4b6cca
--- /dev/null
+++ b/backend/src/migrations/1597109357201-CreateMemberTimezonesTable.ts
@@ -0,0 +1,33 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+import { Table } from "typeorm/index";
+
+export class CreateMemberTimezonesTable1597109357201 implements MigrationInterface {
+  public async up(queryRunner: QueryRunner): Promise<any> {
+    await queryRunner.createTable(
+      new Table({
+        name: "member_timezones",
+        columns: [
+          {
+            name: "guild_id",
+            type: "bigint",
+            isPrimary: true,
+          },
+          {
+            name: "member_id",
+            type: "bigint",
+            isPrimary: true,
+          },
+          {
+            name: "timezone",
+            type: "varchar",
+            length: "255",
+          },
+        ],
+      }),
+    );
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<any> {
+    await queryRunner.dropTable("member_timezones");
+  }
+}
diff --git a/backend/src/pluginUtils.ts b/backend/src/pluginUtils.ts
index 9fa34040..8125ca68 100644
--- a/backend/src/pluginUtils.ts
+++ b/backend/src/pluginUtils.ts
@@ -19,6 +19,7 @@ import { TZeppelinKnub } from "./types";
 import { ExtendedMatchParams } from "knub/dist/config/PluginConfigManager"; // TODO: Export from Knub index
 import * as t from "io-ts";
 import { PluginOverrideCriteria } from "knub/dist/config/configTypes";
+import { Tail } from "./utils/typeUtils";
 
 const { getMemberLevel } = helpers;
 
@@ -168,3 +169,16 @@ export function isOwner(pluginData: PluginData<any>, userId: string) {
 export const isOwnerPreFilter = (_, context: CommandContext<any>) => {
   return isOwner(context.pluginData, context.message.author.id);
 };
+
+type AnyFn = (...args: any[]) => any;
+
+/**
+ * Creates a public plugin function out of a function with pluginData as the first parameter
+ */
+export function mapToPublicFn<T extends AnyFn>(inputFn: T) {
+  return pluginData => {
+    return (...args: Tail<Parameters<typeof inputFn>>): ReturnType<typeof inputFn> => {
+      return inputFn(pluginData, ...args);
+    };
+  };
+}
diff --git a/backend/src/plugins/AutoDelete/AutoDeletePlugin.ts b/backend/src/plugins/AutoDelete/AutoDeletePlugin.ts
index 45374fde..7b5d47aa 100644
--- a/backend/src/plugins/AutoDelete/AutoDeletePlugin.ts
+++ b/backend/src/plugins/AutoDelete/AutoDeletePlugin.ts
@@ -6,6 +6,7 @@ import { GuildLogs } from "src/data/GuildLogs";
 import { onMessageCreate } from "./util/onMessageCreate";
 import { onMessageDelete } from "./util/onMessageDelete";
 import { onMessageDeleteBulk } from "./util/onMessageDeleteBulk";
+import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
 
 const defaultOptions: PluginOptions<AutoDeletePluginType> = {
   config: {
@@ -22,6 +23,7 @@ export const AutoDeletePlugin = zeppelinPlugin<AutoDeletePluginType>()("auto_del
     configurationGuide: "Maximum deletion delay is currently 5 minutes",
   },
 
+  dependencies: [TimeAndDatePlugin],
   configSchema: ConfigSchema,
   defaultOptions,
 
diff --git a/backend/src/plugins/AutoDelete/util/deleteNextItem.ts b/backend/src/plugins/AutoDelete/util/deleteNextItem.ts
index 19d20971..68a10eb0 100644
--- a/backend/src/plugins/AutoDelete/util/deleteNextItem.ts
+++ b/backend/src/plugins/AutoDelete/util/deleteNextItem.ts
@@ -5,13 +5,14 @@ import { LogType } from "src/data/LogType";
 import { stripObjectToScalars, resolveUser } from "src/utils";
 import { logger } from "src/logger";
 import { scheduleNextDeletion } from "./scheduleNextDeletion";
-import { inGuildTz } from "../../../utils/timezones";
-import { getDateFormat } from "../../../utils/dateFormats";
+import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
 
 export async function deleteNextItem(pluginData: PluginData<AutoDeletePluginType>) {
   const [itemToDelete] = pluginData.state.deletionQueue.splice(0, 1);
   if (!itemToDelete) return;
 
+  const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
+
   pluginData.state.guildLogs.ignoreLog(LogType.MESSAGE_DELETE, itemToDelete.message.id);
   pluginData.client.deleteMessage(itemToDelete.message.channel_id, itemToDelete.message.id).catch(logger.warn);
 
@@ -19,9 +20,9 @@ export async function deleteNextItem(pluginData: PluginData<AutoDeletePluginType
 
   const user = await resolveUser(pluginData.client, itemToDelete.message.user_id);
   const channel = pluginData.guild.channels.get(itemToDelete.message.channel_id);
-  const messageDate = inGuildTz(pluginData, moment.utc(itemToDelete.message.data.timestamp, "x")).format(
-    getDateFormat(pluginData, "pretty_datetime"),
-  );
+  const messageDate = timeAndDate
+    .inGuildTz(moment.utc(itemToDelete.message.data.timestamp, "x"))
+    .format(timeAndDate.getDateFormat("pretty_datetime"));
 
   pluginData.state.guildLogs.log(LogType.MESSAGE_DELETE_AUTO, {
     message: itemToDelete.message,
diff --git a/backend/src/plugins/Cases/CasesPlugin.ts b/backend/src/plugins/Cases/CasesPlugin.ts
index 07fbf862..239d5298 100644
--- a/backend/src/plugins/Cases/CasesPlugin.ts
+++ b/backend/src/plugins/Cases/CasesPlugin.ts
@@ -12,6 +12,8 @@ import { getCaseTypeAmountForUserId } from "./functions/getCaseTypeAmountForUser
 import { getCaseEmbed } from "./functions/getCaseEmbed";
 import { trimPluginDescription } from "../../utils";
 import { getCaseSummary } from "./functions/getCaseSummary";
+import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
+import { mapToPublicFn } from "../../pluginUtils";
 
 const defaultOptions = {
   config: {
@@ -33,6 +35,7 @@ export const CasesPlugin = zeppelinPlugin<CasesPluginType>()("cases", {
     `),
   },
 
+  dependencies: [TimeAndDatePlugin],
   configSchema: ConfigSchema,
   defaultOptions,
 
@@ -61,17 +64,8 @@ export const CasesPlugin = zeppelinPlugin<CasesPluginType>()("cases", {
       };
     },
 
-    getCaseEmbed(pluginData) {
-      return (caseOrCaseId: Case | number) => {
-        return getCaseEmbed(pluginData, caseOrCaseId);
-      };
-    },
-
-    getCaseSummary(pluginData) {
-      return (caseOrCaseId: Case | number, withLinks = false) => {
-        return getCaseSummary(pluginData, caseOrCaseId, withLinks);
-      };
-    },
+    getCaseEmbed: mapToPublicFn(getCaseEmbed),
+    getCaseSummary: mapToPublicFn(getCaseSummary),
   },
 
   onLoad(pluginData) {
diff --git a/backend/src/plugins/Cases/functions/getCaseEmbed.ts b/backend/src/plugins/Cases/functions/getCaseEmbed.ts
index 385c6720..4e7ea817 100644
--- a/backend/src/plugins/Cases/functions/getCaseEmbed.ts
+++ b/backend/src/plugins/Cases/functions/getCaseEmbed.ts
@@ -6,17 +6,19 @@ import { PluginData, helpers } from "knub";
 import { CasesPluginType } from "../types";
 import { resolveCaseId } from "./resolveCaseId";
 import { chunkLines, chunkMessageLines, emptyEmbedValue, messageLink } from "../../../utils";
-import { inGuildTz } from "../../../utils/timezones";
-import { getDateFormat } from "../../../utils/dateFormats";
 import { getCaseColor } from "./getCaseColor";
+import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
 
 export async function getCaseEmbed(
   pluginData: PluginData<CasesPluginType>,
   caseOrCaseId: Case | number,
+  requestMemberId?: string,
 ): Promise<AdvancedMessageContent> {
   const theCase = await pluginData.state.cases.with("notes").find(resolveCaseId(caseOrCaseId));
   if (!theCase) return null;
 
+  const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
+
   const createdAt = moment.utc(theCase.created_at);
   const actionTypeStr = CaseTypes[theCase.type].toUpperCase();
 
@@ -26,10 +28,14 @@ export async function getCaseEmbed(
   let modName = theCase.mod_name;
   if (theCase.mod_id) modName += `\n<@!${theCase.mod_id}>`;
 
+  const createdAtWithTz = requestMemberId
+    ? await timeAndDate.inMemberTz(requestMemberId, createdAt)
+    : timeAndDate.inGuildTz(createdAt);
+
   const embed: any = {
     title: `${actionTypeStr} - Case #${theCase.case_number}`,
     footer: {
-      text: `Case created on ${inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime"))}`,
+      text: `Case created on ${createdAtWithTz.format(timeAndDate.getDateFormat("pretty_datetime"))}`,
     },
     fields: [
       {
@@ -56,7 +62,7 @@ export async function getCaseEmbed(
   embed.color = getCaseColor(pluginData, theCase.type);
 
   if (theCase.notes.length) {
-    theCase.notes.forEach((note: any) => {
+    for (const note of theCase.notes) {
       const noteDate = moment.utc(note.created_at);
       let noteBody = note.body.trim();
       if (noteBody === "") {
@@ -67,7 +73,10 @@ export async function getCaseEmbed(
 
       for (let i = 0; i < chunks.length; i++) {
         if (i === 0) {
-          const prettyNoteDate = inGuildTz(pluginData, noteDate).format(getDateFormat(pluginData, "pretty_datetime"));
+          const noteDateWithTz = requestMemberId
+            ? await timeAndDate.inMemberTz(requestMemberId, noteDate)
+            : timeAndDate.inGuildTz(noteDate);
+          const prettyNoteDate = noteDateWithTz.format(timeAndDate.getDateFormat("pretty_datetime"));
           embed.fields.push({
             name: `${note.mod_name} at ${prettyNoteDate}:`,
             value: chunks[i],
@@ -79,7 +88,7 @@ export async function getCaseEmbed(
           });
         }
       }
-    });
+    }
   } else {
     embed.fields.push({
       name: "!!! THIS CASE HAS NO NOTES !!!",
diff --git a/backend/src/plugins/Cases/functions/getCaseSummary.ts b/backend/src/plugins/Cases/functions/getCaseSummary.ts
index 4c7fc322..18034e8e 100644
--- a/backend/src/plugins/Cases/functions/getCaseSummary.ts
+++ b/backend/src/plugins/Cases/functions/getCaseSummary.ts
@@ -1,15 +1,21 @@
 import { PluginData } from "knub";
 import { CasesPluginType } from "../types";
-import { convertDelayStringToMS, DAYS, disableLinkPreviews, emptyEmbedValue, messageLink } from "../../../utils";
-import { DBDateFormat, getDateFormat } from "../../../utils/dateFormats";
+import {
+  convertDelayStringToMS,
+  DAYS,
+  DBDateFormat,
+  disableLinkPreviews,
+  emptyEmbedValue,
+  messageLink,
+} from "../../../utils";
 import { CaseTypes, CaseTypeToName } from "../../../data/CaseTypes";
 import moment from "moment-timezone";
 import { Case } from "../../../data/entities/Case";
-import { inGuildTz } from "../../../utils/timezones";
 import humanizeDuration from "humanize-duration";
 import { humanizeDurationShort } from "../../../humanizeDurationShort";
 import { caseAbbreviations } from "../caseAbbreviations";
 import { getCaseIcon } from "./getCaseIcon";
+import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
 
 const CASE_SUMMARY_REASON_MAX_LENGTH = 300;
 const INCLUDE_MORE_NOTES_THRESHOLD = 20;
@@ -21,8 +27,10 @@ export async function getCaseSummary(
   pluginData: PluginData<CasesPluginType>,
   caseOrCaseId: Case | number,
   withLinks = false,
+  requestMemberId?: string,
 ) {
   const config = pluginData.config.get();
+  const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
 
   const caseId = caseOrCaseId instanceof Case ? caseOrCaseId.id : caseOrCaseId;
   const theCase = await pluginData.state.cases.with("notes").find(caseId);
@@ -50,9 +58,12 @@ export async function getCaseSummary(
   const timestamp = moment.utc(theCase.created_at, DBDateFormat);
   const relativeTimeCutoff = convertDelayStringToMS(config.relative_time_cutoff);
   const useRelativeTime = config.show_relative_times && Date.now() - timestamp.valueOf() < relativeTimeCutoff;
+  const timestampWithTz = requestMemberId
+    ? await timeAndDate.inMemberTz(requestMemberId, timestamp)
+    : timeAndDate.inGuildTz(timestamp);
   const prettyTimestamp = useRelativeTime
     ? moment.utc().to(timestamp)
-    : inGuildTz(pluginData, timestamp).format(getDateFormat(pluginData, "date"));
+    : timestampWithTz.format(timeAndDate.getDateFormat("date"));
 
   const icon = getCaseIcon(pluginData, theCase.type);
 
diff --git a/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts b/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts
index e7012134..9c34054d 100644
--- a/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts
+++ b/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts
@@ -2,9 +2,12 @@ import { zeppelinPlugin } from "../ZeppelinPluginBlueprint";
 import { ChannelArchiverPluginType } from "./types";
 import { ArchiveChannelCmd } from "./commands/ArchiveChannelCmd";
 import * as t from "io-ts";
+import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
 
 export const ChannelArchiverPlugin = zeppelinPlugin<ChannelArchiverPluginType>()("channel_archiver", {
   showInDocs: false,
+
+  dependencies: [TimeAndDatePlugin],
   configSchema: t.type({}),
 
   // prettier-ignore
diff --git a/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts b/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts
index 38297182..4f779feb 100644
--- a/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts
+++ b/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts
@@ -4,8 +4,7 @@ import { isOwner, sendErrorMessage } from "src/pluginUtils";
 import { confirm, SECONDS, noop } from "src/utils";
 import moment from "moment-timezone";
 import { rehostAttachment } from "../rehostAttachment";
-import { inGuildTz } from "../../../utils/timezones";
-import { getDateFormat } from "../../../utils/dateFormats";
+import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
 
 const MAX_ARCHIVED_MESSAGES = 5000;
 const MAX_MESSAGES_PER_FETCH = 100;
@@ -98,7 +97,8 @@ export const ArchiveChannelCmd = channelArchiverCmd({
 
     archiveLines.reverse();
 
-    const nowTs = inGuildTz(pluginData).format(getDateFormat(pluginData, "pretty_datetime"));
+    const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
+    const nowTs = timeAndDate.inGuildTz().format(timeAndDate.getDateFormat("pretty_datetime"));
 
     let result = `Archived ${archiveLines.length} messages from #${args.channel.name} at ${nowTs}`;
     result += `\n\n${archiveLines.join("\n")}\n`;
diff --git a/backend/src/plugins/Logs/LogsPlugin.ts b/backend/src/plugins/Logs/LogsPlugin.ts
index b7ef5132..a678b3e9 100644
--- a/backend/src/plugins/Logs/LogsPlugin.ts
+++ b/backend/src/plugins/Logs/LogsPlugin.ts
@@ -22,6 +22,7 @@ import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners";
 import { disableCodeBlocks } from "../../utils";
 import { logger } from "../../logger";
 import { CasesPlugin } from "../Cases/CasesPlugin";
+import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
 
 const defaultOptions: PluginOptions<LogsPluginType> = {
   config: {
@@ -49,7 +50,7 @@ export const LogsPlugin = zeppelinPlugin<LogsPluginType>()("logs", {
     prettyName: "Logs",
   },
 
-  dependencies: [CasesPlugin],
+  dependencies: [TimeAndDatePlugin, CasesPlugin],
   configSchema: ConfigSchema,
   defaultOptions,
 
diff --git a/backend/src/plugins/Logs/util/getLogMessage.ts b/backend/src/plugins/Logs/util/getLogMessage.ts
index 527f3fe1..92cc073b 100644
--- a/backend/src/plugins/Logs/util/getLogMessage.ts
+++ b/backend/src/plugins/Logs/util/getLogMessage.ts
@@ -13,7 +13,7 @@ import { SavedMessage } from "src/data/entities/SavedMessage";
 import { renderTemplate, TemplateParseError } from "src/templateFormatter";
 import { logger } from "src/logger";
 import moment from "moment-timezone";
-import { inGuildTz } from "../../../utils/timezones";
+import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
 
 export async function getLogMessage(
   pluginData: PluginData<LogsPluginType>,
@@ -88,7 +88,10 @@ export async function getLogMessage(
 
     const timestampFormat = config.format.timestamp;
     if (timestampFormat) {
-      const timestamp = inGuildTz(pluginData).format(timestampFormat);
+      const timestamp = pluginData
+        .getPlugin(TimeAndDatePlugin)
+        .inGuildTz()
+        .format(timestampFormat);
       formatted = `\`[${timestamp}]\` ${formatted}`;
     }
   }
diff --git a/backend/src/plugins/Logs/util/onMessageDelete.ts b/backend/src/plugins/Logs/util/onMessageDelete.ts
index 0ff8de5c..80112e22 100644
--- a/backend/src/plugins/Logs/util/onMessageDelete.ts
+++ b/backend/src/plugins/Logs/util/onMessageDelete.ts
@@ -5,7 +5,7 @@ import { LogType } from "src/data/LogType";
 import moment from "moment-timezone";
 import { PluginData } from "knub";
 import { LogsPluginType } from "../types";
-import { inGuildTz } from "../../../utils/timezones";
+import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
 
 export async function onMessageDelete(pluginData: PluginData<LogsPluginType>, savedMessage: SavedMessage) {
   const user = await resolveUser(pluginData.client, savedMessage.user_id);
@@ -24,9 +24,10 @@ export async function onMessageDelete(pluginData: PluginData<LogsPluginType>, sa
       {
         user: stripObjectToScalars(user),
         channel: stripObjectToScalars(channel),
-        messageDate: inGuildTz(pluginData, moment.utc(savedMessage.data.timestamp, "x")).format(
-          pluginData.config.get().format.timestamp,
-        ),
+        messageDate: pluginData
+          .getPlugin(TimeAndDatePlugin)
+          .inGuildTz(moment.utc(savedMessage.data.timestamp, "x"))
+          .format(pluginData.config.get().format.timestamp),
         message: savedMessage,
       },
       savedMessage.id,
diff --git a/backend/src/plugins/ModActions/ModActionsPlugin.ts b/backend/src/plugins/ModActions/ModActionsPlugin.ts
index d6abe1f4..1714d7b1 100644
--- a/backend/src/plugins/ModActions/ModActionsPlugin.ts
+++ b/backend/src/plugins/ModActions/ModActionsPlugin.ts
@@ -35,6 +35,7 @@ import { banUserId } from "./functions/banUserId";
 import { MassmuteCmd } from "./commands/MassmuteCmd";
 import { trimPluginDescription } from "../../utils";
 import { DeleteCaseCmd } from "./commands/DeleteCaseCmd";
+import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
 
 const defaultOptions = {
   config: {
@@ -103,11 +104,10 @@ export const ModActionsPlugin = zeppelinPlugin<ModActionsPluginType>()("mod_acti
     `),
   },
 
+  dependencies: [TimeAndDatePlugin, CasesPlugin, MutesPlugin],
   configSchema: ConfigSchema,
   defaultOptions,
 
-  dependencies: [CasesPlugin, MutesPlugin],
-
   events: [
     CreateBanCaseOnManualBanEvt,
     CreateUnbanCaseOnManualUnbanEvt,
diff --git a/backend/src/plugins/ModActions/commands/CaseCmd.ts b/backend/src/plugins/ModActions/commands/CaseCmd.ts
index ddb07ee1..36d6a86b 100644
--- a/backend/src/plugins/ModActions/commands/CaseCmd.ts
+++ b/backend/src/plugins/ModActions/commands/CaseCmd.ts
@@ -23,7 +23,7 @@ export const CaseCmd = modActionsCommand({
     }
 
     const casesPlugin = pluginData.getPlugin(CasesPlugin);
-    const embed = await casesPlugin.getCaseEmbed(theCase.id);
+    const embed = await casesPlugin.getCaseEmbed(theCase.id, msg.author.id);
     msg.channel.createMessage(embed);
   },
 });
diff --git a/backend/src/plugins/ModActions/commands/CasesModCmd.ts b/backend/src/plugins/ModActions/commands/CasesModCmd.ts
index 67fbfd0a..f518d92a 100644
--- a/backend/src/plugins/ModActions/commands/CasesModCmd.ts
+++ b/backend/src/plugins/ModActions/commands/CasesModCmd.ts
@@ -36,7 +36,7 @@ export const CasesModCmd = modActionsCommand({
       sendErrorMessage(pluginData, msg.channel, `No cases by **${modName}**`);
     } else {
       const casesPlugin = pluginData.getPlugin(CasesPlugin);
-      const lines = await asyncMap(recentCases, c => casesPlugin.getCaseSummary(c, true));
+      const lines = await asyncMap(recentCases, c => casesPlugin.getCaseSummary(c, true, msg.author.id));
       const prefix = getGuildPrefix(pluginData);
       const embed: EmbedOptions = {
         author: {
diff --git a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts
index 61e51b32..366ae28e 100644
--- a/backend/src/plugins/ModActions/commands/CasesUserCmd.ts
+++ b/backend/src/plugins/ModActions/commands/CasesUserCmd.ts
@@ -67,7 +67,7 @@ export const CasesUserCmd = modActionsCommand({
       } else {
         // Compact view (= regular message with a preview of each case)
         const casesPlugin = pluginData.getPlugin(CasesPlugin);
-        const lines = await asyncMap(casesToDisplay, c => casesPlugin.getCaseSummary(c, true));
+        const lines = await asyncMap(casesToDisplay, c => casesPlugin.getCaseSummary(c, true, msg.author.id));
 
         const prefix = getGuildPrefix(pluginData);
         const linesPerChunk = 15;
diff --git a/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts b/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts
index 3c96577e..ce02c85a 100644
--- a/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts
+++ b/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts
@@ -8,8 +8,7 @@ import { SECONDS, stripObjectToScalars, trimLines } from "../../../utils";
 import { LogsPlugin } from "../../Logs/LogsPlugin";
 import { LogType } from "../../../data/LogType";
 import moment from "moment-timezone";
-import { inGuildTz } from "../../../utils/timezones";
-import { getDateFormat } from "../../../utils/dateFormats";
+import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
 
 export const DeleteCaseCmd = modActionsCommand({
   trigger: ["delete_case", "deletecase"],
@@ -54,7 +53,9 @@ export const DeleteCaseCmd = modActionsCommand({
     }
 
     const deletedByName = `${message.author.username}#${message.author.discriminator}`;
-    const deletedAt = inGuildTz(pluginData).format(getDateFormat(pluginData, "pretty_datetime"));
+
+    const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
+    const deletedAt = timeAndDate.inGuildTz().format(timeAndDate.getDateFormat("pretty_datetime"));
 
     await pluginData.state.cases.softDelete(
       theCase.id,
diff --git a/backend/src/plugins/Mutes/commands/MutesCmd.ts b/backend/src/plugins/Mutes/commands/MutesCmd.ts
index 0ed7ce68..81f06d8d 100644
--- a/backend/src/plugins/Mutes/commands/MutesCmd.ts
+++ b/backend/src/plugins/Mutes/commands/MutesCmd.ts
@@ -1,11 +1,10 @@
 import { command } from "knub";
 import { IMuteWithDetails, MutesPluginType } from "../types";
 import { commandTypeHelpers as ct } from "../../../commandTypes";
-import { isFullMessage, MINUTES, noop, resolveMember } from "../../../utils";
+import { DBDateFormat, isFullMessage, MINUTES, noop, resolveMember } from "../../../utils";
 import moment from "moment-timezone";
 import { humanizeDurationShort } from "../../../humanizeDurationShort";
 import { getBaseUrl } from "../../../pluginUtils";
-import { DBDateFormat } from "../../../utils/dateFormats";
 
 export const MutesCmd = command<MutesPluginType>()({
   trigger: "mutes",
diff --git a/backend/src/plugins/Post/PostPlugin.ts b/backend/src/plugins/Post/PostPlugin.ts
index 6d30dce5..93d46605 100644
--- a/backend/src/plugins/Post/PostPlugin.ts
+++ b/backend/src/plugins/Post/PostPlugin.ts
@@ -12,6 +12,7 @@ import { ScheduledPostsShowCmd } from "./commands/ScheduledPostsShowCmd";
 import { ScheduledPostsListCmd } from "./commands/ScheduledPostsListCmd";
 import { ScheduledPostsDeleteCmd } from "./commands/SchedluedPostsDeleteCmd";
 import { scheduledPostLoop } from "./util/scheduledPostLoop";
+import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
 
 const defaultOptions: PluginOptions<PostPluginType> = {
   config: {
@@ -33,6 +34,7 @@ export const PostPlugin = zeppelinPlugin<PostPluginType>()("post", {
     prettyName: "Post",
   },
 
+  dependencies: [TimeAndDatePlugin],
   configSchema: ConfigSchema,
   defaultOptions,
 
diff --git a/backend/src/plugins/Post/commands/ScheduledPostsListCmd.ts b/backend/src/plugins/Post/commands/ScheduledPostsListCmd.ts
index 82abf191..28b123bc 100644
--- a/backend/src/plugins/Post/commands/ScheduledPostsListCmd.ts
+++ b/backend/src/plugins/Post/commands/ScheduledPostsListCmd.ts
@@ -1,9 +1,15 @@
 import { postCmd } from "../types";
-import { trimLines, sorter, disableCodeBlocks, deactivateMentions, createChunkedMessage } from "src/utils";
+import {
+  trimLines,
+  sorter,
+  disableCodeBlocks,
+  deactivateMentions,
+  createChunkedMessage,
+  DBDateFormat,
+} from "src/utils";
 import humanizeDuration from "humanize-duration";
 import moment from "moment-timezone";
-import { inGuildTz } from "../../../utils/timezones";
-import { DBDateFormat, getDateFormat } from "../../../utils/dateFormats";
+import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
 
 const SCHEDULED_POST_PREVIEW_TEXT_LENGTH = 50;
 
@@ -31,9 +37,10 @@ export const ScheduledPostsListCmd = postCmd({
         .replace(/\s+/g, " ")
         .slice(0, SCHEDULED_POST_PREVIEW_TEXT_LENGTH);
 
-      const prettyPostAt = inGuildTz(pluginData, moment.utc(p.post_at, DBDateFormat)).format(
-        getDateFormat(pluginData, "pretty_datetime"),
-      );
+      const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
+      const prettyPostAt = timeAndDate
+        .inGuildTz(moment.utc(p.post_at, DBDateFormat))
+        .format(timeAndDate.getDateFormat("pretty_datetime"));
       const parts = [`\`#${i++}\` \`[${prettyPostAt}]\` ${previewText}${isTruncated ? "..." : ""}`];
       if (p.attachments.length) parts.push("*(with attachment)*");
       if (p.content.embed) parts.push("*(embed)*");
diff --git a/backend/src/plugins/Post/util/actualPostCmd.ts b/backend/src/plugins/Post/util/actualPostCmd.ts
index 4090eed1..885e32dd 100644
--- a/backend/src/plugins/Post/util/actualPostCmd.ts
+++ b/backend/src/plugins/Post/util/actualPostCmd.ts
@@ -1,5 +1,5 @@
 import { Message, Channel, TextChannel } from "eris";
-import { StrictMessageContent, errorMessage, stripObjectToScalars, MINUTES } from "src/utils";
+import { StrictMessageContent, errorMessage, stripObjectToScalars, MINUTES, DBDateFormat } from "src/utils";
 import moment from "moment-timezone";
 import { LogType } from "src/data/LogType";
 import humanizeDuration from "humanize-duration";
@@ -8,7 +8,7 @@ import { PluginData } from "knub";
 import { PostPluginType } from "../types";
 import { parseScheduleTime } from "./parseScheduleTime";
 import { postMessage } from "./postMessage";
-import { DBDateFormat, getDateFormat } from "../../../utils/dateFormats";
+import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
 
 const MIN_REPEAT_TIME = 5 * MINUTES;
 const MAX_REPEAT_TIME = Math.pow(2, 32);
@@ -54,7 +54,7 @@ export async function actualPostCmd(
   let postAt;
   if (opts.schedule) {
     // Schedule the post to be posted later
-    postAt = parseScheduleTime(pluginData, opts.schedule);
+    postAt = await parseScheduleTime(pluginData, msg.author.id, opts.schedule);
     if (!postAt) {
       return sendErrorMessage(pluginData, msg.channel, "Invalid schedule time");
     }
@@ -68,7 +68,7 @@ export async function actualPostCmd(
   let repeatDetailsStr: string = null;
 
   if (opts["repeat-until"]) {
-    repeatUntil = parseScheduleTime(pluginData, opts["repeat-until"]);
+    repeatUntil = await parseScheduleTime(pluginData, msg.author.id, opts["repeat-until"]);
 
     // Invalid time
     if (!repeatUntil) {
@@ -109,6 +109,8 @@ export async function actualPostCmd(
       : `every ${humanizeDuration(opts.repeat)}, ${repeatTimes} times in total`;
   }
 
+  const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
+
   // Save schedule/repeat information in DB
   if (postAt) {
     if (postAt < moment.utc()) {
@@ -140,9 +142,9 @@ export async function actualPostCmd(
       pluginData.state.logs.log(LogType.SCHEDULED_REPEATED_MESSAGE, {
         author: stripObjectToScalars(msg.author),
         channel: stripObjectToScalars(targetChannel),
-        datetime: postAt.format(getDateFormat(pluginData, "pretty_datetime")),
-        date: postAt.format(getDateFormat(pluginData, "date")),
-        time: postAt.format(getDateFormat(pluginData, "time")),
+        datetime: postAt.format(timeAndDate.getDateFormat("pretty_datetime")),
+        date: postAt.format(timeAndDate.getDateFormat("date")),
+        time: postAt.format(timeAndDate.getDateFormat("time")),
         repeatInterval: humanizeDuration(opts.repeat),
         repeatDetails: repeatDetailsStr,
       });
@@ -150,9 +152,9 @@ export async function actualPostCmd(
       pluginData.state.logs.log(LogType.SCHEDULED_MESSAGE, {
         author: stripObjectToScalars(msg.author),
         channel: stripObjectToScalars(targetChannel),
-        datetime: postAt.format(getDateFormat(pluginData, "pretty_datetime")),
-        date: postAt.format(getDateFormat(pluginData, "date")),
-        time: postAt.format(getDateFormat(pluginData, "time")),
+        datetime: postAt.format(timeAndDate.getDateFormat("pretty_datetime")),
+        date: postAt.format(timeAndDate.getDateFormat("date")),
+        time: postAt.format(timeAndDate.getDateFormat("time")),
       });
     }
   }
@@ -166,9 +168,9 @@ export async function actualPostCmd(
     pluginData.state.logs.log(LogType.REPEATED_MESSAGE, {
       author: stripObjectToScalars(msg.author),
       channel: stripObjectToScalars(targetChannel),
-      datetime: postAt.format(getDateFormat(pluginData, "pretty_datetime")),
-      date: postAt.format(getDateFormat(pluginData, "date")),
-      time: postAt.format(getDateFormat(pluginData, "time")),
+      datetime: postAt.format(timeAndDate.getDateFormat("pretty_datetime")),
+      date: postAt.format(timeAndDate.getDateFormat("date")),
+      time: postAt.format(timeAndDate.getDateFormat("time")),
       repeatInterval: humanizeDuration(opts.repeat),
       repeatDetails: repeatDetailsStr,
     });
@@ -177,7 +179,7 @@ export async function actualPostCmd(
   // Bot reply schenanigans
   let successMessage = opts.schedule
     ? `Message scheduled to be posted in <#${targetChannel.id}> on ${postAt.format(
-        getDateFormat(pluginData, "pretty_datetime"),
+        timeAndDate.getDateFormat("pretty_datetime"),
       )}`
     : `Message posted in <#${targetChannel.id}>`;
 
@@ -185,7 +187,7 @@ export async function actualPostCmd(
     successMessage += `. Message will be automatically reposted every ${humanizeDuration(opts.repeat)}`;
 
     if (repeatUntil) {
-      successMessage += ` until ${repeatUntil.format(getDateFormat(pluginData, "pretty_datetime"))}`;
+      successMessage += ` until ${repeatUntil.format(timeAndDate.getDateFormat("pretty_datetime"))}`;
     } else if (repeatTimes) {
       successMessage += `, ${repeatTimes} times in total`;
     }
diff --git a/backend/src/plugins/Post/util/parseScheduleTime.ts b/backend/src/plugins/Post/util/parseScheduleTime.ts
index 3729df2e..6db95ed0 100644
--- a/backend/src/plugins/Post/util/parseScheduleTime.ts
+++ b/backend/src/plugins/Post/util/parseScheduleTime.ts
@@ -1,11 +1,11 @@
 import moment, { Moment } from "moment-timezone";
 import { convertDelayStringToMS } from "src/utils";
 import { PluginData } from "knub";
-import { getGuildTz } from "../../../utils/timezones";
+import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
 
 // TODO: Extract out of the Post plugin, use everywhere with a date input
-export function parseScheduleTime(pluginData: PluginData<any>, str: string): Moment {
-  const tz = getGuildTz(pluginData);
+export async function parseScheduleTime(pluginData: PluginData<any>, memberId: string, str: string): Promise<Moment> {
+  const tz = await pluginData.getPlugin(TimeAndDatePlugin).getMemberTz(memberId);
 
   const dt1 = moment.tz(str, "YYYY-MM-DD HH:mm:ss", tz);
   if (dt1 && dt1.isValid()) return dt1;
diff --git a/backend/src/plugins/Post/util/scheduledPostLoop.ts b/backend/src/plugins/Post/util/scheduledPostLoop.ts
index 104c61a6..58e0d545 100644
--- a/backend/src/plugins/Post/util/scheduledPostLoop.ts
+++ b/backend/src/plugins/Post/util/scheduledPostLoop.ts
@@ -1,12 +1,11 @@
 import { PluginData } from "knub";
 import { PostPluginType } from "../types";
 import { logger } from "src/logger";
-import { stripObjectToScalars, SECONDS } from "src/utils";
+import { stripObjectToScalars, SECONDS, DBDateFormat } from "src/utils";
 import { LogType } from "src/data/LogType";
 import moment from "moment-timezone";
 import { TextChannel, User } from "eris";
 import { postMessage } from "./postMessage";
-import { DBDateFormat } from "../../../utils/dateFormats";
 
 const SCHEDULED_POST_CHECK_INTERVAL = 5 * SECONDS;
 
diff --git a/backend/src/plugins/Reminders/RemindersPlugin.ts b/backend/src/plugins/Reminders/RemindersPlugin.ts
index c0841f85..80100578 100644
--- a/backend/src/plugins/Reminders/RemindersPlugin.ts
+++ b/backend/src/plugins/Reminders/RemindersPlugin.ts
@@ -6,6 +6,7 @@ import { postDueRemindersLoop } from "./utils/postDueRemindersLoop";
 import { RemindCmd } from "./commands/RemindCmd";
 import { RemindersCmd } from "./commands/RemindersCmd";
 import { RemindersDeleteCmd } from "./commands/RemindersDeleteCmd";
+import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
 
 const defaultOptions: PluginOptions<RemindersPluginType> = {
   config: {
@@ -27,6 +28,7 @@ export const RemindersPlugin = zeppelinPlugin<RemindersPluginType>()("reminders"
     prettyName: "Reminders",
   },
 
+  dependencies: [TimeAndDatePlugin],
   configSchema: ConfigSchema,
   defaultOptions,
 
diff --git a/backend/src/plugins/Reminders/commands/RemindCmd.ts b/backend/src/plugins/Reminders/commands/RemindCmd.ts
index e58a9987..326b9ac3 100644
--- a/backend/src/plugins/Reminders/commands/RemindCmd.ts
+++ b/backend/src/plugins/Reminders/commands/RemindCmd.ts
@@ -4,8 +4,7 @@ import { convertDelayStringToMS, messageLink } from "src/utils";
 import humanizeDuration from "humanize-duration";
 import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils";
 import { remindersCommand } from "../types";
-import { getGuildTz, inGuildTz } from "../../../utils/timezones";
-import { getDateFormat } from "../../../utils/dateFormats";
+import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
 
 export const RemindCmd = remindersCommand({
   trigger: ["remind", "remindme"],
@@ -18,8 +17,10 @@ export const RemindCmd = remindersCommand({
   },
 
   async run({ message: msg, args, pluginData }) {
+    const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
+
     const now = moment.utc();
-    const tz = getGuildTz(pluginData);
+    const tz = await timeAndDate.getMemberTz(msg.author.id);
 
     let reminderTime: moment.Moment;
     if (args.time.match(/^\d{4}-\d{1,2}-\d{1,2}$/)) {
@@ -62,7 +63,10 @@ export const RemindCmd = remindersCommand({
 
     const msUntilReminder = reminderTime.diff(now);
     const timeUntilReminder = humanizeDuration(msUntilReminder, { largest: 2, round: true });
-    const prettyReminderTime = inGuildTz(pluginData, reminderTime).format(getDateFormat(pluginData, "pretty_datetime"));
+    const prettyReminderTime = (await timeAndDate.inMemberTz(msg.author.id, reminderTime)).format(
+      pluginData.getPlugin(TimeAndDatePlugin).getDateFormat("pretty_datetime"),
+    );
+
     sendSuccessMessage(
       pluginData,
       msg.channel,
diff --git a/backend/src/plugins/Reminders/commands/RemindersCmd.ts b/backend/src/plugins/Reminders/commands/RemindersCmd.ts
index 54c36f1d..1c7b3470 100644
--- a/backend/src/plugins/Reminders/commands/RemindersCmd.ts
+++ b/backend/src/plugins/Reminders/commands/RemindersCmd.ts
@@ -1,10 +1,9 @@
 import { remindersCommand } from "../types";
 import { sendErrorMessage } from "src/pluginUtils";
-import { createChunkedMessage, sorter } from "src/utils";
+import { createChunkedMessage, DBDateFormat, sorter } from "src/utils";
 import moment from "moment-timezone";
 import humanizeDuration from "humanize-duration";
-import { inGuildTz } from "../../../utils/timezones";
-import { DBDateFormat, getDateFormat } from "../../../utils/dateFormats";
+import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
 
 export const RemindersCmd = remindersCommand({
   trigger: "reminders",
@@ -17,6 +16,8 @@ export const RemindersCmd = remindersCommand({
       return;
     }
 
+    const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
+
     reminders.sort(sorter("remind_at"));
     const longestNum = (reminders.length + 1).toString().length;
     const lines = Array.from(reminders.entries()).map(([i, reminder]) => {
@@ -25,9 +26,9 @@ export const RemindersCmd = remindersCommand({
       const target = moment.utc(reminder.remind_at, "YYYY-MM-DD HH:mm:ss");
       const diff = target.diff(moment.utc());
       const result = humanizeDuration(diff, { largest: 2, round: true });
-      const prettyRemindAt = inGuildTz(pluginData, moment.utc(reminder.remind_at, DBDateFormat)).format(
-        getDateFormat(pluginData, "pretty_datetime"),
-      );
+      const prettyRemindAt = timeAndDate
+        .inGuildTz(moment.utc(reminder.remind_at, DBDateFormat))
+        .format(timeAndDate.getDateFormat("pretty_datetime"));
       return `\`${paddedNum}.\` \`${prettyRemindAt} (${result})\` ${reminder.body}`;
     });
 
diff --git a/backend/src/plugins/Spam/util/logAndDetectMessageSpam.ts b/backend/src/plugins/Spam/util/logAndDetectMessageSpam.ts
index bff73c2d..801b8510 100644
--- a/backend/src/plugins/Spam/util/logAndDetectMessageSpam.ts
+++ b/backend/src/plugins/Spam/util/logAndDetectMessageSpam.ts
@@ -2,7 +2,7 @@ import { SavedMessage } from "src/data/entities/SavedMessage";
 import { RecentActionType, TBaseSingleSpamConfig, SpamPluginType } from "../types";
 import moment from "moment-timezone";
 import { MuteResult } from "src/plugins/Mutes/types";
-import { convertDelayStringToMS, trimLines, stripObjectToScalars, resolveMember, noop } from "src/utils";
+import { convertDelayStringToMS, trimLines, stripObjectToScalars, resolveMember, noop, DBDateFormat } from "src/utils";
 import { LogType } from "src/data/LogType";
 import { CaseTypes } from "src/data/CaseTypes";
 import { logger } from "src/logger";
@@ -14,7 +14,6 @@ import { getRecentActionCount } from "./getRecentActionCount";
 import { getRecentActions } from "./getRecentActions";
 import { clearRecentUserActions } from "./clearRecentUserActions";
 import { saveSpamArchives } from "./saveSpamArchives";
-import { DBDateFormat } from "../../../utils/dateFormats";
 
 export async function logAndDetectMessageSpam(
   pluginData: PluginData<SpamPluginType>,
diff --git a/backend/src/plugins/Tags/TagsPlugin.ts b/backend/src/plugins/Tags/TagsPlugin.ts
index 65499b90..8497a23b 100644
--- a/backend/src/plugins/Tags/TagsPlugin.ts
+++ b/backend/src/plugins/Tags/TagsPlugin.ts
@@ -15,7 +15,7 @@ import { TagSourceCmd } from "./commands/TagSourceCmd";
 import moment from "moment-timezone";
 import humanizeDuration from "humanize-duration";
 import { convertDelayStringToMS } from "../../utils";
-import { getGuildTz, inGuildTz } from "../../utils/timezones";
+import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
 
 const defaultOptions: PluginOptions<TagsPluginType> = {
   config: {
@@ -77,7 +77,9 @@ export const TagsPlugin = zeppelinPlugin<TagsPluginType>()("tags", {
     state.onMessageDeleteFn = msg => onMessageDelete(pluginData, msg);
     state.savedMessages.events.on("delete", state.onMessageDeleteFn);
 
-    const tz = getGuildTz(pluginData);
+    const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
+
+    const tz = timeAndDate.getGuildTz();
     state.tagFunctions = {
       parseDateTime(str) {
         if (typeof str === "number") {
@@ -154,13 +156,13 @@ export const TagsPlugin = zeppelinPlugin<TagsPluginType>()("tags", {
 
       formatTime(time, format) {
         const parsed = this.parseDateTime(time);
-        return inGuildTz(parsed).format(format);
+        return timeAndDate.inGuildTz(parsed).format(format);
       },
 
       discordDateFormat(time) {
         const parsed = time ? this.parseDateTime(time) : Date.now();
 
-        return inGuildTz(parsed).format("YYYY-MM-DD");
+        return timeAndDate.inGuildTz(parsed).format("YYYY-MM-DD");
       },
 
       mention: input => {
diff --git a/backend/src/plugins/TimeAndDate/TimeAndDatePlugin.ts b/backend/src/plugins/TimeAndDate/TimeAndDatePlugin.ts
new file mode 100644
index 00000000..d9e2e05f
--- /dev/null
+++ b/backend/src/plugins/TimeAndDate/TimeAndDatePlugin.ts
@@ -0,0 +1,50 @@
+import { zeppelinPlugin } from "../ZeppelinPluginBlueprint";
+import { ConfigSchema, TimeAndDatePluginType } from "./types";
+import { GuildMemberTimezones } from "../../data/GuildMemberTimezones";
+import { PluginOptions } from "knub";
+import { SetTimezoneCmd } from "./commands/SetTimezoneCmd";
+import { ViewTimezoneCmd } from "./commands/ViewTimezoneCmd";
+import { defaultDateFormats } from "./defaultDateFormats";
+import { Tail } from "../../utils/typeUtils";
+import { inGuildTz } from "./functions/inGuildTz";
+import { mapToPublicFn } from "../../pluginUtils";
+import { getGuildTz } from "./functions/getGuildTz";
+import { getMemberTz } from "./functions/getMemberTz";
+import { getDateFormat } from "./functions/getDateFormat";
+import { inMemberTz } from "./functions/inMemberTz";
+
+const defaultOptions: PluginOptions<TimeAndDatePluginType> = {
+  config: {
+    timezone: "Etc/UTC",
+    can_set_timezone: false,
+    date_formats: defaultDateFormats,
+  },
+
+  overrides: [
+    {
+      level: ">=50",
+      config: {
+        can_set_timezone: true,
+      },
+    },
+  ],
+};
+
+export const TimeAndDatePlugin = zeppelinPlugin<TimeAndDatePluginType>()("time_and_date", {
+  configSchema: ConfigSchema,
+  defaultOptions,
+
+  commands: [SetTimezoneCmd, ViewTimezoneCmd],
+
+  public: {
+    getGuildTz: mapToPublicFn(getGuildTz),
+    inGuildTz: mapToPublicFn(inGuildTz),
+    getMemberTz: mapToPublicFn(getMemberTz),
+    inMemberTz: mapToPublicFn(inMemberTz),
+    getDateFormat: mapToPublicFn(getDateFormat),
+  },
+
+  onLoad(pluginData) {
+    pluginData.state.memberTimezones = GuildMemberTimezones.getGuildInstance(pluginData.guild.id);
+  },
+});
diff --git a/backend/src/plugins/TimeAndDate/commands/SetTimezoneCmd.ts b/backend/src/plugins/TimeAndDate/commands/SetTimezoneCmd.ts
new file mode 100644
index 00000000..e802f6c5
--- /dev/null
+++ b/backend/src/plugins/TimeAndDate/commands/SetTimezoneCmd.ts
@@ -0,0 +1,17 @@
+import { timeAndDateCmd } from "../types";
+import { commandTypeHelpers as ct } from "../../../commandTypes";
+import { sendSuccessMessage } from "../../../pluginUtils";
+
+export const SetTimezoneCmd = timeAndDateCmd({
+  trigger: "timezone",
+  permission: "can_set_timezone",
+
+  signature: {
+    timezone: ct.timezone(),
+  },
+
+  async run({ pluginData, message, args }) {
+    await pluginData.state.memberTimezones.set(message.author.id, args.timezone);
+    sendSuccessMessage(pluginData, message.channel, `Your timezone is now set to **${args.timezone}**`);
+  },
+});
diff --git a/backend/src/plugins/TimeAndDate/commands/ViewTimezoneCmd.ts b/backend/src/plugins/TimeAndDate/commands/ViewTimezoneCmd.ts
new file mode 100644
index 00000000..292f46de
--- /dev/null
+++ b/backend/src/plugins/TimeAndDate/commands/ViewTimezoneCmd.ts
@@ -0,0 +1,23 @@
+import { timeAndDateCmd } from "../types";
+import { commandTypeHelpers as ct } from "../../../commandTypes";
+import { sendSuccessMessage } from "../../../pluginUtils";
+import { getMemberTz } from "../functions/getMemberTz";
+import { getGuildTz } from "../functions/getGuildTz";
+
+export const ViewTimezoneCmd = timeAndDateCmd({
+  trigger: "timezone",
+  permission: "can_set_timezone",
+
+  signature: {},
+
+  async run({ pluginData, message, args }) {
+    const memberTimezone = await pluginData.state.memberTimezones.get(message.author.id);
+    if (memberTimezone) {
+      message.channel.createMessage(`Your timezone is currently set to **${memberTimezone.timezone}**`);
+      return;
+    }
+
+    const serverTimezone = getGuildTz(pluginData);
+    message.channel.createMessage(`Your timezone is currently set to **${serverTimezone}** (server default)`);
+  },
+});
diff --git a/backend/src/plugins/TimeAndDate/defaultDateFormats.ts b/backend/src/plugins/TimeAndDate/defaultDateFormats.ts
new file mode 100644
index 00000000..bb361c3d
--- /dev/null
+++ b/backend/src/plugins/TimeAndDate/defaultDateFormats.ts
@@ -0,0 +1,5 @@
+export const defaultDateFormats = {
+  date: "MMM D, YYYY",
+  time: "H:mm",
+  pretty_datetime: "MMM D, YYYY [at] H:mm z",
+};
diff --git a/backend/src/plugins/TimeAndDate/functions/getDateFormat.ts b/backend/src/plugins/TimeAndDate/functions/getDateFormat.ts
new file mode 100644
index 00000000..d8e9ab65
--- /dev/null
+++ b/backend/src/plugins/TimeAndDate/functions/getDateFormat.ts
@@ -0,0 +1,6 @@
+import { PluginData } from "knub";
+import { defaultDateFormats } from "../defaultDateFormats";
+
+export function getDateFormat(pluginData: PluginData<any>, formatName: keyof typeof defaultDateFormats) {
+  return pluginData.config.get().date_formats?.[formatName] || defaultDateFormats[formatName];
+}
diff --git a/backend/src/plugins/TimeAndDate/functions/getGuildTz.ts b/backend/src/plugins/TimeAndDate/functions/getGuildTz.ts
new file mode 100644
index 00000000..d619a6fb
--- /dev/null
+++ b/backend/src/plugins/TimeAndDate/functions/getGuildTz.ts
@@ -0,0 +1,7 @@
+import { PluginData } from "knub";
+import { ZeppelinGuildConfig } from "../../../types";
+import { TimeAndDatePluginType } from "../types";
+
+export function getGuildTz(pluginData: PluginData<TimeAndDatePluginType>) {
+  return pluginData.config.get().timezone;
+}
diff --git a/backend/src/plugins/TimeAndDate/functions/getMemberTz.ts b/backend/src/plugins/TimeAndDate/functions/getMemberTz.ts
new file mode 100644
index 00000000..6f8b1e70
--- /dev/null
+++ b/backend/src/plugins/TimeAndDate/functions/getMemberTz.ts
@@ -0,0 +1,8 @@
+import { PluginData } from "knub";
+import { TimeAndDatePluginType } from "../types";
+import { getGuildTz } from "./getGuildTz";
+
+export async function getMemberTz(pluginData: PluginData<TimeAndDatePluginType>, memberId: string) {
+  const memberTz = await pluginData.state.memberTimezones.get(memberId);
+  return memberTz?.timezone || getGuildTz(pluginData);
+}
diff --git a/backend/src/utils/timezones.ts b/backend/src/plugins/TimeAndDate/functions/inGuildTz.ts
similarity index 52%
rename from backend/src/utils/timezones.ts
rename to backend/src/plugins/TimeAndDate/functions/inGuildTz.ts
index 0fe7c372..51485d05 100644
--- a/backend/src/utils/timezones.ts
+++ b/backend/src/plugins/TimeAndDate/functions/inGuildTz.ts
@@ -1,13 +1,9 @@
-import moment from "moment-timezone";
 import { PluginData } from "knub";
-import { ZeppelinGuildConfig } from "../types";
+import { TimeAndDatePluginType } from "../types";
+import moment from "moment-timezone";
+import { getGuildTz } from "./getGuildTz";
 
-export function getGuildTz(pluginData: PluginData<any>) {
-  const guildConfig = pluginData.guildConfig as ZeppelinGuildConfig;
-  return guildConfig.timezone || "Etc/UTC";
-}
-
-export function inGuildTz(pluginData: PluginData<any>, input?: moment.Moment | number) {
+export function inGuildTz(pluginData: PluginData<TimeAndDatePluginType>, input?: moment.Moment | number) {
   let momentObj: moment.Moment;
   if (typeof input === "number") {
     momentObj = moment.utc(input, "x");
diff --git a/backend/src/plugins/TimeAndDate/functions/inMemberTz.ts b/backend/src/plugins/TimeAndDate/functions/inMemberTz.ts
new file mode 100644
index 00000000..ad082e80
--- /dev/null
+++ b/backend/src/plugins/TimeAndDate/functions/inMemberTz.ts
@@ -0,0 +1,22 @@
+import { PluginData } from "knub";
+import { TimeAndDatePluginType } from "../types";
+import moment from "moment-timezone";
+import { getGuildTz } from "./getGuildTz";
+import { getMemberTz } from "./getMemberTz";
+
+export async function inMemberTz(
+  pluginData: PluginData<TimeAndDatePluginType>,
+  memberId: string,
+  input?: moment.Moment | number,
+) {
+  let momentObj: moment.Moment;
+  if (typeof input === "number") {
+    momentObj = moment.utc(input, "x");
+  } else if (moment.isMoment(input)) {
+    momentObj = input.clone();
+  } else {
+    momentObj = moment.utc();
+  }
+
+  return momentObj.tz(await getMemberTz(pluginData, memberId));
+}
diff --git a/backend/src/plugins/TimeAndDate/types.ts b/backend/src/plugins/TimeAndDate/types.ts
new file mode 100644
index 00000000..f38ad34a
--- /dev/null
+++ b/backend/src/plugins/TimeAndDate/types.ts
@@ -0,0 +1,22 @@
+import * as t from "io-ts";
+import { tNullable, tPartialDictionary } from "../../utils";
+import { BasePluginType, command } from "knub";
+import { GuildMemberTimezones } from "../../data/GuildMemberTimezones";
+import { tValidTimezone } from "../../utils/tValidTimezone";
+import { defaultDateFormats } from "./defaultDateFormats";
+
+export const ConfigSchema = t.type({
+  timezone: tValidTimezone,
+  date_formats: tNullable(tPartialDictionary(t.keyof(defaultDateFormats), t.string)),
+  can_set_timezone: t.boolean,
+});
+export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
+
+export interface TimeAndDatePluginType extends BasePluginType {
+  config: TConfigSchema;
+  state: {
+    memberTimezones: GuildMemberTimezones;
+  };
+}
+
+export const timeAndDateCmd = command<TimeAndDatePluginType>();
diff --git a/backend/src/plugins/Utility/UtilityPlugin.ts b/backend/src/plugins/Utility/UtilityPlugin.ts
index b54e7c23..301137c7 100644
--- a/backend/src/plugins/Utility/UtilityPlugin.ts
+++ b/backend/src/plugins/Utility/UtilityPlugin.ts
@@ -33,6 +33,7 @@ import { MessageInfoCmd } from "./commands/MessageInfoCmd";
 import { InfoCmd } from "./commands/InfoCmd";
 import { SnowflakeInfoCmd } from "./commands/SnowflakeInfoCmd";
 import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners";
+import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
 
 const defaultOptions: PluginOptions<UtilityPluginType> = {
   config: {
@@ -101,6 +102,7 @@ export const UtilityPlugin = zeppelinPlugin<UtilityPluginType>()("utility", {
     prettyName: "Utility",
   },
 
+  dependencies: [TimeAndDatePlugin],
   configSchema: ConfigSchema,
   defaultOptions,
 
diff --git a/backend/src/plugins/Utility/commands/AboutCmd.ts b/backend/src/plugins/Utility/commands/AboutCmd.ts
index 7f6fd354..95bcd372 100644
--- a/backend/src/plugins/Utility/commands/AboutCmd.ts
+++ b/backend/src/plugins/Utility/commands/AboutCmd.ts
@@ -6,9 +6,8 @@ import humanizeDuration from "humanize-duration";
 import LCL from "last-commit-log";
 import path from "path";
 import moment from "moment-timezone";
-import { getGuildTz, inGuildTz } from "../../../utils/timezones";
 import { rootDir } from "../../../paths";
-import { getDateFormat } from "../../../utils/dateFormats";
+import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
 
 export const AboutCmd = utilityCmd({
   trigger: "about",
@@ -16,6 +15,8 @@ export const AboutCmd = utilityCmd({
   permission: "can_about",
 
   async run({ message: msg, pluginData }) {
+    const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
+
     const uptime = getCurrentUptime();
     const prettyUptime = humanizeDuration(uptime, { largest: 2, round: true });
 
@@ -30,9 +31,9 @@ export const AboutCmd = utilityCmd({
     let version;
 
     if (lastCommit) {
-      lastUpdate = inGuildTz(pluginData, moment.utc(lastCommit.committer.date, "X")).format(
-        getDateFormat(pluginData, "pretty_datetime"),
-      );
+      lastUpdate = timeAndDate
+        .inGuildTz(moment.utc(lastCommit.committer.date, "X"))
+        .format(pluginData.getPlugin(TimeAndDatePlugin).getDateFormat("pretty_datetime"));
       version = lastCommit.shortHash;
     } else {
       lastUpdate = "?";
@@ -52,7 +53,7 @@ export const AboutCmd = utilityCmd({
       ["Last update", lastUpdate],
       ["Version", version],
       ["API latency", `${shard.latency}ms`],
-      ["Server timezone", getGuildTz(pluginData)],
+      ["Server timezone", timeAndDate.getGuildTz()],
     ];
 
     const loadedPlugins = Array.from(
diff --git a/backend/src/plugins/Utility/commands/InfoCmd.ts b/backend/src/plugins/Utility/commands/InfoCmd.ts
index bc2b1e77..2fd2256a 100644
--- a/backend/src/plugins/Utility/commands/InfoCmd.ts
+++ b/backend/src/plugins/Utility/commands/InfoCmd.ts
@@ -32,7 +32,7 @@ export const InfoCmd = utilityCmd({
     const channelId = getChannelId(value);
     const channel = channelId && pluginData.guild.channels.get(channelId);
     if (channel) {
-      const embed = await getChannelInfoEmbed(pluginData, channelId);
+      const embed = await getChannelInfoEmbed(pluginData, channelId, message.author.id);
       if (embed) {
         message.channel.createMessage({ embed });
         return;
@@ -42,7 +42,7 @@ export const InfoCmd = utilityCmd({
     // 2. Server
     const guild = pluginData.client.guilds.get(value);
     if (guild) {
-      const embed = await getServerInfoEmbed(pluginData, value);
+      const embed = await getServerInfoEmbed(pluginData, value, message.author.id);
       if (embed) {
         message.channel.createMessage({ embed });
         return;
@@ -52,7 +52,7 @@ export const InfoCmd = utilityCmd({
     // 3. User
     const user = await resolveUser(pluginData.client, value);
     if (user) {
-      const embed = await getUserInfoEmbed(pluginData, user.id, Boolean(args.compact));
+      const embed = await getUserInfoEmbed(pluginData, user.id, Boolean(args.compact), message.author.id);
       if (embed) {
         message.channel.createMessage({ embed });
         return;
@@ -63,7 +63,12 @@ export const InfoCmd = utilityCmd({
     const messageTarget = await resolveMessageTarget(pluginData, value);
     if (messageTarget) {
       if (canReadChannel(messageTarget.channel, message.member)) {
-        const embed = await getMessageInfoEmbed(pluginData, messageTarget.channel.id, messageTarget.messageId);
+        const embed = await getMessageInfoEmbed(
+          pluginData,
+          messageTarget.channel.id,
+          messageTarget.messageId,
+          message.author.id,
+        );
         if (embed) {
           message.channel.createMessage({ embed });
           return;
@@ -87,7 +92,7 @@ export const InfoCmd = utilityCmd({
     // 6. Server again (fallback for discovery servers)
     const serverPreview = getGuildPreview(pluginData.client, value).catch(() => null);
     if (serverPreview) {
-      const embed = await getServerInfoEmbed(pluginData, value);
+      const embed = await getServerInfoEmbed(pluginData, value, message.author.id);
       if (embed) {
         message.channel.createMessage({ embed });
         return;
@@ -96,7 +101,7 @@ export const InfoCmd = utilityCmd({
 
     // 7. Arbitrary ID
     if (isValidSnowflake(value)) {
-      const embed = getSnowflakeInfoEmbed(pluginData, value, true);
+      const embed = await getSnowflakeInfoEmbed(pluginData, value, true, message.author.id);
       message.channel.createMessage({ embed });
       return;
     }
diff --git a/backend/src/plugins/Utility/commands/MessageInfoCmd.ts b/backend/src/plugins/Utility/commands/MessageInfoCmd.ts
index 582bc731..2d694d24 100644
--- a/backend/src/plugins/Utility/commands/MessageInfoCmd.ts
+++ b/backend/src/plugins/Utility/commands/MessageInfoCmd.ts
@@ -20,7 +20,12 @@ export const MessageInfoCmd = utilityCmd({
       return;
     }
 
-    const embed = await getMessageInfoEmbed(pluginData, args.message.channel.id, args.message.messageId);
+    const embed = await getMessageInfoEmbed(
+      pluginData,
+      args.message.channel.id,
+      args.message.messageId,
+      message.author.id,
+    );
     if (!embed) {
       sendErrorMessage(pluginData, message.channel, "Unknown message");
       return;
diff --git a/backend/src/plugins/Utility/commands/ServerCmd.ts b/backend/src/plugins/Utility/commands/ServerCmd.ts
index adc15062..2792b821 100644
--- a/backend/src/plugins/Utility/commands/ServerCmd.ts
+++ b/backend/src/plugins/Utility/commands/ServerCmd.ts
@@ -15,7 +15,7 @@ export const ServerCmd = utilityCmd({
 
   async run({ message, pluginData, args }) {
     const serverId = args.serverId || pluginData.guild.id;
-    const serverInfoEmbed = await getServerInfoEmbed(pluginData, serverId);
+    const serverInfoEmbed = await getServerInfoEmbed(pluginData, serverId, message.author.id);
     if (!serverInfoEmbed) {
       sendErrorMessage(pluginData, message.channel, "Could not find information for that server");
       return;
diff --git a/backend/src/plugins/Utility/commands/SnowflakeInfoCmd.ts b/backend/src/plugins/Utility/commands/SnowflakeInfoCmd.ts
index b327dfc0..ee03044e 100644
--- a/backend/src/plugins/Utility/commands/SnowflakeInfoCmd.ts
+++ b/backend/src/plugins/Utility/commands/SnowflakeInfoCmd.ts
@@ -14,8 +14,8 @@ export const SnowflakeInfoCmd = utilityCmd({
     id: ct.anyId(),
   },
 
-  run({ message, args, pluginData }) {
-    const embed = getSnowflakeInfoEmbed(pluginData, args.id);
+  async run({ message, args, pluginData }) {
+    const embed = await getSnowflakeInfoEmbed(pluginData, args.id, false, message.author.id);
     message.channel.createMessage({ embed });
   },
 });
diff --git a/backend/src/plugins/Utility/commands/UserInfoCmd.ts b/backend/src/plugins/Utility/commands/UserInfoCmd.ts
index c28f6109..ffe658c1 100644
--- a/backend/src/plugins/Utility/commands/UserInfoCmd.ts
+++ b/backend/src/plugins/Utility/commands/UserInfoCmd.ts
@@ -17,7 +17,7 @@ export const UserInfoCmd = utilityCmd({
 
   async run({ message, args, pluginData }) {
     const userId = args.user?.id || message.author.id;
-    const embed = await getUserInfoEmbed(pluginData, userId, args.compact);
+    const embed = await getUserInfoEmbed(pluginData, userId, args.compact, message.author.id);
     if (!embed) {
       sendErrorMessage(pluginData, message.channel, "User not found");
       return;
diff --git a/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts b/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts
index a6e513f1..b837cca1 100644
--- a/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts
+++ b/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts
@@ -4,8 +4,7 @@ import { Constants, EmbedOptions } from "eris";
 import moment from "moment-timezone";
 import humanizeDuration from "humanize-duration";
 import { formatNumber, preEmbedPadding, trimLines } from "../../../utils";
-import { inGuildTz } from "../../../utils/timezones";
-import { getDateFormat } from "../../../utils/dateFormats";
+import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
 
 const TEXT_CHANNEL_ICON =
   "https://cdn.discordapp.com/attachments/740650744830623756/740656843545772062/text-channel.png";
@@ -17,6 +16,7 @@ const ANNOUNCEMENT_CHANNEL_ICON =
 export async function getChannelInfoEmbed(
   pluginData: PluginData<UtilityPluginType>,
   channelId: string,
+  requestMemberId?: string,
 ): Promise<EmbedOptions | null> {
   const channel = pluginData.guild.channels.get(channelId);
   if (!channel) {
@@ -58,7 +58,11 @@ export async function getChannelInfoEmbed(
   }
 
   const createdAt = moment.utc(channel.createdAt, "x");
-  const prettyCreatedAt = inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime"));
+  const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
+  const tzCreatedAt = requestMemberId
+    ? await timeAndDate.inMemberTz(requestMemberId, createdAt)
+    : timeAndDate.inGuildTz(createdAt);
+  const prettyCreatedAt = tzCreatedAt.format(timeAndDate.getDateFormat("pretty_datetime"));
   const channelAge = humanizeDuration(Date.now() - channel.createdAt, {
     largest: 2,
     round: true,
diff --git a/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts b/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts
index d2d6a7ce..bde02308 100644
--- a/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts
+++ b/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts
@@ -5,8 +5,7 @@ import moment from "moment-timezone";
 import humanizeDuration from "humanize-duration";
 import { chunkMessageLines, messageLink, preEmbedPadding, trimEmptyLines, trimLines } from "../../../utils";
 import { getDefaultPrefix } from "knub/dist/commands/commandUtils";
-import { inGuildTz } from "../../../utils/timezones";
-import { getDateFormat } from "../../../utils/dateFormats";
+import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
 
 const MESSAGE_ICON = "https://cdn.discordapp.com/attachments/740650744830623756/740685652152025088/message.png";
 
@@ -14,12 +13,15 @@ export async function getMessageInfoEmbed(
   pluginData: PluginData<UtilityPluginType>,
   channelId: string,
   messageId: string,
+  requestMemberId?: string,
 ): Promise<EmbedOptions | null> {
   const message = await pluginData.client.getMessage(channelId, messageId).catch(() => null);
   if (!message) {
     return null;
   }
 
+  const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
+
   const embed: EmbedOptions = {
     fields: [],
   };
@@ -30,14 +32,20 @@ export async function getMessageInfoEmbed(
   };
 
   const createdAt = moment.utc(message.createdAt, "x");
-  const prettyCreatedAt = inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime"));
+  const tzCreatedAt = requestMemberId
+    ? await timeAndDate.inMemberTz(requestMemberId, createdAt)
+    : timeAndDate.inGuildTz(createdAt);
+  const prettyCreatedAt = tzCreatedAt.format(timeAndDate.getDateFormat("pretty_datetime"));
   const messageAge = humanizeDuration(Date.now() - message.createdAt, {
     largest: 2,
     round: true,
   });
 
   const editedAt = message.editedTimestamp && moment.utc(message.editedTimestamp, "x");
-  const prettyEditedAt = inGuildTz(pluginData, editedAt).format(getDateFormat(pluginData, "pretty_datetime"));
+  const tzEditedAt = requestMemberId
+    ? await timeAndDate.inMemberTz(requestMemberId, editedAt)
+    : timeAndDate.inGuildTz(editedAt);
+  const prettyEditedAt = tzEditedAt.format(timeAndDate.getDateFormat("pretty_datetime"));
   const editAge =
     message.editedTimestamp &&
     humanizeDuration(Date.now() - message.editedTimestamp, {
@@ -75,18 +83,20 @@ export async function getMessageInfoEmbed(
   });
 
   const authorCreatedAt = moment.utc(message.author.createdAt, "x");
-  const prettyAuthorCreatedAt = inGuildTz(pluginData, authorCreatedAt).format(
-    getDateFormat(pluginData, "pretty_datetime"),
-  );
+  const tzAuthorCreatedAt = requestMemberId
+    ? await timeAndDate.inMemberTz(requestMemberId, authorCreatedAt)
+    : timeAndDate.inGuildTz(authorCreatedAt);
+  const prettyAuthorCreatedAt = tzAuthorCreatedAt.format(timeAndDate.getDateFormat("pretty_datetime"));
   const authorAccountAge = humanizeDuration(Date.now() - message.author.createdAt, {
     largest: 2,
     round: true,
   });
 
   const authorJoinedAt = message.member && moment.utc(message.member.joinedAt, "x");
-  const prettyAuthorJoinedAt = inGuildTz(pluginData, authorJoinedAt).format(
-    getDateFormat(pluginData, "pretty_datetime"),
-  );
+  const tzAuthorJoinedAt = requestMemberId
+    ? await timeAndDate.inMemberTz(requestMemberId, authorJoinedAt)
+    : timeAndDate.inGuildTz(authorJoinedAt);
+  const prettyAuthorJoinedAt = tzAuthorJoinedAt.format(timeAndDate.getDateFormat("pretty_datetime"));
   const authorServerAge =
     message.member &&
     humanizeDuration(Date.now() - message.member.joinedAt, {
diff --git a/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts b/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts
index 6adf8d30..a5117a43 100644
--- a/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts
+++ b/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts
@@ -5,12 +5,12 @@ import { CategoryChannel, EmbedOptions, Guild, RESTChannelInvite, TextChannel, V
 import moment from "moment-timezone";
 import humanizeDuration from "humanize-duration";
 import { getGuildPreview } from "./getGuildPreview";
-import { inGuildTz } from "../../../utils/timezones";
-import { getDateFormat } from "../../../utils/dateFormats";
+import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
 
 export async function getServerInfoEmbed(
   pluginData: PluginData<UtilityPluginType>,
   serverId: string,
+  requestMemberId?: string,
 ): Promise<EmbedOptions> {
   const thisServer = serverId === pluginData.guild.id ? pluginData.guild : null;
   const [restGuild, guildPreview] = await Promise.all([
@@ -39,8 +39,12 @@ export async function getServerInfoEmbed(
   };
 
   // BASIC INFORMATION
+  const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
   const createdAt = moment.utc((guildPreview || restGuild).createdAt, "x");
-  const prettyCreatedAt = inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime"));
+  const tzCreatedAt = requestMemberId
+    ? await timeAndDate.inMemberTz(requestMemberId, createdAt)
+    : timeAndDate.inGuildTz(createdAt);
+  const prettyCreatedAt = tzCreatedAt.format(timeAndDate.getDateFormat("pretty_datetime"));
   const serverAge = humanizeDuration(moment.utc().valueOf() - createdAt.valueOf(), {
     largest: 2,
     round: true,
diff --git a/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts b/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts
index 7ca674a4..d4561348 100644
--- a/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts
+++ b/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts
@@ -6,16 +6,16 @@ import moment from "moment-timezone";
 import { CaseTypes } from "src/data/CaseTypes";
 import humanizeDuration from "humanize-duration";
 import { snowflakeToTimestamp } from "../../../utils/snowflakeToTimestamp";
-import { inGuildTz } from "../../../utils/timezones";
-import { getDateFormat } from "../../../utils/dateFormats";
+import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
 
 const SNOWFLAKE_ICON = "https://cdn.discordapp.com/attachments/740650744830623756/742020790471491668/snowflake.png";
 
-export function getSnowflakeInfoEmbed(
+export async function getSnowflakeInfoEmbed(
   pluginData: PluginData<UtilityPluginType>,
   snowflake: string,
   showUnknownWarning = false,
-): EmbedOptions {
+  requestMemberId?: string,
+): Promise<EmbedOptions> {
   const embed: EmbedOptions = {
     fields: [],
   };
@@ -30,9 +30,13 @@ export function getSnowflakeInfoEmbed(
       "This is a valid [snowflake ID](https://discord.com/developers/docs/reference#snowflakes), but I don't know what it's for.";
   }
 
+  const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
   const createdAtMS = snowflakeToTimestamp(snowflake);
   const createdAt = moment.utc(createdAtMS, "x");
-  const prettyCreatedAt = inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime"));
+  const tzCreatedAt = requestMemberId
+    ? await timeAndDate.inMemberTz(requestMemberId, createdAt)
+    : timeAndDate.inGuildTz(createdAt);
+  const prettyCreatedAt = tzCreatedAt.format(timeAndDate.getDateFormat("pretty_datetime"));
   const snowflakeAge = humanizeDuration(Date.now() - createdAtMS, {
     largest: 2,
     round: true,
diff --git a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts
index 01af5803..6a127206 100644
--- a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts
+++ b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts
@@ -14,13 +14,13 @@ import {
 import moment from "moment-timezone";
 import { CaseTypes } from "src/data/CaseTypes";
 import humanizeDuration from "humanize-duration";
-import { inGuildTz } from "../../../utils/timezones";
-import { getDateFormat } from "../../../utils/dateFormats";
+import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
 
 export async function getUserInfoEmbed(
   pluginData: PluginData<UtilityPluginType>,
   userId: string,
   compact = false,
+  requestMemberId?: string,
 ): Promise<EmbedOptions | null> {
   const user = await resolveUser(pluginData.client, userId);
   if (!user || user instanceof UnknownUser) {
@@ -33,6 +33,8 @@ export async function getUserInfoEmbed(
     fields: [],
   };
 
+  const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
+
   embed.author = {
     name: `User:  ${user.username}#${user.discriminator}`,
   };
@@ -41,7 +43,10 @@ export async function getUserInfoEmbed(
   embed.author.icon_url = avatarURL;
 
   const createdAt = moment.utc(user.createdAt, "x");
-  const prettyCreatedAt = inGuildTz(pluginData, createdAt).format(getDateFormat(pluginData, "pretty_datetime"));
+  const tzCreatedAt = requestMemberId
+    ? await timeAndDate.inMemberTz(requestMemberId, createdAt)
+    : timeAndDate.inGuildTz(createdAt);
+  const prettyCreatedAt = tzCreatedAt.format(timeAndDate.getDateFormat("pretty_datetime"));
   const accountAge = humanizeDuration(moment.utc().valueOf() - user.createdAt, {
     largest: 2,
     round: true,
@@ -57,7 +62,10 @@ export async function getUserInfoEmbed(
     });
     if (member) {
       const joinedAt = moment.utc(member.joinedAt, "x");
-      const prettyJoinedAt = inGuildTz(pluginData, joinedAt).format(getDateFormat(pluginData, "pretty_datetime"));
+      const tzJoinedAt = requestMemberId
+        ? await timeAndDate.inMemberTz(requestMemberId, joinedAt)
+        : timeAndDate.inGuildTz(joinedAt);
+      const prettyJoinedAt = tzJoinedAt.format(timeAndDate.getDateFormat("pretty_datetime"));
       const joinAge = humanizeDuration(moment.utc().valueOf() - member.joinedAt, {
         largest: 2,
         round: true,
@@ -85,7 +93,10 @@ export async function getUserInfoEmbed(
 
   if (member) {
     const joinedAt = moment.utc(member.joinedAt, "x");
-    const prettyJoinedAt = inGuildTz(pluginData, joinedAt).format(getDateFormat(pluginData, "pretty_datetime"));
+    const tzJoinedAt = requestMemberId
+      ? await timeAndDate.inMemberTz(requestMemberId, joinedAt)
+      : timeAndDate.inGuildTz(joinedAt);
+    const prettyJoinedAt = tzJoinedAt.format(timeAndDate.getDateFormat("pretty_datetime"));
     const joinAge = humanizeDuration(moment.utc().valueOf() - member.joinedAt, {
       largest: 2,
       round: true,
diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts
index f7c31251..149d9d2a 100644
--- a/backend/src/plugins/availablePlugins.ts
+++ b/backend/src/plugins/availablePlugins.ts
@@ -31,6 +31,7 @@ import { CompanionChannelsPlugin } from "./CompanionChannels/CompanionChannelsPl
 import { CustomEventsPlugin } from "./CustomEvents/CustomEventsPlugin";
 import { BotControlPlugin } from "./BotControl/BotControlPlugin";
 import { GuildAccessMonitorPlugin } from "./GuildAccessMonitor/GuildAccessMonitorPlugin";
+import { TimeAndDatePlugin } from "./TimeAndDate/TimeAndDatePlugin";
 
 // prettier-ignore
 export const guildPlugins: Array<ZeppelinPluginBlueprint<any>> = [
@@ -63,6 +64,7 @@ export const guildPlugins: Array<ZeppelinPluginBlueprint<any>> = [
   AutomodPlugin,
   CompanionChannelsPlugin,
   CustomEventsPlugin,
+  TimeAndDatePlugin,
 ];
 
 // prettier-ignore
@@ -79,5 +81,6 @@ export const baseGuildPlugins: Array<ZeppelinPluginBlueprint<any>> = [
   NameHistoryPlugin,
   CasesPlugin,
   MutesPlugin,
+  TimeAndDatePlugin,
   // TODO: Replace these with proper dependencies
 ];
diff --git a/backend/src/types.ts b/backend/src/types.ts
index 3d3f64ca..5e0d8b8a 100644
--- a/backend/src/types.ts
+++ b/backend/src/types.ts
@@ -1,19 +1,13 @@
 import { BaseConfig, Knub } from "knub";
 import * as t from "io-ts";
 
-export const DateFormatsSchema = t.type({
-  date: t.string,
-  time: t.string,
-  pretty_datetime: t.string,
-});
-
-export type DateFormats = t.TypeOf<typeof DateFormatsSchema>;
-
 export interface ZeppelinGuildConfig extends BaseConfig<any> {
   success_emoji?: string;
   error_emoji?: string;
+
+  // Deprecated
   timezone?: string;
-  date_formats?: Partial<DateFormats>;
+  date_formats?: any;
 }
 
 export const ZeppelinGuildConfigSchema = t.type({
@@ -25,8 +19,10 @@ export const ZeppelinGuildConfigSchema = t.type({
   // From ZeppelinGuildConfig
   success_emoji: t.string,
   error_emoji: t.string,
+
+  // Deprecated
   timezone: t.string,
-  date_formats: t.partial(DateFormatsSchema.props),
+  date_formats: t.unknown,
 });
 export const PartialZeppelinGuildConfigSchema = t.partial(ZeppelinGuildConfigSchema.props);
 
diff --git a/backend/src/utils.ts b/backend/src/utils.ts
index 9be52c74..fb2875e5 100644
--- a/backend/src/utils.ts
+++ b/backend/src/utils.ts
@@ -1298,3 +1298,5 @@ export function asyncMap<T, R>(arr: T[], fn: (item: T) => Promise<R>): Promise<R
 export function unique<T>(arr: T[]): T[] {
   return Array.from(new Set(arr));
 }
+
+export const DBDateFormat = "YYYY-MM-DD HH:mm:ss";
diff --git a/backend/src/utils/dateFormats.ts b/backend/src/utils/dateFormats.ts
deleted file mode 100644
index b429f2a5..00000000
--- a/backend/src/utils/dateFormats.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { PluginData } from "knub";
-import { DateFormats } from "../types";
-
-const defaultDateFormats: DateFormats = {
-  date: "MMM D, YYYY",
-  time: "H:mm",
-  pretty_datetime: "MMM D, YYYY [at] H:mm z",
-};
-
-/**
- * Returns the guild-specific date format, falling back to the defaults if one has not been specified
- */
-export function getDateFormat(pluginData: PluginData<any>, formatName: keyof DateFormats) {
-  return pluginData.guildConfig.date_formats?.[formatName] || defaultDateFormats[formatName];
-}
-
-export const DBDateFormat = "YYYY-MM-DD HH:mm:ss";
diff --git a/backend/src/utils/isValidTimezone.ts b/backend/src/utils/isValidTimezone.ts
new file mode 100644
index 00000000..9aeb3eea
--- /dev/null
+++ b/backend/src/utils/isValidTimezone.ts
@@ -0,0 +1,7 @@
+import moment from "moment-timezone";
+
+const validTimezones = moment.tz.names();
+
+export function isValidTimezone(input: string) {
+  return validTimezones.includes(input);
+}
diff --git a/backend/src/utils/tValidTimezone.ts b/backend/src/utils/tValidTimezone.ts
new file mode 100644
index 00000000..9922bd96
--- /dev/null
+++ b/backend/src/utils/tValidTimezone.ts
@@ -0,0 +1,13 @@
+import * as t from "io-ts";
+import { either } from "fp-ts/lib/Either";
+import { isValidTimezone } from "./isValidTimezone";
+
+export const tValidTimezone = new t.Type<string, string>(
+  "tValidTimezone",
+  (s): s is string => typeof s === "string",
+  (from, to) =>
+    either.chain(t.string.validate(from, to), input => {
+      return isValidTimezone(input) ? t.success(input) : t.failure(from, to, `Invalid timezone: ${input}`);
+    }),
+  s => s,
+);
diff --git a/backend/src/utils/typeUtils.ts b/backend/src/utils/typeUtils.ts
new file mode 100644
index 00000000..fe327f8e
--- /dev/null
+++ b/backend/src/utils/typeUtils.ts
@@ -0,0 +1,2 @@
+// From https://stackoverflow.com/a/56370310/316944
+export type Tail<T extends any[]> = ((...t: T) => void) extends (h: any, ...r: infer R) => void ? R : never;