Repository: SwitchbackTech/compass
Pull request #418 (2025-05-15 20:30:06)

✨ Feat: Duplicate event
## Description
Closes https://github.com/SwitchbackTech/compass/issues/191

Implement a duplicate event feature where user can right click to duplicate event or press key combination `META + D` to achieve the same.

## Videos


https://github.com/user-attachments/assets/ad39051c-27a5-4218-a9e7-16a23f2ca1ca




Patch:
diff --git a/jest.config.js b/jest.config.js
index 96cc3698..a023b647 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -148,6 +148,7 @@ module.exports = {
           "<rootDir>/packages/backend/src/calendar/$1",
         "^@backend/common/(.*)$": "<rootDir>/packages/backend/src/common/$1",
         "^@backend/dev/(.*)$": "<rootDir>/packages/backend/src/dev/$1",
+        "^@backend/email/(.*)$": "<rootDir>/packages/backend/src/email/$1",
         "^@backend/event/(.*)$": "<rootDir>/packages/backend/src/event/$1",
         "^@backend/priority/(.*)$":
           "<rootDir>/packages/backend/src/priority/$1",
diff --git a/packages/backend/.env.example b/packages/backend/.env.example
index 168bb57d..970fc39f 100644
--- a/packages/backend/.env.example
+++ b/packages/backend/.env.example
@@ -66,13 +66,18 @@ PROD_DOMAIN=app.yourdomain.com
 ####################################################
 # 6. Email (optional)                              #
 ####################################################
-# Get these from your ConvertKit account
-# Does not capture email during signup if any empty EMAILER_ value
+# Get these from your Kit.com account
+# If any of these EMAILER_ values are empty, email
+# integration during signup will be skipped
 
-EMAILER_API_SECRET=UNIQUE_SECRET_FROM_YOUR_CONVERTKIT_ACCOUNT
-EMAILER_LIST_ID=YOUR_LIST_ID # get this from the URL
+EMAILER_API_SECRET=UNIQUE_SECRET_FROM_YOUR_KIT_ACCOUNT
+EMAILER_TAG_ID=YOUR_TAG_ID # get this from Kit
 
 ####################################################
 # 7. Debug (optional)                              #
 ####################################################
+# Set this to trigger socket events to a client
+# during development. This is helpful for testing
+# the sync process from local host. 
+
 SOCKET_USER=USER_ID_FROM_YOUR_MONGO_DB
\ No newline at end of file
diff --git a/packages/backend/package.json b/packages/backend/package.json
index e233fc2f..5c6c2d03 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -36,9 +36,11 @@
     "@types/module-alias": "^2.0.1",
     "@types/morgan": "^1.9.4",
     "@types/node": "^22.13.10",
+    "@types/supertest": "^6.0.3",
     "alias-hq": "^6.1.0",
     "jest-environment-node": "^29.7.0",
     "socket.io-client": "^4.7.5",
+    "supertest": "^7.1.0",
     "tsconfig-paths": "^4.1.2"
   }
 }
diff --git a/packages/backend/src/auth/controllers/auth.controller.ts b/packages/backend/src/auth/controllers/auth.controller.ts
index bd6a5a87..4db8a1b5 100644
--- a/packages/backend/src/auth/controllers/auth.controller.ts
+++ b/packages/backend/src/auth/controllers/auth.controller.ts
@@ -18,11 +18,9 @@ import compassAuthService from "@backend/auth/services/compass.auth.service";
 import GoogleAuthService, {
   getGAuthClientForUser,
 } from "@backend/auth/services/google.auth.service";
-import {
-  GcalError,
-  SyncError,
-} from "@backend/common/constants/error.constants";
 import { error } from "@backend/common/errors/handlers/error.handler";
+import { GcalError } from "@backend/common/errors/integration/gcal/gcal.errors";
+import { SyncError } from "@backend/common/errors/sync/sync.errors";
 import { isInvalidGoogleToken } from "@backend/common/services/gcal/gcal.utils";
 import {
   ReqBody,
diff --git a/packages/backend/src/auth/middleware/auth.middleware.ts b/packages/backend/src/auth/middleware/auth.middleware.ts
index dfe121eb..7a392747 100644
--- a/packages/backend/src/auth/middleware/auth.middleware.ts
+++ b/packages/backend/src/auth/middleware/auth.middleware.ts
@@ -1,11 +1,9 @@
 import { NextFunction, Request, Response } from "express";
 import { Status } from "@core/errors/status.codes";
 import { ENV, IS_DEV } from "@backend/common/constants/env.constants";
-import {
-  AuthError,
-  GcalError,
-} from "@backend/common/constants/error.constants";
+import { AuthError } from "@backend/common/errors/auth/auth.errors";
 import { error } from "@backend/common/errors/handlers/error.handler";
+import { GcalError } from "@backend/common/errors/integration/gcal/gcal.errors";
 import { SReqBody } from "@backend/common/types/express.types";
 import { hasGoogleHeaders } from "@backend/sync/util/sync.util";
 
diff --git a/packages/backend/src/auth/services/auth.utils.ts b/packages/backend/src/auth/services/auth.utils.ts
index b50e6e2c..ace16fab 100644
--- a/packages/backend/src/auth/services/auth.utils.ts
+++ b/packages/backend/src/auth/services/auth.utils.ts
@@ -1,6 +1,6 @@
 import { Credentials } from "google-auth-library";
 import GoogleAuthService from "@backend/auth/services/google.auth.service";
-import { AuthError } from "@backend/common/constants/error.constants";
+import { AuthError } from "@backend/common/errors/auth/auth.errors";
 import { error } from "@backend/common/errors/handlers/error.handler";
 
 export const initGoogleClient = async (
diff --git a/packages/backend/src/auth/services/compass.auth.service.ts b/packages/backend/src/auth/services/compass.auth.service.ts
index a8e183de..bc9d7458 100644
--- a/packages/backend/src/auth/services/compass.auth.service.ts
+++ b/packages/backend/src/auth/services/compass.auth.service.ts
@@ -1,6 +1,6 @@
 import Session from "supertokens-node/recipe/session";
-import { SyncError } from "@backend/common/constants/error.constants";
 import { error } from "@backend/common/errors/handlers/error.handler";
+import { SyncError } from "@backend/common/errors/sync/sync.errors";
 import { getSync } from "@backend/sync/util/sync.queries";
 import { canDoIncrementalSync } from "@backend/sync/util/sync.util";
 import { findCompassUserBy } from "@backend/user/queries/user.queries";
diff --git a/packages/backend/src/auth/services/google.auth.service.ts b/packages/backend/src/auth/services/google.auth.service.ts
index 8bfa48f4..82360ae9 100644
--- a/packages/backend/src/auth/services/google.auth.service.ts
+++ b/packages/backend/src/auth/services/google.auth.service.ts
@@ -8,11 +8,9 @@ import { UserInfo_Google } from "@core/types/auth.types";
 import { gCalendar } from "@core/types/gcal";
 import { Schema_User } from "@core/types/user.types";
 import { ENV } from "@backend/common/constants/env.constants";
-import {
-  AuthError,
-  UserError,
-} from "@backend/common/constants/error.constants";
+import { AuthError } from "@backend/common/errors/auth/auth.errors";
 import { error } from "@backend/common/errors/handlers/error.handler";
+import { UserError } from "@backend/common/errors/user/user.errors";
 import { findCompassUserBy } from "@backend/user/queries/user.queries";
 import compassAuthService from "./compass.auth.service";
 
diff --git a/packages/backend/src/calendar/controllers/calendar.controller.ts b/packages/backend/src/calendar/controllers/calendar.controller.ts
index 0c572503..6ed37c5f 100644
--- a/packages/backend/src/calendar/controllers/calendar.controller.ts
+++ b/packages/backend/src/calendar/controllers/calendar.controller.ts
@@ -2,7 +2,7 @@ import { SessionRequest } from "supertokens-node/framework/express";
 import { Schema_CalendarList } from "@core/types/calendar.types";
 import { getGcalClient } from "@backend/auth/services/google.auth.service";
 import calendarService from "@backend/calendar/services/calendar.service";
-import { AuthError } from "@backend/common/constants/error.constants";
+import { AuthError } from "@backend/common/errors/auth/auth.errors";
 import { error } from "@backend/common/errors/handlers/error.handler";
 import gcalService from "@backend/common/services/gcal/gcal.service";
 import { Res_Promise } from "@backend/common/types/express.types";
diff --git a/packages/backend/src/common/constants/collections.ts b/packages/backend/src/common/constants/collections.ts
index 7357a397..1d6ebd3e 100644
--- a/packages/backend/src/common/constants/collections.ts
+++ b/packages/backend/src/common/constants/collections.ts
@@ -7,4 +7,5 @@ export const Collections = {
   PRIORITY: IS_DEV ? "_dev.priority" : "priority",
   SYNC: IS_DEV ? "_dev.sync" : "sync",
   USER: IS_DEV ? "_dev.user" : "user",
+  WAITLIST: IS_DEV ? "_dev.waitlist" : "waitlist",
 };
diff --git a/packages/backend/src/common/constants/env.constants.ts b/packages/backend/src/common/constants/env.constants.ts
index fcf4052c..7d17ff5f 100644
--- a/packages/backend/src/common/constants/env.constants.ts
+++ b/packages/backend/src/common/constants/env.constants.ts
@@ -20,7 +20,7 @@ const EnvSchema = z
     CLIENT_SECRET: z.string().nonempty(),
     DB: z.string().nonempty(),
     EMAILER_SECRET: z.string().nonempty().optional(),
-    EMAILER_LIST_ID: z.string().nonempty().optional(),
+    EMAILER_TAG_ID: z.string().nonempty().optional(),
     MONGO_URI: z.string().nonempty(),
     NODE_ENV: z.nativeEnum(NodeEnv),
     ORIGINS_ALLOWED: z.array(z.string().nonempty()).default([]),
@@ -41,7 +41,7 @@ export const ENV = {
   CLIENT_SECRET: process.env["CLIENT_SECRET"],
   DB: IS_DEV ? "dev_calendar" : "prod_calendar",
   EMAILER_SECRET: process.env["EMAILER_API_SECRET"],
-  EMAILER_LIST_ID: process.env["EMAILER_LIST_ID"],
+  EMAILER_TAG_ID: process.env["EMAILER_TAG_ID"],
   MONGO_URI: process.env["MONGO_URI"],
   NODE_ENV: _nodeEnv,
   ORIGINS_ALLOWED: process.env["CORS"] ? process.env["CORS"].split(",") : [],
diff --git a/packages/backend/src/common/constants/error.constants.ts b/packages/backend/src/common/constants/error.constants.ts
deleted file mode 100644
index 315813f5..00000000
--- a/packages/backend/src/common/constants/error.constants.ts
+++ /dev/null
@@ -1,225 +0,0 @@
-import { Status } from "@core/errors/status.codes";
-
-export const AuthError = {
-  DevOnly: {
-    description: "Only available during development",
-    status: Status.FORBIDDEN,
-    isOperational: true,
-  },
-  InadequatePermissions: {
-    description: "You don't have permission to do that",
-    status: Status.FORBIDDEN,
-    isOperational: true,
-  },
-  MissingRefreshToken: {
-    description: "No refresh token",
-    status: Status.BAD_REQUEST,
-    isOperational: true,
-  },
-  NoUserId: {
-    description: "Compass user was not created",
-    status: Status.INTERNAL_SERVER,
-    isOperational: false,
-  },
-  NoGAuthAccessToken: {
-    description: "No gauth access token",
-    status: Status.BAD_REQUEST,
-    isOperational: true,
-  },
-};
-
-export const DbError = {
-  InvalidId: {
-    description: "id is invalid (according to Mongo)",
-    status: Status.BAD_REQUEST,
-    isOperational: true,
-  },
-};
-
-export const EmailerError = {
-  IncorrectApiKey: {
-    description:
-      "Incorrect API key. Please make sure environment variables beginning with EMAILER_ are correct",
-    status: Status.BAD_REQUEST,
-    isOperational: true,
-  },
-  AddToListFailed: {
-    description: "Failed to add email to list",
-    status: Status.UNSURE,
-    isOperational: true,
-  },
-};
-
-export const EventError = {
-  Gone: {
-    description: "Resource is gone",
-    status: Status.GONE,
-    isOperational: true,
-  },
-  InvalidRecurrence: {
-    description: "Invalid recurrence",
-    status: Status.BAD_REQUEST,
-    isOperational: true,
-  },
-  MissingCompassEvent: {
-    description: "Compass event not found",
-    status: Status.NOT_FOUND,
-    isOperational: true,
-  },
-  MissingGevents: {
-    description: "# of created events !== # saved in DB",
-    status: Status.INTERNAL_SERVER,
-    isOperational: true,
-  },
-  MissingProperty: {
-    description: "A required property is missing",
-    status: Status.BAD_REQUEST,
-    isOperational: true,
-  },
-  NoGevents: {
-    description: "Unexpected empty values in events",
-    status: Status.NO_CONTENT,
-    isOperational: true,
-  },
-  NoMatchingEvent: {
-    description: "Invalid event id (most likely)",
-    status: Status.REDUX_REFRESH_NEEDED,
-    isOperational: true,
-  },
-};
-
-export const GenericError = {
-  BadRequest: {
-    description: "Request is malformed",
-    status: Status.BAD_REQUEST,
-    isOperational: true,
-  },
-  DeveloperError: {
-    description: "Developer made a logic error",
-    status: Status.INTERNAL_SERVER,
-    isOperational: true,
-  },
-  NotImplemented: {
-    description: "Not implemented yet",
-    status: Status.UNSURE,
-    isOperational: true,
-  },
-  NotSure: {
-    description: "Not sure why error occured. See logs",
-    status: Status.UNSURE,
-    isOperational: true,
-  },
-};
-
-export const GcalError = {
-  CalendarlistMissing: {
-    description: "No calendarlist",
-    status: Status.BAD_REQUEST,
-    isOperational: true,
-  },
-  CodeInvalid: {
-    description: "Invalid gAPI code",
-    status: Status.BAD_REQUEST,
-    isOperational: true,
-  },
-  CodeMissing: {
-    description: "Missing gAPI code",
-    status: Status.FORBIDDEN,
-    isOperational: true,
-  },
-  NoSyncToken: {
-    description: "nextSyncToken is missing",
-    status: Status.INTERNAL_SERVER,
-    isOperational: true,
-  },
-  PaginationNotSupported: {
-    description: "Compass doesn't support pagination yet",
-    status: Status.INTERNAL_SERVER,
-    isOperational: true,
-  },
-  Unauthorized: {
-    description: "Not Authorized",
-    status: Status.FORBIDDEN,
-    isOperational: true,
-  },
-  Unsure: {
-    description: "generic gCal API Error",
-    status: Status.UNSURE,
-    isOperational: true,
-  },
-};
-
-export const SocketError = {
-  InvalidSocketId: {
-    description: "Invalid socket id",
-    status: Status.BAD_REQUEST,
-    isOperational: true,
-  },
-  ServerNotReady: {
-    description:
-      "WebSocket server not ready (Did you forget to initialize it?)",
-    status: Status.INTERNAL_SERVER,
-    isOperational: false,
-  },
-};
-
-export const SyncError = {
-  AccessRevoked: {
-    description: "User revoked access to their 3rd-party calendar (GCal)",
-    status: Status.GONE,
-    isOperational: true,
-  },
-  CalendarWatchExists: {
-    description: "Watch already exists",
-    status: Status.BAD_REQUEST,
-    isOperational: true,
-  },
-  NoGCalendarId: {
-    description: "No gCalendarId",
-    status: Status.NO_CONTENT,
-    isOperational: true,
-  },
-  NoResourceId: {
-    description: "No resourceId provided",
-    status: Status.NO_CONTENT,
-    isOperational: true,
-  },
-  NoSyncToken: {
-    description: "No syncToken",
-    status: Status.INTERNAL_SERVER,
-    isOperational: true,
-  },
-  NoEventChanges: {
-    description: "Nothing changed",
-    status: Status.BAD_REQUEST,
-    isOperational: true,
-  },
-  NoSyncRecordForUser: {
-    description: "No sync record for user",
-    status: Status.BAD_REQUEST,
-    isOperational: true,
-  },
-};
-
-export const UserError = {
-  InvalidValue: {
-    description: "User has an invalid value",
-    status: Status.BAD_REQUEST,
-    isOperational: true,
-  },
-  MissingGoogleUserField: {
-    description: "Email field is missing from the Google user object",
-    status: Status.NOT_FOUND,
-    isOperational: true,
-  },
-  MissingUserIdField: {
-    description: "Failed to access the userId",
-    status: Status.BAD_REQUEST,
-    isOperational: true,
-  },
-  UserNotFound: {
-    description: "User not found",
-    status: Status.NOT_FOUND,
-    isOperational: true,
-  },
-};
diff --git a/packages/backend/src/common/errors/auth/auth.errors.ts b/packages/backend/src/common/errors/auth/auth.errors.ts
new file mode 100644
index 00000000..ea310982
--- /dev/null
+++ b/packages/backend/src/common/errors/auth/auth.errors.ts
@@ -0,0 +1,38 @@
+import { Status } from "@core/errors/status.codes";
+import { ErrorMetadata } from "@backend/common/types/error.types";
+
+interface AuthErrors {
+  DevOnly: ErrorMetadata;
+  InadequatePermissions: ErrorMetadata;
+  MissingRefreshToken: ErrorMetadata;
+  NoUserId: ErrorMetadata;
+  NoGAuthAccessToken: ErrorMetadata;
+}
+
+export const AuthError: AuthErrors = {
+  DevOnly: {
+    description: "Only available during development",
+    status: Status.FORBIDDEN,
+    isOperational: true,
+  },
+  InadequatePermissions: {
+    description: "You don't have permission to do that",
+    status: Status.FORBIDDEN,
+    isOperational: true,
+  },
+  MissingRefreshToken: {
+    description: "No refresh token",
+    status: Status.BAD_REQUEST,
+    isOperational: true,
+  },
+  NoUserId: {
+    description: "Compass user was not created",
+    status: Status.INTERNAL_SERVER,
+    isOperational: false,
+  },
+  NoGAuthAccessToken: {
+    description: "No gauth access token",
+    status: Status.BAD_REQUEST,
+    isOperational: true,
+  },
+};
diff --git a/packages/backend/src/common/errors/db/db.errors.ts b/packages/backend/src/common/errors/db/db.errors.ts
new file mode 100644
index 00000000..16cd271f
--- /dev/null
+++ b/packages/backend/src/common/errors/db/db.errors.ts
@@ -0,0 +1,13 @@
+import { Status } from "@core/errors/status.codes";
+import { ErrorMetadata } from "@backend/common/types/error.types";
+
+interface DbErrors {
+  InvalidId: ErrorMetadata;
+}
+export const DbError: DbErrors = {
+  InvalidId: {
+    description: "id is invalid (according to Mongo)",
+    status: Status.BAD_REQUEST,
+    isOperational: true,
+  },
+};
diff --git a/packages/backend/src/common/errors/emailer/emailer.errors.ts b/packages/backend/src/common/errors/emailer/emailer.errors.ts
new file mode 100644
index 00000000..c7aa95f5
--- /dev/null
+++ b/packages/backend/src/common/errors/emailer/emailer.errors.ts
@@ -0,0 +1,28 @@
+import { Status } from "@core/errors/status.codes";
+import { ErrorMetadata } from "@backend/common/types/error.types";
+
+interface EmailerErrors {
+  InvalidTagId: ErrorMetadata;
+  InvalidSecret: ErrorMetadata;
+  InvalidSubscriberData: ErrorMetadata;
+}
+
+export const EmailerError: EmailerErrors = {
+  InvalidSubscriberData: {
+    description: "Subscriber data is missing or incorrect",
+    status: Status.BAD_REQUEST,
+    isOperational: true,
+  },
+  InvalidSecret: {
+    description:
+      "Invalid emailer API secret. Please make sure environment variables beginning with EMAILER_ are correct",
+    status: Status.INTERNAL_SERVER,
+    isOperational: true,
+  },
+  InvalidTagId: {
+    description:
+      "Invalid emailer tag id. Please make sure environment variables beginning with EMAILER_ are correct",
+    status: Status.INTERNAL_SERVER,
+    isOperational: true,
+  },
+};
diff --git a/packages/backend/src/common/errors/event/event.errors.ts b/packages/backend/src/common/errors/event/event.errors.ts
new file mode 100644
index 00000000..5feadf72
--- /dev/null
+++ b/packages/backend/src/common/errors/event/event.errors.ts
@@ -0,0 +1,50 @@
+import { Status } from "@core/errors/status.codes";
+import { ErrorMetadata } from "@backend/common/types/error.types";
+
+interface EventErrors {
+  Gone: ErrorMetadata;
+  InvalidRecurrence: ErrorMetadata;
+  MissingCompassEvent: ErrorMetadata;
+  MissingGevents: ErrorMetadata;
+  MissingProperty: ErrorMetadata;
+  NoGevents: ErrorMetadata;
+  NoMatchingEvent: ErrorMetadata;
+}
+
+export const EventError: EventErrors = {
+  Gone: {
+    description: "Resource is gone",
+    status: Status.GONE,
+    isOperational: true,
+  },
+  InvalidRecurrence: {
+    description: "Invalid recurrence",
+    status: Status.BAD_REQUEST,
+    isOperational: true,
+  },
+  MissingCompassEvent: {
+    description: "Compass event not found",
+    status: Status.NOT_FOUND,
+    isOperational: true,
+  },
+  MissingGevents: {
+    description: "# of created events !== # saved in DB",
+    status: Status.INTERNAL_SERVER,
+    isOperational: true,
+  },
+  MissingProperty: {
+    description: "A required property is missing",
+    status: Status.BAD_REQUEST,
+    isOperational: true,
+  },
+  NoGevents: {
+    description: "Unexpected empty values in events",
+    status: Status.NO_CONTENT,
+    isOperational: true,
+  },
+  NoMatchingEvent: {
+    description: "Invalid event id (most likely)",
+    status: Status.REDUX_REFRESH_NEEDED,
+    isOperational: true,
+  },
+};
diff --git a/packages/backend/src/common/errors/generic/generic.errors.ts b/packages/backend/src/common/errors/generic/generic.errors.ts
new file mode 100644
index 00000000..c04cbcd5
--- /dev/null
+++ b/packages/backend/src/common/errors/generic/generic.errors.ts
@@ -0,0 +1,32 @@
+import { Status } from "@core/errors/status.codes";
+import { ErrorMetadata } from "@backend/common/types/error.types";
+
+interface GenericErrors {
+  BadRequest: ErrorMetadata;
+  DeveloperError: ErrorMetadata;
+  NotImplemented: ErrorMetadata;
+  NotSure: ErrorMetadata;
+}
+
+export const GenericError: GenericErrors = {
+  BadRequest: {
+    description: "Request is malformed",
+    status: Status.BAD_REQUEST,
+    isOperational: true,
+  },
+  DeveloperError: {
+    description: "Developer made a logic error",
+    status: Status.INTERNAL_SERVER,
+    isOperational: true,
+  },
+  NotImplemented: {
+    description: "Not implemented yet",
+    status: Status.UNSURE,
+    isOperational: true,
+  },
+  NotSure: {
+    description: "Not sure why error occured. See logs",
+    status: Status.UNSURE,
+    isOperational: true,
+  },
+};
diff --git a/packages/backend/src/common/errors/handlers/error.express.handler.ts b/packages/backend/src/common/errors/handlers/error.express.handler.ts
index 668beb6d..00450d9c 100644
--- a/packages/backend/src/common/errors/handlers/error.express.handler.ts
+++ b/packages/backend/src/common/errors/handlers/error.express.handler.ts
@@ -4,7 +4,7 @@ import { Status } from "@core/errors/status.codes";
 import { Logger } from "@core/logger/winston.logger";
 import compassAuthService from "@backend/auth/services/compass.auth.service";
 import { IS_DEV } from "@backend/common/constants/env.constants";
-import { UserError } from "@backend/common/constants/error.constants";
+import { UserError } from "@backend/common/errors/user/user.errors";
 import {
   getEmailFromUrl,
   isFullSyncRequired,
diff --git a/packages/backend/src/common/errors/handlers/error.handler.ts b/packages/backend/src/common/errors/handlers/error.handler.ts
index 95113bbd..7427ff65 100644
--- a/packages/backend/src/common/errors/handlers/error.handler.ts
+++ b/packages/backend/src/common/errors/handlers/error.handler.ts
@@ -5,6 +5,12 @@ import { ErrorMetadata } from "@backend/common/types/error.types";
 
 const logger = Logger("app:error.handler");
 
+/**
+ * Transforms error metadata into a BaseError class
+ * @param cause The cause of the error
+ * @param result The result of the error
+ * @returns
+ */
 export const error = (cause: ErrorMetadata, result: string) => {
   return new BaseError(
     result,
diff --git a/packages/backend/src/common/errors/integration/gcal/gcal.errors.ts b/packages/backend/src/common/errors/integration/gcal/gcal.errors.ts
new file mode 100644
index 00000000..1fd73827
--- /dev/null
+++ b/packages/backend/src/common/errors/integration/gcal/gcal.errors.ts
@@ -0,0 +1,50 @@
+import { Status } from "@core/errors/status.codes";
+import { ErrorMetadata } from "@backend/common/types/error.types";
+
+interface GcalErrors {
+  CalendarlistMissing: ErrorMetadata;
+  CodeInvalid: ErrorMetadata;
+  CodeMissing: ErrorMetadata;
+  NoSyncToken: ErrorMetadata;
+  PaginationNotSupported: ErrorMetadata;
+  Unauthorized: ErrorMetadata;
+  Unsure: ErrorMetadata;
+}
+
+export const GcalError: GcalErrors = {
+  CalendarlistMissing: {
+    description: "No calendarlist",
+    status: Status.BAD_REQUEST,
+    isOperational: true,
+  },
+  CodeInvalid: {
+    description: "Invalid gAPI code",
+    status: Status.BAD_REQUEST,
+    isOperational: true,
+  },
+  CodeMissing: {
+    description: "Missing gAPI code",
+    status: Status.FORBIDDEN,
+    isOperational: true,
+  },
+  NoSyncToken: {
+    description: "nextSyncToken is missing",
+    status: Status.INTERNAL_SERVER,
+    isOperational: true,
+  },
+  PaginationNotSupported: {
+    description: "Compass doesn't support pagination yet",
+    status: Status.INTERNAL_SERVER,
+    isOperational: true,
+  },
+  Unauthorized: {
+    description: "Not Authorized",
+    status: Status.FORBIDDEN,
+    isOperational: true,
+  },
+  Unsure: {
+    description: "generic gCal API Error",
+    status: Status.UNSURE,
+    isOperational: true,
+  },
+};
diff --git a/packages/backend/src/common/errors/socket/socket.errors.ts b/packages/backend/src/common/errors/socket/socket.errors.ts
new file mode 100644
index 00000000..61ab9fb8
--- /dev/null
+++ b/packages/backend/src/common/errors/socket/socket.errors.ts
@@ -0,0 +1,21 @@
+import { Status } from "@core/errors/status.codes";
+import { ErrorMetadata } from "@backend/common/types/error.types";
+
+interface SocketErrors {
+  InvalidSocketId: ErrorMetadata;
+  ServerNotReady: ErrorMetadata;
+}
+
+export const SocketError: SocketErrors = {
+  InvalidSocketId: {
+    description: "Invalid socket id",
+    status: Status.BAD_REQUEST,
+    isOperational: true,
+  },
+  ServerNotReady: {
+    description:
+      "WebSocket server not ready (Did you forget to initialize it?)",
+    status: Status.INTERNAL_SERVER,
+    isOperational: false,
+  },
+};
diff --git a/packages/backend/src/common/errors/sync/sync.errors.ts b/packages/backend/src/common/errors/sync/sync.errors.ts
new file mode 100644
index 00000000..425d059c
--- /dev/null
+++ b/packages/backend/src/common/errors/sync/sync.errors.ts
@@ -0,0 +1,50 @@
+import { Status } from "@core/errors/status.codes";
+import { ErrorMetadata } from "@backend/common/types/error.types";
+
+interface SyncErrors {
+  AccessRevoked: ErrorMetadata;
+  CalendarWatchExists: ErrorMetadata;
+  NoGCalendarId: ErrorMetadata;
+  NoResourceId: ErrorMetadata;
+  NoSyncToken: ErrorMetadata;
+  NoEventChanges: ErrorMetadata;
+  NoSyncRecordForUser: ErrorMetadata;
+}
+
+export const SyncError: SyncErrors = {
+  AccessRevoked: {
+    description: "User revoked access to their 3rd-party calendar (GCal)",
+    status: Status.GONE,
+    isOperational: true,
+  },
+  CalendarWatchExists: {
+    description: "Watch already exists",
+    status: Status.BAD_REQUEST,
+    isOperational: true,
+  },
+  NoGCalendarId: {
+    description: "No gCalendarId",
+    status: Status.NO_CONTENT,
+    isOperational: true,
+  },
+  NoResourceId: {
+    description: "No resourceId provided",
+    status: Status.NO_CONTENT,
+    isOperational: true,
+  },
+  NoSyncToken: {
+    description: "No syncToken",
+    status: Status.INTERNAL_SERVER,
+    isOperational: true,
+  },
+  NoEventChanges: {
+    description: "Nothing changed",
+    status: Status.BAD_REQUEST,
+    isOperational: true,
+  },
+  NoSyncRecordForUser: {
+    description: "No sync record for user",
+    status: Status.BAD_REQUEST,
+    isOperational: true,
+  },
+};
diff --git a/packages/backend/src/common/errors/user/user.errors.ts b/packages/backend/src/common/errors/user/user.errors.ts
new file mode 100644
index 00000000..91ba0470
--- /dev/null
+++ b/packages/backend/src/common/errors/user/user.errors.ts
@@ -0,0 +1,32 @@
+import { Status } from "@core/errors/status.codes";
+import { ErrorMetadata } from "@backend/common/types/error.types";
+
+interface UserErrors {
+  InvalidValue: ErrorMetadata;
+  MissingGoogleUserField: ErrorMetadata;
+  MissingUserIdField: ErrorMetadata;
+  UserNotFound: ErrorMetadata;
+}
+
+export const UserError: UserErrors = {
+  InvalidValue: {
+    description: "User has an invalid value",
+    status: Status.BAD_REQUEST,
+    isOperational: true,
+  },
+  MissingGoogleUserField: {
+    description: "Email field is missing from the Google user object",
+    status: Status.NOT_FOUND,
+    isOperational: true,
+  },
+  MissingUserIdField: {
+    description: "Failed to access the userId",
+    status: Status.BAD_REQUEST,
+    isOperational: true,
+  },
+  UserNotFound: {
+    description: "User not found",
+    status: Status.NOT_FOUND,
+    isOperational: true,
+  },
+};
diff --git a/packages/backend/src/common/middleware/mongo.validation.middleware.ts b/packages/backend/src/common/middleware/mongo.validation.middleware.ts
index 32ad8e09..ddfe5b3e 100644
--- a/packages/backend/src/common/middleware/mongo.validation.middleware.ts
+++ b/packages/backend/src/common/middleware/mongo.validation.middleware.ts
@@ -1,7 +1,7 @@
 import express from "express";
 import { ObjectId } from "mongodb";
 import { SessionRequest } from "supertokens-node/framework/express";
-import { DbError } from "@backend/common/constants/error.constants";
+import { DbError } from "@backend/common/errors/db/db.errors";
 import { error } from "@backend/common/errors/handlers/error.handler";
 
 export const validateIdParam = (
diff --git a/packages/backend/src/common/services/gcal/gcal.service.ts b/packages/backend/src/common/services/gcal/gcal.service.ts
index e5a72b33..05b87b10 100644
--- a/packages/backend/src/common/services/gcal/gcal.service.ts
+++ b/packages/backend/src/common/services/gcal/gcal.service.ts
@@ -3,8 +3,8 @@ import { gCalendar, gParamsEventsList, gSchema$Event } from "@core/types/gcal";
 import { Params_WatchEvents } from "@core/types/sync.types";
 import { GCAL_PRIMARY } from "@backend/common/constants/backend.constants";
 import { ENV } from "@backend/common/constants/env.constants";
-import { GcalError } from "@backend/common/constants/error.constants";
 import { error } from "@backend/common/errors/handlers/error.handler";
+import { GcalError } from "@backend/common/errors/integration/gcal/gcal.errors";
 
 class GCalService {
   async createEvent(gcal: gCalendar, event: gSchema$Event) {
diff --git a/packages/backend/src/common/services/mongo.service.ts b/packages/backend/src/common/services/mongo.service.ts
index 9a1c2208..1bd5aef2 100644
--- a/packages/backend/src/common/services/mongo.service.ts
+++ b/packages/backend/src/common/services/mongo.service.ts
@@ -3,6 +3,7 @@ import { Logger } from "@core/logger/winston.logger";
 import { Schema_Event } from "@core/types/event.types";
 import { Schema_Sync } from "@core/types/sync.types";
 import { Schema_User } from "@core/types/user.types";
+import { Schema_Waitlist } from "@core/types/waitlist/waitlist.types";
 import { Collections } from "../constants/collections";
 import { ENV } from "../constants/env.constants";
 
@@ -18,6 +19,7 @@ class MongoService {
   public event!: Collection<Schema_Event>;
   public sync!: Collection<Schema_Sync>;
   public user!: Collection<Schema_User>;
+  public waitlist!: Collection<Schema_Waitlist>;
 
   constructor() {
     this._connect();
@@ -35,6 +37,9 @@ class MongoService {
         this.event = this.db.collection<Schema_Event>(Collections.EVENT);
         this.sync = this.db.collection<Schema_Sync>(Collections.SYNC);
         this.user = this.db.collection<Schema_User>(Collections.USER);
+        this.waitlist = this.db.collection<Schema_Waitlist>(
+          Collections.WAITLIST,
+        );
       })
       .catch((err) => {
         const retrySeconds = 5;
@@ -64,6 +69,7 @@ class MongoService {
       this.event = undefined as any;
       this.sync = undefined as any;
       this.user = undefined as any;
+      this.waitlist = undefined as any;
     }
 
     this.count = 0;
diff --git a/packages/backend/src/email/email.service.ts b/packages/backend/src/email/email.service.ts
new file mode 100644
index 00000000..257be31b
--- /dev/null
+++ b/packages/backend/src/email/email.service.ts
@@ -0,0 +1,91 @@
+import axios from "axios";
+import { Logger } from "@core/logger/winston.logger";
+import { Subscriber, SubscriberSchema } from "@core/types/email/email.types";
+import { ENV } from "@backend/common/constants/env.constants";
+import { EmailerError } from "@backend/common/errors/emailer/emailer.errors";
+import { error } from "@backend/common/errors/handlers/error.handler";
+import {
+  Response_TagSubscriber,
+  Response_UpsertSubscriber,
+} from "./email.types";
+
+const logger = Logger("app:email.service");
+class EmailService {
+  private static headers: { headers: Record<string, string> };
+  private static readonly baseUrl = "https://api.kit.com/v4";
+
+  private static initialize() {
+    if (!EmailService.headers) {
+      if (!ENV.EMAILER_SECRET) {
+        throw error(EmailerError.InvalidSecret, "Did not instantiate Emailer");
+      }
+      EmailService.headers = {
+        headers: {
+          "X-Kit-Api-Key": ENV.EMAILER_SECRET,
+          "Content-Type": "application/json",
+          Accept: "application/json",
+        },
+      };
+    }
+  }
+
+  static async addTagToSubscriber(
+    subscriber: Subscriber,
+    tagId: string,
+  ): Promise<Response_TagSubscriber> {
+    EmailService.initialize();
+    try {
+      // upsert subscriber
+      logger.debug(`Adding subscriber: ${subscriber.email_address}`);
+      const upsertedSubscriber = await this.upsertSubscriber(subscriber);
+      const subId = upsertedSubscriber.subscriber.id;
+
+      // add tag to subscriber
+      logger.info(`Tagging subscriber: ${subscriber.email_address}`);
+      const url = `${this.baseUrl}/tags/${tagId}/subscribers/${subId}`;
+      const result = await axios.post(url, {}, this.headers);
+      return result.data;
+    } catch (err) {
+      if (axios.isAxiosError(err)) {
+        logger.error({
+          message: err.message,
+          status: err.response?.status,
+          data: err.response?.data,
+          url: err.config?.url,
+          method: err.config?.method,
+        });
+        switch (err.response?.status) {
+          case 401:
+            throw error(
+              EmailerError.InvalidSecret,
+              "Subscriber not upserted/tagged",
+            );
+          case 404:
+            throw error(EmailerError.InvalidTagId, "Subscriber was not tagged");
+          default:
+            throw err;
+        }
+      } else {
+        throw err;
+      }
+    }
+  }
+
+  static async upsertSubscriber(
+    subscriber: Subscriber,
+  ): Promise<Response_UpsertSubscriber> {
+    const { data, success } = SubscriberSchema.safeParse(subscriber);
+    if (!success) {
+      throw error(
+        EmailerError.InvalidSubscriberData,
+        "Subscriber not upserted",
+      );
+    }
+
+    const url = `${this.baseUrl}/subscribers`;
+    const result = await axios.post(url, data, this.headers);
+    return result.data;
+  }
+}
+
+export default EmailService;
diff --git a/packages/backend/src/email/email.types.ts b/packages/backend/src/email/email.types.ts
new file mode 100644
index 00000000..8a5b515d
--- /dev/null
+++ b/packages/backend/src/email/email.types.ts
@@ -0,0 +1,41 @@
+import { z } from "zod";
+import { SubscriberStateSchema } from "@core/types/email/email.types";
+
+export const RequestBody_AddTagToSubscriberSchema = z.object({
+  email_address: z.string().email(),
+});
+
+export type RequestBody_AddTagToSubscriber = z.infer<
+  typeof RequestBody_AddTagToSubscriberSchema
+>;
+
+export const Response_UpsertSubscriberSchema = z.object({
+  subscriber: z.object({
+    id: z.number().int(),
+    first_name: z.string(),
+    email_address: z.string().email(),
+    state: SubscriberStateSchema,
+    created_at: z.string().datetime(),
+    fields: z.object({}).optional(),
+  }),
+});
+
+export type Response_UpsertSubscriber = z.infer<
+  typeof Response_UpsertSubscriberSchema
+>;
+
+export const Response_TagSubscriberSchema = z.object({
+  subscriber: z.object({
+    id: z.number().int(),
+    first_name: z.string(),
+    email_address: z.string().email(),
+    state: SubscriberStateSchema,
+    created_at: z.string().datetime(),
+    tagged_at: z.string().datetime(),
+    fields: z.object({}).optional(),
+  }),
+});
+
+export type Response_TagSubscriber = z.infer<
+  typeof Response_TagSubscriberSchema
+>;
diff --git a/packages/backend/src/event/services/event.service.ts b/packages/backend/src/event/services/event.service.ts
index fa147cd0..18dc5d60 100644
--- a/packages/backend/src/event/services/event.service.ts
+++ b/packages/backend/src/event/services/event.service.ts
@@ -23,10 +23,8 @@ import {
 import { getCurrentRangeDates } from "@core/util/date/date.util";
 import { getGcalClient } from "@backend/auth/services/google.auth.service";
 import { Collections } from "@backend/common/constants/collections";
-import {
-  EventError,
-  GenericError,
-} from "@backend/common/constants/error.constants";
+import { EventError } from "@backend/common/errors/event/event.errors";
+import { GenericError } from "@backend/common/errors/generic/generic.errors";
 import { error } from "@backend/common/errors/handlers/error.handler";
 import gcalService from "@backend/common/services/gcal/gcal.service";
 import mongoService from "@backend/common/services/mongo.service";
diff --git a/packages/backend/src/event/services/event.service.util.ts b/packages/backend/src/event/services/event.service.util.ts
index fd689985..319d0e98 100644
--- a/packages/backend/src/event/services/event.service.util.ts
+++ b/packages/backend/src/event/services/event.service.util.ts
@@ -9,7 +9,7 @@ import {
   Schema_Event_Core,
 } from "@core/types/event.types";
 import { isSameMonth } from "@core/util/date/date.util";
-import { GenericError } from "@backend/common/constants/error.constants";
+import { GenericError } from "@backend/common/errors/generic/generic.errors";
 import { error } from "@backend/common/errors/handlers/error.handler";
 
 dayjs.extend(tz);
diff --git a/packages/backend/src/event/services/recur/parse/recur.gcal.parse.ts b/packages/backend/src/event/services/recur/parse/recur.gcal.parse.ts
index ce847a78..1cfd807a 100644
--- a/packages/backend/src/event/services/recur/parse/recur.gcal.parse.ts
+++ b/packages/backend/src/event/services/recur/parse/recur.gcal.parse.ts
@@ -1,5 +1,5 @@
 import { gSchema$Event } from "@core/types/gcal";
-import { GenericError } from "@backend/common/constants/error.constants";
+import { GenericError } from "@backend/common/errors/generic/generic.errors";
 import { error } from "@backend/common/errors/handlers/error.handler";
 import { categorizeGcalEvents } from "@backend/common/services/gcal/gcal.utils";
 
diff --git a/packages/backend/src/event/services/recur/repo/recur.event.repo.util.ts b/packages/backend/src/event/services/recur/repo/recur.event.repo.util.ts
index f9787766..48f6bfd5 100644
--- a/packages/backend/src/event/services/recur/repo/recur.event.repo.util.ts
+++ b/packages/backend/src/event/services/recur/repo/recur.event.repo.util.ts
@@ -1,7 +1,7 @@
 import { ObjectId } from "mongodb";
 import { Schema_Event_Recur_Base } from "@core/types/event.types";
 import { Collections } from "@backend/common/constants/collections";
-import { EventError } from "@backend/common/constants/error.constants";
+import { EventError } from "@backend/common/errors/event/event.errors";
 import { error } from "@backend/common/errors/handlers/error.handler";
 import mongoService from "@backend/common/services/mongo.service";
 import { Ids_Event } from "../../../queries/event.queries";
diff --git a/packages/backend/src/event/services/recur/util/recur.util.ts b/packages/backend/src/event/services/recur/util/recur.util.ts
index 9516933d..6d2c9e16 100644
--- a/packages/backend/src/event/services/recur/util/recur.util.ts
+++ b/packages/backend/src/event/services/recur/util/recur.util.ts
@@ -10,7 +10,7 @@ import {
   Schema_Event_Recur_Base,
   Schema_Event_Recur_Instance,
 } from "@core/types/event.types";
-import { GenericError } from "@backend/common/constants/error.constants";
+import { GenericError } from "@backend/common/errors/generic/generic.errors";
 import { error } from "@backend/common/errors/handlers/error.handler";
 
 dayjs.extend(utc);
diff --git a/packages/backend/src/servers/express/express.server.ts b/packages/backend/src/servers/express/express.server.ts
index a092a70d..027e2a83 100644
--- a/packages/backend/src/servers/express/express.server.ts
+++ b/packages/backend/src/servers/express/express.server.ts
@@ -14,6 +14,7 @@ import {
 import { EventRoutes } from "@backend/event/event.routes.config";
 import { PriorityRoutes } from "@backend/priority/priority.routes.config";
 import { SyncRoutes } from "@backend/sync/sync.routes.config";
+import { WaitlistRoutes } from "@backend/waitlist/waitlist.routes.config";
 
 export const initExpressServer = () => {
   /* Express Configuration */
@@ -21,10 +22,9 @@ export const initExpressServer = () => {
 
   // initialize middleware before routes, because
   // some routes depend on them
-  //@ts-ignore
+  //@ts-expect-error this middleware isn't typed correctly
   app.use(requestMiddleware());
   app.use(supertokensCors());
-  // eslint-disable-next-line @typescript-eslint/no-misused-promises
   app.use(supertokensMiddleware());
   app.use(corsWhitelist);
   app.use(helmet());
@@ -37,8 +37,8 @@ export const initExpressServer = () => {
   routes.push(new EventRoutes(app));
   routes.push(new SyncRoutes(app));
   routes.push(new CalendarRoutes(app));
+  routes.push(new WaitlistRoutes(app));
 
-  // eslint-disable-next-line @typescript-eslint/no-misused-promises
   app.use(supertokensErrorHandler()); // Keep this after routes
 
   return app;
diff --git a/packages/backend/src/servers/websocket/websocket.server.ts b/packages/backend/src/servers/websocket/websocket.server.ts
index 56107f5f..8eb1250e 100644
--- a/packages/backend/src/servers/websocket/websocket.server.ts
+++ b/packages/backend/src/servers/websocket/websocket.server.ts
@@ -15,8 +15,8 @@ import {
   SocketData,
 } from "@core/types/websocket.types";
 import { ENV } from "@backend/common/constants/env.constants";
-import { SocketError } from "@backend/common/constants/error.constants";
 import { error } from "@backend/common/errors/handlers/error.handler";
+import { SocketError } from "@backend/common/errors/socket/socket.errors";
 import { handleWsError } from "./websocket.util";
 
 const logger = Logger("app:websocket.server");
diff --git a/packages/backend/src/sync/controllers/sync.controller.ts b/packages/backend/src/sync/controllers/sync.controller.ts
index 516e0a66..dbcf0eb5 100644
--- a/packages/backend/src/sync/controllers/sync.controller.ts
+++ b/packages/backend/src/sync/controllers/sync.controller.ts
@@ -2,10 +2,8 @@ import { Request } from "express";
 import { Status } from "@core/errors/status.codes";
 import { Logger } from "@core/logger/winston.logger";
 import { Payload_Sync_Notif } from "@core/types/sync.types";
-import {
-  SyncError,
-  UserError,
-} from "@backend/common/constants/error.constants";
+import { SyncError } from "@backend/common/errors/sync/sync.errors";
+import { UserError } from "@backend/common/errors/user/user.errors";
 import {
   isFullSyncRequired,
   isInvalidGoogleToken,
diff --git a/packages/backend/src/sync/services/import/all/import.all.gcal.ts b/packages/backend/src/sync/services/import/all/import.all.gcal.ts
index 2694925b..464d4d38 100644
--- a/packages/backend/src/sync/services/import/all/import.all.gcal.ts
+++ b/packages/backend/src/sync/services/import/all/import.all.gcal.ts
@@ -7,7 +7,7 @@ import {
   gParamsImportAllEvents,
   gSchema$Event,
 } from "@core/types/gcal";
-import { EventError } from "@backend/common/constants/error.constants";
+import { EventError } from "@backend/common/errors/event/event.errors";
 import { error } from "@backend/common/errors/handlers/error.handler";
 import gcalService from "@backend/common/services/gcal/gcal.service";
 import eventService from "@backend/event/services/event.service";
diff --git a/packages/backend/src/sync/services/import/sync.import.ts b/packages/backend/src/sync/services/import/sync.import.ts
index 46e0bc97..4f4ae7a0 100644
--- a/packages/backend/src/sync/services/import/sync.import.ts
+++ b/packages/backend/src/sync/services/import/sync.import.ts
@@ -15,12 +15,10 @@ import { categorizeRecurringEvents } from "@core/util/event/event.util";
 import { getGcalClient } from "@backend/auth/services/google.auth.service";
 import { Collections } from "@backend/common/constants/collections";
 import { ENV } from "@backend/common/constants/env.constants";
-import {
-  EventError,
-  GcalError,
-  SyncError,
-} from "@backend/common/constants/error.constants";
+import { EventError } from "@backend/common/errors/event/event.errors";
 import { error } from "@backend/common/errors/handlers/error.handler";
+import { GcalError } from "@backend/common/errors/integration/gcal/gcal.errors";
+import { SyncError } from "@backend/common/errors/sync/sync.errors";
 import gcalService from "@backend/common/services/gcal/gcal.service";
 import mongoService from "@backend/common/services/mongo.service";
 import {
diff --git a/packages/backend/src/sync/services/init/sync.init.ts b/packages/backend/src/sync/services/init/sync.init.ts
index 74234bed..d9ec86ec 100644
--- a/packages/backend/src/sync/services/init/sync.init.ts
+++ b/packages/backend/src/sync/services/init/sync.init.ts
@@ -8,8 +8,8 @@ import {
 } from "@core/types/gcal";
 import calendarService from "@backend/calendar/services/calendar.service";
 import { ENV } from "@backend/common/constants/env.constants";
-import { GcalError } from "@backend/common/constants/error.constants";
 import { error } from "@backend/common/errors/handlers/error.handler";
+import { GcalError } from "@backend/common/errors/integration/gcal/gcal.errors";
 import gcalService from "@backend/common/services/gcal/gcal.service";
 import { createSync } from "@backend/sync/util/sync.queries";
 import { isUsingHttps } from "@backend/sync/util/sync.util";
diff --git a/packages/backend/src/sync/services/notify/gcal.notification.util.ts b/packages/backend/src/sync/services/notify/gcal.notification.util.ts
index 8e8d1cb5..f4a6ee11 100644
--- a/packages/backend/src/sync/services/notify/gcal.notification.util.ts
+++ b/packages/backend/src/sync/services/notify/gcal.notification.util.ts
@@ -1,5 +1,5 @@
-import { SyncError } from "@backend/common/constants/error.constants";
 import { error } from "@backend/common/errors/handlers/error.handler";
+import { SyncError } from "@backend/common/errors/sync/sync.errors";
 import { getSync } from "@backend/sync/util/sync.queries";
 
 /**
diff --git a/packages/backend/src/sync/services/notify/handler/gcal.notification.handler.ts b/packages/backend/src/sync/services/notify/handler/gcal.notification.handler.ts
index 2ec62eac..274db1bf 100644
--- a/packages/backend/src/sync/services/notify/handler/gcal.notification.handler.ts
+++ b/packages/backend/src/sync/services/notify/handler/gcal.notification.handler.ts
@@ -1,7 +1,7 @@
 import { Logger } from "@core/logger/winston.logger";
 import { gCalendar } from "@core/types/gcal";
-import { SyncError } from "@backend/common/constants/error.constants";
 import { error } from "@backend/common/errors/handlers/error.handler";
+import { SyncError } from "@backend/common/errors/sync/sync.errors";
 import gcalService from "@backend/common/services/gcal/gcal.service";
 import { RecurringEventRepository } from "@backend/event/services/recur/repo/recur.event.repo";
 import { getSync, updateSyncTokenFor } from "@backend/sync/util/sync.queries";
diff --git a/packages/backend/src/sync/services/sync.service.ts b/packages/backend/src/sync/services/sync.service.ts
index e007e4a0..a7a5179b 100644
--- a/packages/backend/src/sync/services/sync.service.ts
+++ b/packages/backend/src/sync/services/sync.service.ts
@@ -10,11 +10,9 @@ import {
 } from "@core/types/sync.types";
 import { getGcalClient } from "@backend/auth/services/google.auth.service";
 import { Collections } from "@backend/common/constants/collections";
-import {
-  GenericError,
-  SyncError,
-} from "@backend/common/constants/error.constants";
+import { GenericError } from "@backend/common/errors/generic/generic.errors";
 import { error } from "@backend/common/errors/handlers/error.handler";
+import { SyncError } from "@backend/common/errors/sync/sync.errors";
 import gcalService from "@backend/common/services/gcal/gcal.service";
 import mongoService from "@backend/common/services/mongo.service";
 import { webSocketServer } from "@backend/servers/websocket/websocket.server";
diff --git a/packages/backend/src/sync/services/sync/gcal.sync.processor.ts b/packages/backend/src/sync/services/sync/gcal.sync.processor.ts
index dcfadfb1..d684852d 100644
--- a/packages/backend/src/sync/services/sync/gcal.sync.processor.ts
+++ b/packages/backend/src/sync/services/sync/gcal.sync.processor.ts
@@ -15,10 +15,8 @@ import {
 } from "@core/types/gcal";
 import { convertRruleWithUntilToDate } from "@core/util/date/date.util";
 import { isBase } from "@core/util/event/event.util";
-import {
-  EventError,
-  GenericError,
-} from "@backend/common/constants/error.constants";
+import { EventError } from "@backend/common/errors/event/event.errors";
+import { GenericError } from "@backend/common/errors/generic/generic.errors";
 import { error } from "@backend/common/errors/handlers/error.handler";
 import { findCompassEventBy } from "@backend/event/queries/event.queries";
 import { EventRepository } from "@backend/event/services/recur/repo/event.repo";
diff --git a/packages/backend/src/sync/util/sync.queries.ts b/packages/backend/src/sync/util/sync.queries.ts
index 2f7ee15f..fa1c1480 100644
--- a/packages/backend/src/sync/util/sync.queries.ts
+++ b/packages/backend/src/sync/util/sync.queries.ts
@@ -1,11 +1,9 @@
 import { Origin } from "@core/constants/core.constants";
 import { Schema_CalendarList } from "@core/types/calendar.types";
 import { Payload_Resource_Events, Resource_Sync } from "@core/types/sync.types";
-import {
-  GenericError,
-  SyncError,
-} from "@backend/common/constants/error.constants";
+import { GenericError } from "@backend/common/errors/generic/generic.errors";
 import { error } from "@backend/common/errors/handlers/error.handler";
+import { SyncError } from "@backend/common/errors/sync/sync.errors";
 import { getPrimaryGcalId } from "@backend/common/services/gcal/gcal.utils";
 import mongoService from "@backend/common/services/mongo.service";
 
diff --git a/packages/backend/src/user/services/email.service.ts b/packages/backend/src/user/services/email.service.ts
deleted file mode 100644
index ed369070..00000000
--- a/packages/backend/src/user/services/email.service.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import axios from "axios";
-import { Logger } from "@core/logger/winston.logger";
-import { ENV } from "@backend/common/constants/env.constants";
-import { EmailerError } from "@backend/common/constants/error.constants";
-import { error } from "@backend/common/errors/handlers/error.handler";
-
-const logger = Logger("app:emailer.service");
-
-class EmailService {
-  addToEmailList = async (email: string, firstName: string) => {
-    if (!ENV.EMAILER_LIST_ID && !ENV.EMAILER_SECRET) {
-      logger.warn(
-        "Skipped adding email to list, because EMAILER_ environment variables are missing.",
-      );
-      return;
-    }
-
-    const url = `https://api.convertkit.com/v3/tags/${
-      ENV.EMAILER_LIST_ID as string
-    }/subscribe?api_secret=${
-      ENV.EMAILER_SECRET as string
-    }&email=${email}&first_name=${firstName}`;
-
-    try {
-      const response = await axios.post(url);
-
-      if (response.status !== 200) {
-        throw error(
-          EmailerError.AddToListFailed,
-          "Failed to add email to list",
-        );
-        logger.error(response.data);
-      }
-
-      return response;
-    } catch (e) {
-      if (
-        axios.isAxiosError(e) &&
-        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
-        e?.response?.data?.message === "API Key not valid"
-      ) {
-        throw error(
-          EmailerError.IncorrectApiKey,
-          "Failed to add email to list. Please make sure environment variables beginning with EMAILER_ are correct",
-        );
-      }
-
-      throw e;
-    }
-  };
-}
-
-export default new EmailService();
diff --git a/packages/backend/src/user/services/user.service.ts b/packages/backend/src/user/services/user.service.ts
index 1fede249..728066ee 100644
--- a/packages/backend/src/user/services/user.service.ts
+++ b/packages/backend/src/user/services/user.service.ts
@@ -2,17 +2,21 @@ import { TokenPayload } from "google-auth-library";
 import { BaseError } from "@core/errors/errors.base";
 import { Logger } from "@core/logger/winston.logger";
 import { mapUserToCompass } from "@core/mappers/map.user";
+import { mapCompassUserToEmailSubscriber } from "@core/mappers/subscriber/map.subscriber";
 import { UserInfo_Google } from "@core/types/auth.types";
 import { gCalendar } from "@core/types/gcal";
+import { Schema_User } from "@core/types/user.types";
 import compassAuthService from "@backend/auth/services/compass.auth.service";
 import { getGcalClient } from "@backend/auth/services/google.auth.service";
 import calendarService from "@backend/calendar/services/calendar.service";
-import { AuthError } from "@backend/common/constants/error.constants";
+import { ENV } from "@backend/common/constants/env.constants";
+import { AuthError } from "@backend/common/errors/auth/auth.errors";
 import { error } from "@backend/common/errors/handlers/error.handler";
 import { initSupertokens } from "@backend/common/middleware/supertokens.middleware";
 import mongoService from "@backend/common/services/mongo.service";
 import { CompassError } from "@backend/common/types/error.types";
 import { Summary_Resync } from "@backend/common/types/sync.types";
+import EmailService from "@backend/email/email.service";
 import eventService from "@backend/event/services/event.service";
 import priorityService from "@backend/priority/services/priority.service";
 import {
@@ -24,7 +28,6 @@ import { watchEventsByGcalIds } from "@backend/sync/services/watch/sync.watch";
 import { reInitSyncByIntegration } from "@backend/sync/util/sync.queries";
 import { findCompassUserBy } from "../queries/user.queries";
 import { Summary_Delete } from "../types/user.types";
-import emailService from "./email.service";
 
 const logger = Logger("app:user.service");
 
@@ -37,7 +40,7 @@ class UserService {
   createUser = async (
     gUser: UserInfo_Google["gUser"],
     gRefreshToken: string,
-  ) => {
+  ): Promise<Schema_User & { userId: string }> => {
     const _compassUser = mapUserToCompass(gUser, gRefreshToken);
     const compassUser = { ..._compassUser, signedUpAt: new Date() };
 
@@ -49,8 +52,7 @@ class UserService {
     }
 
     return {
-      email: compassUser.email,
-      firstName: compassUser.firstName,
+      ...compassUser,
       userId,
     };
   };
@@ -121,12 +123,17 @@ class UserService {
     gcalClient: gCalendar,
     gRefreshToken: string,
   ) => {
-    const { email, firstName, userId } = await this.createUser(
-      gUser,
-      gRefreshToken,
-    );
+    const cUser = await this.createUser(gUser, gRefreshToken);
+    const { userId } = cUser;
 
-    await emailService.addToEmailList(email, firstName);
+    if (!ENV.EMAILER_TAG_ID || !ENV.EMAILER_SECRET) {
+      logger.warn(
+        "Did not tag subscriber due to missing EMAILER_ ENV value(s)",
+      );
+    } else {
+      const subscriber = mapCompassUserToEmailSubscriber(cUser);
+      await EmailService.addTagToSubscriber(subscriber, ENV.EMAILER_TAG_ID);
+    }
 
     const gCalendarIds = await initSync(gcalClient, userId);
     await syncService.importFull(gcalClient, gCalendarIds, userId);
diff --git a/packages/backend/src/waitlist/controller/waitlist.controller.ts b/packages/backend/src/waitlist/controller/waitlist.controller.ts
new file mode 100644
index 00000000..b44892bf
--- /dev/null
+++ b/packages/backend/src/waitlist/controller/waitlist.controller.ts
@@ -0,0 +1,52 @@
+import { Request, Response } from "express";
+import { BaseError } from "@core/errors/errors.base";
+import { Logger } from "@core/logger/winston.logger";
+import {
+  AnswerMap,
+  Schema_Waitlist,
+} from "@core/types/waitlist/waitlist.types";
+import { ENV } from "@backend/common/constants/env.constants";
+import WaitlistService from "../service/waitlist.service";
+
+const logger = Logger("app:waitlist.controller");
+
+export class WaitlistController {
+  static async addToWaitlist(
+    req: Request<unknown, unknown, Schema_Waitlist>,
+    res: Response,
+  ) {
+    if (!ENV.EMAILER_SECRET || !ENV.EMAILER_TAG_ID) {
+      return res.status(500).json({ error: "Emailer values are missing" });
+    }
+
+    const parseResult = AnswerMap.v0.safeParse(req.body);
+    if (!parseResult.success) {
+      return res
+        .status(400)
+        .json({ error: "Invalid input", details: parseResult.error.flatten() });
+    }
+
+    const answers = parseResult.data;
+    try {
+      const result = await WaitlistService.addToWaitlist(
+        answers.email,
+        answers,
+      );
+      return res.status(200).json(result);
+    } catch (err) {
+      // If the error is a BaseError (including EmailerError), return its status and description
+      if (err instanceof BaseError) {
+        logger.error(err);
+        return res.status(err.statusCode).json({ error: err.description });
+      }
+      // If it's a generic Error, return its message
+      if (err instanceof Error) {
+        logger.error(err);
+        return res.status(500).json({ error: err.message });
+      }
+      // Otherwise, return a generic server error
+      logger.error("caught unknown error");
+      return res.status(500).json({ error: "Server error" });
+    }
+  }
+}
diff --git a/packages/backend/src/waitlist/repo/waitlist.repo.ts b/packages/backend/src/waitlist/repo/waitlist.repo.ts
new file mode 100644
index 00000000..8d58eb87
--- /dev/null
+++ b/packages/backend/src/waitlist/repo/waitlist.repo.ts
@@ -0,0 +1,13 @@
+import { Schema_Waitlist } from "@core/types/waitlist/waitlist.types";
+import mongoService from "@backend/common/services/mongo.service";
+
+export class WaitlistRepository {
+  static async addToWaitlist(record: Schema_Waitlist) {
+    return mongoService.waitlist.insertOne(record);
+  }
+
+  static async isAlreadyOnWaitlist(email: string) {
+    const match = await mongoService.waitlist.find({ email }).toArray();
+    return match.length > 0;
+  }
+}
diff --git a/packages/backend/src/waitlist/service/waitlist.service.ts b/packages/backend/src/waitlist/service/waitlist.service.ts
new file mode 100644
index 00000000..006ef778
--- /dev/null
+++ b/packages/backend/src/waitlist/service/waitlist.service.ts
@@ -0,0 +1,53 @@
+import { Logger } from "@core/logger/winston.logger";
+import { Subscriber } from "@core/types/email/email.types";
+import {
+  Result_Waitlist,
+  Schema_Answers,
+} from "@core/types/waitlist/waitlist.types";
+import { ENV } from "@backend/common/constants/env.constants";
+import EmailService from "../../email/email.service";
+import { WaitlistRepository } from "../repo/waitlist.repo";
+
+const logger = Logger("app:waitlist.service");
+class WaitlistService {
+  static async addToWaitlist(
+    email: string,
+    answer: Schema_Answers,
+  ): Promise<Result_Waitlist> {
+    if (ENV.EMAILER_SECRET && ENV.EMAILER_TAG_ID) {
+      const subscriber: Subscriber = {
+        email_address: email,
+        first_name: answer.firstName,
+        state: "active",
+        fields: {
+          "Last name": answer.lastName,
+          Birthday: "1970-01-01",
+          Source: answer.source,
+        },
+      };
+      await EmailService.addTagToSubscriber(subscriber, ENV.EMAILER_TAG_ID);
+    } else {
+      logger.warn("Did not tag subscriber due to missing EMAILER env values");
+    }
+
+    // Save to DB
+    const isAlreadyWaitlisted =
+      await WaitlistRepository.isAlreadyOnWaitlist(email);
+    if (isAlreadyWaitlisted) {
+      return {
+        status: "ignored",
+      };
+    }
+
+    await WaitlistRepository.addToWaitlist({
+      ...answer,
+      waitlistedAt: new Date().toISOString(),
+    });
+
+    return {
+      status: "waitlisted",
+    };
+  }
+}
+
+export default WaitlistService;
diff --git a/packages/backend/src/waitlist/waitlist.routes.config.ts b/packages/backend/src/waitlist/waitlist.routes.config.ts
new file mode 100644
index 00000000..fcb60232
--- /dev/null
+++ b/packages/backend/src/waitlist/waitlist.routes.config.ts
@@ -0,0 +1,14 @@
+import express from "express";
+import { CommonRoutesConfig } from "@backend/common/common.routes.config";
+import { WaitlistController } from "./controller/waitlist.controller";
+
+export class WaitlistRoutes extends CommonRoutesConfig {
+  constructor(app: express.Application) {
+    super(app, "WaitlistRoutes");
+  }
+
+  configureRoutes() {
+    this.app.post("/api/waitlist/v0", WaitlistController.addToWaitlist);
+    return this.app;
+  }
+}
diff --git a/packages/core/src/errors/errors.base.ts b/packages/core/src/errors/errors.base.ts
index 74d8967d..b41090e1 100644
--- a/packages/core/src/errors/errors.base.ts
+++ b/packages/core/src/errors/errors.base.ts
@@ -1,8 +1,18 @@
 import { Status } from "./status.codes";
 
+interface ErrorMetadata {
+  description: string;
+  status: Status;
+  isOperational: boolean;
+}
+
+export type ErrorConstant = Record<string, ErrorMetadata>;
+
 export class BaseError extends Error {
   public readonly result: string;
   public readonly description: string;
+  // Tech debt: 'statusCode' does not match the
+  // 'status' key in ErrorMetadata
   public readonly statusCode: Status;
   public readonly isOperational: boolean;
 
@@ -23,16 +33,3 @@ export class BaseError extends Error {
     Error.captureStackTrace(this);
   }
 }
-
-/*example of how to extend
-export class APIError extends BaseError {
-  constructor(
-    name: string,
-    statusCode = StatusCodes.INTERNAL_SERVER,
-    description = "Internal server error",
-    isOperational = true
-  ) {
-    super(name, statusCode, description, isOperational);
-  }
-}
-*/
diff --git a/packages/core/src/mappers/subscriber/map.subscriber.ts b/packages/core/src/mappers/subscriber/map.subscriber.ts
new file mode 100644
index 00000000..67272de0
--- /dev/null
+++ b/packages/core/src/mappers/subscriber/map.subscriber.ts
@@ -0,0 +1,26 @@
+import { Subscriber } from "@core/types/email/email.types";
+import { Schema_User } from "@core/types/user.types";
+
+export const mapCompassUserToEmailSubscriber = (
+  user: Schema_User,
+): Subscriber => {
+  const UNKNOWN = "unknown";
+  return {
+    email_address: user.email,
+    first_name: user.firstName,
+    state: "active",
+    fields: {
+      "Last name": user.lastName,
+      Birthday: "1970-01-01",
+      Source: UNKNOWN,
+      Role: UNKNOWN,
+      Company: UNKNOWN,
+      "Postal code": UNKNOWN,
+      Website: UNKNOWN,
+      "Social media": UNKNOWN,
+      "How did you hear about us?": UNKNOWN,
+      Interests: UNKNOWN,
+      Coupon: UNKNOWN,
+    },
+  };
+};
diff --git a/packages/core/src/types/email/email.types.ts b/packages/core/src/types/email/email.types.ts
new file mode 100644
index 00000000..404fa650
--- /dev/null
+++ b/packages/core/src/types/email/email.types.ts
@@ -0,0 +1,29 @@
+import z from "zod";
+
+export const SubscriberStateSchema = z
+  .enum(["active", "bounced", "cancelled", "complained", "inactive"])
+  .nullable()
+  .optional()
+  .default("active");
+export type SubscriberState = z.infer<typeof SubscriberStateSchema>;
+
+export const SubscriberSchema = z.object({
+  email_address: z.string().email(),
+  first_name: z.string().nullable().optional(),
+  state: SubscriberStateSchema,
+  fields: z.object({
+    "Last name": z.string(),
+    Birthday: z.string(),
+    Source: z.string(),
+    Role: z.string().optional(),
+    Company: z.string().optional(),
+    "Postal code": z.string().optional(),
+    Website: z.string().optional(),
+    "Social media": z.string().optional(),
+    "How did you hear about us?": z.string().optional(),
+    Interests: z.string().optional(),
+    Coupon: z.string().optional(),
+  }),
+});
+
+export type Subscriber = z.infer<typeof SubscriberSchema>;
diff --git a/packages/core/src/types/waitlist/waitlist.types.ts b/packages/core/src/types/waitlist/waitlist.types.ts
new file mode 100644
index 00000000..e8f49829
--- /dev/null
+++ b/packages/core/src/types/waitlist/waitlist.types.ts
@@ -0,0 +1,38 @@
+import { z } from "zod";
+
+export interface Result_Waitlist {
+  status: "waitlisted" | "ignored";
+}
+
+/* v0 */
+export const Schema_Answers_v0 = z.object({
+  email: z.string().email(),
+  schemaVersion: z.literal("0"),
+  source: z.enum(["search-engine", "social-media", "friend", "other"]),
+  firstName: z.string().min(2),
+  lastName: z.string().min(2),
+  currentlyPayingFor: z.array(z.string()).optional(),
+  howClearAboutValues: z.enum(["not-clear", "somewhat-clear", "very-clear"]),
+  workingTowardsMainGoal: z.enum([
+    "yes",
+    "no-but-want-to",
+    "no-and-dont-want-to",
+  ]),
+  isWillingToShare: z.boolean(),
+  anythingElse: z.string().optional(),
+});
+export type Answers_v0 = z.infer<typeof Schema_Answers_v0>;
+
+const Schema_Waitlist_v0 = Schema_Answers_v0.extend({
+  waitlistedAt: z.string().datetime(),
+});
+export type Schema_Waitlist_v0 = z.infer<typeof Schema_Waitlist_v0>;
+
+/* All */
+export type Schema_Waitlist = Schema_Waitlist_v0; // make a union type when adding more versions
+export type Schema_Answers = Answers_v0; // make a union type when adding more versions
+
+export const AnswerMap = {
+  v0: Schema_Answers_v0,
+  //v1: Schema_Answers_v1 <-- Extend/change for new version to avoid migration
+};
diff --git a/packages/web/src/components/ContextMenu/ContextMenuItems.tsx b/packages/web/src/components/ContextMenu/ContextMenuItems.tsx
index 5241afc9..bebb165f 100644
--- a/packages/web/src/components/ContextMenu/ContextMenuItems.tsx
+++ b/packages/web/src/components/ContextMenu/ContextMenuItems.tsx
@@ -1,5 +1,5 @@
 import React, { useState } from "react";
-import { PenNib, Trash } from "@phosphor-icons/react";
+import { Copy, PenNib, Trash } from "@phosphor-icons/react";
 import { Priorities } from "@core/constants/core.constants";
 import { colorByPriority } from "@web/common/styles/theme.util";
 import { Schema_GridEvent } from "@web/common/types/web.event.types";
@@ -27,7 +27,7 @@ interface ContextMenuItemsProps {
 
 export function ContextMenuItems({ event, close }: ContextMenuItemsProps) {
   const { actions, setters } = useDraftContext();
-  const { openForm, deleteEvent, submit } = actions;
+  const { openForm, deleteEvent, duplicateEvent, submit } = actions;
   const { setDraft } = setters;
 
   const [selectedPriority, setSelectedPriority] = useState(event.priority);
@@ -73,6 +73,16 @@ export function ContextMenuItems({ event, close }: ContextMenuItemsProps) {
         </IconButton>
       ),
     },
+    {
+      id: "duplicate",
+      label: "Duplicate",
+      onClick: duplicateEvent,
+      icon: (
+        <IconButton>
+          <Copy />
+        </IconButton>
+      ),
+    },
     {
       id: "delete",
       label: "Delete",
@@ -102,6 +112,7 @@ export function ContextMenuItems({ event, close }: ContextMenuItemsProps) {
           key={item.id}
           onClick={() => {
             item.onClick();
+            close();
           }}
         >
           {item.icon}
diff --git a/packages/web/src/views/Calendar/components/Draft/grid/GridDraft.tsx b/packages/web/src/views/Calendar/components/Draft/grid/GridDraft.tsx
index 1e9add4d..7717778c 100644
--- a/packages/web/src/views/Calendar/components/Draft/grid/GridDraft.tsx
+++ b/packages/web/src/views/Calendar/components/Draft/grid/GridDraft.tsx
@@ -23,7 +23,8 @@ interface Props {
 
 export const GridDraft: FC<Props> = ({ measurements, weekProps }) => {
   const { actions, setters, state } = useDraftContext();
-  const { discard, deleteEvent, submit, startDragging } = actions;
+  const { discard, deleteEvent, duplicateEvent, submit, startDragging } =
+    actions;
   const { setDraft, setDateBeingChanged, setIsResizing } = setters;
   const { draft, isDragging, formProps, isFormOpen, isResizing } = state;
   const { context, getReferenceProps, getFloatingProps, x, y, refs, strategy } =
@@ -106,6 +107,7 @@ export const GridDraft: FC<Props> = ({ measurements, weekProps }) => {
                 onClose={discard}
                 onConvert={onConvert}
                 onDelete={deleteEvent}
+                onDuplicate={duplicateEvent}
                 onSubmit={(_draft: Schema_GridEvent) => submit(_draft)}
                 setEvent={setDraft}
               />
diff --git a/packages/web/src/views/Calendar/components/Draft/hooks/actions/useDraftActions.ts b/packages/web/src/views/Calendar/components/Draft/hooks/actions/useDraftActions.ts
index 761e9f8d..922d7fdc 100644
--- a/packages/web/src/views/Calendar/components/Draft/hooks/actions/useDraftActions.ts
+++ b/packages/web/src/views/Calendar/components/Draft/hooks/actions/useDraftActions.ts
@@ -164,6 +164,14 @@ export const useDraftActions = (
     discard();
   };
 
+  const duplicateEvent = () => {
+    const draft = { ...reduxDraft } as Schema_GridEvent;
+    delete draft._id;
+    draft.title = `${draft.title} (copy)`;
+    submit(draft);
+    discard();
+  };
+
   const discard = useCallback(() => {
     reset();
 
@@ -420,6 +428,7 @@ export const useDraftActions = (
     submit,
     convert,
     deleteEvent,
+    duplicateEvent,
     discard,
     drag,
     openForm,
diff --git a/packages/web/src/views/Calendar/hooks/shortcuts/useShortcuts.ts b/packages/web/src/views/Calendar/hooks/shortcuts/useShortcuts.ts
index c1740c7c..5ae38b60 100644
--- a/packages/web/src/views/Calendar/hooks/shortcuts/useShortcuts.ts
+++ b/packages/web/src/views/Calendar/hooks/shortcuts/useShortcuts.ts
@@ -17,7 +17,10 @@ import {
   selectIsAtMonthlyLimit,
   selectIsAtWeeklyLimit,
 } from "@web/ducks/events/selectors/someday.selectors";
-import { selectSidebarTab, selectIsSidebarOpen } from "@web/ducks/events/selectors/view.selectors";
+import {
+  selectIsSidebarOpen,
+  selectSidebarTab,
+} from "@web/ducks/events/selectors/view.selectors";
 import { draftSlice } from "@web/ducks/events/slices/draft.slice";
 import { viewSlice } from "@web/ducks/events/slices/view.slice";
 import { useAppDispatch, useAppSelector } from "@web/store/store.hooks";
@@ -72,7 +75,7 @@ export const useShortcuts = ({
         return;
       }
 
-        // If sidebar is closed, open it first
+      // If sidebar is closed, open it first
       if (!isSidebarOpen) {
         dispatch(viewSlice.actions.toggleSidebar());
       }
diff --git a/packages/web/src/views/Forms/EventForm/EventForm.tsx b/packages/web/src/views/Forms/EventForm/EventForm.tsx
index 921ac359..fc795107 100644
--- a/packages/web/src/views/Forms/EventForm/EventForm.tsx
+++ b/packages/web/src/views/Forms/EventForm/EventForm.tsx
@@ -42,6 +42,7 @@ export const EventForm: React.FC<FormProps> = ({
   onConvert,
   onDelete,
   onSubmit,
+  onDuplicate,
   setEvent,
   ...props
 }) => {
@@ -137,6 +138,9 @@ export const EventForm: React.FC<FormProps> = ({
   };
 
   const handleIgnoredKeys = (e: KeyboardEvent) => {
+    // Ignores certain keys and key combinations to prevent default behavior.
+    // Allows some of them to be used as hotkeys
+
     if (e.key === Key.Backspace) {
       e.stopPropagation();
     }
@@ -145,6 +149,10 @@ export const EventForm: React.FC<FormProps> = ({
       e.preventDefault();
     }
 
+    if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "d") {
+      e.preventDefault();
+    }
+
     if (e.metaKey && e.key === Key.Enter) {
       e.preventDefault();
       onSubmitForm();
@@ -251,6 +259,14 @@ export const EventForm: React.FC<FormProps> = ({
     hotkeysOptions,
   );
 
+  useHotkeys(
+    "ctrl+d",
+    () => {
+      onDuplicate?.(event);
+    },
+    hotkeysOptions,
+  );
+
   return (
     <StyledEventForm
       {...props}
diff --git a/packages/web/src/views/Forms/EventForm/types.ts b/packages/web/src/views/Forms/EventForm/types.ts
index d9ac3175..5c72cab3 100644
--- a/packages/web/src/views/Forms/EventForm/types.ts
+++ b/packages/web/src/views/Forms/EventForm/types.ts
@@ -9,6 +9,7 @@ export interface FormProps {
   onCloseEventForm?: () => void;
   onConvert?: () => void;
   onDelete?: (eventId?: string) => void;
+  onDuplicate?: (event: Schema_Event) => void;
   onSubmit: (event?: Schema_Event) => void;
   onSubmitEventForm?: (event: Schema_Event) => void;
   priority?: Priority;
diff --git a/yarn.lock b/yarn.lock
index baae5b06..755c0494 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1825,6 +1825,11 @@
     strict-event-emitter "^0.2.4"
     web-encoding "^1.1.5"
 
+"@noble/hashes@^1.1.5":
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a"
+  integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==
+
 "@nodelib/fs.scandir@2.1.5":
   version "2.1.5"
   resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -1851,6 +1856,13 @@
   resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca"
   integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==
 
+"@paralleldrive/cuid2@^2.2.2":
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz#7f91364d53b89e2c9cb9e02e8dd0f129e834455f"
+  integrity sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==
+  dependencies:
+    "@noble/hashes" "^1.1.5"
+
 "@parcel/watcher-android-arm64@2.5.1":
   version "2.5.1"
   resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1"
@@ -2380,6 +2392,11 @@
   resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d"
   integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==
 
+"@types/cookiejar@^2.1.5":
+  version "2.1.5"
+  resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78"
+  integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==
+
 "@types/cors@^2.8.12":
   version "2.8.17"
   resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b"
@@ -2581,6 +2598,11 @@
   resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.16.tgz#94ae78fab4a38d73086e962d0b65c30d816bfb0a"
   integrity sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==
 
+"@types/methods@^1.1.4":
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547"
+  integrity sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==
+
 "@types/mime@^1":
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690"
@@ -2802,6 +2824,24 @@
     "@types/react" "*"
     csstype "^3.0.2"
 
+"@types/superagent@^8.1.0":
+  version "8.1.9"
+  resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-8.1.9.tgz#28bfe4658e469838ed0bf66d898354bcab21f49f"
+  integrity sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==
+  dependencies:
+    "@types/cookiejar" "^2.1.5"
+    "@types/methods" "^1.1.4"
+    "@types/node" "*"
+    form-data "^4.0.0"
+
+"@types/supertest@^6.0.3":
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-6.0.3.tgz#d736f0e994b195b63e1c93e80271a2faf927388c"
+  integrity sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==
+  dependencies:
+    "@types/methods" "^1.1.4"
+    "@types/superagent" "^8.1.0"
+
 "@types/testing-library__jest-dom@^5.9.1":
   version "5.14.9"
   resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz#0fb1e6a0278d87b6737db55af5967570b67cb466"
@@ -3451,6 +3491,11 @@ arrify@^2.0.0:
   resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
   integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
 
+asap@^2.0.0:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+  integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==
+
 asn1.js@^4.10.1:
   version "4.10.1"
   resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
@@ -4231,6 +4276,11 @@ commondir@^1.0.1:
   resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
   integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
 
+component-emitter@^1.3.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17"
+  integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==
+
 compressible@~2.0.18:
   version "2.0.18"
   resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
@@ -4323,6 +4373,11 @@ cookie@~0.7.2:
   resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
   integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
 
+cookiejar@^2.1.4:
+  version "2.1.4"
+  resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b"
+  integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==
+
 copy-anything@^2.0.1:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480"
@@ -4774,6 +4829,14 @@ detect-node@^2.0.4:
   resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
   integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
 
+dezalgo@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81"
+  integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==
+  dependencies:
+    asap "^2.0.0"
+    wrappy "1"
+
 diff-sequences@^29.6.3:
   version "29.6.3"
   resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921"
@@ -5618,6 +5681,11 @@ fast-levenshtein@^2.0.6:
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
   integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
 
+fast-safe-stringify@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
+  integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
+
 fast-text-encoding@^1.0.0:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867"
@@ -5805,6 +5873,15 @@ form-data@^4.0.0:
     es-set-tostringtag "^2.1.0"
     mime-types "^2.1.12"
 
+formidable@^3.5.1:
+  version "3.5.4"
+  resolved "https://registry.yarnpkg.com/formidable/-/formidable-3.5.4.tgz#ac9a593b951e829b3298f21aa9a2243932f32ed9"
+  integrity sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==
+  dependencies:
+    "@paralleldrive/cuid2" "^2.2.2"
+    dezalgo "^1.0.4"
+    once "^1.4.0"
+
 forwarded@0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
@@ -7867,7 +7944,7 @@ merge2@^1.3.0, merge2@^1.4.1:
   resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
   integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
 
-methods@~1.1.2:
+methods@^1.1.2, methods@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
   integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
@@ -7910,6 +7987,11 @@ mime@1.6.0, mime@^1.4.1, mime@^1.6.0:
   resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
   integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
 
+mime@2.6.0:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367"
+  integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==
+
 mimic-fn@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
@@ -8332,7 +8414,7 @@ on-headers@~1.0.2:
   resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
   integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
 
-once@^1.3.0:
+once@^1.3.0, once@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
   integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
@@ -8888,7 +8970,7 @@ qs@6.13.0:
   dependencies:
     side-channel "^1.0.6"
 
-qs@^6.4.0, qs@^6.7.0, qs@^6.9.4:
+qs@^6.11.0, qs@^6.4.0, qs@^6.7.0, qs@^6.9.4:
   version "6.14.0"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930"
   integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==
@@ -10277,6 +10359,29 @@ stylus@^0.62.0:
     sax "~1.3.0"
     source-map "^0.7.3"
 
+superagent@^9.0.1:
+  version "9.0.2"
+  resolved "https://registry.yarnpkg.com/superagent/-/superagent-9.0.2.tgz#a18799473fc57557289d6b63960610e358bdebc1"
+  integrity sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==
+  dependencies:
+    component-emitter "^1.3.0"
+    cookiejar "^2.1.4"
+    debug "^4.3.4"
+    fast-safe-stringify "^2.1.1"
+    form-data "^4.0.0"
+    formidable "^3.5.1"
+    methods "^1.1.2"
+    mime "2.6.0"
+    qs "^6.11.0"
+
+supertest@^7.1.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/supertest/-/supertest-7.1.0.tgz#09b273174a8820e57ccdb03d9ca0d96c08c96b52"
+  integrity sha512-5QeSO8hSrKghtcWEoPiO036fxH0Ii2wVQfFZSP0oqQhmjk8bOLhDFXr4JrvaFmPuEWUoq4znY3uSi8UzLKxGqw==
+  dependencies:
+    methods "^1.1.2"
+    superagent "^9.0.1"
+
 supertokens-auth-react@^0.46.0:
   version "0.46.0"
   resolved "https://registry.yarnpkg.com/supertokens-auth-react/-/supertokens-auth-react-0.46.0.tgz#e3c97d1a9780184bb4bcd398221a1dfc8384faf4"


Test patch:
diff --git a/packages/backend/src/__tests__/backend.test.init.js b/packages/backend/src/__tests__/backend.test.init.js
index 9641b65c..e4b09f2c 100644
--- a/packages/backend/src/__tests__/backend.test.init.js
+++ b/packages/backend/src/__tests__/backend.test.init.js
@@ -12,6 +12,6 @@ process.env.CHANNEL_EXPIRATION_MIN = 5;
 process.env.SUPERTOKENS_URI = "sTUri";
 process.env.SUPERTOKENS_KEY = "sTKey";
 process.env.EMAILER_API_SECRET = "emailerApiSecret";
-process.env.EMAILER_LIST_ID = 1234567;
+process.env.EMAILER_TAG_ID = 1234567;
 process.env.TOKEN_GCAL_NOTIFICATION = "secretToken1";
 process.env.TOKEN_COMPASS_SYNC = "secretToken2";
diff --git a/packages/backend/src/__tests__/helpers/mock.db.queries.ts b/packages/backend/src/__tests__/helpers/mock.db.queries.ts
index e7430949..0fc64852 100644
--- a/packages/backend/src/__tests__/helpers/mock.db.queries.ts
+++ b/packages/backend/src/__tests__/helpers/mock.db.queries.ts
@@ -1,4 +1,5 @@
 import { Event_Core } from "@core/types/event.types";
+import { Schema_Waitlist } from "@core/types/waitlist/waitlist.types";
 import { Collections } from "@backend/common/constants/collections";
 import mongoService from "@backend/common/services/mongo.service";
 import { Event_API } from "@backend/common/types/backend.event.types";
@@ -20,6 +21,20 @@ export const getEventsInDb = async () => {
     .toArray()) as unknown as Event_API[];
 };
 
+export const getEmailsOnWaitlist = async () => {
+  const waitlist = (await mongoService.db
+    .collection(Collections.WAITLIST)
+    .find()
+    .toArray()) as unknown as Schema_Waitlist[];
+
+  const emails = waitlist.map((w) => w.email);
+  return emails;
+};
+
+export const isEmailOnWaitlist = async (email: string) => {
+  return (await getEmailsOnWaitlist()).includes(email);
+};
+
 export const isEventCollectionEmpty = async () => {
   return (
     (await mongoService.db.collection(Collections.EVENT).find().toArray())
diff --git a/packages/backend/src/__tests__/helpers/mock.db.setup.ts b/packages/backend/src/__tests__/helpers/mock.db.setup.ts
index 12d24213..90d2b764 100644
--- a/packages/backend/src/__tests__/helpers/mock.db.setup.ts
+++ b/packages/backend/src/__tests__/helpers/mock.db.setup.ts
@@ -3,6 +3,7 @@ import { MongoMemoryServer } from "mongodb-memory-server";
 import { Schema_Event } from "@core/types/event.types";
 import { Schema_Sync } from "@core/types/sync.types";
 import { Schema_User } from "@core/types/user.types";
+import { Schema_Waitlist } from "@core/types/waitlist/waitlist.types";
 import { Collections } from "@backend/common/constants/collections";
 import mongoService from "@backend/common/services/mongo.service";
 
@@ -11,6 +12,7 @@ export interface TestSetup {
   mongoClient: MongoClient;
   db: Db;
   userId: string;
+  email: string;
 }
 
 jest.mock("@backend/common/middleware/supertokens.middleware", () => ({
@@ -34,19 +36,24 @@ export async function setupTestDb(): Promise<TestSetup> {
   await db.createCollection(Collections.USER);
   await db.createCollection(Collections.SYNC);
   await db.createCollection(Collections.EVENT);
+  await db.createCollection(Collections.WAITLIST);
 
   // Setup mongoService mock to use our test collections
   (mongoService as any).db = db;
   (mongoService as any).user = db.collection<Schema_User>(Collections.USER);
   (mongoService as any).sync = db.collection<Schema_Sync>(Collections.SYNC);
   (mongoService as any).event = db.collection<Schema_Event>(Collections.EVENT);
+  (mongoService as any).waitlist = db.collection<Schema_Waitlist>(
+    Collections.WAITLIST,
+  );
 
   // Create test user
   const userId = new ObjectId();
+  const email = "test@example.com";
   const user: Schema_User = {
     //@ts-expect-error - overriding the _id to simulate a pre-populated collection
     _id: userId,
-    email: "test@example.com",
+    email,
     firstName: "Test",
     lastName: "User",
     name: "Test User",
@@ -83,12 +90,30 @@ export async function setupTestDb(): Promise<TestSetup> {
   };
   await mongoService.sync.insertOne(syncRecord);
 
-  return { mongoServer, mongoClient, db, userId: userId.toString() };
+  // Create waitlist user
+  await mongoService.waitlist.insertOne({
+    email,
+    waitlistedAt: new Date().toISOString(),
+    schemaVersion: "0",
+    source: "other",
+    firstName: "Test",
+    lastName: "User",
+    currentlyPayingFor: ["superhuman", "notion"],
+    howClearAboutValues: "not-clear",
+    workingTowardsMainGoal: "yes",
+    isWillingToShare: false,
+  });
+
+  return { mongoServer, mongoClient, db, userId: userId.toString(), email };
 }
 
 export async function cleanupCollections(db: Db): Promise<void> {
   const collections = await db.collections();
-  const SKIP_COLLECTIONS = [Collections.USER, Collections.SYNC];
+  const SKIP_COLLECTIONS = [
+    Collections.USER,
+    Collections.SYNC,
+    Collections.WAITLIST,
+  ];
 
   const clearPromises = collections
     .filter(
diff --git a/packages/backend/src/auth/controllers/auth.controller.test.ts b/packages/backend/src/auth/controllers/auth.controller.test.ts
index 8d5adc24..11f27b5d 100644
--- a/packages/backend/src/auth/controllers/auth.controller.test.ts
+++ b/packages/backend/src/auth/controllers/auth.controller.test.ts
@@ -2,7 +2,7 @@ import { ObjectId } from "mongodb";
 import { WithId } from "mongodb";
 import { gCalendar } from "@core/types/gcal";
 import { Schema_User } from "@core/types/user.types";
-import { SyncError } from "@backend/common/constants/error.constants";
+import { SyncError } from "@backend/common/errors/sync/sync.errors";
 import mongoService from "@backend/common/services/mongo.service";
 import syncService from "@backend/sync/services/sync.service";
 import userService from "@backend/user/services/user.service";
diff --git a/packages/backend/src/event/services/event.delete.test.ts b/packages/backend/src/event/services/event.delete.test.ts
index e3fd32b1..5d8e7511 100644
--- a/packages/backend/src/event/services/event.delete.test.ts
+++ b/packages/backend/src/event/services/event.delete.test.ts
@@ -1,11 +1,11 @@
 import dayjs from "dayjs";
 import { Collection, Db, MongoClient, ObjectId } from "mongodb";
+import { YEAR_MONTH_DAY_FORMAT } from "@core/constants/date.constants";
 import { Schema_Event } from "@core/types/event.types";
 import {
   mockSomedayRecurrences,
   newsletterId,
 } from "../../../../core/src/__mocks__/v1/events/events.someday.recur";
-import { YEAR_MONTH_DAY_FORMAT } from "../../../../core/src/constants/date.constants";
 import { getDeleteByIdFilter } from "./event.service.util";
 
 describe("Delete Events", () => {
diff --git a/packages/backend/src/servers/websocket/websocket.server.errors.test.ts b/packages/backend/src/servers/websocket/websocket.server.errors.test.ts
index 54216b41..357001d6 100644
--- a/packages/backend/src/servers/websocket/websocket.server.errors.test.ts
+++ b/packages/backend/src/servers/websocket/websocket.server.errors.test.ts
@@ -2,7 +2,7 @@ import { Server as HttpServer } from "node:http";
 import { createServer } from "node:http";
 import { Server as IoServer } from "socket.io";
 import { type Socket as ClientSocket, io as ioc } from "socket.io-client";
-import { SocketError } from "@backend/common/constants/error.constants";
+import { SocketError } from "@backend/common/errors/socket/socket.errors";
 import { WebSocketServer } from "./websocket.server";
 import { getServerUri } from "./websocket.util";
 
diff --git a/packages/backend/src/sync/services/notify/handler/gcal.notification.handler.test.ts b/packages/backend/src/sync/services/notify/handler/gcal.notification.handler.test.ts
index f4b9c107..359c0d97 100644
--- a/packages/backend/src/sync/services/notify/handler/gcal.notification.handler.test.ts
+++ b/packages/backend/src/sync/services/notify/handler/gcal.notification.handler.test.ts
@@ -4,8 +4,8 @@ import {
   setupTestDb,
 } from "@backend/__tests__/helpers/mock.db.setup";
 import { mockRegularGcalEvent } from "@backend/__tests__/mocks.gcal/factories/gcal.event.factory";
-import { SyncError } from "@backend/common/constants/error.constants";
 import { error } from "@backend/common/errors/handlers/error.handler";
+import { SyncError } from "@backend/common/errors/sync/sync.errors";
 import gcalService from "@backend/common/services/gcal/gcal.service";
 import { getSync } from "@backend/sync/util/sync.queries";
 import { GCalNotificationHandler } from "./gcal.notification.handler";
diff --git a/packages/backend/src/waitlist/controller/waitlist.controller.test.ts b/packages/backend/src/waitlist/controller/waitlist.controller.test.ts
new file mode 100644
index 00000000..5fbdccdc
--- /dev/null
+++ b/packages/backend/src/waitlist/controller/waitlist.controller.test.ts
@@ -0,0 +1,89 @@
+import request from "supertest";
+import { Schema_Waitlist } from "@core/types/waitlist/waitlist.types";
+
+describe("POST /api/waitlist", () => {
+  beforeEach(() => {
+    jest.resetModules();
+    jest.clearAllMocks();
+  });
+
+  it("should return 400 if answers are invalid", async () => {
+    jest.doMock("../service/waitlist.service", () => ({
+      __esModule: true,
+      default: {
+        addToWaitlist: jest.fn(),
+      },
+    }));
+    const { WaitlistController } = await import("./waitlist.controller");
+    const express = (await import("express")).default;
+    const app = express();
+    app.use(express.json());
+    app.post("/api/waitlist", WaitlistController.addToWaitlist);
+
+    const res = await request(app)
+      .post("/api/waitlist")
+      .send({ email: "", name: "" });
+
+    expect(res.status).toBe(400);
+    expect(res.error).toBeDefined();
+  });
+
+  it("should return 200 if answers are valid", async () => {
+    jest.doMock("../service/waitlist.service", () => ({
+      __esModule: true,
+      default: {
+        addToWaitlist: jest.fn().mockResolvedValue(undefined),
+      },
+    }));
+    const { WaitlistController } = await import("./waitlist.controller");
+    const express = (await import("express")).default;
+    const app = express();
+    app.use(express.json());
+    app.post("/api/waitlist", WaitlistController.addToWaitlist);
+
+    const answers: Schema_Waitlist = {
+      source: "social-media",
+      firstName: "Jo",
+      lastName: "Schmo",
+      email: "test@example.com",
+      currentlyPayingFor: [],
+      howClearAboutValues: "not-clear",
+      workingTowardsMainGoal: "yes",
+      isWillingToShare: false,
+      waitlistedAt: new Date().toISOString(),
+      schemaVersion: "0",
+    };
+    const res = await request(app).post("/api/waitlist").send(answers);
+
+    expect(res.status).toBe(200);
+    expect(res.body.error).not.toBeDefined();
+  });
+  // this test is at the bottom to avoid
+  // having to reset ENV in each test
+  it("should return 500 if emailer values are missing", async () => {
+    jest.doMock("@backend/common/constants/env.constants", () => ({
+      ENV: {},
+    }));
+    const { WaitlistController } = await import("./waitlist.controller");
+    const express = (await import("express")).default;
+    const app = express();
+    app.use(express.json());
+    app.post("/api/waitlist", WaitlistController.addToWaitlist);
+
+    const answers: Schema_Waitlist = {
+      source: "other",
+      firstName: "Jo",
+      lastName: "Schmo",
+      email: "test@example.com",
+      currentlyPayingFor: [],
+      howClearAboutValues: "not-clear",
+      workingTowardsMainGoal: "yes",
+      isWillingToShare: false,
+      waitlistedAt: new Date().toISOString(),
+      schemaVersion: 0,
+    };
+    const res = await request(app).post("/api/waitlist").send(answers);
+    expect(res.status).toBe(500);
+    expect(res.body.error).toBe("Emailer values are missing");
+  });
+});
diff --git a/packages/backend/src/waitlist/service/waitlist.service.test.ts b/packages/backend/src/waitlist/service/waitlist.service.test.ts
new file mode 100644
index 00000000..881a2b96
--- /dev/null
+++ b/packages/backend/src/waitlist/service/waitlist.service.test.ts
@@ -0,0 +1,135 @@
+import {
+  Result_Waitlist,
+  Schema_Waitlist,
+} from "@core/types/waitlist/waitlist.types";
+import {
+  getEmailsOnWaitlist,
+  isEmailOnWaitlist,
+} from "@backend/__tests__/helpers/mock.db.queries";
+import {
+  cleanupCollections,
+  cleanupTestMongo,
+  setupTestDb,
+} from "@backend/__tests__/helpers/mock.db.setup";
+import EmailService from "../../email/email.service";
+import WaitlistService from "./waitlist.service";
+
+const answer: Schema_Waitlist = {
+  firstName: "Jo",
+  lastName: "Schmo",
+  source: "other",
+  email: "joe@schmo.com",
+  currentlyPayingFor: undefined,
+  howClearAboutValues: "not-clear",
+  workingTowardsMainGoal: "yes",
+  isWillingToShare: false,
+  waitlistedAt: new Date().toISOString(),
+  schemaVersion: "0",
+};
+
+// Mock emailer API calls
+jest.spyOn(EmailService, "upsertSubscriber").mockResolvedValue({
+  subscriber: {
+    id: 1,
+    first_name: answer.firstName,
+    email_address: answer.email,
+    state: "active",
+    created_at: new Date().toISOString(),
+    fields: {
+      "Last name": answer.lastName,
+      Birthday: "1970-01-01",
+      Source: answer.source,
+    },
+  },
+});
+jest.spyOn(EmailService, "addTagToSubscriber").mockResolvedValue({
+  subscriber: {
+    id: 1,
+    first_name: answer.firstName,
+    email_address: answer.email,
+    state: "active",
+    created_at: new Date().toISOString(),
+    tagged_at: new Date().toISOString(),
+    fields: {
+      "Last name": answer.lastName,
+      Birthday: "1970-01-01",
+      Source: answer.source,
+    },
+  },
+});
+
+describe("addToWaitlist", () => {
+  let setup: Awaited<ReturnType<typeof setupTestDb>>;
+
+  beforeAll(async () => {
+    setup = await setupTestDb();
+  });
+
+  beforeEach(async () => {
+    await cleanupCollections(setup.db);
+  });
+
+  afterAll(async () => {
+    await cleanupTestMongo(setup);
+  });
+
+  it("should add to waitlist", async () => {
+    // Act
+    const result = await WaitlistService.addToWaitlist(answer.email, answer);
+
+    // Assert
+    const expected: Result_Waitlist = {
+      status: "waitlisted",
+    };
+    expect(result).toEqual(expected);
+    expect(await isEmailOnWaitlist(answer.email)).toBe(true);
+  });
+
+  it("should ignore if email is already on waitlist", async () => {
+    // Arrange
+    const _answer = { ...answer, email: setup.email }; // same email as what's already in DB
+    await WaitlistService.addToWaitlist(_answer.email, _answer);
+
+    // Act
+    const result = await WaitlistService.addToWaitlist(_answer.email, _answer);
+
+    // Assert
+    const expected: Result_Waitlist = {
+      status: "ignored",
+    };
+    expect(result).toEqual(expected);
+
+    const emailsOnList = await getEmailsOnWaitlist();
+    const noDuplicate =
+      emailsOnList.filter((email) => email === setup.email).length === 1;
+    expect(noDuplicate).toBe(true);
+  });
+
+  it("should skip emailer steps if missing EMAILER_ variables", async () => {
+    // Arrange
+    jest.resetModules();
+    jest.doMock("@backend/common/constants/env.constants", () => ({
+      ENV: {},
+    }));
+    const EmailService = require("../../email/email.service").default;
+    const addTagSpy = jest.spyOn(EmailService, "addTagToSubscriber");
+    const answers: any = {
+      firstName: "Jo",
+      lastName: "Schmo",
+      source: "other",
+      email: setup.email,
+      currentlyPayingFor: undefined,
+      howClearAboutValues: "not-clear",
+      workingTowardsMainGoal: "yes",
+      isWillingToShare: false,
+      waitlistedAt: new Date().toISOString(),
+      schemaVersion: "0",
+    };
+
+    // Act
+    await WaitlistService.addToWaitlist(answers.email, answers);
+
+    // Assert
+    expect(addTagSpy).not.toHaveBeenCalled();
+  });
+});
diff --git a/packages/core/src/mappers/subscriber/map.subscriber.test.ts b/packages/core/src/mappers/subscriber/map.subscriber.test.ts
new file mode 100644
index 00000000..bdf75028
--- /dev/null
+++ b/packages/core/src/mappers/subscriber/map.subscriber.test.ts
@@ -0,0 +1,51 @@
+import { Subscriber, SubscriberSchema } from "@core/types/email/email.types";
+
+describe("Subscriber", () => {
+  it("parses a valid subscriber", () => {
+    const validSubscriber: Subscriber = {
+      email_address: "test@example.com",
+      first_name: "Test",
+      state: "active",
+      fields: {
+        "Last name": "User",
+        Birthday: "1970-01-01",
+        Source: "unknown",
+        Role: "unknown",
+        Company: "unknown",
+        "Postal code": "unknown",
+        Website: "unknown",
+        "Social media": "unknown",
+        "How did you hear about us?": "unknown",
+        Interests: "unknown",
+        Coupon: "unknown",
+      },
+    };
+
+    const result = SubscriberSchema.safeParse(validSubscriber);
+    expect(result.success).toBe(true);
+  });
+
+  it("fails to parse an invalid subscriber", () => {
+    const invalidSubscriber: Subscriber = {
+      email_address: "invalid",
+      first_name: "Test",
+      state: "active",
+      fields: {
+        "Last name": "User",
+        Birthday: "1970-01-01",
+        Source: "unknown",
+        Role: "unknown",
+        Company: "unknown",
+        "Postal code": "unknown",
+        Website: "unknown",
+        "Social media": "unknown",
+        "How did you hear about us?": "unknown",
+        Interests: "unknown",
+        Coupon: "unknown",
+      },
+    };
+
+    const result = SubscriberSchema.safeParse(invalidSubscriber);
+    expect(result.success).toBe(false);
+  });
+});