diff --git a/prisma/migrations/20230803120315_create_notification_channel_table/migration.sql b/prisma/migrations/20230803120315_create_notification_channel_table/migration.sql
new file mode 100644
index 0000000000000000000000000000000000000000..ef3b648b59b55fc0d4b3d12a32a531461291ad51
--- /dev/null
+++ b/prisma/migrations/20230803120315_create_notification_channel_table/migration.sql
@@ -0,0 +1,13 @@
+-- CreateTable
+CREATE TABLE "NotificationChannel" (
+    "id" SERIAL NOT NULL,
+    "userId" INTEGER NOT NULL,
+    "channelType" INTEGER NOT NULL,
+    "notificationTypes" INTEGER NOT NULL,
+    "recipient" TEXT NOT NULL,
+
+    CONSTRAINT "NotificationChannel_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "NotificationChannel" ADD CONSTRAINT "NotificationChannel_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 00f3dfb564b889358834f21a33decaa55c2ab6e2..9f37714a220ae0d639b291d4cf884b24dfb780ee 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -11,20 +11,21 @@ datasource db {
 }
 
 model User {
-	id                     Int               @id @default(autoincrement())
+	id                     Int                   @id @default(autoincrement())
 	email                  String
 	name                   String
-	balance                Int               @default(0)
-	createdAt              DateTime          @default(now())
-	transactions           Transaction[]     @relation("user")
-	transactionsVerified   Transaction[]     @relation("verifiedBy")
+	balance                Int                   @default(0)
+	createdAt              DateTime              @default(now())
+	transactions           Transaction[]         @relation("user")
+	transactionsVerified   Transaction[]         @relation("verifiedBy")
 	itemTransactions       ItemTransaction[]
 	cards                  UserCard[]
-	moneyTransfersSent     MoneyTransfer[]   @relation("from")
-	moneyTransfersReceived MoneyTransfer[]   @relation("to")
-	vouchersUsed           Voucher[]         @relation("vouchersUsed")
-	updatedAt              DateTime          @updatedAt
-	comment                String            @default("")
+	moneyTransfersSent     MoneyTransfer[]       @relation("from")
+	moneyTransfersReceived MoneyTransfer[]       @relation("to")
+	vouchersUsed           Voucher[]             @relation("vouchersUsed")
+	notificationChannels   NotificationChannel[]
+	updatedAt              DateTime              @updatedAt
+	comment                String                @default("")
 }
 
 model UserCard {
@@ -113,3 +114,12 @@ model Voucher {
 	usedById	Int?
 	usedBy    User?    @relation("vouchersUsed", fields: [usedById], references: [id])
 }
+
+model NotificationChannel {
+	id                Int  @id @default(autoincrement())
+	userId            Int
+	user              User @relation(fields: [userId], references: [id])
+	channelType	      Int
+	notificationTypes Int
+	recipient         String
+}
diff --git a/src/lib/notifications/channelTypes.js b/src/lib/notifications/channelTypes.js
new file mode 100644
index 0000000000000000000000000000000000000000..ffbf120472a1eed547104364d6177124199144d9
--- /dev/null
+++ b/src/lib/notifications/channelTypes.js
@@ -0,0 +1,13 @@
+import { handleWebhook } from "./webhookHandler";
+import { handleEmail } from "./emailHandler";
+
+/**
+ * @type {{
+ *  WEBHOOK: import("./types").ChannelType<?>,
+ *  EMAIL: import("./types").ChannelType<?>
+ * }}
+ */
+export const ChannelType = {
+	WEBHOOK: {key: 1<<0, handler: handleWebhook},
+	EMAIL: {key: 1<<1, handler: handleEmail},
+};
diff --git a/src/lib/notifications/emailHandler.ts b/src/lib/notifications/emailHandler.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b05ce35126786325d2d2acb2531a6ac838b36e3d
--- /dev/null
+++ b/src/lib/notifications/emailHandler.ts
@@ -0,0 +1,8 @@
+import { formatMessage, getMessageTitle } from "./formatter";
+import type { HandlerReturnType, NotificationData, NotificationType, PossibleNotificationType } from "./types";
+
+export async function handleEmail<T extends PossibleNotificationType>(recipient: string, type: NotificationType<T>, data: NotificationData<T>): Promise<HandlerReturnType> {
+	const emailTitle = getMessageTitle(type);
+	const emailBody = formatMessage(type, data);
+	return Promise.resolve(); // TODO actually send email
+};
diff --git a/src/lib/notifications/formatter.ts b/src/lib/notifications/formatter.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1bb7a2d78e221005849bc8955629cfa392a34b94
--- /dev/null
+++ b/src/lib/notifications/formatter.ts
@@ -0,0 +1,77 @@
+import type { NotificationData, NotificationType, PossibleNotificationType } from "./types";
+
+export function getMessageTitle<T extends PossibleNotificationType>(type: NotificationType<T>): string {
+	switch(type.name){
+		case "buy": {
+			return "Dein Einkauf";
+		}
+		case "refund": {
+			return "Geld erstattet";
+		}
+		case "deposit": {
+			return "Geld eingezahlt";
+		}
+		case "withdraw": {
+			return "Geld ausgezahlt";
+		}
+		case "use-voucher": {
+			return "Gutschein eingelöst";
+		}
+		case "send-transfer": {
+			return "Überweisung gesendet";
+		}
+		case "receive-transfer": {
+			return "Überweisung erhalten";
+		}
+		default: {
+			console.error(`Unknown notification type`, type);
+			return `Unknown notification type ${type}`;
+		}
+	}
+}			
+
+export function formatMessage<T extends PossibleNotificationType>(type: NotificationType<T>, data: NotificationData<T>): string {
+	switch(type.name){
+		case "buy": {
+			const {total, items, balanceBefore, balanceAfter} = data as NotificationData<"buy">;
+			const premiums = items.filter(item => item.premium && item.premium > 0).map(item => item.premium).reduce((a,b) => a+b, 0);
+			return `Du hast folgende ${items.length} Artikel gekauft:
+${items.map(item => `- ${item.name} (${item.code}) - ${(item.price/100).toFixed(2)}€` + (item.premium && item.premium > 0 ? ` (+${(item.premium/100).toFixed(2)}€)` : "")).join("\n")}
+Gesamt: ${(total/100).toFixed(2)}€` + (premiums > 0 ? ` (davon ${(premiums/100).toFixed(2)}€ Negativaufschlag)` : "") + `\nDein neuer Kontostand beträgt ${(balanceAfter/100).toFixed(2)}€.`;
+		}
+		case "refund": {
+			const {refund, item, balanceBefore, balanceAfter, timeBought} = data as NotificationData<"refund">;
+			return `Dir wurden ${(refund/100).toFixed(2)}€ für ${item.name} (${item.code}) erstattet, gekauft am ${timeBought.toLocaleString()}.
+Dein neuer Kontostand beträgt ${(balanceAfter/100).toFixed(2)}€.`;
+		}
+		case "deposit": {
+			const {amount, balanceBefore, balanceAfter} = data as NotificationData<"deposit">;
+			return `Du hast ${(amount/100).toFixed(2)}€ eingezahlt.
+Dein neuer Kontostand beträgt ${(balanceAfter/100).toFixed(2)}€.`;
+		}
+		case "withdraw": {
+			const {amount, balanceBefore, balanceAfter} = data as NotificationData<"withdraw">;
+			return `Du hast ${(amount/100).toFixed(2)}€ ausgezahlt.
+Dein neuer Kontostand beträgt ${(balanceAfter/100).toFixed(2)}€.`;
+		}
+		case "use-voucher": {
+			const {voucher, balanceBefore, balanceAfter} = data as NotificationData<"use-voucher">;
+			return `Du hast einen Gutschein im Wert von ${(voucher.value/100).toFixed(2)}€ eingelöst.
+Dein neuer Kontostand beträgt ${(balanceAfter/100).toFixed(2)}€.`;
+		}
+		case "send-transfer": {
+			const {amount, balanceBefore, balanceAfter, receiver} = data as NotificationData<"send-transfer">;
+			return `Du hast ${(amount/100).toFixed(2)}€ an ${receiver.name} überwiesen.
+Dein neuer Kontostand beträgt ${(balanceAfter/100).toFixed(2)}€.`;
+		}
+		case "receive-transfer": {
+			const {amount, balanceBefore, balanceAfter, sender} = data as NotificationData<"receive-transfer">;
+			return `Du hast ${(amount/100).toFixed(2)}€ von ${sender.name} erhalten.
+Dein neuer Kontostand beträgt ${(balanceAfter/100).toFixed(2)}€.`;
+		}
+		default: {
+			console.error(`Unknown notification type`, type);
+			return `Unknown notification type ${type}`;
+		}
+	}
+}
diff --git a/src/lib/notifications/handler.ts b/src/lib/notifications/handler.ts
new file mode 100644
index 0000000000000000000000000000000000000000..780dda94901fe3f7c0ef4130babf3093bd4e6cd0
--- /dev/null
+++ b/src/lib/notifications/handler.ts
@@ -0,0 +1,38 @@
+import { db } from "$lib/server/database";
+import { ChannelType } from "./channelTypes";
+import type { HandlerReturnType, NotificationChannel, NotificationData, NotificationType, PossibleNotificationType } from "./types";
+
+export async function sendNotification<T extends PossibleNotificationType>(user: { id: number, notificationChannels: NotificationChannel[] | undefined } | number, type: NotificationType<T>, data: NotificationData<T>): Promise<HandlerReturnType[]>{
+	let notificationChannels: NotificationChannel[];
+	let userId: number;
+	if(typeof user === "number"){
+		userId = user;
+	}else{
+		userId = user.id;
+		notificationChannels = user.notificationChannels?.filter(channel => channel.notificationTypes & type.key);
+	}
+	if(!notificationChannels){
+		notificationChannels = await getNotificationChannels(userId, type.key);
+	}
+	return Promise.all(notificationChannels.map(channel => {
+		switch(channel.channelType){
+			case ChannelType.EMAIL.key: {
+				return ChannelType.EMAIL.handler(channel.recipient, type, data);
+			}
+			case ChannelType.WEBHOOK.key: {
+				return ChannelType.WEBHOOK.handler(channel.recipient, type, data);
+			}
+			default: {
+				console.error(`Unknown channel type`, channel.channelType);
+				return Promise.resolve();
+			}
+		};
+	}));
+};
+
+async function getNotificationChannels(userId: number, notificationType: number): Promise<NotificationChannel[]> {
+	const allChannels = await db.notificationChannel.findMany({
+		where: { userId }
+	});
+	return allChannels.filter(channel => channel.notificationTypes & notificationType);
+}
diff --git a/src/lib/notifications/notificationTypes.js b/src/lib/notifications/notificationTypes.js
new file mode 100644
index 0000000000000000000000000000000000000000..399eab045e4ede0eaac44bf2e1610175c97410c9
--- /dev/null
+++ b/src/lib/notifications/notificationTypes.js
@@ -0,0 +1,19 @@
+/** @type {{
+ * BUY: import("./types").NotificationType<"buy">,
+ * REFUND: import("./types").NotificationType<"refund">,
+ * DEPOSIT: import("./types").NotificationType<"deposit">,
+ * WITHDRAW: import("./types").NotificationType<"withdraw">,
+ * USE_VOUCHER: import("./types").NotificationType<"use-voucher">,
+ * SEND_TRANSFER: import("./types").NotificationType<"send-transfer">,
+ * RECEIVE_TRANSFER: import("./types").NotificationType<"receive-transfer">,
+ * }}
+ */
+export const NotificationType = {
+	BUY: {key: 1<<0, name: "buy"},
+	REFUND: {key: 1<<1, name: "refund"},
+	DEPOSIT: {key: 1<<2, name: "deposit"},
+	WITHDRAW: {key: 1<<3, name: "withdraw"},
+	USE_VOUCHER: {key: 1<<4, name: "use-voucher"},
+	SEND_TRANSFER: {key: 1<<5, name: "send-transfer"},
+	RECEIVE_TRANSFER: {key: 1<<6, name: "receive-transfer"},
+};
diff --git a/src/lib/notifications/types.ts b/src/lib/notifications/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a7f1eb858a5bf9ab4492e03e41d67d9b459c3c15
--- /dev/null
+++ b/src/lib/notifications/types.ts
@@ -0,0 +1,35 @@
+export type PossibleNotificationType = "buy" | "refund" | "deposit" | "withdraw" | "use-voucher" | "send-transfer" | "receive-transfer";
+
+export type Item = {name: string, code: string, price: number, premium: number|undefined};
+export type User = {name: string, id: number}
+
+export type NotificationType<T extends PossibleNotificationType> = {key: number, name: T};
+export type BuyNotificationData = {total: number, items: Item[], balanceBefore: number, balanceAfter: number};
+export type RefundNotificationData = {refund: number, item: Item, balanceBefore: number, balanceAfter: number, timeBought: Date};
+export type DepositNotificationData = {amount: number, balanceBefore: number, balanceAfter: number};
+export type WithdrawNotificationData = {amount: number, balanceBefore: number, balanceAfter: number};
+export type UseVoucherNotificationData = {voucher: {code: string, value: number}, balanceBefore: number, balanceAfter: number};
+export type SendTransferNotificationData = {amount: number, receiver: User, balanceBefore: number, balanceAfter: number};
+export type ReceiveTransferNotificationData = {amount: number, sender: User, balanceBefore: number, balanceAfter: number};
+export type NotificationData<T extends PossibleNotificationType> =
+    T extends "buy" ? BuyNotificationData :
+    T extends "refund" ? RefundNotificationData :
+    T extends "deposit" ? DepositNotificationData :
+    T extends "withdraw" ? WithdrawNotificationData :
+    T extends "use-voucher" ? UseVoucherNotificationData :
+    T extends "send-transfer" ? SendTransferNotificationData :
+    T extends "receive-transfer" ? ReceiveTransferNotificationData :
+    never;
+
+export type HandlerReturnType = any;
+export type NotificationHandler<T extends PossibleNotificationType> = (recipient: string, type: NotificationType<T>, data: NotificationData<T>) => Promise<HandlerReturnType>;
+export type ChannelType<T extends PossibleNotificationType> = {key: number, handler: NotificationHandler<T>};
+
+export type NotificationChannel = {
+	id: number,
+	userId: number,
+	channelType: number,
+	notificationTypes: number,
+	recipient: string
+};
+export type GenericNotificationHandler<T extends PossibleNotificationType> = (user: {id:number,notificationChannels:NotificationChannel[]|undefined}|number, type: NotificationType<T>, data: NotificationData<T>) => Promise<any>;
diff --git a/src/lib/notifications/webhookHandler.ts b/src/lib/notifications/webhookHandler.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7f7429860028c84bb18e192320102812f2cdfeb7
--- /dev/null
+++ b/src/lib/notifications/webhookHandler.ts
@@ -0,0 +1,15 @@
+import { formatMessage } from "./formatter";
+import type { HandlerReturnType, NotificationData, NotificationType, PossibleNotificationType } from "./types";
+
+export async function handleWebhook<T extends PossibleNotificationType>(recipient: string, type: NotificationType<T>, data: NotificationData<T>): Promise<HandlerReturnType> {
+	const body = JSON.stringify(Object.assign({}, data, {type: type.name, content: formatMessage(type, data)}));
+	const headers = {
+		"Content-Type": "application/json"
+	};
+	console.log({recipient, type, data, body});
+	return fetch(recipient, {
+		method: "POST",
+		body,
+		headers
+	});
+};