diff --git a/codegen.json b/codegen.json
index 7eab6a1c503..4f9bd5d3aea 100644
--- a/codegen.json
+++ b/codegen.json
@@ -129,18 +129,19 @@
           "NewMeeting": "../../postgres/types/Meeting#AnyMeeting",
           "NewMeetingPhase": "./types/NewMeetingPhase#NewMeetingPhaseSource",
           "NewMeetingStage": "./types/NewMeetingStage#NewMeetingStageSource",
-          "NotificationMeetingStageTimeLimitEnd": "../../database/types/NotificationMeetingStageTimeLimitEnd#default as NotificationMeetingStageTimeLimitEndDB",
-          "NotificationTeamInvitation": "../../database/types/NotificationTeamInvitation#default as NotificationTeamInvitationDB",
-          "NotifyDiscussionMentioned": "../../database/types/NotificationDiscussionMentioned#default as NotificationDiscussionMentionedDB",
-          "NotifyKickedOut": "../../database/types/NotificationKickedOut#default",
-          "NotifyMentioned": "../../database/types/NotificationMentioned#default as NotificationMentionedDB",
-          "NotifyPaymentRejected": "../../database/types/NotificationPaymentRejected#default",
-          "NotifyPromoteToOrgLeader": "../../database/types/NotificationPromoteToBillingLeader#default",
-          "NotifyRequestToJoinOrg": "../../database/types/NotificationRequestToJoinOrg#default",
-          "NotifyResponseMentioned": "../../database/types/NotificationResponseMentioned#default as NotificationResponseMentionedDB",
-          "NotifyResponseReplied": "../../database/types/NotifyResponseReplied#default as NotifyResponseRepliedDB",
-          "NotifyTaskInvolves": "../../database/types/NotificationTaskInvolves#default",
-          "NotifyTeamArchived": "../../database/types/NotificationTeamArchived#default",
+          "Notification": "../../postgres/types/Notification.d#AnyNotification as AnyNotificationDB",
+          "NotificationMeetingStageTimeLimitEnd": "../../postgres/types/Notification.d#MeetingStageTimeLimitEndNotification as MeetingStageTimeLimitEndNotificationDB",
+          "NotificationTeamInvitation": "../../postgres/types/Notification.d#TeamInvitationNotification as TeamInvitationNotificationDB",
+          "NotifyDiscussionMentioned": "../../postgres/types/Notification.d#DiscussionMentionedNotification as DiscussionMentionedNotificationDB",
+          "NotifyKickedOut": "../../postgres/types/Notification.d#KickedOutNotification as KickedOutNotificationDB",
+          "NotifyMentioned": "../../postgres/types/Notification.d#MentionedNotification as MentionedNotificationDB",
+          "NotifyPaymentRejected": "../../postgres/types/Notification.d#PaymentRejectedNotification as PaymentRejectedNotificationDB",
+          "NotifyPromoteToOrgLeader": "../../postgres/types/Notification.d#PromoteToBillingLeaderNotification as PromoteToBillingLeaderNotificationDB",
+          "NotifyRequestToJoinOrg": "../../postgres/types/Notification.d#RequestToJoinOrgNotification as RequestToJoinOrgNotificationDB",
+          "NotifyResponseMentioned": "../../postgres/types/Notification.d#ResponseMentionedNotification as ResponseMentionedNotificationDB",
+          "NotifyResponseReplied": "../../postgres/types/Notification.d#ResponseRepliedNotification as ResponseRepliedNotificationDB",
+          "NotifyTaskInvolves": "../../postgres/types/Notification.d#TaskInvolvesNotification as TaskInvolvesNotificationDB",
+          "NotifyTeamArchived": "../../postgres/types/Notification.d#TeamArchivedNotification as TeamArchivedNotificationDB",
           "Organization": "../../postgres/types/index#Organization as OrganizationDB",
           "TemplateScaleValue": "./types/TemplateScaleValue#TemplateScaleValueSource as TemplateScaleValueSourceDB",
           "SuggestedAction": "../../postgres/types/index#SuggestedAction as SuggestedActionDB",
diff --git a/packages/server/billing/helpers/removeTeamsLimitObjects.ts b/packages/server/billing/helpers/removeTeamsLimitObjects.ts
index 14d46788aac..56ee4cf58db 100644
--- a/packages/server/billing/helpers/removeTeamsLimitObjects.ts
+++ b/packages/server/billing/helpers/removeTeamsLimitObjects.ts
@@ -1,5 +1,3 @@
-import {r} from 'rethinkdb-ts'
-import {RValue} from '../../database/stricterR'
 import {DataLoaderWorker} from '../../graphql/graphql'
 import updateNotification from '../../graphql/public/mutations/helpers/updateNotification'
 import getKysely from '../../postgres/getKysely'
@@ -10,37 +8,22 @@ const removeTeamsLimitObjects = async (orgId: string, dataLoader: DataLoaderWork
   const pg = getKysely()
 
   // Remove team limits jobs and existing notifications
-  const [, updateNotificationsChanges] = await Promise.all([
-    pg
-      .with('NotificationUpdate', (qb) =>
-        qb
-          .updateTable('Notification')
-          .set({status: 'CLICKED'})
-          .where('orgId', '=', orgId)
-          .where('type', 'in', removeNotificationTypes)
-      )
-      .deleteFrom('ScheduledJob')
-      .where('orgId', '=', orgId)
-      .where('type', 'in', removeJobTypes)
-      .execute(),
-    r
-      .table('Notification')
-      .getAll(orgId, {index: 'orgId'})
-      .filter((row: RValue) => r.expr(removeNotificationTypes).contains(row('type')))
-      .update(
-        // not really clicked, but no longer important
-        {status: 'CLICKED'},
-        {returnChanges: true}
-      )('changes')
-      .default([])
-      .run()
-  ])
+  const updateNotificationsChanges = await pg
+    .with('ScheduledJobDelete', (qb) =>
+      qb.deleteFrom('ScheduledJob').where('orgId', '=', orgId).where('type', 'in', removeJobTypes)
+    )
+    .updateTable('Notification')
+    .set({status: 'CLICKED'})
+    .where('orgId', '=', orgId)
+    .where('type', 'in', removeNotificationTypes)
+    .returning(['id', 'userId'])
+    .execute()
 
   const operationId = dataLoader.share()
   const subOptions = {operationId}
 
   updateNotificationsChanges?.forEach((change) => {
-    updateNotification(change.new_val, subOptions)
+    updateNotification(change, subOptions)
   })
 }
 
diff --git a/packages/server/billing/helpers/teamLimitsCheck.ts b/packages/server/billing/helpers/teamLimitsCheck.ts
index 94757589f45..fb16cfdca80 100644
--- a/packages/server/billing/helpers/teamLimitsCheck.ts
+++ b/packages/server/billing/helpers/teamLimitsCheck.ts
@@ -2,9 +2,8 @@ import ms from 'ms'
 import {Threshold} from 'parabol-client/types/constEnums'
 // Uncomment for easier testing
 // import { ThresholdTest as Threshold } from "~/types/constEnums";
-import {r} from 'rethinkdb-ts'
-import NotificationTeamsLimitExceeded from '../../database/types/NotificationTeamsLimitExceeded'
 import scheduleTeamLimitsJobs from '../../database/types/scheduleTeamLimitsJobs'
+import generateUID from '../../generateUID'
 import {DataLoaderWorker} from '../../graphql/graphql'
 import publishNotification from '../../graphql/public/mutations/helpers/publishNotification'
 import getActiveTeamCountByTeamIds from '../../graphql/public/types/helpers/getActiveTeamCountByTeamIds'
@@ -51,16 +50,15 @@ const sendWebsiteNotifications = async (
   const {id: orgId, name: orgName, picture: orgPicture} = organization
   const operationId = dataLoader.share()
   const subOptions = {operationId}
-  const notificationsToInsert = userIds.map((userId) => {
-    return new NotificationTeamsLimitExceeded({
-      userId,
-      orgId,
-      orgName,
-      orgPicture
-    })
-  })
+  const notificationsToInsert = userIds.map((userId) => ({
+    id: generateUID(),
+    type: 'TEAMS_LIMIT_EXCEEDED' as const,
+    userId,
+    orgId,
+    orgName,
+    orgPicture
+  }))
 
-  await r.table('Notification').insert(notificationsToInsert).run()
   await pg.insertInto('Notification').values(notificationsToInsert).execute()
   notificationsToInsert.forEach((notification) => {
     publishNotification(notification, subOptions)
diff --git a/packages/server/database/rethinkDriver.ts b/packages/server/database/rethinkDriver.ts
index e909172b381..1b2078fdcfc 100644
--- a/packages/server/database/rethinkDriver.ts
+++ b/packages/server/database/rethinkDriver.ts
@@ -1,33 +1,8 @@
 import {MasterPool, r} from 'rethinkdb-ts'
 import getRethinkConfig from './getRethinkConfig'
 import {R} from './stricterR'
-import NotificationKickedOut from './types/NotificationKickedOut'
-import NotificationMeetingStageTimeLimitEnd from './types/NotificationMeetingStageTimeLimitEnd'
-import NotificationMentioned from './types/NotificationMentioned'
-import NotificationPaymentRejected from './types/NotificationPaymentRejected'
-import NotificationPromoteToBillingLeader from './types/NotificationPromoteToBillingLeader'
-import NotificationResponseMentioned from './types/NotificationResponseMentioned'
-import NotificationResponseReplied from './types/NotificationResponseReplied'
-import NotificationTaskInvolves from './types/NotificationTaskInvolves'
-import NotificationTeamArchived from './types/NotificationTeamArchived'
-import NotificationTeamInvitation from './types/NotificationTeamInvitation'
 
-export type RethinkSchema = {
-  Notification: {
-    type:
-      | NotificationTaskInvolves
-      | NotificationTeamArchived
-      | NotificationMeetingStageTimeLimitEnd
-      | NotificationPaymentRejected
-      | NotificationKickedOut
-      | NotificationPromoteToBillingLeader
-      | NotificationTeamInvitation
-      | NotificationResponseMentioned
-      | NotificationResponseReplied
-      | NotificationMentioned
-    index: 'userId'
-  }
-}
+export type RethinkSchema = {}
 
 export type DBType = {
   [P in keyof RethinkSchema]: any
diff --git a/packages/server/database/types/Notification.ts b/packages/server/database/types/Notification.ts
deleted file mode 100644
index fcea79d8f4e..00000000000
--- a/packages/server/database/types/Notification.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import generateUID from '../../generateUID'
-import {NotificationStatusEnumType} from '../../graphql/types/NotificationStatusEnum'
-
-export type NotificationEnum =
-  | 'DISCUSSION_MENTIONED'
-  | 'KICKED_OUT'
-  | 'MEETING_STAGE_TIME_LIMIT_END'
-  | 'PAYMENT_REJECTED'
-  | 'PROMOTE_TO_BILLING_LEADER'
-  | 'RESPONSE_MENTIONED'
-  | 'RESPONSE_REPLIED'
-  | 'MENTIONED'
-  | 'TASK_INVOLVES'
-  | 'TEAM_ARCHIVED'
-  | 'TEAM_INVITATION'
-  | 'TEAMS_LIMIT_EXCEEDED'
-  | 'TEAMS_LIMIT_REMINDER'
-  | 'PROMPT_TO_JOIN_ORG'
-  | 'REQUEST_TO_JOIN_ORG'
-
-export interface NotificationInput {
-  type: NotificationEnum
-  userId: string
-}
-
-export default abstract class Notification {
-  id = generateUID()
-  status: NotificationStatusEnumType = 'UNREAD'
-  createdAt = new Date()
-  readonly type: NotificationEnum
-  userId: string
-
-  constructor({type, userId}: NotificationInput) {
-    this.type = type
-    this.userId = userId
-  }
-}
diff --git a/packages/server/database/types/NotificationDiscussionMentioned.ts b/packages/server/database/types/NotificationDiscussionMentioned.ts
deleted file mode 100644
index aa0a26b14cc..00000000000
--- a/packages/server/database/types/NotificationDiscussionMentioned.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import Notification from './Notification'
-
-interface Input {
-  meetingId: string
-  authorId: string
-  userId: string
-  commentId: string
-  discussionId: string
-}
-
-export default class NotificationDiscussionMentioned extends Notification {
-  readonly type = 'DISCUSSION_MENTIONED'
-  meetingId: string
-  authorId: string
-  commentId: string
-  discussionId: string
-
-  constructor(input: Input) {
-    const {meetingId, authorId, userId, commentId, discussionId} = input
-    super({userId, type: 'DISCUSSION_MENTIONED'})
-    this.meetingId = meetingId
-    this.authorId = authorId
-    this.commentId = commentId
-    this.discussionId = discussionId
-  }
-}
diff --git a/packages/server/database/types/NotificationKickedOut.ts b/packages/server/database/types/NotificationKickedOut.ts
deleted file mode 100644
index f8396284921..00000000000
--- a/packages/server/database/types/NotificationKickedOut.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import Notification from './Notification'
-
-interface Input {
-  teamId: string
-  userId: string
-  evictorUserId: string
-}
-
-export default class NotificationKickedOut extends Notification {
-  readonly type = 'KICKED_OUT'
-  teamId: string
-  evictorUserId: string
-  constructor(input: Input) {
-    const {evictorUserId, teamId, userId} = input
-    super({userId, type: 'KICKED_OUT'})
-    this.teamId = teamId
-    this.evictorUserId = evictorUserId
-  }
-}
diff --git a/packages/server/database/types/NotificationMeetingStageTimeLimitEnd.ts b/packages/server/database/types/NotificationMeetingStageTimeLimitEnd.ts
deleted file mode 100644
index 79e245fb0e5..00000000000
--- a/packages/server/database/types/NotificationMeetingStageTimeLimitEnd.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import Notification from './Notification'
-
-interface Input {
-  meetingId: string
-  userId: string
-}
-
-export default class NotificationMeetingStageTimeLimitEnd extends Notification {
-  readonly type = 'MEETING_STAGE_TIME_LIMIT_END'
-  meetingId: string
-  constructor(input: Input) {
-    const {meetingId, userId} = input
-    super({userId, type: 'MEETING_STAGE_TIME_LIMIT_END'})
-    this.meetingId = meetingId
-  }
-}
diff --git a/packages/server/database/types/NotificationMentioned.ts b/packages/server/database/types/NotificationMentioned.ts
deleted file mode 100644
index c35dff270bb..00000000000
--- a/packages/server/database/types/NotificationMentioned.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import Notification from './Notification'
-
-interface Input {
-  userId: string
-  senderName: string | null
-  senderPicture: string | null
-  senderUserId: string
-  meetingName: string
-  meetingId: string
-  retroReflectionId?: string | null
-  retroDiscussStageIdx?: number | null
-}
-
-// TODO: replace NotificationResponseMentioned and NotificationResponseReplied with NotificationMentioned
-export default class NotificationMentioned extends Notification {
-  readonly type = 'MENTIONED'
-  senderName: string | null
-  senderPicture: string | null
-  senderUserId: string
-  meetingName: string
-  meetingId: string
-  retroReflectionId?: string | null
-  retroDiscussStageIdx?: number | null
-
-  constructor(input: Input) {
-    const {
-      userId,
-      senderName,
-      senderPicture,
-      senderUserId,
-      meetingName,
-      meetingId,
-      retroReflectionId,
-      retroDiscussStageIdx
-    } = input
-    super({userId, type: 'MENTIONED'})
-    this.senderName = senderName
-    this.senderPicture = senderPicture
-    this.senderUserId = senderUserId
-    this.meetingName = meetingName
-    this.meetingId = meetingId
-    this.retroReflectionId = retroReflectionId
-    this.retroDiscussStageIdx = retroDiscussStageIdx
-  }
-}
diff --git a/packages/server/database/types/NotificationPaymentRejected.ts b/packages/server/database/types/NotificationPaymentRejected.ts
deleted file mode 100644
index 707a65f5634..00000000000
--- a/packages/server/database/types/NotificationPaymentRejected.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import Notification from './Notification'
-
-interface Input {
-  orgId: string
-  last4: string | number
-  brand: string
-  userId: string
-}
-
-export default class NotificationPaymentRejected extends Notification {
-  readonly type = 'PAYMENT_REJECTED'
-  orgId: string
-  last4: number
-  brand: string
-
-  constructor(input: Input) {
-    const {orgId, last4, brand, userId} = input
-    super({userId, type: 'PAYMENT_REJECTED'})
-    this.orgId = orgId
-    this.last4 = Number(last4)
-    this.brand = brand
-  }
-}
diff --git a/packages/server/database/types/NotificationPromoteToBillingLeader.ts b/packages/server/database/types/NotificationPromoteToBillingLeader.ts
deleted file mode 100644
index adbaed292d7..00000000000
--- a/packages/server/database/types/NotificationPromoteToBillingLeader.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import Notification from './Notification'
-
-interface Input {
-  orgId: string
-  userId: string
-}
-
-export default class NotificationPromoteToBillingLeader extends Notification {
-  readonly type = 'PROMOTE_TO_BILLING_LEADER'
-  orgId: string
-
-  constructor(input: Input) {
-    const {orgId, userId} = input
-    super({userId, type: 'PROMOTE_TO_BILLING_LEADER'})
-    this.orgId = orgId
-  }
-}
diff --git a/packages/server/database/types/NotificationPromptToJoinOrg.ts b/packages/server/database/types/NotificationPromptToJoinOrg.ts
deleted file mode 100644
index 010813bb191..00000000000
--- a/packages/server/database/types/NotificationPromptToJoinOrg.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import Notification from './Notification'
-
-interface Input {
-  activeDomain: string
-  userId: string
-}
-
-export default class NotificationPromptToJoinOrg extends Notification {
-  readonly type = 'PROMPT_TO_JOIN_ORG'
-  activeDomain: string
-  constructor(input: Input) {
-    const {userId, activeDomain} = input
-    super({userId, type: 'PROMPT_TO_JOIN_ORG'})
-    this.activeDomain = activeDomain
-  }
-}
diff --git a/packages/server/database/types/NotificationRequestToJoinOrg.ts b/packages/server/database/types/NotificationRequestToJoinOrg.ts
deleted file mode 100644
index bb9c56e0695..00000000000
--- a/packages/server/database/types/NotificationRequestToJoinOrg.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import Notification from './Notification'
-
-interface Input {
-  domainJoinRequestId: number
-  email: string
-  name: string
-  picture: string
-  userId: string
-  requestCreatedBy: string
-}
-
-export default class NotificationRequestToJoinOrg extends Notification {
-  readonly type = 'REQUEST_TO_JOIN_ORG'
-  domainJoinRequestId: number
-  email: string
-  name: string
-  picture: string
-  requestCreatedBy: string
-  constructor(input: Input) {
-    const {domainJoinRequestId, requestCreatedBy, email, name, picture, userId} = input
-    super({userId, type: 'REQUEST_TO_JOIN_ORG'})
-    this.domainJoinRequestId = domainJoinRequestId
-    this.email = email
-    this.name = name
-    this.picture = picture
-    this.requestCreatedBy = requestCreatedBy
-  }
-}
diff --git a/packages/server/database/types/NotificationResponseMentioned.ts b/packages/server/database/types/NotificationResponseMentioned.ts
deleted file mode 100644
index 3368b54babb..00000000000
--- a/packages/server/database/types/NotificationResponseMentioned.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import Notification from './Notification'
-
-interface Input {
-  responseId: string
-  meetingId: string
-  userId: string
-}
-
-export default class NotificationResponseMentioned extends Notification {
-  readonly type = 'RESPONSE_MENTIONED'
-  responseId: string
-  meetingId: string
-
-  constructor(input: Input) {
-    const {responseId, meetingId, userId} = input
-    super({userId, type: 'RESPONSE_MENTIONED'})
-    this.responseId = responseId
-    this.meetingId = meetingId
-  }
-}
diff --git a/packages/server/database/types/NotificationResponseReplied.ts b/packages/server/database/types/NotificationResponseReplied.ts
deleted file mode 100644
index 8f254a43990..00000000000
--- a/packages/server/database/types/NotificationResponseReplied.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import Notification from './Notification'
-
-interface Input {
-  meetingId: string
-  authorId: string
-  userId: string
-  commentId: string
-}
-
-export default class NotificationResponseReplied extends Notification {
-  readonly type = 'RESPONSE_REPLIED'
-  meetingId: string
-  authorId: string
-  commentId: string
-
-  constructor(input: Input) {
-    const {meetingId, authorId, userId, commentId} = input
-    super({userId, type: 'RESPONSE_REPLIED'})
-    this.meetingId = meetingId
-    this.authorId = authorId
-    this.commentId = commentId
-  }
-}
diff --git a/packages/server/database/types/NotificationTaskInvolves.ts b/packages/server/database/types/NotificationTaskInvolves.ts
deleted file mode 100644
index 44fd3e83b09..00000000000
--- a/packages/server/database/types/NotificationTaskInvolves.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import Notification from './Notification'
-
-export type TaskInvolvement = 'ASSIGNEE' | 'MENTIONEE'
-
-interface Input {
-  changeAuthorId: string
-  involvement: TaskInvolvement
-  taskId: string
-  teamId: string
-  userId: string
-}
-
-export default class NotificationTaskInvolves extends Notification {
-  readonly type = 'TASK_INVOLVES'
-  changeAuthorId: string
-  involvement: TaskInvolvement
-  taskId: string
-  teamId: string
-
-  constructor(input: Input) {
-    const {teamId, changeAuthorId, involvement, taskId, userId} = input
-    super({userId, type: 'TASK_INVOLVES'})
-    this.changeAuthorId = changeAuthorId
-    this.involvement = involvement
-    this.taskId = taskId
-    this.teamId = teamId
-  }
-}
diff --git a/packages/server/database/types/NotificationTeamArchived.ts b/packages/server/database/types/NotificationTeamArchived.ts
deleted file mode 100644
index 3b48290d046..00000000000
--- a/packages/server/database/types/NotificationTeamArchived.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import Notification from './Notification'
-
-interface Input {
-  archivorUserId: string
-  teamId: string
-  userId: string
-}
-
-export default class NotificationTeamArchived extends Notification {
-  readonly type = 'TEAM_ARCHIVED'
-  archivorUserId: string
-  teamId: string
-  constructor(input: Input) {
-    const {archivorUserId, teamId, userId} = input
-    super({userId, type: 'TEAM_ARCHIVED'})
-    this.archivorUserId = archivorUserId
-    this.teamId = teamId
-  }
-}
diff --git a/packages/server/database/types/NotificationTeamInvitation.ts b/packages/server/database/types/NotificationTeamInvitation.ts
deleted file mode 100644
index e0995ae8393..00000000000
--- a/packages/server/database/types/NotificationTeamInvitation.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import Notification from './Notification'
-
-interface Input {
-  invitationId: string
-  teamId: string
-  userId: string
-}
-
-export default class NotificationTeamInvitation extends Notification {
-  readonly type = 'TEAM_INVITATION'
-  invitationId: string
-  teamId: string
-  constructor(input: Input) {
-    const {invitationId, teamId, userId} = input
-    super({userId, type: 'TEAM_INVITATION'})
-    this.invitationId = invitationId
-    this.teamId = teamId
-  }
-}
diff --git a/packages/server/database/types/NotificationTeamsLimitExceeded.ts b/packages/server/database/types/NotificationTeamsLimitExceeded.ts
deleted file mode 100644
index 8a070846a9c..00000000000
--- a/packages/server/database/types/NotificationTeamsLimitExceeded.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import Notification from './Notification'
-
-interface Input {
-  orgId: string
-  orgName: string
-  orgPicture?: string | null
-  userId: string
-}
-
-export default class NotificationTeamsLimitExceeded extends Notification {
-  readonly type = 'TEAMS_LIMIT_EXCEEDED'
-  orgId: string
-  orgName: string
-  orgPicture?: string | null
-  constructor(input: Input) {
-    const {userId, orgId, orgName, orgPicture} = input
-    super({userId, type: 'TEAMS_LIMIT_EXCEEDED'})
-    this.orgId = orgId
-    this.orgName = orgName
-    this.orgPicture = orgPicture
-  }
-}
diff --git a/packages/server/database/types/NotificationTeamsLimitReminder.ts b/packages/server/database/types/NotificationTeamsLimitReminder.ts
deleted file mode 100644
index a03c33edb9c..00000000000
--- a/packages/server/database/types/NotificationTeamsLimitReminder.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import Notification from './Notification'
-
-interface Input {
-  orgId: string
-  orgName: string
-  orgPicture?: string | null
-  userId: string
-  scheduledLockAt: Date
-}
-
-export default class NotificationTeamsLimitReminder extends Notification {
-  readonly type = 'TEAMS_LIMIT_REMINDER'
-  orgId: string
-  orgName: string
-  orgPicture?: string | null
-  scheduledLockAt: Date
-  constructor(input: Input) {
-    const {userId, orgId, orgName, orgPicture, scheduledLockAt} = input
-    super({userId, type: 'TEAMS_LIMIT_REMINDER'})
-    this.orgId = orgId
-    this.scheduledLockAt = scheduledLockAt
-    this.orgName = orgName
-    this.orgPicture = orgPicture
-  }
-}
diff --git a/packages/server/database/types/processTeamsLimitsJob.ts b/packages/server/database/types/processTeamsLimitsJob.ts
index bead3705f80..58c5bde0dd8 100644
--- a/packages/server/database/types/processTeamsLimitsJob.ts
+++ b/packages/server/database/types/processTeamsLimitsJob.ts
@@ -1,10 +1,9 @@
-import {r} from 'rethinkdb-ts'
 import sendTeamsLimitEmail from '../../billing/helpers/sendTeamsLimitEmail'
+import generateUID from '../../generateUID'
 import {DataLoaderWorker} from '../../graphql/graphql'
 import isValid from '../../graphql/isValid'
 import publishNotification from '../../graphql/public/mutations/helpers/publishNotification'
 import getKysely from '../../postgres/getKysely'
-import NotificationTeamsLimitReminder from './NotificationTeamsLimitReminder'
 import ScheduledTeamLimitsJob from './ScheduledTeamLimitsJob'
 
 const processTeamsLimitsJob = async (job: ScheduledTeamLimitsJob, dataLoader: DataLoaderWorker) => {
@@ -35,17 +34,16 @@ const processTeamsLimitsJob = async (job: ScheduledTeamLimitsJob, dataLoader: Da
       .execute()
     organization.lockedAt = lockedAt
   } else if (type === 'WARN_ORGANIZATION') {
-    const notificationsToInsert = billingLeadersIds.map((userId) => {
-      return new NotificationTeamsLimitReminder({
-        userId,
-        orgId,
-        orgName,
-        orgPicture,
-        scheduledLockAt
-      })
-    })
+    const notificationsToInsert = billingLeadersIds.map((userId) => ({
+      id: generateUID(),
+      type: 'TEAMS_LIMIT_REMINDER' as const,
+      userId,
+      orgId,
+      orgName,
+      orgPicture,
+      scheduledLockAt
+    }))
 
-    await r.table('Notification').insert(notificationsToInsert).run()
     await getKysely().insertInto('Notification').values(notificationsToInsert).execute()
     const operationId = dataLoader.share()
     const subOptions = {operationId}
diff --git a/packages/server/dataloader/LocalCache.ts b/packages/server/dataloader/LocalCache.ts
index 90bbd22fb7c..ab73c74efa0 100644
--- a/packages/server/dataloader/LocalCache.ts
+++ b/packages/server/dataloader/LocalCache.ts
@@ -1,6 +1,6 @@
 import {DBType} from '../database/rethinkDriver'
 import RedisCache, {CacheType} from './RedisCache'
-import {RWrite, Updater} from './RethinkDBCache'
+import {Updater} from './RethinkDBCache'
 
 const resolvedPromise = Promise.resolve()
 
@@ -95,7 +95,7 @@ export default class LocalCache<T extends keyof CacheType> {
     writes.forEach(({resolve, table, id}, idx) => {
       const key = `${table}:${id}`
       const result = results[idx]
-      this.primeLocal(key, result)
+      this.primeLocal(key, result!)
       resolve(result)
     })
   }
@@ -107,7 +107,7 @@ export default class LocalCache<T extends keyof CacheType> {
   }
 
   async prime(table: T, docs: CacheType[T][]) {
-    docs.forEach((doc) => {
+    docs.forEach((doc: any) => {
       const key = `${table}:${doc.id}`
       this.primeLocal(key, doc)
     })
@@ -149,10 +149,10 @@ export default class LocalCache<T extends keyof CacheType> {
     })
     return Promise.all(loadPromises)
   }
-  async write<P extends T>(table: P, id: string, updater: Updater<CacheType[P]>) {
+  async write<P extends T>(table: P, id: string, updater: any) {
     if (this.hasWriteDispatched) {
       this.hasWriteDispatched = false
-      this.writes = [] as (RWrite<CacheType[P]> & {resolve: (payload: any) => void})[]
+      this.writes = [] as any[]
       resolvedPromise.then(() => {
         process.nextTick(this.dispatchWriteBatch)
       })
diff --git a/packages/server/dataloader/NullableDataLoader.ts b/packages/server/dataloader/NullableDataLoader.ts
index 0cfb4393e1a..1e3fc17b064 100644
--- a/packages/server/dataloader/NullableDataLoader.ts
+++ b/packages/server/dataloader/NullableDataLoader.ts
@@ -17,16 +17,16 @@ class NullableDataLoader<Key, Value, CacheKey = Key> extends UpdatableCacheDataL
     super(batchLoadFn, options)
   }
 
-  load<NarrowType extends Value>(key: Key) {
-    return super.load(key) as Promise<(Value & NarrowType) | undefined>
+  load<NarrowType = Value>(key: Key) {
+    return super.load(key) as Promise<NarrowType | undefined>
   }
 
-  async loadNonNull(key: Key): Promise<Value> {
+  async loadNonNull<NarrowType = Value>(key: Key) {
     const value = await this.load(key)
     if (value === undefined) {
       throw new Error('Non-nullable value is undefined')
     }
-    return value
+    return value as NarrowType
   }
 }
 
diff --git a/packages/server/dataloader/RedisCache.ts b/packages/server/dataloader/RedisCache.ts
index 2a2d01f08a3..89d9064d921 100644
--- a/packages/server/dataloader/RedisCache.ts
+++ b/packages/server/dataloader/RedisCache.ts
@@ -10,7 +10,7 @@ export type RedisType = {
   [P in keyof typeof customRedisQueries]: Unpromise<ReturnType<(typeof customRedisQueries)[P]>>[0]
 }
 
-export type CacheType = RedisType & DBType
+export type CacheType = RedisType
 
 const TTL = ms('3h')
 
@@ -134,7 +134,7 @@ export default class RedisCache<T extends keyof CacheType> {
     return this.getRedis().del(key)
   }
   prime = async (table: T, docs: CacheType[T][]) => {
-    const writes = docs.map((doc) => {
+    const writes = docs.map((doc: any) => {
       return msetpx(`${table}:${doc.id}`, doc)
     })
     await this.getRedis().multi(writes).exec()
diff --git a/packages/server/dataloader/RethinkPrimaryKeyLoaderMaker.ts b/packages/server/dataloader/RethinkPrimaryKeyLoaderMaker.ts
deleted file mode 100644
index c8d21cb3548..00000000000
--- a/packages/server/dataloader/RethinkPrimaryKeyLoaderMaker.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import {DBType} from '../database/rethinkDriver'
-
-/**
- * Used to register rethink types in the dataloader
- */
-export default class RethinkPrimaryKeyLoaderMaker<T extends keyof DBType> {
-  constructor(public table: T) {}
-}
diff --git a/packages/server/dataloader/RootDataLoader.ts b/packages/server/dataloader/RootDataLoader.ts
index 9bf6bd6a34e..587d3e67826 100644
--- a/packages/server/dataloader/RootDataLoader.ts
+++ b/packages/server/dataloader/RootDataLoader.ts
@@ -1,5 +1,4 @@
 import DataLoader from 'dataloader'
-import RethinkPrimaryKeyLoaderMaker from './RethinkPrimaryKeyLoaderMaker'
 import * as atlassianLoaders from './atlassianLoaders'
 import * as azureDevOpsLoaders from './azureDevOpsLoaders'
 import * as customLoaderMakers from './customLoaderMakers'
@@ -11,8 +10,6 @@ import * as integrationAuthLoaders from './integrationAuthLoaders'
 import * as jiraServerLoaders from './jiraServerLoaders'
 import * as pollLoaders from './pollsLoaders'
 import * as primaryKeyLoaderMakers from './primaryKeyLoaderMakers'
-import rethinkPrimaryKeyLoader from './rethinkPrimaryKeyLoader'
-import * as rethinkPrimaryKeyLoaderMakers from './rethinkPrimaryKeyLoaderMakers'
 
 interface LoaderDict {
   [loaderName: string]: DataLoader<any, any>
@@ -20,13 +17,11 @@ interface LoaderDict {
 
 // Register all loaders
 const loaderMakers = {
-  ...rethinkPrimaryKeyLoaderMakers,
   ...primaryKeyLoaderMakers,
   ...foreignKeyLoaderMakers,
   ...customLoaderMakers,
   ...atlassianLoaders,
   ...jiraServerLoaders,
-  ...customLoaderMakers,
   ...githubLoaders,
   ...gitlabLoaders,
   ...gcalLoaders,
@@ -37,9 +32,8 @@ const loaderMakers = {
 
 export type Loaders = keyof typeof loaderMakers
 
-export type AllPrimaryLoaders =
-  | keyof typeof primaryKeyLoaderMakers
-  | keyof typeof rethinkPrimaryKeyLoaderMakers
+export type AllPrimaryLoaders = keyof typeof primaryKeyLoaderMakers
+
 export type RegisterDependsOn = (primaryLoaders: AllPrimaryLoaders | AllPrimaryLoaders[]) => void
 
 // The RethinkDB logic is a leaky abstraction! It will be gone soon & this will be generic enough to put in its own package
@@ -47,12 +41,7 @@ interface GenericDataLoader<TLoaders, TPrimaryLoaderNames> {
   clearAll(pkLoaderName: TPrimaryLoaderNames | TPrimaryLoaderNames[]): void
   get<LoaderName extends keyof TLoaders, Loader extends TLoaders[LoaderName]>(
     loaderName: LoaderName
-  ): Loader extends (...args: any[]) => any
-    ? ReturnType<Loader>
-    : // can delete below this line after RethinkDB is gone
-      Loader extends RethinkPrimaryKeyLoaderMaker<infer U>
-      ? ReturnType<typeof rethinkPrimaryKeyLoader<U>>
-      : never
+  ): Loader extends (...args: any[]) => any ? ReturnType<Loader> : never
 }
 
 export type DataLoaderInstance = GenericDataLoader<typeof loaderMakers, AllPrimaryLoaders>
@@ -88,12 +77,7 @@ export default class RootDataLoader<
         ;(this.dependentLoaders[primaryLoader] ??= []).push(loaderName)
       })
     }
-    if (loaderMaker instanceof RethinkPrimaryKeyLoaderMaker) {
-      const {table} = loaderMaker
-      loader = rethinkPrimaryKeyLoader(this.dataLoaderOptions, table)
-    } else {
-      loader = (loaderMaker as any)(this, dependsOn)
-    }
+    loader = (loaderMaker as any)(this, dependsOn)
     this.loaders[loaderName] = loader!
     return loader as any
   }
diff --git a/packages/server/dataloader/foreignKeyLoaderMaker.ts b/packages/server/dataloader/foreignKeyLoaderMaker.ts
index 88594e3d8e2..69e042e8aa7 100644
--- a/packages/server/dataloader/foreignKeyLoaderMaker.ts
+++ b/packages/server/dataloader/foreignKeyLoaderMaker.ts
@@ -1,4 +1,5 @@
 import DataLoader from 'dataloader'
+import NullableDataLoader from './NullableDataLoader'
 import RootDataLoader, {RegisterDependsOn} from './RootDataLoader'
 import UpdatableCacheDataLoader from './UpdatableCacheDataLoader'
 import * as primaryKeyLoaderMakers from './primaryKeyLoaderMakers'
@@ -7,7 +8,7 @@ type LoaderMakers = typeof primaryKeyLoaderMakers
 type LoaderKeys = keyof LoaderMakers
 type Loader<LoaderName extends LoaderKeys> = ReturnType<LoaderMakers[LoaderName]>
 type LoaderType<LoaderName extends LoaderKeys> =
-  Loader<LoaderName> extends DataLoader<any, infer T, any> ? NonNullable<T> : any
+  Loader<LoaderName> extends NullableDataLoader<any, infer T, any> ? NonNullable<T> : any
 
 /**
  * Used to register loaders for types by foreign key.
@@ -16,6 +17,7 @@ type LoaderType<LoaderName extends LoaderKeys> =
  * When an item is loaded via this loader, the primary loader will be primed with the result as well.
  * It reflects a one to many relationship, i.e. for each key passed, an array will be returned.
  */
+
 export function foreignKeyLoaderMaker<
   LoaderName extends LoaderKeys,
   T extends LoaderType<LoaderName>,
diff --git a/packages/server/dataloader/getLoaderNameByTable.ts b/packages/server/dataloader/getLoaderNameByTable.ts
deleted file mode 100644
index 0449dd2a631..00000000000
--- a/packages/server/dataloader/getLoaderNameByTable.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import * as rethinkPrimaryKeyLoaderMakers from './rethinkPrimaryKeyLoaderMakers'
-
-const loadersByTable = {} as Record<string, any>
-Object.keys(rethinkPrimaryKeyLoaderMakers).forEach((loaderName) => {
-  const loader =
-    rethinkPrimaryKeyLoaderMakers[loaderName as keyof typeof rethinkPrimaryKeyLoaderMakers]
-  loadersByTable[loader.table] = loaderName
-})
-
-const getLoaderNameByTable = (table: string) => {
-  return loadersByTable[table]
-}
-
-export default getLoaderNameByTable
diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts
index 47ee113669d..2e0c20e33a0 100644
--- a/packages/server/dataloader/primaryKeyLoaderMakers.ts
+++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts
@@ -13,6 +13,7 @@ import {
   selectMeetingSettings,
   selectNewFeatures,
   selectNewMeetings,
+  selectNotifications,
   selectOrganizations,
   selectReflectPrompts,
   selectRetroReflections,
@@ -149,3 +150,7 @@ export const teamInvitations = primaryKeyLoaderMaker((ids: readonly string[]) =>
 export const tasks = primaryKeyLoaderMaker((ids: readonly string[]) => {
   return selectTasks().where('id', 'in', ids).execute()
 })
+
+export const notifications = primaryKeyLoaderMaker((ids: readonly string[]) => {
+  return selectNotifications().where('id', 'in', ids).execute()
+})
diff --git a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts
deleted file mode 100644
index ec24d52c90a..00000000000
--- a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import RethinkPrimaryKeyLoaderMaker from './RethinkPrimaryKeyLoaderMaker'
-
-/**
- * all rethink dataloader types which also must exist in {@link rethinkDriver/RethinkSchema}
- */
-export const notifications = new RethinkPrimaryKeyLoaderMaker('Notification')
diff --git a/packages/server/graphql/mutations/archiveTeam.ts b/packages/server/graphql/mutations/archiveTeam.ts
index edfca0f7108..c9ccd474666 100644
--- a/packages/server/graphql/mutations/archiveTeam.ts
+++ b/packages/server/graphql/mutations/archiveTeam.ts
@@ -2,8 +2,7 @@ import {GraphQLID, GraphQLNonNull, GraphQLObjectType} from 'graphql'
 import {SubscriptionChannel} from 'parabol-client/types/constEnums'
 import TeamMemberId from '../../../client/shared/gqlIds/TeamMemberId'
 import {maybeRemoveRestrictions} from '../../billing/helpers/teamLimitsCheck'
-import getRethink from '../../database/rethinkDriver'
-import NotificationTeamArchived from '../../database/types/NotificationTeamArchived'
+import generateUID from '../../generateUID'
 import getKysely from '../../postgres/getKysely'
 import removeMeetingTemplatesForTeam from '../../postgres/queries/removeMeetingTemplatesForTeam'
 import safeArchiveTeam from '../../safeMutations/safeArchiveTeam'
@@ -32,7 +31,6 @@ export default {
     {authToken, dataLoader, socketId: mutatorId}: GQLContext
   ) {
     const pg = getKysely()
-    const r = await getRethink()
     const operationId = dataLoader.share()
     const subOptions = {operationId, mutatorId}
 
@@ -65,13 +63,15 @@ export default {
     const notifications = users
       .map((user) => user?.id)
       .filter((userId) => userId !== undefined && userId !== viewerId)
-      .map(
-        (notifiedUserId) =>
-          new NotificationTeamArchived({userId: notifiedUserId!, teamId, archivorUserId: viewerId})
-      )
+      .map((notifiedUserId) => ({
+        id: generateUID(),
+        type: 'TEAM_ARCHIVED' as const,
+        userId: notifiedUserId!,
+        teamId,
+        archivorUserId: viewerId
+      }))
 
     if (notifications.length) {
-      await r.table('Notification').insert(notifications).run()
       await pg.insertInto('Notification').values(notifications).execute()
     }
 
diff --git a/packages/server/graphql/mutations/createTask.ts b/packages/server/graphql/mutations/createTask.ts
index a6013bc1c54..98958388cd4 100644
--- a/packages/server/graphql/mutations/createTask.ts
+++ b/packages/server/graphql/mutations/createTask.ts
@@ -1,4 +1,5 @@
 import {GraphQLNonNull, GraphQLObjectType, GraphQLResolveInfo} from 'graphql'
+import {Insertable} from 'kysely'
 import {SubscriptionChannel} from 'parabol-client/types/constEnums'
 import getTypeFromEntityMap from 'parabol-client/utils/draftjs/getTypeFromEntityMap'
 import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId'
@@ -7,11 +8,10 @@ import MeetingMemberId from '../../../client/shared/gqlIds/MeetingMemberId'
 import dndNoise from '../../../client/utils/dndNoise'
 import extractTextFromDraftString from '../../../client/utils/draftjs/extractTextFromDraftString'
 import getTagsFromEntityMap from '../../../client/utils/draftjs/getTagsFromEntityMap'
-import getRethink from '../../database/rethinkDriver'
-import NotificationTaskInvolves from '../../database/types/NotificationTaskInvolves'
 import generateUID from '../../generateUID'
 import updatePrevUsedRepoIntegrationsCache from '../../integrations/updatePrevUsedRepoIntegrationsCache'
 import getKysely from '../../postgres/getKysely'
+import {Notification} from '../../postgres/pg'
 import {Task, TaskTag} from '../../postgres/types/index.d'
 import {TaskServiceEnum} from '../../postgres/types/TaskIntegration'
 import {analytics} from '../../utils/analytics/analytics'
@@ -69,24 +69,23 @@ const handleAddTaskNotifications = async (
   subOptions: SubOptions
 ) => {
   const pg = getKysely()
-  const r = await getRethink()
   const {id: taskId, content, tags, userId} = task
   const usersIdsToIgnore = await getUsersToIgnore(viewerId, teamId)
 
   // Handle notifications
   // Almost always you start out with a blank card assigned to you (except for filtered team dash)
   const changeAuthorId = toTeamMemberId(teamId, viewerId)
-  const notificationsToAdd = [] as NotificationTaskInvolves[]
+  const notificationsToAdd = [] as Insertable<Notification>[]
   if (userId && viewerId !== userId && !usersIdsToIgnore.includes(userId)) {
-    notificationsToAdd.push(
-      new NotificationTaskInvolves({
-        involvement: 'ASSIGNEE',
-        taskId,
-        changeAuthorId,
-        teamId,
-        userId
-      })
-    )
+    notificationsToAdd.push({
+      id: generateUID(),
+      type: 'TASK_INVOLVES' as const,
+      involvement: 'ASSIGNEE' as const,
+      taskId,
+      changeAuthorId,
+      teamId,
+      userId
+    })
   }
 
   const {entityMap} = JSON.parse(content)
@@ -95,20 +94,19 @@ const handleAddTaskNotifications = async (
       (mention) => mention !== viewerId && mention !== userId && !usersIdsToIgnore.includes(mention)
     )
     .forEach((mentioneeUserId) => {
-      notificationsToAdd.push(
-        new NotificationTaskInvolves({
-          userId: mentioneeUserId,
-          involvement: 'MENTIONEE',
-          taskId,
-          changeAuthorId,
-          teamId
-        })
-      )
+      notificationsToAdd.push({
+        id: generateUID(),
+        type: 'TASK_INVOLVES' as const,
+        userId: mentioneeUserId,
+        involvement: 'MENTIONEE',
+        taskId,
+        changeAuthorId,
+        teamId
+      })
     })
   const data = {taskId, notifications: notificationsToAdd}
 
   if (notificationsToAdd.length) {
-    await r.table('Notification').insert(notificationsToAdd).run()
     await pg.insertInto('Notification').values(notificationsToAdd).execute()
     notificationsToAdd.forEach((notification) => {
       publish(
diff --git a/packages/server/graphql/mutations/helpers/handleTeamInviteToken.ts b/packages/server/graphql/mutations/helpers/handleTeamInviteToken.ts
index 8205be8c520..f32aa5781a0 100644
--- a/packages/server/graphql/mutations/helpers/handleTeamInviteToken.ts
+++ b/packages/server/graphql/mutations/helpers/handleTeamInviteToken.ts
@@ -1,6 +1,6 @@
 import {InvitationTokenError} from 'parabol-client/types/constEnums'
-import getRethink from '../../../database/rethinkDriver'
 import getKysely from '../../../postgres/getKysely'
+import {selectNotifications} from '../../../postgres/select'
 
 const handleTeamInviteToken = async (
   invitationToken: string,
@@ -8,7 +8,6 @@ const handleTeamInviteToken = async (
   tms: string[],
   notificationId?: string
 ) => {
-  const r = await getRethink()
   const pg = getKysely()
   const invitation = await pg
     .selectFrom('TeamInvitation')
@@ -21,9 +20,12 @@ const handleTeamInviteToken = async (
   if (expiresAt.getTime() < Date.now()) {
     // using the notification has no expiry
     const notification = notificationId
-      ? await r.table('Notification').get(notificationId).run()
+      ? await selectNotifications()
+          .where('id', '=', notificationId)
+          .where('userId', '=', viewerId)
+          .executeTakeFirst()
       : undefined
-    if (!notification || notification.userId !== viewerId) {
+    if (!notification) {
       return {error: InvitationTokenError.EXPIRED}
     }
   }
diff --git a/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts b/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts
index e617f31810c..319c90e78fd 100644
--- a/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts
+++ b/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts
@@ -5,8 +5,6 @@ import {EMAIL_CORS_OPTIONS} from '../../../../client/types/cors'
 import makeAppURL from '../../../../client/utils/makeAppURL'
 import {isNotNull} from '../../../../client/utils/predicates'
 import appOrigin from '../../../appOrigin'
-import getRethink from '../../../database/rethinkDriver'
-import NotificationTeamInvitation from '../../../database/types/NotificationTeamInvitation'
 import getMailManager from '../../../email/getMailManager'
 import teamInviteEmailCreator from '../../../email/teamInviteEmailCreator'
 import generateUID from '../../../generateUID'
@@ -34,7 +32,6 @@ const inviteToTeamHelper = async (
 ) => {
   const {authToken, dataLoader, socketId: mutatorId} = context
   const viewerId = getUserId(authToken)
-  const r = await getRethink()
   const pg = getKysely()
   const operationId = dataLoader.share()
   const subOptions = {mutatorId, operationId}
@@ -139,21 +136,21 @@ const inviteToTeamHelper = async (
     removedSuggestedActionId = await removeSuggestedAction(viewerId, 'inviteYourTeam')
   }
   // insert notification records
-  const notificationsToInsert = [] as NotificationTeamInvitation[]
-  teamInvitationsToInsert.forEach((invitation) => {
-    const user = users.find((user) => user.email === invitation.email)
-    if (user) {
-      notificationsToInsert.push(
-        new NotificationTeamInvitation({
-          userId: user.id,
-          invitationId: invitation.id,
-          teamId
-        })
-      )
-    }
-  })
+  const notificationsToInsert = teamInvitationsToInsert
+    .map((invitation) => {
+      const user = users.find((user) => user.email === invitation.email)
+      if (!user) return null
+      return {
+        id: generateUID(),
+        type: 'TEAM_INVITATION' as const,
+        userId: user.id,
+        invitationId: invitation.id,
+        teamId
+      }
+    })
+    .filter(isValid)
+
   if (notificationsToInsert.length > 0) {
-    await r.table('Notification').insert(notificationsToInsert).run()
     await pg.insertInto('Notification').values(notificationsToInsert).execute()
   }
 
diff --git a/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts b/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts
index aa64b773863..d21f0c7653b 100644
--- a/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts
+++ b/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts
@@ -277,8 +277,7 @@ const getSlackMessageForNotification = async (
       buttonText: 'See the discussion'
     }
   } else if (notification.type === 'RESPONSE_MENTIONED') {
-    // Notification Phase 3 do not split the responseId
-    const responseId = TeamPromptResponseId.split(notification.responseId)
+    const responseId = notification.responseId
     const response = await dataLoader.get('teamPromptResponses').loadNonNull(responseId)
     const author = await dataLoader.get('users').loadNonNull(response.userId)
     const title = `*${author.preferredName}* mentioned you in their response in *${meeting.name}*`
@@ -288,7 +287,7 @@ const getSlackMessageForNotification = async (
         utm_source: 'slack standup notification',
         utm_medium: 'product',
         utm_campaign: 'notifications',
-        responseId: notification.responseId
+        responseId: TeamPromptResponseId.join(notification.responseId)
       }
     }
 
@@ -300,6 +299,8 @@ const getSlackMessageForNotification = async (
       buttonText: 'See their response'
     }
   } else if (notification.type === 'MENTIONED') {
+    // This type is no longer created anywhere in the app but is still in the DB.
+    // We should remove this logic & the remaining DB notifications
     const authorName = notification.senderName ?? 'Someone'
     const {meetingId} = notification
 
@@ -682,7 +683,7 @@ export const SlackNotifier = {
     notificationId: string,
     userId: string
   ) {
-    const notification = await dataLoader.get('notifications').load(notificationId)
+    const notification = await dataLoader.get('notifications').loadNonNull(notificationId)
     if (
       notification.type !== 'RESPONSE_MENTIONED' &&
       notification.type !== 'RESPONSE_REPLIED' &&
diff --git a/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts b/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts
index b1e75a9b2d5..17851cdbb78 100644
--- a/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts
+++ b/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts
@@ -1,9 +1,9 @@
-import {ASSIGNEE, MENTIONEE} from 'parabol-client/utils/constants'
 import getTypeFromEntityMap from 'parabol-client/utils/draftjs/getTypeFromEntityMap'
-import getRethink from '../../../database/rethinkDriver'
-import NotificationTaskInvolves from '../../../database/types/NotificationTaskInvolves'
+import generateUID from '../../../generateUID'
 import getKysely from '../../../postgres/getKysely'
+import {selectNotifications} from '../../../postgres/select'
 import {Task} from '../../../postgres/types'
+import {TaskInvolvesNotification} from '../../../postgres/types/Notification'
 import {analytics} from '../../../utils/analytics/analytics'
 
 const publishChangeNotifications = async (
@@ -13,7 +13,6 @@ const publishChangeNotifications = async (
   usersToIgnore: string[]
 ) => {
   const pg = getKysely()
-  const r = await getRethink()
   const changeAuthorId = `${changeUser.id}::${task.teamId}`
   const {entityMap: oldEntityMap, blocks: oldBlocks} = JSON.parse(oldTask.content)
   const {entityMap, blocks} = JSON.parse(task.content)
@@ -35,16 +34,15 @@ const publishChangeNotifications = async (
         // it isn't someone in a meeting
         !usersToIgnore.includes(userId)
     )
-    .map(
-      (userId) =>
-        new NotificationTaskInvolves({
-          userId,
-          involvement: MENTIONEE,
-          taskId: task.id,
-          changeAuthorId,
-          teamId: task.teamId
-        })
-    )
+    .map((userId) => ({
+      id: generateUID(),
+      type: 'TASK_INVOLVES' as const,
+      userId,
+      involvement: 'MENTIONEE' as TaskInvolvesNotification['involvement'],
+      taskId: task.id,
+      changeAuthorId,
+      teamId: task.teamId
+    }))
 
   mentions.forEach((mentionedUserId) => {
     analytics.mentionedOnTask(changeUser, mentionedUserId, task.teamId)
@@ -52,15 +50,15 @@ const publishChangeNotifications = async (
   // add in the assignee changes
   if (oldTask.userId && oldTask.userId !== task.userId) {
     if (task.userId && task.userId !== changeUser.id && !usersToIgnore.includes(task.userId)) {
-      notificationsToAdd.push(
-        new NotificationTaskInvolves({
-          userId: task.userId,
-          involvement: ASSIGNEE,
-          taskId: task.id,
-          changeAuthorId,
-          teamId: task.teamId
-        })
-      )
+      notificationsToAdd.push({
+        id: generateUID(),
+        type: 'TASK_INVOLVES' as const,
+        userId: task.userId,
+        involvement: 'ASSIGNEE' as const,
+        taskId: task.id,
+        changeAuthorId,
+        teamId: task.teamId
+      })
     }
     userIdsToRemove.push(oldTask.userId)
   }
@@ -71,21 +69,18 @@ const publishChangeNotifications = async (
     const contentLen = blocks[0] ? blocks[0].text.length : 0
     if (contentLen > oldContentLen && task.userId) {
       const maybeInvolvedUserIds = mentions.concat(task.userId)
-      const existingTaskNotifications = (await r
-        .table('Notification')
-        .getAll(r.args(maybeInvolvedUserIds), {index: 'userId'})
-        .filter({
-          taskId: task.id,
-          type: 'TASK_INVOLVES'
-        })
-        .run()) as NotificationTaskInvolves[]
+      const existingTaskNotifications = await selectNotifications()
+        .where('userId', 'in', maybeInvolvedUserIds)
+        .where('type', '=', 'TASK_INVOLVES')
+        .where('taskId', '=', task.id)
+        .$narrowType<TaskInvolvesNotification>()
+        .execute()
       notificationsToAdd.push(...existingTaskNotifications)
     }
   }
 
   // update changes in the db
   if (notificationsToAdd.length) {
-    await r.table('Notification').insert(notificationsToAdd).run()
     await pg.insertInto('Notification').values(notificationsToAdd).execute()
   }
   return {notificationsToAdd}
diff --git a/packages/server/graphql/mutations/helpers/removeTeamMember.ts b/packages/server/graphql/mutations/helpers/removeTeamMember.ts
index 3e8e7307962..c98b2955fc0 100644
--- a/packages/server/graphql/mutations/helpers/removeTeamMember.ts
+++ b/packages/server/graphql/mutations/helpers/removeTeamMember.ts
@@ -1,11 +1,10 @@
 import {sql} from 'kysely'
 import fromTeamMemberId from 'parabol-client/utils/relay/fromTeamMemberId'
-import getRethink from '../../../database/rethinkDriver'
 import AgendaItemsStage from '../../../database/types/AgendaItemsStage'
 import CheckInStage from '../../../database/types/CheckInStage'
 import EstimateStage from '../../../database/types/EstimateStage'
-import NotificationKickedOut from '../../../database/types/NotificationKickedOut'
 import UpdatesStage from '../../../database/types/UpdatesStage'
+import generateUID from '../../../generateUID'
 import getKysely from '../../../postgres/getKysely'
 import {selectTasks} from '../../../postgres/select'
 import archiveTasksForDB from '../../../safeMutations/archiveTasksForDB'
@@ -25,7 +24,6 @@ const removeTeamMember = async (
   dataLoader: DataLoaderWorker
 ) => {
   const {evictorUserId} = options
-  const r = await getRethink()
   const pg = getKysely()
   const {userId, teamId} = fromTeamMemberId(teamMemberId)
   // see if they were a leader, make a new guy leader so later we can reassign tasks
@@ -101,9 +99,14 @@ const removeTeamMember = async (
 
   let notificationId: string | undefined
   if (evictorUserId) {
-    const notification = new NotificationKickedOut({teamId, userId, evictorUserId})
+    const notification = {
+      id: generateUID(),
+      type: 'KICKED_OUT' as const,
+      teamId,
+      userId,
+      evictorUserId
+    }
     notificationId = notification.id
-    await r.table('Notification').insert(notification).run()
     await pg.insertInto('Notification').values(notification).execute()
   }
 
diff --git a/packages/server/graphql/mutations/helpers/safeCreateRetrospective.ts b/packages/server/graphql/mutations/helpers/safeCreateRetrospective.ts
index 8a9f2e8ef0e..58e85eb4556 100644
--- a/packages/server/graphql/mutations/helpers/safeCreateRetrospective.ts
+++ b/packages/server/graphql/mutations/helpers/safeCreateRetrospective.ts
@@ -1,5 +1,6 @@
 import MeetingRetrospective from '../../../database/types/MeetingRetrospective'
 import generateUID from '../../../generateUID'
+import getKysely from '../../../postgres/getKysely'
 import {MeetingTypeEnum, RetrospectiveMeeting} from '../../../postgres/types/Meeting'
 import {RetroMeetingPhase} from '../../../postgres/types/NewMeetingPhase'
 import {DataLoaderWorker} from '../../graphql'
@@ -20,6 +21,7 @@ const safeCreateRetrospective = async (
   },
   dataLoader: DataLoaderWorker
 ) => {
+  const pg = getKysely()
   const {teamId, facilitatorUserId, name} = meetingSettings
   const meetingType: MeetingTypeEnum = 'retrospective'
   const [meetingCount, team] = await Promise.all([
@@ -40,7 +42,7 @@ const safeCreateRetrospective = async (
     dataLoader
   )
 
-  return new MeetingRetrospective({
+  const meeting = new MeetingRetrospective({
     id: meetingId,
     meetingCount,
     phases,
@@ -48,6 +50,16 @@ const safeCreateRetrospective = async (
     ...meetingSettings,
     name
   }) as RetrospectiveMeeting
+  try {
+    await pg
+      .insertInto('NewMeeting')
+      .values({...meeting, phases: JSON.stringify(meeting.phases)})
+      .execute()
+  } catch (e) {
+    // meeting already started
+    return null
+  }
+  return meeting
 }
 
 export default safeCreateRetrospective
diff --git a/packages/server/graphql/mutations/helpers/safeCreateTeamPrompt.ts b/packages/server/graphql/mutations/helpers/safeCreateTeamPrompt.ts
index 7bb25288232..a123cb02c6f 100644
--- a/packages/server/graphql/mutations/helpers/safeCreateTeamPrompt.ts
+++ b/packages/server/graphql/mutations/helpers/safeCreateTeamPrompt.ts
@@ -15,6 +15,7 @@ const safeCreateTeamPrompt = async (
   dataLoader: DataLoaderWorker,
   meetingOverrideProps = {}
 ) => {
+  const pg = getKysely()
   const meetingType: MeetingTypeEnum = 'teamPrompt'
   const meetingCount = await dataLoader.get('meetingCount').load({teamId, meetingType})
   const meetingId = generateUID()
@@ -22,20 +23,15 @@ const safeCreateTeamPrompt = async (
   const teamMemberIds = teamMembers.map(({id}) => id)
   const teamPromptResponsesPhase = new TeamPromptResponsesPhase(teamMemberIds)
   const {stages: teamPromptStages} = teamPromptResponsesPhase
-  await getKysely()
-    .insertInto('Discussion')
-    .values(
-      teamPromptStages.map((stage) => ({
-        id: stage.discussionId,
-        teamId,
-        meetingId,
-        discussionTopicId: stage.teamMemberId,
-        discussionTopicType: 'teamPromptResponse'
-      }))
-    )
-    .execute()
+  const discussions = teamPromptStages.map((stage) => ({
+    id: stage.discussionId,
+    teamId,
+    meetingId,
+    discussionTopicId: stage.teamMemberId,
+    discussionTopicType: 'teamPromptResponse' as const
+  }))
   primePhases([teamPromptResponsesPhase])
-  return new MeetingTeamPrompt({
+  const meeting = new MeetingTeamPrompt({
     id: meetingId,
     name,
     teamId,
@@ -45,6 +41,22 @@ const safeCreateTeamPrompt = async (
     meetingPrompt: DEFAULT_PROMPT, // :TODO: (jmtaber129): Get this from meeting settings.
     ...meetingOverrideProps
   }) as TeamPromptMeeting
+  try {
+    await pg
+      .insertInto('NewMeeting')
+      .values({...meeting, phases: JSON.stringify(meeting.phases)})
+      .execute()
+  } catch {
+    // can't insert, meeting already exists?
+    return null
+  }
+  await pg
+    .with('DiscussionInsert', (qb) => qb.insertInto('Discussion').values(discussions))
+    .updateTable('Team')
+    .set({lastMeetingType: 'teamPrompt'})
+    .where('id', '=', teamId)
+    .execute()
+  return meeting
 }
 
 export default safeCreateTeamPrompt
diff --git a/packages/server/graphql/mutations/moveTeamToOrg.ts b/packages/server/graphql/mutations/moveTeamToOrg.ts
index 1caf980f8a6..2747ad99b65 100644
--- a/packages/server/graphql/mutations/moveTeamToOrg.ts
+++ b/packages/server/graphql/mutations/moveTeamToOrg.ts
@@ -1,8 +1,6 @@
 import {GraphQLID, GraphQLList, GraphQLNonNull, GraphQLString} from 'graphql'
 import {InvoiceItemType} from 'parabol-client/types/constEnums'
 import adjustUserCount from '../../billing/helpers/adjustUserCount'
-import getRethink from '../../database/rethinkDriver'
-import {RDatum} from '../../database/stricterR'
 import getKysely from '../../postgres/getKysely'
 import safeArchiveEmptyStarterOrganization from '../../safeMutations/safeArchiveEmptyStarterOrganization'
 import {Logger} from '../../utils/Logger'
@@ -19,7 +17,6 @@ const moveToOrg = async (
   authToken: any,
   dataLoader: DataLoaderWorker
 ) => {
-  const r = await getRethink()
   const pg = getKysely()
 
   // AUTH
@@ -87,29 +84,21 @@ const moveToOrg = async (
   const newToOrgUserIds = teamMemberUserIds.filter(
     (userId) => !existingOrgUsers.find((orgUser) => orgUser.userId === userId)
   )
-  await Promise.all([
-    r
-      .table('Notification')
-      .filter({teamId})
-      .filter((notification: RDatum) => notification('orgId').default(null).ne(null))
-      .update({orgId})
-      .run(),
-    pg
-      .with('NotificationUpdate', (qb) =>
-        qb
-          .updateTable('Notification')
-          .set({orgId})
-          .where('teamId', '=', teamId)
-          .where('orgId', 'is not', null)
-      )
-      .with('MeetingTemplateUpdate', (qb) =>
-        qb.updateTable('MeetingTemplate').set({orgId}).where('orgId', '=', currentOrgId)
-      )
-      .updateTable('Team')
-      .set(updates)
-      .where('id', '=', teamId)
-      .execute()
-  ])
+  await pg
+    .with('NotificationUpdate', (qb) =>
+      qb
+        .updateTable('Notification')
+        .set({orgId})
+        .where('teamId', '=', teamId)
+        .where('orgId', 'is not', null)
+    )
+    .with('MeetingTemplateUpdate', (qb) =>
+      qb.updateTable('MeetingTemplate').set({orgId}).where('orgId', '=', currentOrgId)
+    )
+    .updateTable('Team')
+    .set(updates)
+    .where('id', '=', teamId)
+    .execute()
   dataLoader.clearAll('teams')
   // if no teams remain on the org, remove it
   await safeArchiveEmptyStarterOrganization(currentOrgId, dataLoader)
diff --git a/packages/server/graphql/mutations/setNotificationStatus.ts b/packages/server/graphql/mutations/setNotificationStatus.ts
index 21647608b3e..c35988e3986 100644
--- a/packages/server/graphql/mutations/setNotificationStatus.ts
+++ b/packages/server/graphql/mutations/setNotificationStatus.ts
@@ -1,6 +1,5 @@
 import {GraphQLID, GraphQLNonNull, GraphQLObjectType} from 'graphql'
 import {SubscriptionChannel} from 'parabol-client/types/constEnums'
-import getRethink from '../../database/rethinkDriver'
 import getKysely from '../../postgres/getKysely'
 import {getUserId} from '../../utils/authorization'
 import publish from '../../utils/publish'
@@ -29,7 +28,6 @@ export default {
     {authToken, dataLoader, socketId: mutatorId}: GQLContext
   ) {
     const pg = getKysely()
-    const r = await getRethink()
     const operationId = dataLoader.share()
     const subOptions = {mutatorId, operationId}
 
@@ -42,7 +40,6 @@ export default {
     }
 
     // RESOLUTION
-    await r.table('Notification').get(notificationId).update({status}).run()
     await pg.updateTable('Notification').set({status}).where('id', '=', notificationId).execute()
     // mutate dataloader cache
     notification.status = status
diff --git a/packages/server/graphql/private/mutations/__tests__/intranetJobsQuery.test.js b/packages/server/graphql/private/mutations/__tests__/intranetJobsQuery.test.js
index 8b3ceed96c2..80b79c68c13 100644
--- a/packages/server/graphql/private/mutations/__tests__/intranetJobsQuery.test.js
+++ b/packages/server/graphql/private/mutations/__tests__/intranetJobsQuery.test.js
@@ -2,7 +2,6 @@
 import mockAuthToken from '../../../../__tests__/setup/mockAuthToken'
 import MockDB from '../../../../__tests__/setup/MockDB'
 import {__anHourAgo, __now, __overADayAgo} from '../../../../__tests__/setup/mockTimes'
-import getRethink from '../../../../database/rethinkDriver'
 import {sendBatchEmail} from '../../../../email/sendEmail'
 import getKysely from '../../../../postgres/getKysely'
 import sendBatchNotificationEmails from '../sendBatchNotificationEmails'
@@ -19,8 +18,6 @@ describe('sendBatchNotificationEmails', () => {
     // Unfortunately, other tests are not cleaning up after themselves. Since
     // "sending everyone with pending notifications an email" relies on the
     // global DB state, there's no getting around this.
-    const r = await getRethink()
-    await r.table('Notification').delete()
     await sql`TRUNCATE TABLE "Notification"`.execute(getKysely())
   })
 
diff --git a/packages/server/graphql/private/mutations/hardDeleteUser.ts b/packages/server/graphql/private/mutations/hardDeleteUser.ts
index d2f91d82044..e830f4d95dc 100644
--- a/packages/server/graphql/private/mutations/hardDeleteUser.ts
+++ b/packages/server/graphql/private/mutations/hardDeleteUser.ts
@@ -1,4 +1,3 @@
-import getRethink from '../../../database/rethinkDriver'
 import {DataLoaderInstance} from '../../../dataloader/RootDataLoader'
 import getKysely from '../../../postgres/getKysely'
 import {getUserByEmail} from '../../../postgres/queries/getUsersByEmails'
@@ -51,7 +50,6 @@ const hardDeleteUser: MutationResolvers['hardDeleteUser'] = async (
   if (!userId && !email) {
     return {error: {message: 'Provide a userId or email'}}
   }
-  const r = await getRethink()
   const pg = getKysely()
 
   const user = userId ? await getUserById(userId) : email ? await getUserByEmail(email) : null
@@ -84,9 +82,6 @@ const hardDeleteUser: MutationResolvers['hardDeleteUser'] = async (
     .where('teamId', 'in', teamIds)
     .where('createdBy', '=', userIdToDelete)
     .execute()
-  await r({
-    notification: r.table('Notification').getAll(userIdToDelete, {index: 'userId'}).delete()
-  }).run()
 
   // now postgres, after FKs are added then triggers should take care of children
   // TODO when we're done migrating to PG, these should have constraints that ON DELETE CASCADE
diff --git a/packages/server/graphql/private/mutations/processRecurrence.ts b/packages/server/graphql/private/mutations/processRecurrence.ts
index 737ab494653..3ea6b08b9ac 100644
--- a/packages/server/graphql/private/mutations/processRecurrence.ts
+++ b/packages/server/graphql/private/mutations/processRecurrence.ts
@@ -6,7 +6,6 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums'
 import {DateTime, RRuleSet} from 'rrule-rust'
 import TeamMemberId from '../../../../client/shared/gqlIds/TeamMemberId'
 import {fromDateTime, toDateTime} from '../../../../client/shared/rruleUtil'
-import getKysely from '../../../postgres/getKysely'
 import {getActiveMeetingSeries} from '../../../postgres/queries/getActiveMeetingSeries'
 import {selectNewMeetings} from '../../../postgres/select'
 import {RetrospectiveMeeting, TeamPromptMeeting} from '../../../postgres/types/Meeting'
@@ -32,7 +31,6 @@ const startRecurringMeeting = async (
   dataLoader: DataLoaderWorker,
   subOptions: SubOptions
 ) => {
-  const pg = getKysely()
   const {id: meetingSeriesId, teamId, facilitatorId, meetingType} = meetingSeries
 
   // AUTH
@@ -59,10 +57,9 @@ const startRecurringMeeting = async (
         meetingSeriesId: meetingSeries.id,
         meetingPrompt: teamPromptMeeting?.meetingPrompt ?? DEFAULT_PROMPT
       })
-      await pg
-        .insertInto('NewMeeting')
-        .values({...meeting, phases: JSON.stringify(meeting.phases)})
-        .execute()
+      if (!meeting) {
+        return {error: {message: 'Unable to create meeting. Perhaps one was just created?'}}
+      }
       const data = {teamId, meetingId: meeting.id}
       publish(SubscriptionChannel.TEAM, teamId, 'StartTeamPromptSuccess', data, subOptions)
       return meeting
@@ -87,10 +84,9 @@ const startRecurringMeeting = async (
         },
         dataLoader
       )
-      await pg
-        .insertInto('NewMeeting')
-        .values({...meeting, phases: JSON.stringify(meeting.phases)})
-        .execute()
+      if (!meeting) {
+        return {error: {message: 'Unable to create meeting. Perhaps one was just created?'}}
+      }
       const data = {teamId, meetingId: meeting.id}
       publish(SubscriptionChannel.TEAM, teamId, 'StartRetrospectiveSuccess', data, subOptions)
       return meeting
diff --git a/packages/server/graphql/private/mutations/runScheduledJobs.ts b/packages/server/graphql/private/mutations/runScheduledJobs.ts
index 66bf0898eea..465f13bb2df 100644
--- a/packages/server/graphql/private/mutations/runScheduledJobs.ts
+++ b/packages/server/graphql/private/mutations/runScheduledJobs.ts
@@ -1,10 +1,9 @@
 import {Selectable} from 'kysely'
 import {SubscriptionChannel} from 'parabol-client/types/constEnums'
-import getRethink from '../../../database/rethinkDriver'
-import NotificationMeetingStageTimeLimitEnd from '../../../database/types/NotificationMeetingStageTimeLimitEnd'
 import ScheduledJobMeetingStageTimeLimit from '../../../database/types/ScheduledJobMetingStageTimeLimit'
 import ScheduledTeamLimitsJob from '../../../database/types/ScheduledTeamLimitsJob'
 import processTeamsLimitsJob from '../../../database/types/processTeamsLimitsJob'
+import generateUID from '../../../generateUID'
 import getKysely from '../../../postgres/getKysely'
 import {DB} from '../../../postgres/pg'
 import {Logger} from '../../../utils/Logger'
@@ -29,13 +28,13 @@ const processMeetingStageTimeLimits = async (
   const {teamId, facilitatorUserId} = meeting
   IntegrationNotifier.endTimeLimit(dataLoader, meetingId, teamId)
 
-  const notification = new NotificationMeetingStageTimeLimitEnd({
+  const notification = {
+    id: generateUID(),
+    type: 'MEETING_STAGE_TIME_LIMIT_END' as const,
     meetingId,
     userId: facilitatorUserId!
-  })
+  }
   const pg = getKysely()
-  const r = await getRethink()
-  await r.table('Notification').insert(notification).run()
   await pg.insertInto('Notification').values(notification).execute()
   publish(SubscriptionChannel.NOTIFICATION, facilitatorUserId!, 'MeetingStageTimeLimitPayload', {
     notification
diff --git a/packages/server/graphql/private/mutations/sendBatchNotificationEmails.ts b/packages/server/graphql/private/mutations/sendBatchNotificationEmails.ts
index d320cd90028..8df8ea6a54d 100644
--- a/packages/server/graphql/private/mutations/sendBatchNotificationEmails.ts
+++ b/packages/server/graphql/private/mutations/sendBatchNotificationEmails.ts
@@ -1,11 +1,10 @@
 import ms from 'ms'
 import appOrigin from '../../../appOrigin'
-import getRethink from '../../../database/rethinkDriver'
-import {RValue} from '../../../database/stricterR'
 import AuthToken from '../../../database/types/AuthToken'
 import ServerEnvironment from '../../../email/ServerEnvironment'
 import getMailManager from '../../../email/getMailManager'
 import notificationSummaryCreator from '../../../email/notificationSummaryCreator'
+import getKysely from '../../../postgres/getKysely'
 import isValid from '../../isValid'
 import {MutationResolvers} from '../resolverTypes'
 
@@ -14,36 +13,27 @@ const sendBatchNotificationEmails: MutationResolvers['sendBatchNotificationEmail
   _args,
   {dataLoader}
 ) => {
+  const pg = getKysely()
   // RESOLUTION
   // Note - this may be a lot of data one day. userNotifications is an array
   // of all the users who have not logged in within the last 24 hours and their
   // associated notifications.
-  const r = await getRethink()
   const now = Date.now()
   const yesterday = new Date(now - ms('1d'))
-  const userNotificationCount = (await (
-    r
-      .table('Notification')
-      // Only include notifications which occurred within the last day
-      .filter((row: RValue) => row('createdAt').gt(yesterday))
-      .filter({status: 'UNREAD'})
-      // de-dup users
-      .group('userId') as any
-  )
-    .count()
-    .ungroup()
-    .map((group: RValue) => ({
-      userId: group('group'),
-      notificationCount: group('reduction')
-    }))
-    .run()) as {userId: string; notificationCount: number}[]
+  const userNotificationCount = await pg
+    .selectFrom('Notification')
+    .select(({fn}) => ['userId', fn.count('id').as('notificationCount')])
+    .where('createdAt', '>', yesterday)
+    .where('status', '=', 'UNREAD')
+    .groupBy('userId')
+    .execute()
 
   // :TODO: (jmtaber129): Filter out team invitations for users who are already on the team.
   // :TODO: (jmtaber129): Filter out "stage timer" notifications if the meeting has already
   // progressed to the next stage.
 
   const userNotificationMap = new Map(
-    userNotificationCount.map((value) => [value.userId, value.notificationCount])
+    userNotificationCount.map((value) => [value.userId, Number(value.notificationCount)])
   )
   const users = (await dataLoader.get('users').loadMany([...userNotificationMap.keys()])).filter(
     isValid
diff --git a/packages/server/graphql/private/mutations/stripeFailPayment.ts b/packages/server/graphql/private/mutations/stripeFailPayment.ts
index 07b41564634..bd0354f4eb8 100644
--- a/packages/server/graphql/private/mutations/stripeFailPayment.ts
+++ b/packages/server/graphql/private/mutations/stripeFailPayment.ts
@@ -1,8 +1,7 @@
 import {SubscriptionChannel} from 'parabol-client/types/constEnums'
 import Stripe from 'stripe'
 import terminateSubscription from '../../../billing/helpers/terminateSubscription'
-import getRethink from '../../../database/rethinkDriver'
-import NotificationPaymentRejected from '../../../database/types/NotificationPaymentRejected'
+import generateUID from '../../../generateUID'
 import getKysely from '../../../postgres/getKysely'
 import {isSuperUser} from '../../../utils/authorization'
 import publish from '../../../utils/publish'
@@ -27,7 +26,6 @@ const stripeFailPayment: MutationResolvers['stripeFailPayment'] = async (
   }
 
   const pg = getKysely()
-  const r = await getRethink()
   const manager = getStripeManager()
 
   // VALIDATION
@@ -98,13 +96,15 @@ const stripeFailPayment: MutationResolvers['stripeFailPayment'] = async (
   }
   const {last4, brand} = creditCard
 
-  const notifications = billingLeaderUserIds.map(
-    (userId) => new NotificationPaymentRejected({orgId, last4, brand, userId})
-  )
+  const notifications = billingLeaderUserIds.map((userId) => ({
+    id: generateUID(),
+    type: 'PAYMENT_REJECTED' as const,
+    orgId,
+    last4: Number(last4),
+    brand,
+    userId
+  }))
 
-  await r({
-    insert: r.table('Notification').insert(notifications)
-  }).run()
   await pg.insertInto('Notification').values(notifications).execute()
 
   notifications.forEach((notification) => {
diff --git a/packages/server/graphql/public/mutations/addComment.ts b/packages/server/graphql/public/mutations/addComment.ts
index 978c4981bb6..6fd9250283f 100644
--- a/packages/server/graphql/public/mutations/addComment.ts
+++ b/packages/server/graphql/public/mutations/addComment.ts
@@ -4,13 +4,10 @@ import MeetingMemberId from '../../../../client/shared/gqlIds/MeetingMemberId'
 import TeamMemberId from '../../../../client/shared/gqlIds/TeamMemberId'
 import extractTextFromDraftString from '../../../../client/utils/draftjs/extractTextFromDraftString'
 import getTypeFromEntityMap from '../../../../client/utils/draftjs/getTypeFromEntityMap'
-import getRethink from '../../../database/rethinkDriver'
 import GenericMeetingPhase, {
   NewMeetingPhaseTypeEnum
 } from '../../../database/types/GenericMeetingPhase'
 import GenericMeetingStage from '../../../database/types/GenericMeetingStage'
-import NotificationDiscussionMentioned from '../../../database/types/NotificationDiscussionMentioned'
-import NotificationResponseReplied from '../../../database/types/NotificationResponseReplied'
 import generateUID from '../../../generateUID'
 import getKysely from '../../../postgres/getKysely'
 import {IGetDiscussionsByIdsQueryResult} from '../../../postgres/queries/generated/getDiscussionsByIdsQuery'
@@ -56,16 +53,15 @@ const getMentionNotifications = (
       // relevant page.
       return true
     })
-    .map(
-      (mentioneeUserId) =>
-        new NotificationDiscussionMentioned({
-          userId: mentioneeUserId,
-          meetingId: meetingId,
-          authorId: viewerId,
-          commentId,
-          discussionId: discussion.id
-        })
-    )
+    .map((mentioneeUserId) => ({
+      id: generateUID(),
+      type: 'DISCUSSION_MENTIONED' as const,
+      userId: mentioneeUserId,
+      meetingId: meetingId,
+      authorId: viewerId,
+      commentId,
+      discussionId: discussion.id
+    }))
 }
 
 const addComment: MutationResolvers['addComment'] = async (
@@ -74,7 +70,6 @@ const addComment: MutationResolvers['addComment'] = async (
   {authToken, dataLoader, socketId: mutatorId}
 ) => {
   const pg = getKysely()
-  const r = await getRethink()
   const viewerId = getUserId(authToken)
   const operationId = dataLoader.share()
   const subOptions = {mutatorId, operationId}
@@ -123,14 +118,15 @@ const addComment: MutationResolvers['addComment'] = async (
     const {userId: responseUserId} = TeamMemberId.split(discussion.discussionTopicId)
 
     if (responseUserId !== viewerId) {
-      const notification = new NotificationResponseReplied({
+      const notification = {
+        id: generateUID(),
+        type: 'RESPONSE_REPLIED' as const,
         userId: responseUserId,
         meetingId: meetingId,
         authorId: viewerId,
         commentId
-      })
+      }
 
-      await r.table('Notification').insert(notification).run()
       await pg.insertInto('Notification').values(notification).execute()
 
       IntegrationNotifier.sendNotificationToUser?.(dataLoader, notification.id, notification.userId)
@@ -147,7 +143,6 @@ const addComment: MutationResolvers['addComment'] = async (
   )
 
   if (notificationsToAdd.length) {
-    await r.table('Notification').insert(notificationsToAdd).run()
     await pg.insertInto('Notification').values(notificationsToAdd).execute()
     notificationsToAdd.forEach((notification) => {
       publishNotification(notification, subOptions)
diff --git a/packages/server/graphql/public/mutations/helpers/publishNotification.ts b/packages/server/graphql/public/mutations/helpers/publishNotification.ts
index 2abfe2b1c0b..23b8cff7d70 100644
--- a/packages/server/graphql/public/mutations/helpers/publishNotification.ts
+++ b/packages/server/graphql/public/mutations/helpers/publishNotification.ts
@@ -1,8 +1,10 @@
 import {SubscriptionChannel} from 'parabol-client/types/constEnums'
-import Notification from '../../../../database/types/Notification'
 import publish, {SubOptions} from '../../../../utils/publish'
 
-const publishNotification = (notification: Notification, subOptions: SubOptions) => {
+const publishNotification = (
+  notification: {id: string; userId: string},
+  subOptions: SubOptions
+) => {
   publish(
     SubscriptionChannel.NOTIFICATION,
     notification.userId,
diff --git a/packages/server/graphql/public/mutations/helpers/publishTeamPromptMentions.ts b/packages/server/graphql/public/mutations/helpers/publishTeamPromptMentions.ts
index 30591488c5f..cb928928d40 100644
--- a/packages/server/graphql/public/mutations/helpers/publishTeamPromptMentions.ts
+++ b/packages/server/graphql/public/mutations/helpers/publishTeamPromptMentions.ts
@@ -1,7 +1,5 @@
 import {JSONContent} from '@tiptap/core'
-import TeamPromptResponseId from '../../../../../client/shared/gqlIds/TeamPromptResponseId'
-import getRethink from '../../../../database/rethinkDriver'
-import NotificationResponseMentioned from '../../../../database/types/NotificationResponseMentioned'
+import generateUID from '../../../../generateUID'
 import getKysely from '../../../../postgres/getKysely'
 import {TeamPromptResponse} from '../../../../postgres/types'
 
@@ -38,24 +36,16 @@ const createTeamPromptMentionNotifications = async (
     return []
   }
 
-  const notificationsToAdd = addedMentions.map((mention) => {
-    return new NotificationResponseMentioned({
-      userId: mention,
-      // hack to turn the DB id into the GQL ID. The GDL ID should only be used in GQL resolvers, but i didn't catch this before it got built
-      responseId: TeamPromptResponseId.join(newResponse.id),
-      meetingId: newResponse.meetingId
-    })
-  })
+  const notificationsToAdd = addedMentions.map((mention) => ({
+    id: generateUID(),
+    type: 'RESPONSE_MENTIONED' as const,
+    userId: mention,
+    responseId: newResponse.id,
+    meetingId: newResponse.meetingId
+  }))
 
-  const r = await getRethink()
   const pg = getKysely()
-  await r.table('Notification').insert(notificationsToAdd).run()
-  await pg
-    .insertInto('Notification')
-    .values(
-      notificationsToAdd.map((n) => ({...n, responseId: TeamPromptResponseId.split(n.responseId)}))
-    )
-    .execute()
+  await pg.insertInto('Notification').values(notificationsToAdd).execute()
   return notificationsToAdd
 }
 
diff --git a/packages/server/graphql/public/mutations/helpers/updateNotification.ts b/packages/server/graphql/public/mutations/helpers/updateNotification.ts
index b1f14ffa3e5..58e1d2f498c 100644
--- a/packages/server/graphql/public/mutations/helpers/updateNotification.ts
+++ b/packages/server/graphql/public/mutations/helpers/updateNotification.ts
@@ -1,8 +1,7 @@
 import {SubscriptionChannel} from 'parabol-client/types/constEnums'
-import Notification from '../../../../database/types/Notification'
 import publish, {SubOptions} from '../../../../utils/publish'
 
-const updateNotification = (notification: Notification, subOptions: SubOptions) => {
+const updateNotification = (notification: {id: string; userId: string}, subOptions: SubOptions) => {
   publish(
     SubscriptionChannel.NOTIFICATION,
     notification.userId,
diff --git a/packages/server/graphql/public/mutations/requestToJoinDomain.ts b/packages/server/graphql/public/mutations/requestToJoinDomain.ts
index e279a04156f..6e5262a10f2 100644
--- a/packages/server/graphql/public/mutations/requestToJoinDomain.ts
+++ b/packages/server/graphql/public/mutations/requestToJoinDomain.ts
@@ -1,6 +1,5 @@
 import ms from 'ms'
-import getRethink from '../../../database/rethinkDriver'
-import NotificationRequestToJoinOrg from '../../../database/types/NotificationRequestToJoinOrg'
+import generateUID from '../../../generateUID'
 import getKysely from '../../../postgres/getKysely'
 import {getUserId} from '../../../utils/authorization'
 import getDomainFromEmail from '../../../utils/getDomainFromEmail'
@@ -17,7 +16,6 @@ const requestToJoinDomain: MutationResolvers['requestToJoinDomain'] = async (
   _args,
   {authToken, dataLoader}
 ) => {
-  const r = await getRethink()
   const operationId = dataLoader.share()
   const subOptions = {operationId}
   const pg = getKysely()
@@ -56,18 +54,17 @@ const requestToJoinDomain: MutationResolvers['requestToJoinDomain'] = async (
   const leadTeamMembers = teamMembers.filter(({isLead}) => isLead)
   const leadUserIds = [...new Set(leadTeamMembers.map(({userId}) => userId))]
 
-  const notificationsToInsert = leadUserIds.map((userId) => {
-    return new NotificationRequestToJoinOrg({
-      userId,
-      email: viewer.email,
-      name: viewer.preferredName,
-      picture: viewer.picture,
-      requestCreatedBy: viewerId,
-      domainJoinRequestId: insertResult.id
-    })
-  })
+  const notificationsToInsert = leadUserIds.map((userId) => ({
+    id: generateUID(),
+    type: 'REQUEST_TO_JOIN_ORG' as const,
+    userId,
+    email: viewer.email,
+    name: viewer.preferredName,
+    picture: viewer.picture,
+    requestCreatedBy: viewerId,
+    domainJoinRequestId: insertResult.id
+  }))
 
-  await r.table('Notification').insert(notificationsToInsert).run()
   await pg.insertInto('Notification').values(notificationsToInsert).execute()
 
   notificationsToInsert.forEach((notification) => {
diff --git a/packages/server/graphql/public/mutations/setOrgUserRole.ts b/packages/server/graphql/public/mutations/setOrgUserRole.ts
index 14a2f67cf46..d107658d119 100644
--- a/packages/server/graphql/public/mutations/setOrgUserRole.ts
+++ b/packages/server/graphql/public/mutations/setOrgUserRole.ts
@@ -1,6 +1,5 @@
 import {SubscriptionChannel} from 'parabol-client/types/constEnums'
-import getRethink from '../../../database/rethinkDriver'
-import NotificationPromoteToBillingLeader from '../../../database/types/NotificationPromoteToBillingLeader'
+import generateUID from '../../../generateUID'
 import getKysely from '../../../postgres/getKysely'
 import {analytics} from '../../../utils/analytics/analytics'
 import {getUserId, isSuperUser, isUserBillingLeader} from '../../../utils/authorization'
@@ -10,11 +9,16 @@ import {MutationResolvers} from '../resolverTypes'
 
 const addNotifications = async (orgId: string, userId: string) => {
   const pg = getKysely()
-  const r = await getRethink()
-  const promotionNotification = new NotificationPromoteToBillingLeader({orgId, userId})
-  const {id: promotionNotificationId} = promotionNotification
-  await r.table('Notification').insert(promotionNotification).run()
-  await pg.insertInto('Notification').values(promotionNotification).execute()
+  const promotionNotificationId = generateUID()
+  await pg
+    .insertInto('Notification')
+    .values({
+      id: promotionNotificationId,
+      type: 'PROMOTE_TO_BILLING_LEADER',
+      orgId,
+      userId
+    })
+    .execute()
   return [promotionNotificationId]
 }
 
diff --git a/packages/server/graphql/public/mutations/startRetrospective.ts b/packages/server/graphql/public/mutations/startRetrospective.ts
index f45d27aae82..36b42743c63 100644
--- a/packages/server/graphql/public/mutations/startRetrospective.ts
+++ b/packages/server/graphql/public/mutations/startRetrospective.ts
@@ -67,19 +67,12 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async (
     },
     dataLoader
   )
-  const meetingId = meeting.id
-
-  const template = await dataLoader.get('meetingTemplates').load(selectedTemplateId)
-  const [newMeetingRes] = await Promise.allSettled([
-    pg
-      .insertInto('NewMeeting')
-      .values({...meeting, phases: JSON.stringify(meeting.phases)})
-      .execute(),
-    updateMeetingTemplateLastUsedAt(selectedTemplateId, teamId)
-  ])
-  if (newMeetingRes.status === 'rejected') {
+  if (!meeting) {
     return {error: {message: 'Meeting already started'}}
   }
+  const meetingId = meeting.id
+  const template = await dataLoader.get('meetingTemplates').load(selectedTemplateId)
+  await updateMeetingTemplateLastUsedAt(selectedTemplateId, teamId)
 
   const meetingMember = createMeetingMember(meeting, {
     userId: viewerId,
diff --git a/packages/server/graphql/public/mutations/startTeamPrompt.ts b/packages/server/graphql/public/mutations/startTeamPrompt.ts
index bf0b8b32612..2dd22822eea 100644
--- a/packages/server/graphql/public/mutations/startTeamPrompt.ts
+++ b/packages/server/graphql/public/mutations/startTeamPrompt.ts
@@ -20,7 +20,6 @@ const startTeamPrompt: MutationResolvers['startTeamPrompt'] = async (
   {teamId, name, rrule, gcalInput},
   {authToken, dataLoader, socketId: mutatorId}
 ) => {
-  const pg = getKysely()
   const operationId = dataLoader.share()
   const subOptions = {mutatorId, operationId}
 
@@ -48,18 +47,7 @@ const startTeamPrompt: MutationResolvers['startTeamPrompt'] = async (
   const meetingName = createMeetingSeriesTitle(name || 'Standup', new Date(), 'UTC')
   const eventName = rrule ? name || 'Standup' : meetingName
   const meeting = await safeCreateTeamPrompt(meetingName, teamId, viewerId, dataLoader)
-
-  const [newMeetingRes] = await Promise.allSettled([
-    pg
-      .with('NewMeetingInsert', (qb) =>
-        qb.insertInto('NewMeeting').values({...meeting, phases: JSON.stringify(meeting.phases)})
-      )
-      .updateTable('Team')
-      .set({lastMeetingType: 'teamPrompt'})
-      .where('id', '=', teamId)
-      .execute()
-  ])
-  if (newMeetingRes.status === 'rejected') {
+  if (!meeting) {
     return {error: {message: 'Meeting already started'}}
   }
   const {id: meetingId} = meeting
diff --git a/packages/server/graphql/public/types/AcceptTeamInvitationPayload.ts b/packages/server/graphql/public/types/AcceptTeamInvitationPayload.ts
index ff21995cf78..33620c041df 100644
--- a/packages/server/graphql/public/types/AcceptTeamInvitationPayload.ts
+++ b/packages/server/graphql/public/types/AcceptTeamInvitationPayload.ts
@@ -1,4 +1,3 @@
-import NotificationTeamInvitation from '../../../database/types/NotificationTeamInvitation'
 import {getUserId, isTeamMember} from '../../../utils/authorization'
 import standardError from '../../../utils/standardError'
 import {GQLContext} from '../../graphql'
@@ -45,7 +44,9 @@ const AcceptTeamInvitationPayload: AcceptTeamInvitationPayloadResolvers = {
     }
     const teamInvitationNotifications = (
       await dataLoader.get('notifications').loadMany(invitationNotificationIds)
-    ).filter(isValid) as NotificationTeamInvitation[]
+    )
+      .filter(isValid)
+      .filter((n) => n.type === 'TEAM_INVITATION')
     return teamInvitationNotifications
   },
 
diff --git a/packages/server/graphql/public/types/AddedNotification.ts b/packages/server/graphql/public/types/AddedNotification.ts
index 5a5406c2da2..e96176cef7c 100644
--- a/packages/server/graphql/public/types/AddedNotification.ts
+++ b/packages/server/graphql/public/types/AddedNotification.ts
@@ -6,7 +6,7 @@ export type AddedNotificationSource = {addedNotificationId: string}
 const AddedNotification: AddedNotificationResolvers = {
   addedNotification: async ({addedNotificationId}, _args: unknown, {dataLoader, authToken}) => {
     const viewerId = getUserId(authToken)
-    const notification = await dataLoader.get('notifications').load(addedNotificationId)
+    const notification = await dataLoader.get('notifications').loadNonNull(addedNotificationId)
     if (notification.userId !== viewerId) {
       throw new Error(
         `Viewer ID does not match notification user ID: notification ${addedNotificationId} for user ${notification.userId} published to user ${viewerId}`
diff --git a/packages/server/graphql/public/types/ArchiveTeamPayload.ts b/packages/server/graphql/public/types/ArchiveTeamPayload.ts
index be91d0e6765..af6079cdb75 100644
--- a/packages/server/graphql/public/types/ArchiveTeamPayload.ts
+++ b/packages/server/graphql/public/types/ArchiveTeamPayload.ts
@@ -1,6 +1,5 @@
-import NotificationTeamArchived from '../../../database/types/NotificationTeamArchived'
 import {getUserId} from '../../../utils/authorization'
-import errorFilter from '../../errorFilter'
+import isValid from '../../isValid'
 import {ArchiveTeamPayloadResolvers} from '../resolverTypes'
 
 export type ArchiveTeamPayloadSource = {
@@ -16,15 +15,13 @@ const ArchiveTeamPayload: ArchiveTeamPayloadResolvers = {
   },
   notification: async ({notificationIds}, _args, {authToken, dataLoader}) => {
     if (!notificationIds) return null
-    const notifications = (await dataLoader.get('notifications').loadMany(notificationIds)).filter(
-      errorFilter
-    )
     const viewerId = getUserId(authToken)
-    const archivedNotification = notifications.find(
-      (notification) => notification.userId === viewerId
-    )
+    const archivedNotification = (await dataLoader.get('notifications').loadMany(notificationIds))
+      .filter(isValid)
+      .filter((notification) => notification.type === 'TEAM_ARCHIVED')
+      .find((notification) => notification.userId === viewerId)
     if (!archivedNotification) return null
-    return archivedNotification as NotificationTeamArchived
+    return archivedNotification
   }
 }
 
diff --git a/packages/server/graphql/public/types/CreateTaskPayload.ts b/packages/server/graphql/public/types/CreateTaskPayload.ts
index e18f2f6d822..45a144bf13d 100644
--- a/packages/server/graphql/public/types/CreateTaskPayload.ts
+++ b/packages/server/graphql/public/types/CreateTaskPayload.ts
@@ -1,6 +1,5 @@
-import NotificationTaskInvolves from '../../../database/types/NotificationTaskInvolves'
 import {getUserId} from '../../../utils/authorization'
-import errorFilter from '../../errorFilter'
+import isValid from '../../isValid'
 import {CreateTaskPayloadResolvers} from '../resolverTypes'
 
 export type CreateTaskPayloadSource = {
@@ -28,9 +27,9 @@ const CreateTaskPayload: CreateTaskPayloadResolvers = {
 
   involvementNotification: async ({notificationIds}, _args, {authToken, dataLoader}) => {
     if (!notificationIds) return null
-    const notifications = (await dataLoader.get('notifications').loadMany(notificationIds)).filter(
-      errorFilter
-    ) as NotificationTaskInvolves[]
+    const notifications = (await dataLoader.get('notifications').loadMany(notificationIds))
+      .filter(isValid)
+      .filter((n) => n.type === 'TASK_INVOLVES')
     const viewerId = getUserId(authToken)
     return notifications.find((notification) => notification.userId === viewerId) || null
   }
diff --git a/packages/server/graphql/public/types/InviteToTeamPayload.ts b/packages/server/graphql/public/types/InviteToTeamPayload.ts
index 231c82f6469..50e2967f5fe 100644
--- a/packages/server/graphql/public/types/InviteToTeamPayload.ts
+++ b/packages/server/graphql/public/types/InviteToTeamPayload.ts
@@ -1,4 +1,4 @@
-import NotificationTeamInvitation from '../../../database/types/NotificationTeamInvitation'
+import {TeamInvitationNotification} from '../../../postgres/types/Notification'
 import {InviteToTeamPayloadResolvers} from '../resolverTypes'
 
 export type InviteToTeamPayloadSource = {
@@ -14,8 +14,10 @@ const InviteToTeamPayload: InviteToTeamPayloadResolvers = {
   },
   teamInvitationNotification: async ({teamInvitationNotificationId}, _args, {dataLoader}) => {
     if (!teamInvitationNotificationId) return null
-    const teamInvitation = await dataLoader.get('notifications').load(teamInvitationNotificationId)
-    return teamInvitation as NotificationTeamInvitation
+    const teamInvitation = await dataLoader
+      .get('notifications')
+      .loadNonNull<TeamInvitationNotification>(teamInvitationNotificationId)
+    return teamInvitation
   }
 }
 
diff --git a/packages/server/graphql/public/types/NotifyResponseMentioned.ts b/packages/server/graphql/public/types/NotifyResponseMentioned.ts
index 06a3a6cd992..584abebbd2a 100644
--- a/packages/server/graphql/public/types/NotifyResponseMentioned.ts
+++ b/packages/server/graphql/public/types/NotifyResponseMentioned.ts
@@ -1,4 +1,3 @@
-import TeamPromptResponseId from '../../../../client/shared/gqlIds/TeamPromptResponseId'
 import {NotifyResponseMentionedResolvers} from '../resolverTypes'
 
 const NotifyResponseMentioned: NotifyResponseMentionedResolvers = {
@@ -9,9 +8,7 @@ const NotifyResponseMentioned: NotifyResponseMentionedResolvers = {
     return meeting
   },
   response: ({responseId}, _args, {dataLoader}) => {
-    // Hack, in a perfect world, this notification would have the numeric DB ID saved on it
-    const dbId = TeamPromptResponseId.split(responseId)
-    return dataLoader.get('teamPromptResponses').loadNonNull(dbId)
+    return dataLoader.get('teamPromptResponses').loadNonNull(responseId)
   }
 }
 
diff --git a/packages/server/graphql/public/types/PokerMeetingSettings.ts b/packages/server/graphql/public/types/PokerMeetingSettings.ts
index 40df579d5a2..145e1c939a4 100644
--- a/packages/server/graphql/public/types/PokerMeetingSettings.ts
+++ b/packages/server/graphql/public/types/PokerMeetingSettings.ts
@@ -1,4 +1,3 @@
-import MeetingTemplate from '../../../database/types/MeetingTemplate'
 import db from '../../../db'
 import {ORG_HOTNESS_FACTOR, TEAM_HOTNESS_FACTOR} from '../../../utils/getTemplateScore'
 import connectionFromTemplateArray from '../../queries/helpers/connectionFromTemplateArray'
@@ -23,7 +22,7 @@ const PokerMeetingSettings: PokerMeetingSettingsResolvers = {
     const {orgId} = team
     const templates = await dataLoader.get('meetingTemplatesByOrgId').load(orgId)
     const organizationTemplates = templates.filter(
-      (template: MeetingTemplate) =>
+      (template) =>
         template.scope !== 'TEAM' && template.teamId !== teamId && template.type === 'poker'
     )
     const scoredTemplates = await getScoredTemplates(organizationTemplates, ORG_HOTNESS_FACTOR)
diff --git a/packages/server/graphql/public/types/RemoveTeamMemberPayload.ts b/packages/server/graphql/public/types/RemoveTeamMemberPayload.ts
index dedf7714255..f5e607ed40a 100644
--- a/packages/server/graphql/public/types/RemoveTeamMemberPayload.ts
+++ b/packages/server/graphql/public/types/RemoveTeamMemberPayload.ts
@@ -1,5 +1,5 @@
 import nullIfEmpty from 'parabol-client/utils/nullIfEmpty'
-import NotificationKickedOut from '../../../database/types/NotificationKickedOut'
+import {KickedOutNotification} from '../../../postgres/types/Notification'
 import {getUserId} from '../../../utils/authorization'
 import {GQLContext} from '../../graphql'
 import isValid from '../../isValid'
@@ -38,9 +38,11 @@ const RemoveTeamMemberPayload: RemoveTeamMemberPayloadResolvers = {
   kickOutNotification: async ({notificationId}, _args, {authToken, dataLoader}) => {
     if (!notificationId) return null
     const viewerId = getUserId(authToken)
-    const notification = await dataLoader.get('notifications').load(notificationId)
+    const notification = await dataLoader
+      .get('notifications')
+      .load<KickedOutNotification>(notificationId)
     if (!notification || notification.userId !== viewerId) return null
-    return notification as NotificationKickedOut
+    return notification
   }
 }
 
diff --git a/packages/server/graphql/public/types/SetNotificationStatusPayload.ts b/packages/server/graphql/public/types/SetNotificationStatusPayload.ts
index 18b287143ec..5ae28c86755 100644
--- a/packages/server/graphql/public/types/SetNotificationStatusPayload.ts
+++ b/packages/server/graphql/public/types/SetNotificationStatusPayload.ts
@@ -6,7 +6,7 @@ export type SetNotificationStatusPayloadSource = {
 
 const SetNotificationStatusPayload: SetNotificationStatusPayloadResolvers = {
   notification: ({notificationId}, _args, {dataLoader}) => {
-    return dataLoader.get('notifications').load(notificationId)
+    return dataLoader.get('notifications').loadNonNull(notificationId)
   }
 }
 
diff --git a/packages/server/graphql/public/types/SetOrgUserRoleSuccess.ts b/packages/server/graphql/public/types/SetOrgUserRoleSuccess.ts
index 3954347afdf..6ca671dc943 100644
--- a/packages/server/graphql/public/types/SetOrgUserRoleSuccess.ts
+++ b/packages/server/graphql/public/types/SetOrgUserRoleSuccess.ts
@@ -1,5 +1,5 @@
 import {getUserId} from '../../../utils/authorization'
-import errorFilter from '../../errorFilter'
+import isValid from '../../isValid'
 import {SetOrgUserRoleSuccessResolvers} from '../resolverTypes'
 
 export type SetOrgUserRoleSuccessSource = {
@@ -20,7 +20,7 @@ const SetOrgUserRoleSuccess: SetOrgUserRoleSuccessResolvers = {
     const viewerId = getUserId(authToken)
     const notifications = (
       await dataLoader.get('notifications').loadMany(notificationIdsAdded)
-    ).filter(errorFilter)
+    ).filter(isValid)
     return notifications.filter((notification) => notification.userId === viewerId)
   }
 }
diff --git a/packages/server/graphql/public/types/StripeFailPaymentPayload.ts b/packages/server/graphql/public/types/StripeFailPaymentPayload.ts
index e04fa093e86..73c4c915c25 100644
--- a/packages/server/graphql/public/types/StripeFailPaymentPayload.ts
+++ b/packages/server/graphql/public/types/StripeFailPaymentPayload.ts
@@ -1,4 +1,4 @@
-import NotificationPaymentRejected from '../../../database/types/NotificationPaymentRejected'
+import {PaymentRejectedNotification} from '../../../postgres/types/Notification'
 import {StripeFailPaymentPayloadResolvers} from '../resolverTypes'
 
 export type StripeFailPaymentPayloadSource = {
@@ -11,8 +11,10 @@ const StripeFailPaymentPayload: StripeFailPaymentPayloadResolvers = {
     return dataLoader.get('organizations').loadNonNull(orgId)
   },
   notification: async ({notificationId}, _args, {dataLoader}) => {
-    const notification = await dataLoader.get('notifications').load(notificationId)
-    return notification as NotificationPaymentRejected
+    const notification = await dataLoader
+      .get('notifications')
+      .loadNonNull<PaymentRejectedNotification>(notificationId)
+    return notification
   }
 }
 
diff --git a/packages/server/graphql/public/types/UpdateTaskPayload.ts b/packages/server/graphql/public/types/UpdateTaskPayload.ts
index b9b886dc629..59e5f35fc53 100644
--- a/packages/server/graphql/public/types/UpdateTaskPayload.ts
+++ b/packages/server/graphql/public/types/UpdateTaskPayload.ts
@@ -1,12 +1,11 @@
-import Notification from '../../../database/types/Notification'
-import NotificationTaskInvolves from '../../../database/types/NotificationTaskInvolves'
+import {TaskInvolvesNotification} from '../../../postgres/types/Notification'
 import {getUserId} from '../../../utils/authorization'
 import {UpdateTaskPayloadResolvers} from '../resolverTypes'
 
 export type UpdateTaskPayloadSource = {
   taskId: string
   isPrivatized: boolean
-  notificationsToAdd?: NotificationTaskInvolves[]
+  notificationsToAdd?: TaskInvolvesNotification[]
 }
 
 const UpdateTaskPayload: UpdateTaskPayloadResolvers = {
@@ -25,10 +24,7 @@ const UpdateTaskPayload: UpdateTaskPayloadResolvers = {
 
   addedNotification: async ({notificationsToAdd}, _args, {authToken}) => {
     const viewerId = getUserId(authToken)
-    return (
-      notificationsToAdd?.find((notification: Notification) => notification.userId === viewerId) ??
-      null
-    )
+    return notificationsToAdd?.find((notification) => notification.userId === viewerId) ?? null
   }
 }
 
diff --git a/packages/server/graphql/public/types/UpdatedNotification.ts b/packages/server/graphql/public/types/UpdatedNotification.ts
index 588854c3145..708e8b8418f 100644
--- a/packages/server/graphql/public/types/UpdatedNotification.ts
+++ b/packages/server/graphql/public/types/UpdatedNotification.ts
@@ -6,7 +6,7 @@ export type UpdatedNotificationSource = {updatedNotificationId: string}
 const UpdatedNotification: UpdatedNotificationResolvers = {
   updatedNotification: async ({updatedNotificationId}, _args: unknown, {dataLoader, authToken}) => {
     const viewerId = getUserId(authToken)
-    const notification = await dataLoader.get('notifications').load(updatedNotificationId)
+    const notification = await dataLoader.get('notifications').loadNonNull(updatedNotificationId)
     if (notification.userId !== viewerId) {
       throw new Error(
         `Viewer ID does not match notification user ID: notification ${updatedNotificationId} published to user ${viewerId}`
diff --git a/packages/server/graphql/public/types/User.ts b/packages/server/graphql/public/types/User.ts
index ab06417cd2a..6cf83e0eb6d 100644
--- a/packages/server/graphql/public/types/User.ts
+++ b/packages/server/graphql/public/types/User.ts
@@ -12,11 +12,9 @@ import {
   MAX_RESULT_GROUP_SIZE
 } from '../../../../client/utils/constants'
 import groupReflections from '../../../../client/utils/smartGroup/groupReflections'
-import getRethink from '../../../database/rethinkDriver'
-import {RDatum} from '../../../database/stricterR'
 import MeetingTemplate from '../../../database/types/MeetingTemplate'
 import getKysely from '../../../postgres/getKysely'
-import {selectTasks} from '../../../postgres/select'
+import {selectNotifications, selectTasks} from '../../../postgres/select'
 import {getUserId, isSuperUser, isTeamMember} from '../../../utils/authorization'
 import getDomainFromEmail from '../../../utils/getDomainFromEmail'
 import getMonthlyStreak from '../../../utils/getMonthlyStreak'
@@ -156,26 +154,16 @@ const User: ReqResolvers<'User'> = {
     return meeting
   },
   notifications: async (_source, {first, after, types}, {authToken}) => {
-    const r = await getRethink()
-    // AUTH
     const userId = getUserId(authToken)
-    const dbAfter = after || r.maxval
-    // RESOLUTION
+    const hasTypes = types ? types.length > 0 : false
     // TODO consider moving the requestedFields to all queries
-    const nodesPlus1 = await r
-      .table('Notification')
-      .getAll(userId, {index: 'userId'})
-      .orderBy(r.desc('createdAt'))
-      .filter((row: RDatum) => {
-        if (types) {
-          return row('createdAt')
-            .lt(dbAfter)
-            .and(r.expr(types).contains(row('type')))
-        }
-        return row('createdAt').lt(dbAfter)
-      })
+    const nodesPlus1 = await selectNotifications()
+      .where('userId', '=', userId)
+      .$if(hasTypes, (qb) => qb.where('type', 'in', types!))
+      .$if(!!after, (qb) => qb.where('createdAt', '<', after!))
+      .orderBy('createdAt desc')
       .limit(first + 1)
-      .run()
+      .execute()
 
     const nodes = nodesPlus1.slice(0, first)
     const edges = nodes.map((node) => ({
diff --git a/packages/server/graphql/types/RemoveOrgUserPayload.ts b/packages/server/graphql/types/RemoveOrgUserPayload.ts
index 667e339e306..1f840c53419 100644
--- a/packages/server/graphql/types/RemoveOrgUserPayload.ts
+++ b/packages/server/graphql/types/RemoveOrgUserPayload.ts
@@ -1,7 +1,7 @@
 import {GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString} from 'graphql'
 import {getUserId} from '../../utils/authorization'
-import errorFilter from '../errorFilter'
 import {GQLContext} from '../graphql'
+import isValid from '../isValid'
 import {
   resolveFilterByTeam,
   resolveOrganization,
@@ -64,7 +64,7 @@ const RemoveOrgUserPayload = new GraphQLObjectType<any, GQLContext>({
         const viewerId = getUserId(authToken)
         const notifications = (
           await dataLoader.get('notifications').loadMany(kickOutNotificationIds)
-        ).filter(errorFilter)
+        ).filter(isValid)
         return notifications.filter((notification) => notification.userId === viewerId)
       }
     },
diff --git a/packages/server/postgres/migrations/1729098152007_Notification-phase2.ts b/packages/server/postgres/migrations/1729098152007_Notification-phase2.ts
new file mode 100644
index 00000000000..0332d5e7361
--- /dev/null
+++ b/packages/server/postgres/migrations/1729098152007_Notification-phase2.ts
@@ -0,0 +1,193 @@
+import {Kysely, PostgresDialect, sql} from 'kysely'
+import {r} from 'rethinkdb-ts'
+import connectRethinkDB from '../../database/connectRethinkDB'
+import getPg from '../getPg'
+
+export async function up() {
+  await connectRethinkDB()
+  const pg = new Kysely<any>({
+    dialect: new PostgresDialect({
+      pool: getPg()
+    })
+  })
+
+  try {
+    console.log('Adding index')
+    await r
+      .table('Notification')
+      .indexCreate('updatedAtId', (row: any) => [row('createdAt'), row('id')])
+      .run()
+    await r.table('Notification').indexWait().run()
+  } catch {
+    // index already exists
+  }
+
+  console.log('Adding index complete')
+
+  const MAX_PG_PARAMS = 65545
+  const PG_COLS = [
+    'id',
+    'status',
+    'createdAt',
+    'type',
+    'userId',
+    'meetingId',
+    'authorId',
+    'commentId',
+    'discussionId',
+    'teamId',
+    'evictorUserId',
+    'senderName',
+    'senderPicture',
+    'senderUserId',
+    'meetingName',
+    'retroReflectionId',
+    'retroDiscussStageIdx',
+    'orgId',
+    'last4',
+    'brand',
+    'activeDomain',
+    'domainJoinRequestId',
+    'email',
+    'name',
+    'picture',
+    'requestCreatedBy',
+    'responseId',
+    'changeAuthorId',
+    'involvement',
+    'taskId',
+    'archivorUserId',
+    'invitationId',
+    'orgName',
+    'orgPicture',
+    'scheduledLockAt'
+  ] as const
+  type Notification = {
+    [K in (typeof PG_COLS)[number]]: any
+  }
+  const BATCH_SIZE = Math.trunc(MAX_PG_PARAMS / PG_COLS.length)
+
+  let curUpdatedAt = r.minval
+  let curId = r.minval
+
+  const insertRow = async (row) => {
+    if (!row.type) {
+      console.log('Notification has no type, skipping insert', row.id)
+      return
+    }
+    try {
+      await pg
+        .insertInto('Notification')
+        .values(row)
+        .onConflict((oc) => oc.doNothing())
+        .execute()
+    } catch (e) {
+      if (e.constraint === 'fk_meetingId') {
+        console.log('Notification has no meeting, skipping insert', row.id)
+        return
+      }
+      if (e.constraint === 'fk_userId') {
+        console.log('Notification has no user, skipping insert', row.id)
+        return
+      }
+      if (e.constraint === 'fk_changeAuthorId') {
+        console.log('Notification has no fk_changeAuthorId, skipping insert', row.id)
+        return
+      }
+      if (e.constraint === 'fk_taskId') {
+        console.log('Notification has no fk_taskId, skipping insert', row.id)
+        return
+      }
+      if (e.constraint === 'fk_archivorUserId') {
+        console.log('Notification has no fk_archivorUserId, skipping insert', row.id)
+        return
+      }
+      if (e.constraint === 'fk_orgId') {
+        console.log('Notification has no fk_orgId, skipping insert', row.id)
+        return
+      }
+      if (e.constraint === 'fk_evictorUserId') {
+        console.log('Notification has no fk_evictorUserId, skipping insert', row.id)
+        return
+      }
+      if (e.constraint === 'fk_invitationId') {
+        console.log('Notification has no fk_invitationId, skipping insert', row.id)
+        return
+      }
+      if (e.constraint === 'fk_responseId') {
+        console.log('Notification has no fk_responseId, skipping insert', row.id)
+        return
+      }
+      if (e.constraint === 'fk_authorId') {
+        console.log('Notification has no fk_authorId, skipping insert', row.id)
+        return
+      }
+      if (e.constraint === 'fk_commentId') {
+        console.log('Notification has no fk_commentId, skipping insert', row.id)
+        return
+      }
+      if (e.constraint === 'fk_teamId') {
+        console.log('Notification has no fk_teamId, skipping insert', row.id)
+        return
+      }
+      if (e.constraint === 'fk_discussionId') {
+        console.log('Notification has no fk_discussionId, skipping insert', row.id)
+        return
+      }
+      if (e.constraint === 'fk_senderUserId') {
+        console.log('Notification has no fk_senderUserId, skipping insert', row.id)
+        return
+      }
+      if (e.constraint === 'fk_retroReflectionId') {
+        console.log('Notification has no fk_retroReflectionId, skipping insert', row.id)
+        return
+      }
+      if (e.constraint === 'fk_domainJoinRequestId') {
+        console.log('Notification has no fk_domainJoinRequestId, skipping insert', row.id)
+        return
+      }
+      if (e.constraint === 'fk_requestCreatedBy') {
+        console.log('Notification has no fk_requestCreatedBy, skipping insert', row.id)
+        return
+      }
+      throw e
+    }
+  }
+  for (let i = 0; i < 1e6; i++) {
+    console.log('inserting row', i * BATCH_SIZE, String(curUpdatedAt), String(curId))
+    const rawRowsToInsert = (await r
+      .table('Notification')
+      .between([curUpdatedAt, curId], [r.maxval, r.maxval], {
+        index: 'updatedAtId',
+        leftBound: 'open',
+        rightBound: 'closed'
+      })
+      .orderBy({index: 'updatedAtId'})
+      .limit(BATCH_SIZE)
+      .pluck(...PG_COLS)
+      .run()) as Notification[]
+
+    const rowsToInsert = rawRowsToInsert.map((row) => {
+      const {responseId, ...rest} = row as any
+      return {
+        ...rest,
+        responseId: responseId ? Number(responseId.split(':')[1]) : null
+      }
+    })
+
+    if (rowsToInsert.length === 0) break
+    const lastRow = rowsToInsert[rowsToInsert.length - 1]
+    curUpdatedAt = lastRow.createdAt
+    curId = lastRow.id
+    await Promise.all(rowsToInsert.map(async (row) => insertRow(row)))
+  }
+}
+
+export async function down() {
+  const pg = new Kysely<any>({
+    dialect: new PostgresDialect({
+      pool: getPg()
+    })
+  })
+  await sql`TRUNCATE TABLE "Notification" CASCADE`.execute(pg)
+}
diff --git a/packages/server/postgres/types/Notification.d.ts b/packages/server/postgres/types/Notification.d.ts
index cc10388b8ee..14a71b0700d 100644
--- a/packages/server/postgres/types/Notification.d.ts
+++ b/packages/server/postgres/types/Notification.d.ts
@@ -2,12 +2,12 @@ import type {Notification} from '../pg.d'
 
 interface BaseNotification {
   id: string
-  status: Notification['status']
+  status: 'CLICKED' | 'READ' | 'UNREAD'
   type: Notification['type']
   userId: string
 }
 
-interface DiscussionMentionedNotification extends BaseNotification {
+export interface DiscussionMentionedNotification extends BaseNotification {
   type: 'DISCUSSION_MENTIONED'
   meetingId: string
   authorId: string
@@ -15,46 +15,46 @@ interface DiscussionMentionedNotification extends BaseNotification {
   discussionId: string
 }
 
-interface KickedOutNotification extends BaseNotification {
+export interface KickedOutNotification extends BaseNotification {
   type: 'KICKED_OUT'
   teamId: string
   evictorUserId: string
 }
 
-interface MeetingStageTimeLimitEndNotification extends BaseNotification {
+export interface MeetingStageTimeLimitEndNotification extends BaseNotification {
   type: 'MEETING_STAGE_TIME_LIMIT_END'
   meetingId: string
 }
 
-interface MentionedNotification extends BaseNotification {
+export interface MentionedNotification extends BaseNotification {
   type: 'MENTIONED'
   senderName: string | null
   senderPicture: string | null
   senderUserId: string
   meetingName: string
   meetingId: string
-  retroReflectionId?: string | null
-  retroDiscussStageIdx?: number | null
+  retroReflectionId: string | null
+  retroDiscussStageIdx: number | null
 }
 
-interface PaymentRejectedNotification extends BaseNotification {
+export interface PaymentRejectedNotification extends BaseNotification {
   type: 'PAYMENT_REJECTED'
   orgId: string
-  last4: string
+  last4: number
   brand: string
 }
 
-interface PromoteToBillingLeaderNotification extends BaseNotification {
+export interface PromoteToBillingLeaderNotification extends BaseNotification {
   type: 'PROMOTE_TO_BILLING_LEADER'
   orgId: string
 }
 
-interface PromptToJoinOrgNotification extends BaseNotification {
+export interface PromptToJoinOrgNotification extends BaseNotification {
   type: 'PROMPT_TO_JOIN_ORG'
   activeDomain: string
 }
 
-interface RequestToJoinOrgNotification extends BaseNotification {
+export interface RequestToJoinOrgNotification extends BaseNotification {
   type: 'REQUEST_TO_JOIN_ORG'
   domainJoinRequestId: number
   email: string
@@ -63,20 +63,20 @@ interface RequestToJoinOrgNotification extends BaseNotification {
   requestCreatedBy: string
 }
 
-interface ResponseMentionedNotification extends BaseNotification {
+export interface ResponseMentionedNotification extends BaseNotification {
   type: 'RESPONSE_MENTIONED'
-  responseId: string
+  responseId: number
   meetingId: string
 }
 
-interface ResponseRepliedNotification extends BaseNotification {
+export interface ResponseRepliedNotification extends BaseNotification {
   type: 'RESPONSE_REPLIED'
   meetingId: string
   authorId: string
   commentId: string
 }
 
-interface TaskInvolvesNotification extends BaseNotification {
+export interface TaskInvolvesNotification extends BaseNotification {
   type: 'TASK_INVOLVES'
   changeAuthorId: string
   involvement: TaskInvolvement
@@ -84,26 +84,26 @@ interface TaskInvolvesNotification extends BaseNotification {
   teamId: string
 }
 
-interface TeamArchivedNotification extends BaseNotification {
+export interface TeamArchivedNotification extends BaseNotification {
   type: 'TEAM_ARCHIVED'
   archivorUserId: string
   teamId: string
 }
 
-interface TeamInvitationNotification extends BaseNotification {
+export interface TeamInvitationNotification extends BaseNotification {
   type: 'TEAM_INVITATION'
   invitationId: string
   teamId: string
 }
 
-interface TeamsLimitExceededNotification extends BaseNotification {
+export interface TeamsLimitExceededNotification extends BaseNotification {
   type: 'TEAMS_LIMIT_EXCEEDED'
   orgId: string
   orgName: string
   orgPicture: string | null
 }
 
-interface TeamsLimitReminderNotification extends BaseNotification {
+export interface TeamsLimitReminderNotification extends BaseNotification {
   type: 'TEAMS_LIMIT_REMINDER'
   orgId: string
   orgName: string
diff --git a/packages/server/safeMutations/acceptTeamInvitation.ts b/packages/server/safeMutations/acceptTeamInvitation.ts
index ade90d9b059..d5b9a4507eb 100644
--- a/packages/server/safeMutations/acceptTeamInvitation.ts
+++ b/packages/server/safeMutations/acceptTeamInvitation.ts
@@ -2,7 +2,6 @@ import {sql} from 'kysely'
 import {InvoiceItemType} from 'parabol-client/types/constEnums'
 import TeamMemberId from '../../client/shared/gqlIds/TeamMemberId'
 import adjustUserCount from '../billing/helpers/adjustUserCount'
-import getRethink from '../database/rethinkDriver'
 import {DataLoaderInstance} from '../dataloader/RootDataLoader'
 import generateUID from '../generateUID'
 import {DataLoaderWorker} from '../graphql/graphql'
@@ -61,7 +60,6 @@ const handleFirstAcceptedInvitation = async (
 }
 
 const acceptTeamInvitation = async (team: Team, userId: string, dataLoader: DataLoaderWorker) => {
-  const r = await getRethink()
   const pg = getKysely()
   const {id: teamId, orgId} = team
   const [user, organizationUser] = await Promise.all([
@@ -70,57 +68,42 @@ const acceptTeamInvitation = async (team: Team, userId: string, dataLoader: Data
   ])
   const {email, picture, preferredName} = user
   const teamLeadUserIdWithNewActions = await handleFirstAcceptedInvitation(team, dataLoader)
-  const [invitationNotificationIds] = await Promise.all([
-    r
-      .table('Notification')
-      .getAll(userId, {index: 'userId'})
-      .filter({
-        type: 'TEAM_INVITATION',
-        teamId
-      })
-      .update(
-        // not really clicked, but no longer important
-        {status: 'CLICKED'},
-        {returnChanges: true}
-      )('changes')('new_val')('id')
-      .default([])
-      .run(),
-    pg
-      .with('NotificationUpdate', (qc) =>
-        qc
-          .updateTable('Notification')
-          .set({status: 'CLICKED'})
-          .where('userId', '=', userId)
-          .where('teamId', '=', teamId)
-          .where('type', '=', 'TEAM_INVITATION')
-      )
-      .with('UserUpdate', (qc) =>
-        qc
-          .updateTable('User')
-          .set({tms: sql`arr_append_uniq("tms", ${teamId})`})
-          .where('id', '=', userId)
-      )
-      .with('TeamInvitationUpdate', (qb) =>
-        // redeem all invitations, otherwise if they have 2 someone could join after they've been kicked out
-        qb
-          .updateTable('TeamInvitation')
-          .set({acceptedAt: sql`CURRENT_TIMESTAMP`, acceptedBy: userId})
-          .where('email', '=', email)
-          .where('teamId', '=', teamId)
-      )
-      .insertInto('TeamMember')
-      .values({
-        id: TeamMemberId.join(teamId, userId),
-        teamId,
-        userId,
-        picture,
-        preferredName,
-        email,
-        openDrawer: 'manageTeam'
-      })
-      .onConflict((oc) => oc.column('id').doUpdateSet({isNotRemoved: true, isLead: false}))
-      .execute()
-  ])
+  const invitationNotifications = await pg
+    .with('TeamMemberInsert', (qc) =>
+      qc
+        .insertInto('TeamMember')
+        .values({
+          id: TeamMemberId.join(teamId, userId),
+          teamId,
+          userId,
+          picture,
+          preferredName,
+          email,
+          openDrawer: 'manageTeam'
+        })
+        .onConflict((oc) => oc.column('id').doUpdateSet({isNotRemoved: true, isLead: false}))
+    )
+    .with('UserUpdate', (qc) =>
+      qc
+        .updateTable('User')
+        .set({tms: sql`arr_append_uniq("tms", ${teamId})`})
+        .where('id', '=', userId)
+    )
+    .with('TeamInvitationUpdate', (qb) =>
+      // redeem all invitations, otherwise if they have 2 someone could join after they've been kicked out
+      qb
+        .updateTable('TeamInvitation')
+        .set({acceptedAt: sql`CURRENT_TIMESTAMP`, acceptedBy: userId})
+        .where('email', '=', email)
+        .where('teamId', '=', teamId)
+    )
+    .updateTable('Notification')
+    .set({status: 'CLICKED'})
+    .where('userId', '=', userId)
+    .where('teamId', '=', teamId)
+    .where('type', '=', 'TEAM_INVITATION')
+    .returning('id')
+    .execute()
   dataLoader.clearAll(['teamMembers', 'users', 'notifications'])
   if (!organizationUser) {
     // clear the cache, adjustUserCount will mutate these
@@ -133,7 +116,7 @@ const acceptTeamInvitation = async (team: Team, userId: string, dataLoader: Data
     }
     await setUserTierForUserIds([userId])
   }
-
+  const invitationNotificationIds = invitationNotifications.map(({id}) => id)
   // if accepted to team, don't count it towards the global denial count
   await pg
     .deleteFrom('PushInvitation')
@@ -142,7 +125,7 @@ const acceptTeamInvitation = async (team: Team, userId: string, dataLoader: Data
     .execute()
   return {
     teamLeadUserIdWithNewActions,
-    invitationNotificationIds: invitationNotificationIds as string[]
+    invitationNotificationIds
   }
 }
 
diff --git a/packages/server/utils/sendPromptToJoinOrg.ts b/packages/server/utils/sendPromptToJoinOrg.ts
index d97dbec630e..5c3ea988538 100644
--- a/packages/server/utils/sendPromptToJoinOrg.ts
+++ b/packages/server/utils/sendPromptToJoinOrg.ts
@@ -1,6 +1,5 @@
-import getRethink from '../database/rethinkDriver'
-import NotificationPromptToJoinOrg from '../database/types/NotificationPromptToJoinOrg'
 import User from '../database/types/User'
+import generateUID from '../generateUID'
 import {DataLoaderWorker} from '../graphql/graphql'
 import getKysely from '../postgres/getKysely'
 import getDomainFromEmail from './getDomainFromEmail'
@@ -9,21 +8,21 @@ import isRequestToJoinDomainAllowed from './isRequestToJoinDomainAllowed'
 const sendPromptToJoinOrg = async (newUser: User, dataLoader: DataLoaderWorker) => {
   const {id: userId, email} = newUser
   const pg = getKysely()
-  const r = await getRethink()
-
   const activeDomain = getDomainFromEmail(email)
 
   if (!(await isRequestToJoinDomainAllowed(activeDomain, newUser, dataLoader))) {
     return
   }
 
-  const notificationToInsert = new NotificationPromptToJoinOrg({
-    userId,
-    activeDomain
-  })
-
-  await r.table('Notification').insert(notificationToInsert).run()
-  await pg.insertInto('Notification').values(notificationToInsert).execute()
+  await pg
+    .insertInto('Notification')
+    .values({
+      id: generateUID(),
+      type: 'PROMPT_TO_JOIN_ORG',
+      userId,
+      activeDomain
+    })
+    .execute()
 }
 
 export default sendPromptToJoinOrg