nexu-io/open-design · error · DeployError

Cloudflare zone is required for a custom domain.

Error message

Cloudflare zone is required for a custom domain.

What it means

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.

Source

Thrown at apps/daemon/src/deploy.ts:574

  };
}

function normalizeDeploymentLinkStatus(status: unknown): DeployLinkStatus {
  return status === 'ready' || status === 'protected' || status === 'failed'
    ? status
    : 'link-delayed';
}

function normalizeCloudflarePagesDeploySelection(input: unknown): CloudflarePagesDeploySelection | null {
  if (!input || typeof input !== 'object') return null;
  const source = input as JsonObject;
  const rawZoneId = typeof source.zoneId === 'string' ? source.zoneId.trim() : '';
  const rawZoneName = typeof source.zoneName === 'string' ? source.zoneName.trim() : '';
  const rawPrefix = typeof source.domainPrefix === 'string' ? source.domainPrefix.trim() : '';
  if (!rawZoneId && !rawZoneName && !rawPrefix) return null;
  const zoneName = normalizeCloudflareZoneName(rawZoneName);
  const domainPrefix = normalizeCloudflareDomainPrefix(rawPrefix);
  if (!rawZoneId) throw new DeployError('Cloudflare zone is required for a custom domain.', 400);
  if (!zoneName || !isValidCloudflareZoneName(zoneName)) {
    throw new DeployError('Select a valid Cloudflare domain for the custom domain.', 400);
  }
  if (!domainPrefix) {
    throw new DeployError('Enter a valid subdomain prefix, for example "demo".', 400);
  }
  return {
    zoneId: rawZoneId,
    zoneName,
    domainPrefix,
    hostname: `${domainPrefix}.${zoneName}`,
  };
}

async function validateCloudflarePagesDeploySelection(config: DeployConfig, selection: CloudflarePagesDeploySelection | null): Promise<CloudflarePagesDeploySelection | null> {
  if (!selection) return null;
  const resp = await fetch(`${CLOUDFLARE_API}/zones/${encodeURIComponent(selection.zoneId)}`, {
    headers: cloudflareHeaders(config),

View on GitHub (pinned to 5be4028344)

Solutions

  1. Ensure the selection object includes a non-empty trimmed `zoneId` (the id from listCloudflarePagesZones).
  2. In the UI, require the zone dropdown to be selected before enabling the prefix field / submit.
  3. If only a domain name is known, resolve it to a zoneId first via listCloudflarePagesZones and match by zoneName.

Example fix

// before
normalizeCloudflarePagesDeploySelection({ zoneName: 'example.com', domainPrefix: 'demo' }); // no zoneId

// after
normalizeCloudflarePagesDeploySelection({ zoneId: 'abc', zoneName: 'example.com', domainPrefix: 'demo' });
Defensive patterns

Strategy: validation

Validate before calling

function hasZoneId(sel: unknown): boolean {
  return typeof (sel as any)?.zoneId === 'string' && (sel as any).zoneId.trim().length > 0;
}
if (!hasZoneId(selection)) {
  return res.status(400).json({ error: 'Select a Cloudflare zone.' });
}

Type guard

function isCompleteZoneSelection(sel: unknown): sel is { zoneId: string; zoneName?: string; domainPrefix?: string } {
  return typeof sel === 'object' && sel !== null
    && typeof (sel as any).zoneId === 'string'
    && (sel as any).zoneId.trim().length > 0;
}

Try / catch

try {
  await deployToCloudflarePages({ config, files, projectId, cloudflarePages: selection });
} catch (err) {
  if (err instanceof DeployError && /zone is required/i.test(err.message)) {
    return res.status(400).json({ error: 'Pick a zone before setting a custom domain.' });
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/b502eb85c9ff0b37. Report an issue: GitHub.