From b610f7aeffccbb6f841bcb73ec50a4b68733d152 Mon Sep 17 00:00:00 2001
From: Mauricio Siu
Date: Fri, 24 Apr 2026 21:40:08 -0600
Subject: [PATCH 1/2] feat: implement invitation email functionality for
organization creation
- Added `sendInvitationEmail` function to send invitation emails when a new organization is created in the cloud environment.
- Updated email template to enhance the invitation message and included a direct link for users to accept the invitation.
- Refactored email sending logic in the user router to utilize the new invitation email rendering function.
- Improved organization invitation email design for better user experience.
---
.../server/api/routers/organization.ts | 20 +++-
apps/dokploy/server/api/routers/user.ts | 32 +++---
.../server/src/emails/emails/invitation.tsx | 106 ++++++++++--------
packages/server/src/index.ts | 1 +
packages/server/src/lib/auth.ts | 17 ---
.../verification/send-verification-email.tsx | 36 ++++++
6 files changed, 132 insertions(+), 80 deletions(-)
diff --git a/apps/dokploy/server/api/routers/organization.ts b/apps/dokploy/server/api/routers/organization.ts
index 2f9da6d71..51c1fec5d 100644
--- a/apps/dokploy/server/api/routers/organization.ts
+++ b/apps/dokploy/server/api/routers/organization.ts
@@ -1,5 +1,5 @@
import { db } from "@dokploy/server/db";
-import { IS_CLOUD } from "@dokploy/server/index";
+import { IS_CLOUD, sendInvitationEmail } from "@dokploy/server/index";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, exists } from "drizzle-orm";
import { nanoid } from "nanoid";
@@ -325,6 +325,24 @@ export const organizationRouter = createTRPCRouter({
})
.returning();
+ if (IS_CLOUD && created) {
+ const host =
+ process.env.NODE_ENV === "development"
+ ? "http://localhost:3000"
+ : "https://app.dokploy.com";
+ const inviteLink = `${host}/invitation?token=${created.id}`;
+
+ const org = await db.query.organization.findFirst({
+ where: eq(organization.id, orgId),
+ });
+
+ await sendInvitationEmail({
+ email,
+ inviteLink,
+ organizationName: org?.name || "organization",
+ });
+ }
+
await audit(ctx, {
action: "create",
resourceType: "organization",
diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts
index 63578b099..93b7e6cf6 100644
--- a/apps/dokploy/server/api/routers/user.ts
+++ b/apps/dokploy/server/api/routers/user.ts
@@ -9,12 +9,12 @@ import {
getWebServerSettings,
IS_CLOUD,
removeUserById,
+ renderInvitationEmail,
sendEmailNotification,
sendResendNotification,
updateUser,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
-import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
import {
account,
apiAssignPermissions,
@@ -29,6 +29,7 @@ import {
hasPermission,
resolvePermissions,
} from "@dokploy/server/services/permission";
+import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { and, asc, eq, gt } from "drizzle-orm";
@@ -639,27 +640,26 @@ export const userRouter = createTRPCRouter({
);
try {
- const htmlContent = `
-\t\t\t\tYou are invited to join ${organization?.name || "organization"} on Dokploy. Click the link to accept the invitation: Accept Invitation
-\t\t\t\t`;
+ const toEmail = currentInvitation?.email || "";
+ const orgName = organization?.name || "organization";
+ const subject = `You've been invited to join ${orgName} on Dokploy`;
+ const html = await renderInvitationEmail({
+ email: toEmail,
+ inviteLink,
+ organizationName: orgName,
+ });
if (email) {
await sendEmailNotification(
- {
- ...email,
- toAddresses: [currentInvitation?.email || ""],
- },
- "Invitation to join organization",
- htmlContent,
+ { ...email, toAddresses: [toEmail] },
+ subject,
+ html,
);
} else if (resend) {
await sendResendNotification(
- {
- ...resend,
- toAddresses: [currentInvitation?.email || ""],
- },
- "Invitation to join organization",
- htmlContent,
+ { ...resend, toAddresses: [toEmail] },
+ subject,
+ html,
);
}
} catch (error) {
diff --git a/packages/server/src/emails/emails/invitation.tsx b/packages/server/src/emails/emails/invitation.tsx
index 833b77286..dd075aecf 100644
--- a/packages/server/src/emails/emails/invitation.tsx
+++ b/packages/server/src/emails/emails/invitation.tsx
@@ -14,21 +14,18 @@ import {
Text,
} from "@react-email/components";
-export type TemplateProps = {
- email: string;
- name: string;
-};
-
-interface VercelInviteUserEmailProps {
+interface InvitationEmailProps {
inviteLink: string;
toEmail: string;
+ organizationName: string;
}
export const InvitationEmail = ({
inviteLink,
toEmail,
-}: VercelInviteUserEmailProps) => {
- const previewText = "Join to Dokploy";
+ organizationName = "an organization",
+}: InvitationEmailProps) => {
+ const previewText = `You've been invited to join ${organizationName} on Dokploy`;
return (
@@ -44,50 +41,67 @@ export const InvitationEmail = ({
},
}}
>
-
-
-
+
+
+ {/* Header */}
+
-
- Join to Dokploy
-
-
- Hello,
-
-
- You have been invited to join Dokploy , a platform
- that helps for deploying your apps to the cloud.
-
-
-
- Join the team 🚀
-
+
+ {/* Body */}
+
+
+ You've been invited to join {organizationName}
+
+
+ You have been invited to join{" "}
+ {organizationName} {" "}
+ on Dokploy, the platform for deploying your apps to the cloud.
+ Click the button below to accept the invitation.
+
+
+ {/* CTA Button */}
+
+
+ Accept Invitation
+
+
+
+
+ If the button above doesn't work, copy and paste the following
+ link into your browser:
+
+
+ {inviteLink}
+
+
+
+ {/* Footer */}
+
+
+
+ This invitation was intended for{" "}
+ {toEmail} . This invite
+ was sent from{" "}
+
+ Dokploy Cloud
+
+ . If you were not expecting this invitation, you can safely
+ ignore this email.
+
-
- or copy and paste this URL into your browser:{" "}
-
- https://dokploy.com
-
-
-
-
- This invitation was intended for {toEmail}. This invite was sent
- from dokploy.com . If you
- were not expecting this invitation, you can ignore this email. If
- you are concerned about your account's safety, please reply to
-
diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
index e6fd0ba59..717c20246 100644
--- a/packages/server/src/index.ts
+++ b/packages/server/src/index.ts
@@ -108,6 +108,7 @@ export * from "./utils/notifications/docker-cleanup";
export * from "./utils/notifications/dokploy-restart";
export * from "./utils/notifications/server-threshold";
export * from "./utils/notifications/utils";
+export * from "./verification/send-verification-email";
export * from "./utils/process/execAsync";
export * from "./utils/process/spawnAsync";
export * from "./utils/providers/bitbucket";
diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts
index afbc57881..069be48cc 100644
--- a/packages/server/src/lib/auth.ts
+++ b/packages/server/src/lib/auth.ts
@@ -409,23 +409,6 @@ const { handler, api } = betterAuth({
enabled: true,
maximumRolesPerOrganization: 10,
},
- async sendInvitationEmail(data, _request) {
- if (IS_CLOUD) {
- const host =
- process.env.NODE_ENV === "development"
- ? "http://localhost:3000"
- : "https://app.dokploy.com";
- const inviteLink = `${host}/invitation?token=${data.id}`;
-
- await sendEmail({
- email: data.email,
- subject: "Invitation to join organization",
- text: `
- You are invited to join ${data.organization.name} on Dokploy. Click the link to accept the invitation: Accept Invitation
- `,
- });
- }
- },
}),
...(IS_CLOUD
? [
diff --git a/packages/server/src/verification/send-verification-email.tsx b/packages/server/src/verification/send-verification-email.tsx
index 098789a2b..fe9cd6ee6 100644
--- a/packages/server/src/verification/send-verification-email.tsx
+++ b/packages/server/src/verification/send-verification-email.tsx
@@ -1,4 +1,5 @@
import { renderAsync } from "@react-email/components";
+import InvitationEmail from "../emails/emails/invitation";
import VerifyEmailTemplate from "../emails/emails/verify-email";
import { sendEmailNotification } from "../utils/notifications/utils";
@@ -51,3 +52,38 @@ export const sendVerificationEmail = async ({
text: html,
});
};
+
+export const renderInvitationEmail = async ({
+ email,
+ inviteLink,
+ organizationName,
+}: {
+ email: string;
+ inviteLink: string;
+ organizationName: string;
+}) => {
+ return renderAsync(
+ InvitationEmail({
+ inviteLink,
+ toEmail: email,
+ organizationName,
+ }),
+ );
+};
+
+export const sendInvitationEmail = async ({
+ email,
+ inviteLink,
+ organizationName,
+}: {
+ email: string;
+ inviteLink: string;
+ organizationName: string;
+}) => {
+ const html = await renderInvitationEmail({ email, inviteLink, organizationName });
+ await sendEmail({
+ email,
+ subject: `You've been invited to join ${organizationName} on Dokploy`,
+ text: html,
+ });
+};
From c41b69c925d88dab8dffc228882cf3a56a7a185d Mon Sep 17 00:00:00 2001
From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com>
Date: Sat, 25 Apr 2026 03:40:50 +0000
Subject: [PATCH 2/2] [autofix.ci] apply automated fixes
---
.../server/src/verification/send-verification-email.tsx | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/packages/server/src/verification/send-verification-email.tsx b/packages/server/src/verification/send-verification-email.tsx
index fe9cd6ee6..e6ae0250a 100644
--- a/packages/server/src/verification/send-verification-email.tsx
+++ b/packages/server/src/verification/send-verification-email.tsx
@@ -80,7 +80,11 @@ export const sendInvitationEmail = async ({
inviteLink: string;
organizationName: string;
}) => {
- const html = await renderInvitationEmail({ email, inviteLink, organizationName });
+ const html = await renderInvitationEmail({
+ email,
+ inviteLink,
+ organizationName,
+ });
await sendEmail({
email,
subject: `You've been invited to join ${organizationName} on Dokploy`,