feat: add resend notification functionality

- Introduced a new notification type "resend" to the system.
- Added database schema for resend notifications including fields for apiKey, fromAddress, and toAddress.
- Implemented functions to create, update, and send resend notifications.
- Updated notification router to handle resend notifications with appropriate API endpoints.
- Enhanced existing notification services to support sending notifications via the Resend service.
- Modified various notification utilities to accommodate the new resend functionality.
This commit is contained in:
mhbdev
2026-01-24 21:29:19 +03:30
parent bc6647071f
commit 57eee45dbb
21 changed files with 7677 additions and 54 deletions

View File

@@ -73,6 +73,7 @@
"qrcode": "^1.5.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"resend": "^6.0.2",
"shell-quote": "^1.8.1",
"slugify": "^1.6.6",
"ssh2": "1.15.0",

View File

@@ -69,6 +69,7 @@ enum notificationType {
telegram
discord
email
resend
gotify
ntfy
custom
@@ -456,6 +457,13 @@ table email {
toAddress text[] [not null]
}
table resend {
resendId text [pk, not null]
apiKey text [not null]
fromAddress text [not null]
toAddress text[] [not null]
}
table environment {
environmentId text [pk, not null]
name text [not null]
@@ -695,6 +703,7 @@ table notification {
telegramId text
discordId text
emailId text
resendId text
gotifyId text
ntfyId text
customId text
@@ -1139,6 +1148,8 @@ ref: notification.discordId - discord.discordId
ref: notification.emailId - email.emailId
ref: notification.resendId - resend.resendId
ref: notification.gotifyId - gotify.gotifyId
ref: notification.ntfyId - ntfy.ntfyId
@@ -1197,4 +1208,4 @@ ref: volume_backup.redisId - redis.redisId
ref: volume_backup.composeId - compose.composeId
ref: volume_backup.destinationId - destination.destinationId
ref: volume_backup.destinationId - destination.destinationId

View File

@@ -17,6 +17,7 @@ export const notificationType = pgEnum("notificationType", [
"telegram",
"discord",
"email",
"resend",
"gotify",
"ntfy",
"custom",
@@ -52,6 +53,9 @@ export const notifications = pgTable("notification", {
emailId: text("emailId").references(() => email.emailId, {
onDelete: "cascade",
}),
resendId: text("resendId").references(() => resend.resendId, {
onDelete: "cascade",
}),
gotifyId: text("gotifyId").references(() => gotify.gotifyId, {
onDelete: "cascade",
}),
@@ -110,6 +114,16 @@ export const email = pgTable("email", {
toAddresses: text("toAddress").array().notNull(),
});
export const resend = pgTable("resend", {
resendId: text("resendId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
apiKey: text("apiKey").notNull(),
fromAddress: text("fromAddress").notNull(),
toAddresses: text("toAddress").array().notNull(),
});
export const gotify = pgTable("gotify", {
gotifyId: text("gotifyId")
.notNull()
@@ -166,6 +180,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
fields: [notifications.emailId],
references: [email.emailId],
}),
resend: one(resend, {
fields: [notifications.resendId],
references: [resend.resendId],
}),
gotify: one(gotify, {
fields: [notifications.gotifyId],
references: [gotify.gotifyId],
@@ -315,6 +333,36 @@ export const apiTestEmailConnection = apiCreateEmail.pick({
fromAddress: true,
});
export const apiCreateResend = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
})
.extend({
apiKey: z.string().min(1),
fromAddress: z.string().min(1),
toAddresses: z.array(z.string()).min(1),
})
.required();
export const apiUpdateResend = apiCreateResend.partial().extend({
notificationId: z.string().min(1),
resendId: z.string().min(1),
organizationId: z.string().optional(),
});
export const apiTestResendConnection = apiCreateResend.pick({
apiKey: true,
fromAddress: true,
toAddresses: true,
});
export const apiCreateGotify = notificationsSchema
.pick({
appBuildError: true,
@@ -451,6 +499,7 @@ export const apiSendTest = notificationsSchema
username: z.string(),
password: z.string(),
toAddresses: z.array(z.string()),
apiKey: z.string(),
serverUrl: z.string(),
topic: z.string(),
appToken: z.string(),

View File

@@ -6,6 +6,7 @@ import {
type apiCreateGotify,
type apiCreateLark,
type apiCreateNtfy,
type apiCreateResend,
type apiCreateSlack,
type apiCreateTelegram,
type apiUpdateCustom,
@@ -14,6 +15,7 @@ import {
type apiUpdateGotify,
type apiUpdateLark,
type apiUpdateNtfy,
type apiUpdateResend,
type apiUpdateSlack,
type apiUpdateTelegram,
custom,
@@ -23,6 +25,7 @@ import {
lark,
notifications,
ntfy,
resend,
slack,
telegram,
} from "@dokploy/server/db/schema";
@@ -409,6 +412,100 @@ export const updateEmailNotification = async (
});
};
export const createResendNotification = async (
input: typeof apiCreateResend._type,
organizationId: string,
) => {
await db.transaction(async (tx) => {
const newResend = await tx
.insert(resend)
.values({
apiKey: input.apiKey,
fromAddress: input.fromAddress,
toAddresses: input.toAddresses,
})
.returning()
.then((value) => value[0]);
if (!newResend) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting resend",
});
}
const newDestination = await tx
.insert(notifications)
.values({
resendId: newResend.resendId,
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "resend",
organizationId: organizationId,
serverThreshold: input.serverThreshold,
})
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting notification",
});
}
return newDestination;
});
};
export const updateResendNotification = async (
input: typeof apiUpdateResend._type,
) => {
await db.transaction(async (tx) => {
const newDestination = await tx
.update(notifications)
.set({
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
organizationId: input.organizationId,
serverThreshold: input.serverThreshold,
})
.where(eq(notifications.notificationId, input.notificationId))
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error Updating notification",
});
}
await tx
.update(resend)
.set({
apiKey: input.apiKey,
fromAddress: input.fromAddress,
toAddresses: input.toAddresses,
})
.where(eq(resend.resendId, input.resendId))
.returning()
.then((value) => value[0]);
return newDestination;
});
};
export const createGotifyNotification = async (
input: typeof apiCreateGotify._type,
organizationId: string,
@@ -690,6 +787,7 @@ export const findNotificationById = async (notificationId: string) => {
telegram: true,
discord: true,
email: true,
resend: true,
gotify: true,
ntfy: true,
custom: true,

View File

@@ -11,6 +11,7 @@ import {
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendResendNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -44,6 +45,7 @@ export const sendBuildErrorNotifications = async ({
discord: true,
telegram: true,
slack: true,
resend: true,
gotify: true,
ntfy: true,
custom: true,
@@ -52,10 +54,10 @@ export const sendBuildErrorNotifications = async ({
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
const { email, resend, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification;
try {
if (email) {
if (email || resend) {
const template = await renderAsync(
BuildFailedEmail({
projectName,
@@ -66,11 +68,22 @@ export const sendBuildErrorNotifications = async ({
date: date.toLocaleString(),
}),
).catch();
await sendEmailNotification(
email,
"Build failed for dokploy",
template,
);
if (email) {
await sendEmailNotification(
email,
"Build failed for dokploy",
template,
);
}
if (resend) {
await sendResendNotification(
resend,
"Build failed for dokploy",
template,
);
}
}
if (discord) {

View File

@@ -12,6 +12,7 @@ import {
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendResendNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -47,6 +48,7 @@ export const sendBuildSuccessNotifications = async ({
discord: true,
telegram: true,
slack: true,
resend: true,
gotify: true,
ntfy: true,
custom: true,
@@ -55,10 +57,10 @@ export const sendBuildSuccessNotifications = async ({
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
const { email, resend, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification;
try {
if (email) {
if (email || resend) {
const template = await renderAsync(
BuildSuccessEmail({
projectName,
@@ -69,11 +71,22 @@ export const sendBuildSuccessNotifications = async ({
environmentName,
}),
).catch();
await sendEmailNotification(
email,
"Build success for dokploy",
template,
);
if (email) {
await sendEmailNotification(
email,
"Build success for dokploy",
template,
);
}
if (resend) {
await sendResendNotification(
resend,
"Build success for dokploy",
template,
);
}
}
if (discord) {

View File

@@ -11,6 +11,7 @@ import {
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendResendNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -44,6 +45,7 @@ export const sendDatabaseBackupNotifications = async ({
discord: true,
telegram: true,
slack: true,
resend: true,
gotify: true,
ntfy: true,
custom: true,
@@ -52,10 +54,10 @@ export const sendDatabaseBackupNotifications = async ({
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
const { email, resend, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification;
try {
if (email) {
if (email || resend) {
const template = await renderAsync(
DatabaseBackupEmail({
projectName,
@@ -66,11 +68,22 @@ export const sendDatabaseBackupNotifications = async ({
date: date.toLocaleString(),
}),
).catch();
await sendEmailNotification(
email,
"Database backup for dokploy",
template,
);
if (email) {
await sendEmailNotification(
email,
"Database backup for dokploy",
template,
);
}
if (resend) {
await sendResendNotification(
resend,
"Database backup for dokploy",
template,
);
}
}
if (discord) {

View File

@@ -11,6 +11,7 @@ import {
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendResendNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -31,6 +32,7 @@ export const sendDockerCleanupNotifications = async (
discord: true,
telegram: true,
slack: true,
resend: true,
gotify: true,
ntfy: true,
custom: true,
@@ -39,19 +41,29 @@ export const sendDockerCleanupNotifications = async (
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
const { email, resend, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification;
try {
if (email) {
if (email || resend) {
const template = await renderAsync(
DockerCleanupEmail({ message, date: date.toLocaleString() }),
).catch();
await sendEmailNotification(
email,
"Docker cleanup for dokploy",
template,
);
if (email) {
await sendEmailNotification(
email,
"Docker cleanup for dokploy",
template,
);
}
if (resend) {
await sendResendNotification(
resend,
"Docker cleanup for dokploy",
template,
);
}
}
if (discord) {

View File

@@ -11,6 +11,7 @@ import {
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendResendNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -25,6 +26,7 @@ export const sendDokployRestartNotifications = async () => {
discord: true,
telegram: true,
slack: true,
resend: true,
gotify: true,
ntfy: true,
custom: true,
@@ -33,20 +35,30 @@ export const sendDokployRestartNotifications = async () => {
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
const { email, resend, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification;
try {
if (email) {
if (email || resend) {
const template = await renderAsync(
DokployRestartEmail({ date: date.toLocaleString() }),
).catch();
await sendEmailNotification(
email,
"Dokploy Server Restarted",
template,
);
if (email) {
await sendEmailNotification(
email,
"Dokploy Server Restarted",
template,
);
}
if (resend) {
await sendResendNotification(
resend,
"Dokploy Server Restarted",
template,
);
}
}
if (discord) {

View File

@@ -5,10 +5,12 @@ import type {
gotify,
lark,
ntfy,
resend,
slack,
telegram,
} from "@dokploy/server/db/schema";
import nodemailer from "nodemailer";
import { Resend } from "resend";
export const sendEmailNotification = async (
connection: typeof email.$inferInsert,
@@ -45,6 +47,32 @@ export const sendEmailNotification = async (
}
};
export const sendResendNotification = async (
connection: typeof resend.$inferInsert,
subject: string,
htmlContent: string,
) => {
try {
const client = new Resend(connection.apiKey);
const result = await client.emails.send({
from: connection.fromAddress,
to: connection.toAddresses,
subject,
html: htmlContent,
});
if (result.error) {
throw new Error(result.error.message);
}
} catch (err) {
console.log(err);
throw new Error(
`Failed to send Resend notification ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
};
export const sendDiscordNotification = async (
connection: typeof discord.$inferInsert,
embed: any,

View File

@@ -9,6 +9,7 @@ import {
sendEmailNotification,
sendGotifyNotification,
sendNtfyNotification,
sendResendNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -51,15 +52,17 @@ export const sendVolumeBackupNotifications = async ({
discord: true,
telegram: true,
slack: true,
resend: true,
gotify: true,
ntfy: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy } = notification;
const { email, resend, discord, telegram, slack, gotify, ntfy } =
notification;
if (email) {
if (email || resend) {
const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`;
const htmlContent = await renderAsync(
VolumeBackupEmail({
@@ -73,7 +76,12 @@ export const sendVolumeBackupNotifications = async ({
date: date.toISOString(),
}),
);
await sendEmailNotification(email, subject, htmlContent);
if (email) {
await sendEmailNotification(email, subject, htmlContent);
}
if (resend) {
await sendResendNotification(resend, subject, htmlContent);
}
}
if (discord) {