Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 122 additions & 7 deletions apps/builder/app/builder/features/publish/publish.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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:<id>` key so a project keeps the
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -592,6 +641,10 @@ const Publish = ({
onRenderModeChange,
host,
onHostChange,
coolifyWebhookUrl,
onCoolifyWebhookUrlChange,
coolifyWebhookToken,
onCoolifyWebhookTokenChange,
}: {
project: Project;
timesLeft: number;
Expand All @@ -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();
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -837,6 +900,9 @@ const Publish = ({
const showPendingState =
isPublishInProgress && (countdown === undefined || countdown === 0);

const missingCoolifyWebhook =
host === "coolify" && coolifyWebhookUrl.trim() === "";

return (
<Flex gap={2} shrink={false} direction={"column"}>
{publishError && <Text color="destructive">{publishError}</Text>}
Expand Down Expand Up @@ -900,6 +966,41 @@ const Publish = ({
}}
onChange={onHostChange}
/>
{host === "coolify" && (
<Flex direction="column" gap="1">
<Label htmlFor="coolifyWebhookUrl">
Coolify deploy webhook URL
</Label>
<InputField
id="coolifyWebhookUrl"
type="url"
placeholder="https://coolify.example.com/api/v1/deploy?uuid=…"
value={coolifyWebhookUrl}
onChange={(event) =>
onCoolifyWebhookUrlChange(event.target.value)
}
/>
<InputField
id="coolifyWebhookToken"
type="password"
placeholder="Bearer token (optional)"
value={coolifyWebhookToken}
onChange={(event) =>
onCoolifyWebhookTokenChange(event.target.value)
}
/>
<Text color="subtle">
Create a Docker Image app on your Coolify, then paste its
deploy webhook here.{" "}
<Link
href="https://github.com/webstudio-community/webstudio-self-host#coolify-remote-ssr"
target="_blank"
>
Host with Coolify
</Link>
</Text>
</Flex>
)}
</Flex>
</Collapsible.Content>
</Flex>
Expand All @@ -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"
}
>
<Button
Expand All @@ -929,6 +1032,7 @@ const Publish = ({
disabled={
hasSelectedDomains === false ||
disabled ||
missingCoolifyWebhook ||
(restrictedFeatures.size > 0 && hasCustomDomainsSelected) ||
userPublishCount >= maxDailyPublishesPerUser
}
Expand Down Expand Up @@ -1292,9 +1396,16 @@ const Content = (props: {
throw new Error("Project not found");
}
const projectState = "idle";
const { renderMode, setRenderMode, host, setHost } = usePublishTarget(
project.id
);
const {
renderMode,
setRenderMode,
host,
setHost,
coolifyWebhookUrl,
setCoolifyWebhookUrl,
coolifyWebhookToken,
setCoolifyWebhookToken,
} = usePublishTarget(project.id);

const { userPublishCount, maxDailyPublishesPerUser } = useUserPublishCount();

Expand Down Expand Up @@ -1383,6 +1494,10 @@ const Content = (props: {
onRenderModeChange={setRenderMode}
host={host}
onHostChange={setHost}
coolifyWebhookUrl={coolifyWebhookUrl}
onCoolifyWebhookUrlChange={setCoolifyWebhookUrl}
coolifyWebhookToken={coolifyWebhookToken}
onCoolifyWebhookTokenChange={setCoolifyWebhookToken}
/>
</Flex>
</form>
Expand Down
4 changes: 4 additions & 0 deletions apps/builder/app/services/api-router.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,8 @@ export const apiRouter = router({
idempotencyKey: z.string().optional(),
renderMode: z.enum(["ssg", "ssr"]).optional(),
host: z.enum(["local", "cloudflare", "coolify", "ssh"]).optional(),
coolifyWebhookUrl: z.string().optional(),
coolifyWebhookToken: z.string().optional(),
}),
"edit",
async ({ auth, ctx, input }) => {
Expand All @@ -1088,6 +1090,8 @@ export const apiRouter = router({
target: input.target,
renderMode: input.renderMode,
host: input.host,
coolifyWebhookUrl: input.coolifyWebhookUrl,
coolifyWebhookToken: input.coolifyWebhookToken,
},
ctx
);
Expand Down
7 changes: 7 additions & 0 deletions packages/domain/src/project-domain-api.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ export const publishProject = async (
target,
renderMode,
host,
coolifyWebhookUrl,
coolifyWebhookToken,
}: {
project: LoadedProject;
domains: string[];
Expand All @@ -234,6 +236,9 @@ export const publishProject = async (
*/
renderMode?: "ssg" | "ssr";
host?: "local" | "cloudflare" | "coolify" | "ssh";
/** host: "coolify" — the target app's deploy webhook, forwarded, not stored. */
coolifyWebhookUrl?: string;
coolifyWebhookToken?: string;
},
context: AppContext
) => {
Expand Down Expand Up @@ -264,6 +269,8 @@ export const publishProject = async (
destination: "saas",
renderMode,
host,
coolifyWebhookUrl,
coolifyWebhookToken,
logProjectName: `${project.title} - ${project.id}`,
});

Expand Down
5 changes: 5 additions & 0 deletions packages/domain/src/trpc/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export const domainRouter = router({
host: z
.enum(["local", "cloudflare", "coolify", "ssh"])
.default("local"),
// host: "coolify" only — the target app's deploy webhook (per site)
coolifyWebhookUrl: z.string().optional(),
coolifyWebhookToken: z.string().optional(),
}),
z.object({
projectId: z.string(),
Expand All @@ -106,6 +109,8 @@ export const domainRouter = router({
target: getPublishTargetForDomains(project, domains),
renderMode: input.renderMode,
host: input.host,
coolifyWebhookUrl: input.coolifyWebhookUrl,
coolifyWebhookToken: input.coolifyWebhookToken,
},
ctx
);
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions packages/trpc-interface/src/shared/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export const publishInput = z.object({
// host — "local" (default) | "cloudflare" | "coolify" | "ssh"
renderMode: z.enum(["ssg", "ssr"]).default("ssg"),
host: z.enum(["local", "cloudflare", "coolify", "ssh"]).default("local"),
// host: "coolify" only — the target app's deploy webhook (per site, not stored)
coolifyWebhookUrl: z.string().optional(),
coolifyWebhookToken: z.string().optional(),
// preview support
branchName: z.string(),
// action log helper (not used for deployment, but for action logs readablity)
Expand Down Expand Up @@ -105,6 +108,8 @@ export const deploymentRouter = router({
builderOrigin: input.builderOrigin,
renderMode: input.renderMode,
host: input.host,
coolifyWebhookUrl: input.coolifyWebhookUrl,
coolifyWebhookToken: input.coolifyWebhookToken,
destination: input.destination,
}),
});
Expand Down
Loading