{"record":{"id":"b502eb85c9ff0b37","repo":"nexu-io/open-design","slug":"cloudflare-zone-is-required-for-a-custom-domain","errorCode":null,"errorMessage":"Cloudflare zone is required for a custom domain.","messagePattern":"Cloudflare zone is required for a custom domain\\.","errorType":"http","errorClass":"DeployError","httpStatus":400,"severity":"error","filePath":"apps/daemon/src/deploy.ts","lineNumber":574,"sourceCode":"  };\n}\n\nfunction normalizeDeploymentLinkStatus(status: unknown): DeployLinkStatus {\n  return status === 'ready' || status === 'protected' || status === 'failed'\n    ? status\n    : 'link-delayed';\n}\n\nfunction normalizeCloudflarePagesDeploySelection(input: unknown): CloudflarePagesDeploySelection | null {\n  if (!input || typeof input !== 'object') return null;\n  const source = input as JsonObject;\n  const rawZoneId = typeof source.zoneId === 'string' ? source.zoneId.trim() : '';\n  const rawZoneName = typeof source.zoneName === 'string' ? source.zoneName.trim() : '';\n  const rawPrefix = typeof source.domainPrefix === 'string' ? source.domainPrefix.trim() : '';\n  if (!rawZoneId && !rawZoneName && !rawPrefix) return null;\n  const zoneName = normalizeCloudflareZoneName(rawZoneName);\n  const domainPrefix = normalizeCloudflareDomainPrefix(rawPrefix);\n  if (!rawZoneId) throw new DeployError('Cloudflare zone is required for a custom domain.', 400);\n  if (!zoneName || !isValidCloudflareZoneName(zoneName)) {\n    throw new DeployError('Select a valid Cloudflare domain for the custom domain.', 400);\n  }\n  if (!domainPrefix) {\n    throw new DeployError('Enter a valid subdomain prefix, for example \"demo\".', 400);\n  }\n  return {\n    zoneId: rawZoneId,\n    zoneName,\n    domainPrefix,\n    hostname: `${domainPrefix}.${zoneName}`,\n  };\n}\n\nasync function validateCloudflarePagesDeploySelection(config: DeployConfig, selection: CloudflarePagesDeploySelection | null): Promise<CloudflarePagesDeploySelection | null> {\n  if (!selection) return null;\n  const resp = await fetch(`${CLOUDFLARE_API}/zones/${encodeURIComponent(selection.zoneId)}`, {\n    headers: cloudflareHeaders(config),","sourceCodeStart":556,"sourceCodeEnd":592,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/deploy.ts#L556-L592","documentation":"Thrown by normalizeCloudflarePagesDeploySelection in apps/daemon/src/deploy.ts:574 as `DeployError('Cloudflare zone is required for a custom domain.', 400)`. The function parses a custom-domain selection object; if all three of zoneId/zoneName/domainPrefix are empty it returns null (no custom domain requested), but if ANY field is present and `rawZoneId` is empty, it throws — a custom domain cannot be configured without the zone to create DNS records in.","triggerScenarios":"Submitting a custom-domain selection where `zoneName` or `domainPrefix` is set but `zoneId` is missing/empty after trimming. Commonly the frontend sent the human-readable domain and prefix from the form but the zone dropdown selection (which carries zoneId) was not included.","commonSituations":"Zone dropdown was cleared or never selected; stale form state where the user typed a prefix but the zone picker returned empty; frontend bug dropping the zoneId field from the payload.","solutions":["Ensure the selection object includes a non-empty trimmed `zoneId` (the id from listCloudflarePagesZones).","In the UI, require the zone dropdown to be selected before enabling the prefix field / submit.","If only a domain name is known, resolve it to a zoneId first via listCloudflarePagesZones and match by zoneName."],"exampleFix":"// before\nnormalizeCloudflarePagesDeploySelection({ zoneName: 'example.com', domainPrefix: 'demo' }); // no zoneId\n\n// after\nnormalizeCloudflarePagesDeploySelection({ zoneId: 'abc', zoneName: 'example.com', domainPrefix: 'demo' });","handlingStrategy":"validation","validationCode":"function hasZoneId(sel: unknown): boolean {\n  return typeof (sel as any)?.zoneId === 'string' && (sel as any).zoneId.trim().length > 0;\n}\nif (!hasZoneId(selection)) {\n  return res.status(400).json({ error: 'Select a Cloudflare zone.' });\n}","typeGuard":"function isCompleteZoneSelection(sel: unknown): sel is { zoneId: string; zoneName?: string; domainPrefix?: string } {\n  return typeof sel === 'object' && sel !== null\n    && typeof (sel as any).zoneId === 'string'\n    && (sel as any).zoneId.trim().length > 0;\n}","tryCatchPattern":"try {\n  await deployToCloudflarePages({ config, files, projectId, cloudflarePages: selection });\n} catch (err) {\n  if (err instanceof DeployError && /zone is required/i.test(err.message)) {\n    return res.status(400).json({ error: 'Pick a zone before setting a custom domain.' });\n  }\n  throw err;\n}","preventionTips":["Force the user to pick a zone from the dropdown (which carries zoneId) before enabling the prefix field.","Send zoneId, zoneName, and domainPrefix together from a single selection object.","Clear the whole selection if the zone dropdown is reset."],"tags":["deploy","cloudflare","validation","custom-domain","zone"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}