Skip to content

Commit d6d2709

Browse files
Seth Raphaelclaude
andcommitted
Add custom domain setup to Workers wizard and improve docs
- Setup wizard now prompts to configure custom domains for Workers - Automatically sets SSL mode to "full" if on "flexible" - Retries domain configuration after deployment if worker didn't exist - Added note about running `convex dev` for HTTP actions error - Added section for non-Vite bundlers (Expo, Next.js) env var handling Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d6c23c3 commit d6d2709

2 files changed

Lines changed: 187 additions & 4 deletions

File tree

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ export const { generateUploadUrl, recordAsset, gcOldAssets, listAssets } =
9090
exposeUploadApi(components.selfStaticHosting);
9191
```
9292

93+
**Note:** Run `npx convex dev` at least once after setup to push your schema and
94+
enable HTTP actions. If you see the error "This Convex deployment does not have
95+
HTTP actions enabled", it means the Convex backend hasn't been deployed yet.
96+
9397
### 3. Add deploy script to package.json
9498

9599
```json
@@ -138,6 +142,37 @@ npx @get-convex/self-static-hosting upload --build --prod --cloudflare-workers -
138142
npx @get-convex/self-static-hosting upload --build
139143
```
140144

145+
### Using Non-Vite Bundlers
146+
147+
The CLI's `--build` flag sets `VITE_CONVEX_URL` when running your build command.
148+
For bundlers that use different environment variable conventions, wrap your build
149+
script to pass through the value:
150+
151+
**For Expo:**
152+
153+
```json
154+
{
155+
"scripts": {
156+
"build": "EXPO_PUBLIC_CONVEX_URL=${VITE_CONVEX_URL:-$EXPO_PUBLIC_CONVEX_URL} npx expo export --platform web"
157+
}
158+
}
159+
```
160+
161+
**For Next.js:**
162+
163+
```json
164+
{
165+
"scripts": {
166+
"build": "NEXT_PUBLIC_CONVEX_URL=${VITE_CONVEX_URL:-$NEXT_PUBLIC_CONVEX_URL} next build"
167+
}
168+
}
169+
```
170+
171+
The pattern `${VITE_CONVEX_URL:-$VAR}` uses `VITE_CONVEX_URL` if set (by the CLI),
172+
otherwise falls back to your bundler-specific variable. This allows the CLI's
173+
`--build` flag to work correctly while keeping your standalone `npm run build`
174+
functional.
175+
141176
## Deployment
142177

143178
### One-Shot Deployment (Recommended)

src/cli/setup-cloudflare.ts

Lines changed: 152 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,28 @@ async function getWorkerSubdomain(
532532
return null;
533533
}
534534

535+
/**
536+
* Add a custom domain to a Cloudflare Worker
537+
*/
538+
async function addWorkerCustomDomain(
539+
token: string,
540+
accountId: string,
541+
workerName: string,
542+
zoneId: string,
543+
hostname: string,
544+
): Promise<boolean> {
545+
const data = await cfApi(`/accounts/${accountId}/workers/domains`, token, {
546+
method: "PUT",
547+
body: JSON.stringify({
548+
hostname,
549+
service: workerName,
550+
environment: "production",
551+
zone_id: zoneId,
552+
}),
553+
});
554+
return data.success;
555+
}
556+
535557
/**
536558
* Run the Cloudflare Workers setup flow
537559
*/
@@ -585,9 +607,104 @@ async function runWorkersSetup(token: string): Promise<void> {
585607
saveWorkerNameToEnv(workerName);
586608
success("Saved CLOUDFLARE_WORKER_NAME to .env.local");
587609

610+
// Step 3: Custom domain setup
611+
log("");
612+
log("Step 3: Custom domain setup...");
613+
log("");
614+
615+
const wantCustomDomain = await promptYesNo(
616+
"Would you like to add a custom domain?",
617+
true,
618+
);
619+
620+
let customDomain: string | null = null;
621+
let selectedZone: { id: string; name: string } | null = null;
622+
623+
if (wantCustomDomain) {
624+
// List zones
625+
const zones = await listZones(token);
626+
627+
if (zones.length === 0) {
628+
warn("No domains found in your Cloudflare account.");
629+
log("");
630+
log("To add a domain to Cloudflare:");
631+
log(" 1. Go to https://dash.cloudflare.com/");
632+
log(" 2. Click 'Add a site'");
633+
log(" 3. Follow the setup wizard");
634+
log("");
635+
log("You can add a custom domain later in the Cloudflare dashboard.");
636+
} else {
637+
log("");
638+
log("Your domains in Cloudflare:");
639+
zones.forEach((zone, i) => {
640+
log(` ${i + 1}. ${zone.name} (${zone.status})`);
641+
});
642+
log("");
643+
644+
const choice = await prompt(`Select a domain [1-${zones.length}]: `);
645+
const choiceNum = parseInt(choice, 10);
646+
647+
if (choiceNum >= 1 && choiceNum <= zones.length) {
648+
selectedZone = zones[choiceNum - 1];
649+
650+
// Ask about subdomain
651+
const useSubdomain = await promptYesNo(
652+
`Use a subdomain (e.g., app.${selectedZone.name})? Otherwise will use root domain.`,
653+
false,
654+
);
655+
656+
if (useSubdomain) {
657+
const subdomainName = await prompt("Enter subdomain (e.g., app, www): ");
658+
customDomain = subdomainName
659+
? `${subdomainName}.${selectedZone.name}`
660+
: `app.${selectedZone.name}`;
661+
} else {
662+
customDomain = selectedZone.name;
663+
}
664+
665+
log("");
666+
log(`Configuring custom domain: ${customDomain}`);
667+
668+
// Check SSL mode first
669+
const currentSslMode = await getSslMode(token, selectedZone.id);
670+
if (currentSslMode === "flexible") {
671+
warn(`SSL mode is "flexible" - this may cause issues.`);
672+
log("Changing to \"full\"...");
673+
const sslSuccess = await setSslMode(token, selectedZone.id, "full");
674+
if (sslSuccess) {
675+
success("SSL mode set to \"full\"");
676+
} else {
677+
warn("Could not change SSL mode automatically.");
678+
log(`Please set SSL/TLS mode to "Full" at:`);
679+
log(` https://dash.cloudflare.com → ${selectedZone.name} → SSL/TLS`);
680+
}
681+
} else if (currentSslMode) {
682+
success(`SSL mode is "${currentSslMode}" (OK)`);
683+
}
684+
685+
// Try to add custom domain
686+
// Note: The worker must exist first, so we'll try but it may fail
687+
const domainAdded = await addWorkerCustomDomain(
688+
token,
689+
accountId,
690+
workerName,
691+
selectedZone.id,
692+
customDomain,
693+
);
694+
695+
if (domainAdded) {
696+
success(`Custom domain configured: ${customDomain}`);
697+
} else {
698+
info("Custom domain will be configured after first deploy.");
699+
log("(The worker must exist before adding a custom domain)");
700+
}
701+
}
702+
}
703+
}
704+
588705
// Offer to deploy
589706
log("");
590-
log("Step 3: Deploy static files...");
707+
log("Step 4: Deploy static files...");
591708
log("");
592709
const shouldDeploy = await promptYesNo(
593710
"Would you like to build and deploy static files now?",
@@ -616,6 +733,27 @@ async function runWorkersSetup(token: string): Promise<void> {
616733

617734
if (deployResult.status === 0) {
618735
success("Deployment complete!");
736+
737+
// If we had a custom domain that failed earlier, try again now
738+
if (customDomain && selectedZone) {
739+
log("");
740+
log("Configuring custom domain...");
741+
const domainAdded = await addWorkerCustomDomain(
742+
token,
743+
accountId,
744+
workerName,
745+
selectedZone.id,
746+
customDomain,
747+
);
748+
749+
if (domainAdded) {
750+
success(`Custom domain configured: ${customDomain}`);
751+
} else {
752+
warn("Could not configure custom domain automatically.");
753+
log("Add it manually in the Cloudflare dashboard:");
754+
log(` Workers & Pages → ${workerName} → Settings → Domains & Routes`);
755+
}
756+
}
619757
} else {
620758
warn("Deployment failed. Try running manually:");
621759
log(` npx @get-convex/self-static-hosting upload --cloudflare-workers --worker-name ${workerName} --prod`);
@@ -625,6 +763,11 @@ async function runWorkersSetup(token: string): Promise<void> {
625763
log(` npm run build`);
626764
log(` npx @get-convex/self-static-hosting upload --cloudflare-workers --worker-name ${workerName} --prod`);
627765
}
766+
} else if (customDomain && selectedZone) {
767+
log("");
768+
info("After deploying, add your custom domain:");
769+
log(` Workers & Pages → ${workerName} → Settings → Domains & Routes`);
770+
log(` Or run this wizard again after deploying.`);
628771
}
629772

630773
// Build the URL
@@ -640,10 +783,15 @@ async function runWorkersSetup(token: string): Promise<void> {
640783
log("Your configuration:");
641784
log(` Worker: ${workerName}`);
642785
log(` URL: ${workerUrl}`);
786+
if (customDomain) {
787+
log(` Custom domain: https://${customDomain}`);
788+
}
643789
log("");
644-
log("To add a custom domain, go to:");
645-
log(" https://dash.cloudflare.com -> Workers & Pages -> your worker -> Settings -> Triggers");
646-
log("");
790+
if (!customDomain) {
791+
log("To add a custom domain later:");
792+
log(" Workers & Pages → your worker → Settings → Domains & Routes");
793+
log("");
794+
}
647795
log("To deploy in the future, run:");
648796
log(` npx @get-convex/self-static-hosting upload --build --prod --cloudflare-workers`);
649797
log("");

0 commit comments

Comments
 (0)