From e9b1050dd342c54a76770b220df54580c44d0b96 Mon Sep 17 00:00:00 2001 From: Vianney MORAIN Date: Tue, 8 Sep 2026 22:16:16 +0200 Subject: [PATCH] feat(publish): Coolify deploy webhook field for host: "coolify" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Hosting = "Coolify (remote)" is selected in Advanced settings, show a required "Coolify deploy webhook URL" input (+ optional Bearer token) and a "Host with Coolify" link to the self-host docs. Publish is blocked until the webhook URL is filled. The webhook is persisted per-project in localStorage (publish:coolifyWebhook:) and forwarded to the publisher on each publish (coolifyWebhookUrl / coolifyWebhookToken) — never stored server-side or in Build.deployment. Threaded through publishInput, the domain publish procedure, publishProject, and the REST publish.create input (metadata regenerated). Publisher side: webstudio-publisher#37. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017rF3HMB1Z7V3LLsYaUrELs --- .../app/builder/features/publish/publish.tsx | 129 +++++++++++++++++- .../builder/app/services/api-router.server.ts | 4 + .../domain/src/project-domain-api.server.ts | 7 + packages/domain/src/trpc/domain.ts | 5 + .../server-only-router-operation-metadata.ts | 6 + .../trpc-interface/src/shared/deployment.ts | 5 + 6 files changed, 149 insertions(+), 7 deletions(-) diff --git a/apps/builder/app/builder/features/publish/publish.tsx b/apps/builder/app/builder/features/publish/publish.tsx index 7f404d35e081..7939bcdb5e51 100644 --- a/apps/builder/app/builder/features/publish/publish.tsx +++ b/apps/builder/app/builder/features/publish/publish.tsx @@ -491,6 +491,30 @@ const usePublishCountdown = (isPublishing: boolean) => { const renderModeStorageKey = (projectId: string) => `publish:renderMode:${projectId}`; const hostStorageKey = (projectId: string) => `publish:host:${projectId}`; +const coolifyWebhookStorageKey = (projectId: string) => + `publish:coolifyWebhook:${projectId}`; +const coolifyWebhookTokenStorageKey = (projectId: string) => + `publish:coolifyWebhookToken:${projectId}`; + +const readStored = (key: string) => { + try { + return localStorage.getItem(key) ?? ""; + } catch { + return ""; + } +}; + +const writeStored = (key: string, value: string) => { + try { + if (value === "") { + localStorage.removeItem(key); + } else { + localStorage.setItem(key, value); + } + } catch { + // localStorage unavailable — in-memory state only + } +}; /** * One-time seed from the pre-split `buildMode:` key so a project keeps the @@ -549,7 +573,32 @@ export const usePublishTarget = (projectId: string) => { localStorage.setItem(hostStorageKey(projectId), value); setHostState(value); }; - return { renderMode, setRenderMode, host, setHost }; + // host: "coolify" — the target app's deploy webhook, per project. Not stored + // server-side; forwarded to the publisher on each publish. + const [coolifyWebhookUrl, setCoolifyWebhookUrlState] = useState(() => + readStored(coolifyWebhookStorageKey(projectId)) + ); + const [coolifyWebhookToken, setCoolifyWebhookTokenState] = useState(() => + readStored(coolifyWebhookTokenStorageKey(projectId)) + ); + const setCoolifyWebhookUrl = (value: string) => { + writeStored(coolifyWebhookStorageKey(projectId), value); + setCoolifyWebhookUrlState(value); + }; + const setCoolifyWebhookToken = (value: string) => { + writeStored(coolifyWebhookTokenStorageKey(projectId), value); + setCoolifyWebhookTokenState(value); + }; + return { + renderMode, + setRenderMode, + host, + setHost, + coolifyWebhookUrl, + setCoolifyWebhookUrl, + coolifyWebhookToken, + setCoolifyWebhookToken, + }; }; const advancedPublishOpenStorageKey = "publish:advancedOpen"; @@ -592,6 +641,10 @@ const Publish = ({ onRenderModeChange, host, onHostChange, + coolifyWebhookUrl, + onCoolifyWebhookUrlChange, + coolifyWebhookToken, + onCoolifyWebhookTokenChange, }: { project: Project; timesLeft: number; @@ -602,6 +655,10 @@ const Publish = ({ onRenderModeChange: (value: RenderMode) => void; host: PublishHost; onHostChange: (value: PublishHost) => void; + coolifyWebhookUrl: string; + onCoolifyWebhookUrlChange: (value: string) => void; + coolifyWebhookToken: string; + onCoolifyWebhookTokenChange: (value: string) => void; }) => { const { maxDailyPublishesPerUser } = useStore($permissions); const { userPublishCount } = useUserPublishCount(); @@ -689,6 +746,12 @@ const Publish = ({ destination: "saas", renderMode, host, + coolifyWebhookUrl: + host === "coolify" ? coolifyWebhookUrl.trim() : undefined, + coolifyWebhookToken: + host === "coolify" && coolifyWebhookToken.trim() !== "" + ? coolifyWebhookToken.trim() + : undefined, }); if (publishResult.success === false) { @@ -837,6 +900,9 @@ const Publish = ({ const showPendingState = isPublishInProgress && (countdown === undefined || countdown === 0); + const missingCoolifyWebhook = + host === "coolify" && coolifyWebhookUrl.trim() === ""; + return ( {publishError && {publishError}} @@ -900,6 +966,41 @@ const Publish = ({ }} onChange={onHostChange} /> + {host === "coolify" && ( + + + + onCoolifyWebhookUrlChange(event.target.value) + } + /> + + onCoolifyWebhookTokenChange(event.target.value) + } + /> + + Create a Docker Image app on your Coolify, then paste its + deploy webhook here.{" "} + + Host with Coolify + + + + )} @@ -910,9 +1011,11 @@ const Publish = ({ content={ isPublishInProgress ? "Publish process in progress" - : hasSelectedDomains - ? undefined - : "Select at least one domain to publish" + : missingCoolifyWebhook + ? "Enter the Coolify deploy webhook URL in Advanced settings" + : hasSelectedDomains + ? undefined + : "Select at least one domain to publish" } >