Merge pull request #2699 from ChristoferMendes/feature/add-custom-webhook-notification-provider

feat(notifications): add custom webhook notification provider
This commit is contained in:
Mauricio Siu
2025-12-07 13:45:16 -06:00
committed by GitHub
27 changed files with 10255 additions and 129 deletions
@@ -1,7 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ApplicationNested } from "@dokploy/server/utils/builders"; import type { ApplicationNested } from "@dokploy/server/utils/builders";
import { mechanizeDockerContainer } from "@dokploy/server/utils/builders"; import { mechanizeDockerContainer } from "@dokploy/server/utils/builders";
import { beforeEach, describe, expect, it, vi } from "vitest";
type MockCreateServiceOptions = { type MockCreateServiceOptions = {
TaskTemplate?: { TaskTemplate?: {
@@ -1,15 +1,8 @@
import type { findEnvironmentsByProjectId } from "@dokploy/server"; import type { findEnvironmentsByProjectId } from "@dokploy/server";
import { import { ChevronDownIcon, PencilIcon, PlusIcon, TrashIcon } from "lucide-react";
ChevronDownIcon,
PencilIcon,
PlusIcon,
Terminal,
TrashIcon,
} from "lucide-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { EnvironmentVariables } from "@/components/dashboard/project/environment-variables";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -246,20 +239,6 @@ export const AdvancedEnvironmentSelector = ({
)} )}
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
{/* Action buttons for non-production environments */}
{/* <EnvironmentVariables environmentId={environment.environmentId}>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
onClick={(e) => {
e.stopPropagation();
}}
>
<Terminal className="h-3 w-3" />
</Button>
</EnvironmentVariables> */}
{environment.name !== "production" && ( {environment.name !== "production" && (
<div className="flex items-center gap-1 px-2"> <div className="flex items-center gap-1 px-2">
<Button <Button
@@ -1,5 +1,11 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Mail, PenBoxIcon, PlusIcon } from "lucide-react"; import {
AlertTriangle,
Mail,
PenBoxIcon,
PlusIcon,
Trash2,
} from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form"; import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -108,6 +114,21 @@ export const notificationSchema = z.discriminatedUnion("type", [
priority: z.number().min(1).max(5).default(3), priority: z.number().min(1).max(5).default(3),
}) })
.merge(notificationBaseSchema), .merge(notificationBaseSchema),
z
.object({
type: z.literal("custom"),
endpoint: z.string().min(1, { message: "Endpoint URL is required" }),
headers: z
.array(
z.object({
key: z.string(),
value: z.string(),
}),
)
.optional()
.default([]),
})
.merge(notificationBaseSchema),
z z
.object({ .object({
type: z.literal("lark"), type: z.literal("lark"),
@@ -145,6 +166,10 @@ export const notificationsMap = {
icon: <NtfyIcon />, icon: <NtfyIcon />,
label: "ntfy", label: "ntfy",
}, },
custom: {
icon: <PenBoxIcon size={29} className="text-muted-foreground" />,
label: "Custom",
},
}; };
export type NotificationSchema = z.infer<typeof notificationSchema>; export type NotificationSchema = z.infer<typeof notificationSchema>;
@@ -180,6 +205,13 @@ export const HandleNotifications = ({ notificationId }: Props) => {
api.notification.testNtfyConnection.useMutation(); api.notification.testNtfyConnection.useMutation();
const { mutateAsync: testLarkConnection, isLoading: isLoadingLark } = const { mutateAsync: testLarkConnection, isLoading: isLoadingLark } =
api.notification.testLarkConnection.useMutation(); api.notification.testLarkConnection.useMutation();
const { mutateAsync: testCustomConnection, isLoading: isLoadingCustom } =
api.notification.testCustomConnection.useMutation();
const customMutation = notificationId
? api.notification.updateCustom.useMutation()
: api.notification.createCustom.useMutation();
const slackMutation = notificationId const slackMutation = notificationId
? api.notification.updateSlack.useMutation() ? api.notification.updateSlack.useMutation()
: api.notification.createSlack.useMutation(); : api.notification.createSlack.useMutation();
@@ -218,6 +250,15 @@ export const HandleNotifications = ({ notificationId }: Props) => {
name: "toAddresses" as never, name: "toAddresses" as never,
}); });
const {
fields: headerFields,
append: appendHeader,
remove: removeHeader,
} = useFieldArray({
control: form.control,
name: "headers" as never,
});
useEffect(() => { useEffect(() => {
if (type === "email" && fields.length === 0) { if (type === "email" && fields.length === 0) {
append(""); append("");
@@ -330,6 +371,26 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dockerCleanup: notification.dockerCleanup, dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold, serverThreshold: notification.serverThreshold,
}); });
} else if (notification.notificationType === "custom") {
form.reset({
appBuildError: notification.appBuildError,
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
type: notification.notificationType,
endpoint: notification.custom?.endpoint || "",
headers: notification.custom?.headers
? Object.entries(notification.custom.headers).map(
([key, value]) => ({
key,
value,
}),
)
: [],
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
});
} }
} else { } else {
form.reset(); form.reset();
@@ -344,6 +405,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
gotify: gotifyMutation, gotify: gotifyMutation,
ntfy: ntfyMutation, ntfy: ntfyMutation,
lark: larkMutation, lark: larkMutation,
custom: customMutation,
}; };
const onSubmit = async (data: NotificationSchema) => { const onSubmit = async (data: NotificationSchema) => {
@@ -467,6 +529,32 @@ export const HandleNotifications = ({ notificationId }: Props) => {
larkId: notification?.larkId || "", larkId: notification?.larkId || "",
serverThreshold: serverThreshold, serverThreshold: serverThreshold,
}); });
} else if (data.type === "custom") {
// Convert headers array to object
const headersRecord =
data.headers && data.headers.length > 0
? data.headers.reduce(
(acc, { key, value }) => {
if (key.trim()) acc[key] = value;
return acc;
},
{} as Record<string, string>,
)
: undefined;
promise = customMutation.mutateAsync({
appBuildError: appBuildError,
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
endpoint: data.endpoint,
headers: headersRecord,
name: data.name,
dockerCleanup: dockerCleanup,
serverThreshold: serverThreshold,
notificationId: notificationId || "",
customId: notification?.customId || "",
});
} }
if (promise) { if (promise) {
@@ -1057,7 +1145,92 @@ export const HandleNotifications = ({ notificationId }: Props) => {
/> />
</> </>
)} )}
{type === "custom" && (
<div className="space-y-4">
<FormField
control={form.control}
name="endpoint"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input
placeholder="https://api.example.com/webhook"
{...field}
/>
</FormControl>
<FormDescription>
The URL where POST requests will be sent with
notification data.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-3">
<div>
<FormLabel>Headers</FormLabel>
<FormDescription>
Optional. Custom headers for your POST request (e.g.,
Authorization, Content-Type).
</FormDescription>
</div>
<div className="space-y-2">
{headerFields.map((field, index) => (
<div
key={field.id}
className="flex items-center gap-2 p-2 border rounded-md bg-muted/50"
>
<FormField
control={form.control}
name={`headers.${index}.key` as never}
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Input placeholder="Key" {...field} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name={`headers.${index}.value` as never}
render={({ field }) => (
<FormItem className="flex-[2]">
<FormControl>
<Input placeholder="Value" {...field} />
</FormControl>
</FormItem>
)}
/>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => removeHeader(index)}
className="text-red-500 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => appendHeader({ key: "", value: "" })}
className="w-full"
>
<PlusIcon className="h-4 w-4 mr-2" />
Add header
</Button>
</div>
</div>
)}
{type === "lark" && ( {type === "lark" && (
<> <>
<FormField <FormField
@@ -1250,7 +1423,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
isLoadingEmail || isLoadingEmail ||
isLoadingGotify || isLoadingGotify ||
isLoadingNtfy || isLoadingNtfy ||
isLoadingLark isLoadingLark ||
isLoadingCustom
} }
variant="secondary" variant="secondary"
type="button" type="button"
@@ -1304,6 +1478,21 @@ export const HandleNotifications = ({ notificationId }: Props) => {
await testLarkConnection({ await testLarkConnection({
webhookUrl: data.webhookUrl, webhookUrl: data.webhookUrl,
}); });
} else if (data.type === "custom") {
const headersRecord =
data.headers && data.headers.length > 0
? data.headers.reduce(
(acc, { key, value }) => {
if (key.trim()) acc[key] = value;
return acc;
},
{} as Record<string, string>,
)
: undefined;
await testCustomConnection({
endpoint: data.endpoint,
headers: headersRecord,
});
} }
toast.success("Connection Success"); toast.success("Connection Success");
} catch (error) { } catch (error) {
@@ -1,4 +1,4 @@
import { Bell, Loader2, Mail, Trash2 } from "lucide-react"; import { Bell, Loader2, Mail, PenBoxIcon, Trash2 } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
DiscordIcon, DiscordIcon,
@@ -96,6 +96,11 @@ export const ShowNotifications = () => {
<NtfyIcon className="size-6" /> <NtfyIcon className="size-6" />
</div> </div>
)} )}
{notification.notificationType === "custom" && (
<div className="flex items-center justify-center rounded-lg ">
<PenBoxIcon className="size-6 text-muted-foreground" />
</div>
)}
{notification.notificationType === "lark" && ( {notification.notificationType === "lark" && (
<div className="flex items-center justify-center rounded-lg"> <div className="flex items-center justify-center rounded-lg">
<LarkIcon className="size-7 text-muted-foreground" /> <LarkIcon className="size-7 text-muted-foreground" />
@@ -0,0 +1,9 @@
ALTER TYPE "public"."notificationType" ADD VALUE 'custom' BEFORE 'lark';--> statement-breakpoint
CREATE TABLE "custom" (
"customId" text PRIMARY KEY NOT NULL,
"endpoint" text NOT NULL,
"headers" jsonb
);
--> statement-breakpoint
ALTER TABLE "notification" ADD COLUMN "customId" text;--> statement-breakpoint
ALTER TABLE "notification" ADD CONSTRAINT "notification_customId_custom_customId_fk" FOREIGN KEY ("customId") REFERENCES "public"."custom"("customId") ON DELETE cascade ON UPDATE no action;
File diff suppressed because it is too large Load Diff
+7
View File
@@ -904,6 +904,13 @@
"when": 1765101709413, "when": 1765101709413,
"tag": "0128_hard_falcon", "tag": "0128_hard_falcon",
"breakpoints": true "breakpoints": true
},
{
"idx": 129,
"version": "7",
"when": 1765136384035,
"tag": "0129_pale_roughhouse",
"breakpoints": true
} }
] ]
} }
+1 -1
View File
@@ -5,8 +5,8 @@ import {
createMount, createMount,
deployMariadb, deployMariadb,
findBackupsByDbId, findBackupsByDbId,
findMariadbById,
findEnvironmentById, findEnvironmentById,
findMariadbById,
findProjectById, findProjectById,
IS_CLOUD, IS_CLOUD,
rebuildDatabase, rebuildDatabase,
+1 -1
View File
@@ -5,8 +5,8 @@ import {
createMount, createMount,
deployMongo, deployMongo,
findBackupsByDbId, findBackupsByDbId,
findMongoById,
findEnvironmentById, findEnvironmentById,
findMongoById,
findProjectById, findProjectById,
IS_CLOUD, IS_CLOUD,
rebuildDatabase, rebuildDatabase,
@@ -1,4 +1,5 @@
import { import {
createCustomNotification,
createDiscordNotification, createDiscordNotification,
createEmailNotification, createEmailNotification,
createGotifyNotification, createGotifyNotification,
@@ -9,6 +10,7 @@ import {
findNotificationById, findNotificationById,
IS_CLOUD, IS_CLOUD,
removeNotificationById, removeNotificationById,
sendCustomNotification,
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
sendGotifyNotification, sendGotifyNotification,
@@ -17,6 +19,7 @@ import {
sendServerThresholdNotifications, sendServerThresholdNotifications,
sendSlackNotification, sendSlackNotification,
sendTelegramNotification, sendTelegramNotification,
updateCustomNotification,
updateDiscordNotification, updateDiscordNotification,
updateEmailNotification, updateEmailNotification,
updateGotifyNotification, updateGotifyNotification,
@@ -36,6 +39,7 @@ import {
} from "@/server/api/trpc"; } from "@/server/api/trpc";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { import {
apiCreateCustom,
apiCreateDiscord, apiCreateDiscord,
apiCreateEmail, apiCreateEmail,
apiCreateGotify, apiCreateGotify,
@@ -44,6 +48,7 @@ import {
apiCreateSlack, apiCreateSlack,
apiCreateTelegram, apiCreateTelegram,
apiFindOneNotification, apiFindOneNotification,
apiTestCustomConnection,
apiTestDiscordConnection, apiTestDiscordConnection,
apiTestEmailConnection, apiTestEmailConnection,
apiTestGotifyConnection, apiTestGotifyConnection,
@@ -51,6 +56,7 @@ import {
apiTestNtfyConnection, apiTestNtfyConnection,
apiTestSlackConnection, apiTestSlackConnection,
apiTestTelegramConnection, apiTestTelegramConnection,
apiUpdateCustom,
apiUpdateDiscord, apiUpdateDiscord,
apiUpdateEmail, apiUpdateEmail,
apiUpdateGotify, apiUpdateGotify,
@@ -334,6 +340,7 @@ export const notificationRouter = createTRPCRouter({
email: true, email: true,
gotify: true, gotify: true,
ntfy: true, ntfy: true,
custom: true,
lark: true, lark: true,
}, },
orderBy: desc(notifications.createdAt), orderBy: desc(notifications.createdAt),
@@ -518,6 +525,59 @@ export const notificationRouter = createTRPCRouter({
}); });
} }
}), }),
createCustom: adminProcedure
.input(apiCreateCustom)
.mutation(async ({ input, ctx }) => {
try {
return await createCustomNotification(
input,
ctx.session.activeOrganizationId,
);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the notification",
cause: error,
});
}
}),
updateCustom: adminProcedure
.input(apiUpdateCustom)
.mutation(async ({ input, ctx }) => {
try {
const notification = await findNotificationById(input.notificationId);
if (notification.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this notification",
});
}
return await updateCustomNotification({
...input,
organizationId: ctx.session.activeOrganizationId,
});
} catch (error) {
throw error;
}
}),
testCustomConnection: adminProcedure
.input(apiTestCustomConnection)
.mutation(async ({ input }) => {
try {
await sendCustomNotification(input, {
title: "Test Notification",
message: "Hi, From Dokploy 👋",
timestamp: new Date().toISOString(),
});
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `${error instanceof Error ? error.message : "Unknown error"}`,
cause: error,
});
}
}),
createLark: adminProcedure createLark: adminProcedure
.input(apiCreateLark) .input(apiCreateLark)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
+2 -1
View File
@@ -25,7 +25,8 @@
"switch:prod": "node scripts/switchToDist.js", "switch:prod": "node scripts/switchToDist.js",
"dev": "rm -rf ./dist && pnpm esbuild && tsc --emitDeclarationOnly --outDir dist -p tsconfig.server.json", "dev": "rm -rf ./dist && pnpm esbuild && tsc --emitDeclarationOnly --outDir dist -p tsconfig.server.json",
"esbuild": "tsx ./esbuild.config.ts && tsc --project tsconfig.server.json --emitDeclarationOnly ", "esbuild": "tsx ./esbuild.config.ts && tsc --project tsconfig.server.json --emitDeclarationOnly ",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit",
"dbml:generate": "npx tsx src/db/schema/dbml.ts"
}, },
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "^2.0.5", "@ai-sdk/anthropic": "^2.0.5",
File diff suppressed because it is too large Load Diff
+53 -1
View File
@@ -1,5 +1,12 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core"; import {
boolean,
integer,
jsonb,
pgEnum,
pgTable,
text,
} from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
@@ -12,6 +19,7 @@ export const notificationType = pgEnum("notificationType", [
"email", "email",
"gotify", "gotify",
"ntfy", "ntfy",
"custom",
"lark", "lark",
]); ]);
@@ -50,6 +58,9 @@ export const notifications = pgTable("notification", {
ntfyId: text("ntfyId").references(() => ntfy.ntfyId, { ntfyId: text("ntfyId").references(() => ntfy.ntfyId, {
onDelete: "cascade", onDelete: "cascade",
}), }),
customId: text("customId").references(() => custom.customId, {
onDelete: "cascade",
}),
larkId: text("larkId").references(() => lark.larkId, { larkId: text("larkId").references(() => lark.larkId, {
onDelete: "cascade", onDelete: "cascade",
}), }),
@@ -121,6 +132,15 @@ export const ntfy = pgTable("ntfy", {
priority: integer("priority").notNull().default(3), priority: integer("priority").notNull().default(3),
}); });
export const custom = pgTable("custom", {
customId: text("customId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
endpoint: text("endpoint").notNull(),
headers: jsonb("headers").$type<Record<string, string>>(),
});
export const lark = pgTable("lark", { export const lark = pgTable("lark", {
larkId: text("larkId") larkId: text("larkId")
.notNull() .notNull()
@@ -154,6 +174,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
fields: [notifications.ntfyId], fields: [notifications.ntfyId],
references: [ntfy.ntfyId], references: [ntfy.ntfyId],
}), }),
custom: one(custom, {
fields: [notifications.customId],
references: [custom.customId],
}),
lark: one(lark, { lark: one(lark, {
fields: [notifications.larkId], fields: [notifications.larkId],
references: [lark.larkId], references: [lark.larkId],
@@ -362,6 +386,32 @@ export const apiFindOneNotification = notificationsSchema
}) })
.required(); .required();
export const apiCreateCustom = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
})
.extend({
endpoint: z.string().min(1),
headers: z.record(z.string()).optional(),
});
export const apiUpdateCustom = apiCreateCustom.partial().extend({
notificationId: z.string().min(1),
customId: z.string().min(1),
organizationId: z.string().optional(),
});
export const apiTestCustomConnection = z.object({
endpoint: z.string().min(1),
headers: z.record(z.string()).optional(),
});
export const apiCreateLark = notificationsSchema export const apiCreateLark = notificationsSchema
.pick({ .pick({
appBuildError: true, appBuildError: true,
@@ -404,5 +454,7 @@ export const apiSendTest = notificationsSchema
appToken: z.string(), appToken: z.string(),
accessToken: z.string().optional(), accessToken: z.string().optional(),
priority: z.number(), priority: z.number(),
endpoint: z.string(),
headers: z.string(),
}) })
.partial(); .partial();
+358 -82
View File
@@ -5,17 +5,24 @@ enum applicationStatus {
error error
} }
enum backupType {
database
compose
}
enum buildType { enum buildType {
dockerfile dockerfile
heroku_buildpacks heroku_buildpacks
paketo_buildpacks paketo_buildpacks
nixpacks nixpacks
static static
railpack
} }
enum certificateType { enum certificateType {
letsencrypt letsencrypt
none none
custom
} }
enum composeType { enum composeType {
@@ -28,6 +35,7 @@ enum databaseType {
mariadb mariadb
mysql mysql
mongo mongo
"web-server"
} }
enum deploymentStatus { enum deploymentStatus {
@@ -61,6 +69,8 @@ enum notificationType {
discord discord
email email
gotify gotify
ntfy
custom
} }
enum protocolType { enum protocolType {
@@ -68,14 +78,21 @@ enum protocolType {
udp udp
} }
enum publishModeType {
ingress
host
}
enum RegistryType { enum RegistryType {
selfHosted selfHosted
cloud cloud
} }
enum Roles { enum scheduleType {
admin application
user compose
server
"dokploy-server"
} }
enum serverStatus { enum serverStatus {
@@ -93,6 +110,11 @@ enum serviceType {
compose compose
} }
enum shellType {
bash
sh
}
enum sourceType { enum sourceType {
docker docker
git git
@@ -112,6 +134,11 @@ enum sourceTypeCompose {
raw raw
} }
enum triggerType {
push
tag
}
table account { table account {
id text [pk, not null] id text [pk, not null]
account_id text [not null] account_id text [not null]
@@ -133,7 +160,39 @@ table account {
confirmationExpiresAt text confirmationExpiresAt text
} }
table admin { table ai {
aiId text [pk, not null]
name text [not null]
apiUrl text [not null]
apiKey text [not null]
model text [not null]
isEnabled boolean [not null, default: true]
organizationId text [not null]
createdAt text [not null]
}
table apikey {
id text [pk, not null]
name text
start text
prefix text
key text [not null]
user_id text [not null]
refill_interval integer
refill_amount integer
last_refill_at timestamp
enabled boolean
rate_limit_enabled boolean
rate_limit_time_window integer
rate_limit_max integer
request_count integer
remaining integer
last_request timestamp
expires_at timestamp
created_at timestamp [not null]
updated_at timestamp [not null]
permissions text
metadata text
} }
table application { table application {
@@ -143,14 +202,19 @@ table application {
description text description text
env text env text
previewEnv text previewEnv text
watchPaths text[]
previewBuildArgs text previewBuildArgs text
previewLabels text[]
previewWildcard text previewWildcard text
previewPort integer [default: 3000] previewPort integer [default: 3000]
previewHttps boolean [not null, default: false] previewHttps boolean [not null, default: false]
previewPath text [default: '/'] previewPath text [default: '/']
certificateType certificateType [not null, default: 'none'] certificateType certificateType [not null, default: 'none']
previewCustomCertResolver text
previewLimit integer [default: 3] previewLimit integer [default: 3]
isPreviewDeploymentsActive boolean [default: false] isPreviewDeploymentsActive boolean [default: false]
previewRequireCollaboratorPermissions boolean [default: true]
rollbackActive boolean [default: false]
buildArgs text buildArgs text
memoryReservation text memoryReservation text
memoryLimit text memoryLimit text
@@ -167,6 +231,7 @@ table application {
owner text owner text
branch text branch text
buildPath text [default: '/'] buildPath text [default: '/']
triggerType triggerType [default: 'push']
autoDeploy boolean autoDeploy boolean
gitlabProjectId integer gitlabProjectId integer
gitlabRepository text gitlabRepository text
@@ -174,6 +239,10 @@ table application {
gitlabBranch text gitlabBranch text
gitlabBuildPath text [default: '/'] gitlabBuildPath text [default: '/']
gitlabPathNamespace text gitlabPathNamespace text
giteaRepository text
giteaOwner text
giteaBranch text
giteaBuildPath text [default: '/']
bitbucketRepository text bitbucketRepository text
bitbucketOwner text bitbucketOwner text
bitbucketBranch text bitbucketBranch text
@@ -186,6 +255,7 @@ table application {
customGitBranch text customGitBranch text
customGitBuildPath text customGitBuildPath text
customGitSSHKeyId text customGitSSHKeyId text
enableSubmodules boolean [not null, default: false]
dockerfile text dockerfile text
dockerContextPath text dockerContextPath text
dockerBuildStage text dockerBuildStage text
@@ -201,52 +271,47 @@ table application {
replicas integer [not null, default: 1] replicas integer [not null, default: 1]
applicationStatus applicationStatus [not null, default: 'idle'] applicationStatus applicationStatus [not null, default: 'idle']
buildType buildType [not null, default: 'nixpacks'] buildType buildType [not null, default: 'nixpacks']
railpackVersion text [default: '0.2.2']
herokuVersion text [default: '24'] herokuVersion text [default: '24']
publishDirectory text publishDirectory text
isStaticSpa boolean
createdAt text [not null] createdAt text [not null]
registryId text registryId text
projectId text [not null] environmentId text [not null]
githubId text githubId text
gitlabId text gitlabId text
bitbucketId text
giteaId text giteaId text
bitbucketId text
serverId text serverId text
} }
table auth {
id text [pk, not null]
email text [not null, unique]
password text [not null]
rol Roles [not null]
image text
secret text
token text
is2FAEnabled boolean [not null, default: false]
createdAt text [not null]
resetPasswordToken text
resetPasswordExpiresAt text
confirmationToken text
confirmationExpiresAt text
}
table backup { table backup {
backupId text [pk, not null] backupId text [pk, not null]
appName text [not null, unique]
schedule text [not null] schedule text [not null]
enabled boolean enabled boolean
database text [not null] database text [not null]
prefix text [not null] prefix text [not null]
serviceName text
destinationId text [not null] destinationId text [not null]
keepLatestCount integer
backupType backupType [not null, default: 'database']
databaseType databaseType [not null] databaseType databaseType [not null]
composeId text
postgresId text postgresId text
mariadbId text mariadbId text
mysqlId text mysqlId text
mongoId text mongoId text
userId text
metadata jsonb
} }
table bitbucket { table bitbucket {
bitbucketId text [pk, not null] bitbucketId text [pk, not null]
bitbucketUsername text bitbucketUsername text
bitbucketEmail text
appPassword text appPassword text
apiToken text
bitbucketWorkspaceName text bitbucketWorkspaceName text
gitProviderId text [not null] gitProviderId text [not null]
} }
@@ -258,7 +323,7 @@ table certificate {
privateKey text [not null] privateKey text [not null]
certificatePath text [not null, unique] certificatePath text [not null, unique]
autoRenew boolean autoRenew boolean
userId text organizationId text [not null]
serverId text serverId text
} }
@@ -291,13 +356,17 @@ table compose {
customGitBranch text customGitBranch text
customGitSSHKeyId text customGitSSHKeyId text
command text [not null, default: ''] command text [not null, default: '']
enableSubmodules boolean [not null, default: false]
composePath text [not null, default: './docker-compose.yml'] composePath text [not null, default: './docker-compose.yml']
suffix text [not null, default: ''] suffix text [not null, default: '']
randomize boolean [not null, default: false] randomize boolean [not null, default: false]
isolatedDeployment boolean [not null, default: false] isolatedDeployment boolean [not null, default: false]
isolatedDeploymentsVolume boolean [not null, default: false]
triggerType triggerType [default: 'push']
composeStatus applicationStatus [not null, default: 'idle'] composeStatus applicationStatus [not null, default: 'idle']
projectId text [not null] environmentId text [not null]
createdAt text [not null] createdAt text [not null]
watchPaths text[]
githubId text githubId text
gitlabId text gitlabId text
bitbucketId text bitbucketId text
@@ -305,19 +374,32 @@ table compose {
serverId text serverId text
} }
table custom {
customId text [pk, not null]
endpoint text [not null]
headers text
}
table deployment { table deployment {
deploymentId text [pk, not null] deploymentId text [pk, not null]
title text [not null] title text [not null]
description text description text
status deploymentStatus [default: 'running'] status deploymentStatus [default: 'running']
logPath text [not null] logPath text [not null]
pid text
applicationId text applicationId text
composeId text composeId text
serverId text serverId text
isPreviewDeployment boolean [default: false] isPreviewDeployment boolean [default: false]
previewDeploymentId text previewDeploymentId text
createdAt text [not null] createdAt text [not null]
startedAt text
finishedAt text
errorMessage text errorMessage text
scheduleId text
backupId text
rollbackId text
volumeBackupId text
} }
table destination { table destination {
@@ -329,7 +411,8 @@ table destination {
bucket text [not null] bucket text [not null]
region text [not null] region text [not null]
endpoint text [not null] endpoint text [not null]
userId text [not null] organizationId text [not null]
createdAt timestamp [not null, default: `now()`]
} }
table discord { table discord {
@@ -349,9 +432,12 @@ table domain {
uniqueConfigKey serial [not null, increment] uniqueConfigKey serial [not null, increment]
createdAt text [not null] createdAt text [not null]
composeId text composeId text
customCertResolver text
applicationId text applicationId text
previewDeploymentId text previewDeploymentId text
certificateType certificateType [not null, default: 'none'] certificateType certificateType [not null, default: 'none']
internalPath text [default: '/']
stripPath boolean [not null, default: false]
} }
table email { table email {
@@ -364,12 +450,36 @@ table email {
toAddress text[] [not null] toAddress text[] [not null]
} }
table environment {
environmentId text [pk, not null]
name text [not null]
description text
createdAt text [not null]
env text [not null, default: '']
projectId text [not null]
}
table git_provider { table git_provider {
gitProviderId text [pk, not null] gitProviderId text [pk, not null]
name text [not null] name text [not null]
providerType gitProviderType [not null, default: 'github'] providerType gitProviderType [not null, default: 'github']
createdAt text [not null] createdAt text [not null]
userId text organizationId text [not null]
userId text [not null]
}
table gitea {
giteaId text [pk, not null]
giteaUrl text [not null, default: 'https://gitea.com']
redirect_uri text
client_id text
client_secret text
gitProviderId text [not null]
access_token text
refresh_token text
expires_at integer
scopes text [default: 'repo,repo:status,read:user,read:org']
last_authenticated_at integer
} }
table github { table github {
@@ -397,20 +507,6 @@ table gitlab {
gitProviderId text [not null] gitProviderId text [not null]
} }
table gitea {
giteaId text [pk, not null]
giteaUrl text [not null, default: 'https://gitea.com']
redirect_uri text
client_id text [not null]
client_secret text [not null]
access_token text
refresh_token text
expires_at integer
gitProviderId text [not null]
scopes text [default: 'repo,repo:status,read:user,read:org']
last_authenticated_at integer
}
table gotify { table gotify {
gotifyId text [pk, not null] gotifyId text [pk, not null]
serverUrl text [not null] serverUrl text [not null]
@@ -427,6 +523,7 @@ table invitation {
status text [not null] status text [not null]
expires_at timestamp [not null] expires_at timestamp [not null]
inviter_id text [not null] inviter_id text [not null]
team_id text
} }
table mariadb { table mariadb {
@@ -447,8 +544,17 @@ table mariadb {
cpuLimit text cpuLimit text
externalPort integer externalPort integer
applicationStatus applicationStatus [not null, default: 'idle'] applicationStatus applicationStatus [not null, default: 'idle']
healthCheckSwarm json
restartPolicySwarm json
placementSwarm json
updateConfigSwarm json
rollbackConfigSwarm json
modeSwarm json
labelsSwarm json
networkSwarm json
replicas integer [not null, default: 1]
createdAt text [not null] createdAt text [not null]
projectId text [not null] environmentId text [not null]
serverId text serverId text
} }
@@ -458,6 +564,19 @@ table member {
user_id text [not null] user_id text [not null]
role text [not null] role text [not null]
created_at timestamp [not null] created_at timestamp [not null]
team_id text
canCreateProjects boolean [not null, default: false]
canAccessToSSHKeys boolean [not null, default: false]
canCreateServices boolean [not null, default: false]
canDeleteProjects boolean [not null, default: false]
canDeleteServices boolean [not null, default: false]
canAccessToDocker boolean [not null, default: false]
canAccessToAPI boolean [not null, default: false]
canAccessToGitProviders boolean [not null, default: false]
canAccessToTraefikFiles boolean [not null, default: false]
accesedProjects text[] [not null, default: `ARRAY[]::text[]`]
accessedEnvironments text[] [not null, default: `ARRAY[]::text[]`]
accesedServices text[] [not null, default: `ARRAY[]::text[]`]
} }
table mongo { table mongo {
@@ -476,8 +595,17 @@ table mongo {
cpuLimit text cpuLimit text
externalPort integer externalPort integer
applicationStatus applicationStatus [not null, default: 'idle'] applicationStatus applicationStatus [not null, default: 'idle']
healthCheckSwarm json
restartPolicySwarm json
placementSwarm json
updateConfigSwarm json
rollbackConfigSwarm json
modeSwarm json
labelsSwarm json
networkSwarm json
replicas integer [not null, default: 1]
createdAt text [not null] createdAt text [not null]
projectId text [not null] environmentId text [not null]
serverId text serverId text
replicaSets boolean [default: false] replicaSets boolean [default: false]
} }
@@ -518,8 +646,17 @@ table mysql {
cpuLimit text cpuLimit text
externalPort integer externalPort integer
applicationStatus applicationStatus [not null, default: 'idle'] applicationStatus applicationStatus [not null, default: 'idle']
healthCheckSwarm json
restartPolicySwarm json
placementSwarm json
updateConfigSwarm json
rollbackConfigSwarm json
modeSwarm json
labelsSwarm json
networkSwarm json
replicas integer [not null, default: 1]
createdAt text [not null] createdAt text [not null]
projectId text [not null] environmentId text [not null]
serverId text serverId text
} }
@@ -539,7 +676,17 @@ table notification {
discordId text discordId text
emailId text emailId text
gotifyId text gotifyId text
userId text ntfyId text
customId text
organizationId text [not null]
}
table ntfy {
ntfyId text [pk, not null]
serverUrl text [not null]
topic text [not null]
accessToken text [not null]
priority integer [not null, default: 3]
} }
table organization { table organization {
@@ -555,6 +702,7 @@ table organization {
table port { table port {
portId text [pk, not null] portId text [pk, not null]
publishedPort integer [not null] publishedPort integer [not null]
publishMode publishModeType [not null, default: 'host']
targetPort integer [not null] targetPort integer [not null]
protocol protocolType [not null] protocol protocolType [not null]
applicationId text [not null] applicationId text [not null]
@@ -577,8 +725,17 @@ table postgres {
cpuReservation text cpuReservation text
cpuLimit text cpuLimit text
applicationStatus applicationStatus [not null, default: 'idle'] applicationStatus applicationStatus [not null, default: 'idle']
healthCheckSwarm json
restartPolicySwarm json
placementSwarm json
updateConfigSwarm json
rollbackConfigSwarm json
modeSwarm json
labelsSwarm json
networkSwarm json
replicas integer [not null, default: 1]
createdAt text [not null] createdAt text [not null]
projectId text [not null] environmentId text [not null]
serverId text serverId text
} }
@@ -603,7 +760,7 @@ table project {
name text [not null] name text [not null]
description text description text
createdAt text [not null] createdAt text [not null]
userId text [not null] organizationId text [not null]
env text [not null, default: ''] env text [not null, default: '']
} }
@@ -633,7 +790,16 @@ table redis {
externalPort integer externalPort integer
createdAt text [not null] createdAt text [not null]
applicationStatus applicationStatus [not null, default: 'idle'] applicationStatus applicationStatus [not null, default: 'idle']
projectId text [not null] healthCheckSwarm json
restartPolicySwarm json
placementSwarm json
updateConfigSwarm json
rollbackConfigSwarm json
modeSwarm json
labelsSwarm json
networkSwarm json
replicas integer [not null, default: 1]
environmentId text [not null]
serverId text serverId text
} }
@@ -646,7 +812,34 @@ table registry {
registryUrl text [not null, default: ''] registryUrl text [not null, default: '']
createdAt text [not null] createdAt text [not null]
selfHosted RegistryType [not null, default: 'cloud'] selfHosted RegistryType [not null, default: 'cloud']
userId text [not null] organizationId text [not null]
}
table rollback {
rollbackId text [pk, not null]
deploymentId text [not null]
version serial [not null, increment]
image text
createdAt text [not null]
fullContext jsonb
}
table schedule {
scheduleId text [pk, not null]
name text [not null]
cronExpression text [not null]
appName text [not null]
serviceName text
shellType shellType [not null, default: 'bash']
scheduleType scheduleType [not null, default: 'application']
command text [not null]
script text
applicationId text
composeId text
serverId text
userId text
enabled boolean [not null, default: true]
createdAt text [not null]
} }
table security { table security {
@@ -671,14 +864,14 @@ table server {
appName text [not null] appName text [not null]
enableDockerCleanup boolean [not null, default: false] enableDockerCleanup boolean [not null, default: false]
createdAt text [not null] createdAt text [not null]
userId text [not null] organizationId text [not null]
serverStatus serverStatus [not null, default: 'active'] serverStatus serverStatus [not null, default: 'active']
command text [not null, default: ''] command text [not null, default: '']
sshKeyId text sshKeyId text
metricsConfig jsonb [not null, default: `{"server":{"type":"Remote","refreshRate":60,"port":4500,"token":"","urlCallback":"","cronJob":"","retentionDays":2,"thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}`] metricsConfig jsonb [not null, default: `{"server":{"type":"Remote","refreshRate":60,"port":4500,"token":"","urlCallback":"","cronJob":"","retentionDays":2,"thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}`]
} }
table session { table session_temp {
id text [pk, not null] id text [pk, not null]
expires_at timestamp [not null] expires_at timestamp [not null]
token text [not null, unique] token text [not null, unique]
@@ -705,49 +898,49 @@ table "ssh-key" {
description text description text
createdAt text [not null] createdAt text [not null]
lastUsedAt text lastUsedAt text
userId text organizationId text [not null]
} }
table telegram { table telegram {
telegramId text [pk, not null] telegramId text [pk, not null]
botToken text [not null] botToken text [not null]
chatId text [not null] chatId text [not null]
messageThreadId text
} }
table user { table two_factor {
id text [pk, not null]
secret text [not null]
backup_codes text [not null]
user_id text [not null]
}
table user_temp {
id text [pk, not null] id text [pk, not null]
name text [not null, default: ''] name text [not null, default: '']
token text [not null]
isRegistered boolean [not null, default: false] isRegistered boolean [not null, default: false]
expirationDate text [not null] expirationDate text [not null]
createdAt text [not null] createdAt text [not null]
canCreateProjects boolean [not null, default: false] created_at timestamp [default: `now()`]
canAccessToSSHKeys boolean [not null, default: false] two_factor_enabled boolean
canCreateServices boolean [not null, default: false]
canDeleteProjects boolean [not null, default: false]
canDeleteServices boolean [not null, default: false]
canAccessToDocker boolean [not null, default: false]
canAccessToAPI boolean [not null, default: false]
canAccessToGitProviders boolean [not null, default: false]
canAccessToTraefikFiles boolean [not null, default: false]
accesedProjects text[] [not null, default: `ARRAY[]::text[]`]
accesedServices text[] [not null, default: `ARRAY[]::text[]`]
email text [not null, unique] email text [not null, unique]
email_verified boolean [not null] email_verified boolean [not null]
image text image text
role text
banned boolean banned boolean
ban_reason text ban_reason text
ban_expires timestamp ban_expires timestamp
updated_at timestamp [not null] updated_at timestamp [not null]
serverIp text serverIp text
certificateType certificateType [not null, default: 'none'] certificateType certificateType [not null, default: 'none']
https boolean [not null, default: false]
host text host text
letsEncryptEmail text letsEncryptEmail text
sshPrivateKey text sshPrivateKey text
enableDockerCleanup boolean [not null, default: false] enableDockerCleanup boolean [not null, default: false]
enableLogRotation boolean [not null, default: false] logCleanupCron text [default: '0 0 * * *']
role text [not null, default: 'user']
enablePaidFeatures boolean [not null, default: false] enablePaidFeatures boolean [not null, default: false]
allowImpersonation boolean [not null, default: false]
metricsConfig jsonb [not null, default: `{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}`] metricsConfig jsonb [not null, default: `{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}`]
cleanupCacheApplications boolean [not null, default: false] cleanupCacheApplications boolean [not null, default: false]
cleanupCacheOnPreviews boolean [not null, default: false] cleanupCacheOnPreviews boolean [not null, default: false]
@@ -766,6 +959,29 @@ table verification {
updated_at timestamp updated_at timestamp
} }
table volume_backup {
volumeBackupId text [pk, not null]
name text [not null]
volumeName text [not null]
prefix text [not null]
serviceType serviceType [not null, default: 'application']
appName text [not null]
serviceName text
turnOff boolean [not null, default: false]
cronExpression text [not null]
keepLatestCount integer
enabled boolean
applicationId text
postgresId text
mariadbId text
mongoId text
mysqlId text
redisId text
composeId text
createdAt text [not null]
destinationId text [not null]
}
ref: mount.applicationId > application.applicationId ref: mount.applicationId > application.applicationId
ref: mount.postgresId > postgres.postgresId ref: mount.postgresId > postgres.postgresId
@@ -780,7 +996,13 @@ ref: mount.redisId > redis.redisId
ref: mount.composeId > compose.composeId ref: mount.composeId > compose.composeId
ref: application.projectId > project.projectId ref: user_temp.id - account.user_id
ref: ai.organizationId - organization.id
ref: apikey.user_id > user_temp.id
ref: application.environmentId > environment.environmentId
ref: application.customGitSSHKeyId > "ssh-key".sshKeyId ref: application.customGitSSHKeyId > "ssh-key".sshKeyId
@@ -790,6 +1012,8 @@ ref: application.githubId - github.githubId
ref: application.gitlabId - gitlab.gitlabId ref: application.gitlabId - gitlab.gitlabId
ref: application.giteaId - gitea.giteaId
ref: application.bitbucketId - bitbucket.bitbucketId ref: application.bitbucketId - bitbucket.bitbucketId
ref: application.serverId > server.serverId ref: application.serverId > server.serverId
@@ -804,13 +1028,17 @@ ref: backup.mysqlId > mysql.mysqlId
ref: backup.mongoId > mongo.mongoId ref: backup.mongoId > mongo.mongoId
ref: backup.userId > user_temp.id
ref: backup.composeId > compose.composeId
ref: git_provider.gitProviderId - bitbucket.gitProviderId ref: git_provider.gitProviderId - bitbucket.gitProviderId
ref: certificate.serverId > server.serverId ref: certificate.serverId > server.serverId
ref: certificate.userId - user.id ref: certificate.organizationId - organization.id
ref: compose.projectId > project.projectId ref: compose.environmentId > environment.environmentId
ref: compose.customGitSSHKeyId > "ssh-key".sshKeyId ref: compose.customGitSSHKeyId > "ssh-key".sshKeyId
@@ -820,6 +1048,8 @@ ref: compose.gitlabId - gitlab.gitlabId
ref: compose.bitbucketId - bitbucket.bitbucketId ref: compose.bitbucketId - bitbucket.bitbucketId
ref: compose.giteaId - gitea.giteaId
ref: compose.serverId > server.serverId ref: compose.serverId > server.serverId
ref: deployment.applicationId > application.applicationId ref: deployment.applicationId > application.applicationId
@@ -830,7 +1060,15 @@ ref: deployment.serverId > server.serverId
ref: deployment.previewDeploymentId > preview_deployments.previewDeploymentId ref: deployment.previewDeploymentId > preview_deployments.previewDeploymentId
ref: destination.userId - user.id ref: deployment.scheduleId > schedule.scheduleId
ref: deployment.backupId > backup.backupId
ref: rollback.deploymentId - deployment.deploymentId
ref: deployment.volumeBackupId > volume_backup.volumeBackupId
ref: destination.organizationId - organization.id
ref: domain.applicationId > application.applicationId ref: domain.applicationId > application.applicationId
@@ -838,23 +1076,33 @@ ref: domain.composeId > compose.composeId
ref: preview_deployments.domainId - domain.domainId ref: preview_deployments.domainId - domain.domainId
ref: environment.projectId > project.projectId
ref: github.gitProviderId - git_provider.gitProviderId ref: github.gitProviderId - git_provider.gitProviderId
ref: gitlab.gitProviderId - git_provider.gitProviderId ref: gitlab.gitProviderId - git_provider.gitProviderId
ref: gitea.gitProviderId - git_provider.gitProviderId ref: gitea.gitProviderId - git_provider.gitProviderId
ref: git_provider.userId - user.id ref: git_provider.organizationId - organization.id
ref: mariadb.projectId > project.projectId ref: git_provider.userId - user_temp.id
ref: invitation.organization_id - organization.id
ref: mariadb.environmentId > environment.environmentId
ref: mariadb.serverId > server.serverId ref: mariadb.serverId > server.serverId
ref: mongo.projectId > project.projectId ref: member.organization_id > organization.id
ref: member.user_id - user_temp.id
ref: mongo.environmentId > environment.environmentId
ref: mongo.serverId > server.serverId ref: mongo.serverId > server.serverId
ref: mysql.projectId > project.projectId ref: mysql.environmentId > environment.environmentId
ref: mysql.serverId > server.serverId ref: mysql.serverId > server.serverId
@@ -868,30 +1116,58 @@ ref: notification.emailId - email.emailId
ref: notification.gotifyId - gotify.gotifyId ref: notification.gotifyId - gotify.gotifyId
ref: notification.userId - user.id ref: notification.ntfyId - ntfy.ntfyId
ref: notification.customId - custom.customId
ref: notification.organizationId - organization.id
ref: organization.owner_id > user_temp.id
ref: port.applicationId > application.applicationId ref: port.applicationId > application.applicationId
ref: postgres.projectId > project.projectId ref: postgres.environmentId > environment.environmentId
ref: postgres.serverId > server.serverId ref: postgres.serverId > server.serverId
ref: preview_deployments.applicationId > application.applicationId ref: preview_deployments.applicationId > application.applicationId
ref: project.userId - user.id ref: project.organizationId > organization.id
ref: redirect.applicationId > application.applicationId ref: redirect.applicationId > application.applicationId
ref: redis.projectId > project.projectId ref: redis.environmentId > environment.environmentId
ref: redis.serverId > server.serverId ref: redis.serverId > server.serverId
ref: registry.userId - user.id ref: schedule.applicationId - application.applicationId
ref: schedule.composeId > compose.composeId
ref: schedule.serverId > server.serverId
ref: schedule.userId > user_temp.id
ref: security.applicationId > application.applicationId ref: security.applicationId > application.applicationId
ref: server.userId - user.id
ref: server.sshKeyId > "ssh-key".sshKeyId ref: server.sshKeyId > "ssh-key".sshKeyId
ref: "ssh-key".userId - user.id ref: server.organizationId > organization.id
ref: "ssh-key".organizationId - organization.id
ref: volume_backup.applicationId - application.applicationId
ref: volume_backup.postgresId - postgres.postgresId
ref: volume_backup.mariadbId - mariadb.mariadbId
ref: volume_backup.mongoId - mongo.mongoId
ref: volume_backup.mysqlId - mysql.mysqlId
ref: volume_backup.redisId - redis.redisId
ref: volume_backup.composeId - compose.composeId
ref: volume_backup.destinationId - destination.destinationId
+95 -3
View File
@@ -1,23 +1,26 @@
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { import {
type apiCreateCustom,
type apiCreateDiscord, type apiCreateDiscord,
type apiCreateEmail, type apiCreateEmail,
type apiCreateLark,
type apiCreateGotify, type apiCreateGotify,
type apiCreateLark,
type apiCreateNtfy, type apiCreateNtfy,
type apiCreateSlack, type apiCreateSlack,
type apiCreateTelegram, type apiCreateTelegram,
type apiUpdateCustom,
type apiUpdateDiscord, type apiUpdateDiscord,
type apiUpdateEmail, type apiUpdateEmail,
type apiUpdateLark,
type apiUpdateGotify, type apiUpdateGotify,
type apiUpdateLark,
type apiUpdateNtfy, type apiUpdateNtfy,
type apiUpdateSlack, type apiUpdateSlack,
type apiUpdateTelegram, type apiUpdateTelegram,
custom,
discord, discord,
email, email,
lark,
gotify, gotify,
lark,
notifications, notifications,
ntfy, ntfy,
slack, slack,
@@ -590,6 +593,94 @@ export const updateNtfyNotification = async (
}); });
}; };
export const createCustomNotification = async (
input: typeof apiCreateCustom._type,
organizationId: string,
) => {
await db.transaction(async (tx) => {
const newCustom = await tx
.insert(custom)
.values({
endpoint: input.endpoint,
headers: input.headers,
})
.returning()
.then((value) => value[0]);
if (!newCustom) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting custom",
});
}
const newDestination = await tx
.insert(notifications)
.values({
customId: newCustom.customId,
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "custom",
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 updateCustomNotification = async (
input: typeof apiUpdateCustom._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,
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(custom)
.set({
endpoint: input.endpoint,
headers: input.headers,
})
.where(eq(custom.customId, input.customId));
return newDestination;
});
};
export const findNotificationById = async (notificationId: string) => { export const findNotificationById = async (notificationId: string) => {
const notification = await db.query.notifications.findFirst({ const notification = await db.query.notifications.findFirst({
where: eq(notifications.notificationId, notificationId), where: eq(notifications.notificationId, notificationId),
@@ -600,6 +691,7 @@ export const findNotificationById = async (notificationId: string) => {
email: true, email: true,
gotify: true, gotify: true,
ntfy: true, ntfy: true,
custom: true,
lark: true, lark: true,
}, },
}); });
+1 -1
View File
@@ -3,8 +3,8 @@ import {
createDeploymentBackup, createDeploymentBackup,
updateDeploymentStatus, updateDeploymentStatus,
} from "@dokploy/server/services/deployment"; } from "@dokploy/server/services/deployment";
import type { Mariadb } from "@dokploy/server/services/mariadb";
import { findEnvironmentById } from "@dokploy/server/services/environment"; import { findEnvironmentById } from "@dokploy/server/services/environment";
import type { Mariadb } from "@dokploy/server/services/mariadb";
import { findProjectById } from "@dokploy/server/services/project"; import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
+1 -1
View File
@@ -3,8 +3,8 @@ import {
createDeploymentBackup, createDeploymentBackup,
updateDeploymentStatus, updateDeploymentStatus,
} from "@dokploy/server/services/deployment"; } from "@dokploy/server/services/deployment";
import type { Mongo } from "@dokploy/server/services/mongo";
import { findEnvironmentById } from "@dokploy/server/services/environment"; import { findEnvironmentById } from "@dokploy/server/services/environment";
import type { Mongo } from "@dokploy/server/services/mongo";
import { findProjectById } from "@dokploy/server/services/project"; import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
+1 -1
View File
@@ -3,8 +3,8 @@ import {
createDeploymentBackup, createDeploymentBackup,
updateDeploymentStatus, updateDeploymentStatus,
} from "@dokploy/server/services/deployment"; } from "@dokploy/server/services/deployment";
import type { MySql } from "@dokploy/server/services/mysql";
import { findEnvironmentById } from "@dokploy/server/services/environment"; import { findEnvironmentById } from "@dokploy/server/services/environment";
import type { MySql } from "@dokploy/server/services/mysql";
import { findProjectById } from "@dokploy/server/services/project"; import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
@@ -3,8 +3,8 @@ import {
createDeploymentBackup, createDeploymentBackup,
updateDeploymentStatus, updateDeploymentStatus,
} from "@dokploy/server/services/deployment"; } from "@dokploy/server/services/deployment";
import type { Postgres } from "@dokploy/server/services/postgres";
import { findEnvironmentById } from "@dokploy/server/services/environment"; import { findEnvironmentById } from "@dokploy/server/services/environment";
import type { Postgres } from "@dokploy/server/services/postgres";
import { findProjectById } from "@dokploy/server/services/project"; import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
@@ -5,6 +5,7 @@ import { renderAsync } from "@react-email/components";
import { format } from "date-fns"; import { format } from "date-fns";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { import {
sendCustomNotification,
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
sendGotifyNotification, sendGotifyNotification,
@@ -45,12 +46,13 @@ export const sendBuildErrorNotifications = async ({
slack: true, slack: true,
gotify: true, gotify: true,
ntfy: true, ntfy: true,
custom: true,
lark: true, lark: true,
}, },
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, lark } = const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification; notification;
try { try {
if (email) { if (email) {
@@ -220,6 +222,22 @@ export const sendBuildErrorNotifications = async ({
}); });
} }
if (custom) {
await sendCustomNotification(custom, {
title: "Build Error",
message: "Build failed with errors",
projectName,
applicationName,
applicationType,
errorMessage,
buildLink,
timestamp: date.toISOString(),
date: date.toLocaleString(),
status: "error",
type: "build",
});
}
if (lark) { if (lark) {
const limitCharacter = 800; const limitCharacter = 800;
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter); const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
@@ -6,6 +6,7 @@ import { renderAsync } from "@react-email/components";
import { format } from "date-fns"; import { format } from "date-fns";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { import {
sendCustomNotification,
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
sendGotifyNotification, sendGotifyNotification,
@@ -48,12 +49,13 @@ export const sendBuildSuccessNotifications = async ({
slack: true, slack: true,
gotify: true, gotify: true,
ntfy: true, ntfy: true,
custom: true,
lark: true, lark: true,
}, },
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, lark } = const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification; notification;
try { try {
if (email) { if (email) {
@@ -181,7 +183,10 @@ export const sendBuildSuccessNotifications = async ({
await sendTelegramNotification( await sendTelegramNotification(
telegram, telegram,
`<b>✅ Build Success</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Environment:</b> ${environmentName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`, `<b>✅ Build Success</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Environment:</b> ${environmentName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(
date,
"PP",
)}\n<b>Time:</b> ${format(date, "pp")}`,
inlineButton, inlineButton,
); );
} }
@@ -233,6 +238,22 @@ export const sendBuildSuccessNotifications = async ({
}); });
} }
if (custom) {
await sendCustomNotification(custom, {
title: "Build Success",
message: "Build completed successfully",
projectName,
applicationName,
applicationType,
buildLink,
timestamp: date.toISOString(),
date: date.toLocaleString(),
domains: domains.map((domain) => domain.host).join(", "),
status: "success",
type: "build",
});
}
if (lark) { if (lark) {
await sendLarkNotification(lark, { await sendLarkNotification(lark, {
msg_type: "interactive", msg_type: "interactive",
@@ -5,6 +5,7 @@ import { renderAsync } from "@react-email/components";
import { format } from "date-fns"; import { format } from "date-fns";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { import {
sendCustomNotification,
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
sendGotifyNotification, sendGotifyNotification,
@@ -45,12 +46,13 @@ export const sendDatabaseBackupNotifications = async ({
slack: true, slack: true,
gotify: true, gotify: true,
ntfy: true, ntfy: true,
custom: true,
lark: true, lark: true,
}, },
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, lark } = const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification; notification;
try { try {
if (email) { if (email) {
@@ -242,6 +244,25 @@ export const sendDatabaseBackupNotifications = async ({
}); });
} }
if (custom) {
await sendCustomNotification(custom, {
title: `Database Backup ${type === "success" ? "Successful" : "Failed"}`,
message:
type === "success"
? "Database backup completed successfully"
: "Database backup failed",
projectName,
applicationName,
databaseType,
databaseName,
type,
errorMessage: errorMessage || "",
timestamp: date.toISOString(),
date: date.toLocaleString(),
status: type,
});
}
if (lark) { if (lark) {
const limitCharacter = 800; const limitCharacter = 800;
const truncatedErrorMessage = const truncatedErrorMessage =
@@ -5,6 +5,7 @@ import { renderAsync } from "@react-email/components";
import { format } from "date-fns"; import { format } from "date-fns";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { import {
sendCustomNotification,
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
sendGotifyNotification, sendGotifyNotification,
@@ -32,12 +33,13 @@ export const sendDockerCleanupNotifications = async (
slack: true, slack: true,
gotify: true, gotify: true,
ntfy: true, ntfy: true,
custom: true,
lark: true, lark: true,
}, },
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, lark } = const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification; notification;
try { try {
if (email) { if (email) {
@@ -139,6 +141,18 @@ export const sendDockerCleanupNotifications = async (
}); });
} }
if (custom) {
await sendCustomNotification(custom, {
title: "Docker Cleanup",
message: "Docker cleanup completed successfully",
cleanupMessage: message,
timestamp: date.toISOString(),
date: date.toLocaleString(),
status: "success",
type: "docker-cleanup",
});
}
if (lark) { if (lark) {
await sendLarkNotification(lark, { await sendLarkNotification(lark, {
msg_type: "interactive", msg_type: "interactive",
@@ -5,6 +5,7 @@ import { renderAsync } from "@react-email/components";
import { format } from "date-fns"; import { format } from "date-fns";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { import {
sendCustomNotification,
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
sendGotifyNotification, sendGotifyNotification,
@@ -26,12 +27,13 @@ export const sendDokployRestartNotifications = async () => {
slack: true, slack: true,
gotify: true, gotify: true,
ntfy: true, ntfy: true,
custom: true,
lark: true, lark: true,
}, },
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, lark } = const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification; notification;
try { try {
@@ -101,7 +103,10 @@ export const sendDokployRestartNotifications = async () => {
if (telegram) { if (telegram) {
await sendTelegramNotification( await sendTelegramNotification(
telegram, telegram,
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`, `<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(
date,
"PP",
)}\n<b>Time:</b> ${format(date, "pp")}`,
); );
} }
@@ -125,6 +130,21 @@ export const sendDokployRestartNotifications = async () => {
}); });
} }
if (custom) {
try {
await sendCustomNotification(custom, {
title: "Dokploy Server Restarted",
message: "Dokploy server has been restarted successfully",
timestamp: date.toISOString(),
date: date.toLocaleString(),
status: "success",
type: "dokploy-restart",
});
} catch (error) {
console.log(error);
}
}
if (lark) { if (lark) {
await sendLarkNotification(lark, { await sendLarkNotification(lark, {
msg_type: "interactive", msg_type: "interactive",
@@ -181,7 +201,10 @@ export const sendDokployRestartNotifications = async () => {
elements: [ elements: [
{ {
tag: "markdown", tag: "markdown",
content: `**Restart Time:**\n${format(date, "PP pp")}`, content: `**Restart Time:**\n${format(
date,
"PP pp",
)}`,
text_align: "left", text_align: "left",
text_size: "normal_v2", text_size: "normal_v2",
}, },
@@ -2,6 +2,7 @@ import { and, eq } from "drizzle-orm";
import { db } from "../../db"; import { db } from "../../db";
import { notifications } from "../../db/schema"; import { notifications } from "../../db/schema";
import { import {
sendCustomNotification,
sendDiscordNotification, sendDiscordNotification,
sendLarkNotification, sendLarkNotification,
sendSlackNotification, sendSlackNotification,
@@ -35,6 +36,7 @@ export const sendServerThresholdNotifications = async (
discord: true, discord: true,
telegram: true, telegram: true,
slack: true, slack: true,
custom: true,
lark: true, lark: true,
}, },
}); });
@@ -43,7 +45,7 @@ export const sendServerThresholdNotifications = async (
const typeColor = 0xff0000; // Rojo para indicar alerta const typeColor = 0xff0000; // Rojo para indicar alerta
for (const notification of notificationList) { for (const notification of notificationList) {
const { discord, telegram, slack, lark } = notification; const { discord, telegram, slack, custom, lark } = notification;
if (discord) { if (discord) {
const decorate = (decoration: string, text: string) => const decorate = (decoration: string, text: string) =>
@@ -154,6 +156,21 @@ export const sendServerThresholdNotifications = async (
}); });
} }
if (custom) {
await sendCustomNotification(custom, {
title: `Server ${payload.Type} Alert`,
message: payload.Message,
serverName: payload.ServerName,
type: payload.Type,
currentValue: payload.Value,
threshold: payload.Threshold,
timestamp: date.toISOString(),
date: date.toLocaleString(),
status: "alert",
alertType: "server-threshold",
});
}
if (lark) { if (lark) {
await sendLarkNotification(lark, { await sendLarkNotification(lark, {
msg_type: "interactive", msg_type: "interactive",
@@ -1,4 +1,5 @@
import type { import type {
custom,
discord, discord,
email, email,
gotify, gotify,
@@ -175,6 +176,39 @@ export const sendNtfyNotification = async (
} }
}; };
export const sendCustomNotification = async (
connection: typeof custom.$inferInsert,
payload: Record<string, any>,
) => {
try {
// Merge default headers with custom headers (now already an object from jsonb)
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(connection.headers || {}),
};
// Default body with payload
const body = JSON.stringify(payload);
const response = await fetch(connection.endpoint, {
method: "POST",
headers,
body,
});
if (!response.ok) {
throw new Error(
`Failed to send custom notification: ${response.statusText}`,
);
}
return response;
} catch (error) {
console.error("Error sending custom notification:", error);
throw error;
}
};
export const sendLarkNotification = async ( export const sendLarkNotification = async (
connection: typeof lark.$inferInsert, connection: typeof lark.$inferInsert,
message: any, message: any,
+1194
View File
File diff suppressed because it is too large Load Diff