Skip to content

Feat/345 add support for custom domains#369

Open
piffio wants to merge 12 commits into
mainfrom
feat/345-add-support-for-custom-domains
Open

Feat/345 add support for custom domains#369
piffio wants to merge 12 commits into
mainfrom
feat/345-add-support-for-custom-domains

Conversation

@piffio
Copy link
Copy Markdown
Owner

@piffio piffio commented May 15, 2026

This PR implements organization-level custom domains with SSL
certificates via Cloudflare for SaaS, including:

Backend:

  • Custom domain model and repository
  • Cloudflare for SaaS API integration
  • Custom domain CRUD API endpoints
  • KV dual-write for hostname:short_code mappings
  • Redirect handler updates for custom domain resolution
  • Scheduled polling for domain status updates
  • Admin API for manual domain polling and listing
  • Database migration for custom_domains table

Frontend:

  • Custom domain management UI
  • Admin domains page with manual polling button
  • Admin API client updates
  • Domains API client
  • Admin sidebar navigation updates

Infrastructure:

  • Added CF_SAAS_API_TOKEN secret to staging and production workflows
  • Added domain polling cron to production

Tier limits are enforced at the organisation level.

Closes #345

piffio added 2 commits May 15, 2026 16:50
This commit implements organization-level custom domains with SSL
certificates via Cloudflare for SaaS, including:

Backend:
- Custom domain model and repository
- Cloudflare for SaaS API integration
- Custom domain CRUD API endpoints
- KV dual-write for hostname:short_code mappings
- Redirect handler updates for custom domain resolution
- Scheduled polling for domain status updates
- Admin API for manual domain polling and listing
- Database migration for custom_domains table

Frontend:
- Custom domain management UI
- Admin domains page with manual polling button
- Admin API client updates
- Domains API client
- Admin sidebar navigation updates

Infrastructure:
- Added CF_SAAS_API_TOKEN secret to staging and production workflows
- Added domain polling cron to production

Tier limits enforced at organization level.
@github-actions
Copy link
Copy Markdown

🚀 Ephemeral Environment Deployed (Unified Worker)

Application: https://rushomon-pr-369.piffio.workers.dev

This unified Worker deployment serves both frontend and backend from the same domain for better security (httpOnly cookies work correctly).

This environment will be automatically cleaned up when the PR is closed.

Worker: rushomon-pr-369


💡 Tip: To skip preview deployment, add the skip-preview label to this PR.

Add the support for a custom fallback domain so that we're not limited
to using the existing one, and allow the use of a sub-domain such as
`redirect.my-domain.com` if preferred.
@github-actions
Copy link
Copy Markdown

🚀 Ephemeral Environment Deployed (Unified Worker)

Application: https://rushomon-pr-369.piffio.workers.dev

This unified Worker deployment serves both frontend and backend from the same domain for better security (httpOnly cookies work correctly).

This environment will be automatically cleaned up when the PR is closed.

Worker: rushomon-pr-369


💡 Tip: To skip preview deployment, add the skip-preview label to this PR.

@github-actions
Copy link
Copy Markdown

🚀 Ephemeral Environment Deployed (Unified Worker)

Application: https://rushomon-pr-369.piffio.workers.dev

This unified Worker deployment serves both frontend and backend from the same domain for better security (httpOnly cookies work correctly).

This environment will be automatically cleaned up when the PR is closed.

Worker: rushomon-pr-369


💡 Tip: To skip preview deployment, add the skip-preview label to this PR.

Cloudflare for SaaS is an Enterprise-only feature. When quota is not allocated,
fall back to dev/test mode instead of failing with 500 error.

- Add quota error detection (error 1404 or 'quota' in message)
- Return stub DNS instructions when CF for SaaS unavailable
- Update documentation to clarify Enterprise plan requirement
@github-actions
Copy link
Copy Markdown

🚀 Ephemeral Environment Deployed (Unified Worker)

Application: https://rushomon-pr-369.piffio.workers.dev

This unified Worker deployment serves both frontend and backend from the same domain for better security (httpOnly cookies work correctly).

This environment will be automatically cleaned up when the PR is closed.

Worker: rushomon-pr-369


💡 Tip: To skip preview deployment, add the skip-preview label to this PR.

…tion

Cloudflare for SaaS now returns ACME validation records in ssl.validation_records
instead of ownership_verification. This fixes the TXT record mismatch where
the dashboard showed _cf-custom-hostname but Cloudflare expected _acme-challenge.

- Add validation_records field to CfSslResult
- Update domain creation to check both validation_records and ownership_verification
- Prioritize ACME validation records if available
@github-actions
Copy link
Copy Markdown

🚀 Ephemeral Environment Deployed (Unified Worker)

Application: https://rushomon-pr-369.piffio.workers.dev

This unified Worker deployment serves both frontend and backend from the same domain for better security (httpOnly cookies work correctly).

This environment will be automatically cleaned up when the PR is closed.

Worker: rushomon-pr-369


💡 Tip: To skip preview deployment, add the skip-preview label to this PR.

… domains

Cloudflare for SaaS requires TWO different TXT verification methods:
1. Domain ownership verification (_cf-custom-hostname.*) - validates hostname ownership
2. SSL certificate validation (_acme-challenge.*) - validates for certificate issuance

Both are required for production traffic. The previous implementation only showed
one TXT record, causing verification failures when Cloudflare expected the other type.

This ensures users see all required TXT records with clear labels indicating
which is for domain ownership vs SSL certificate validation.
@github-actions
Copy link
Copy Markdown

🚀 Ephemeral Environment Deployed (Unified Worker)

Application: https://rushomon-pr-369.piffio.workers.dev

This unified Worker deployment serves both frontend and backend from the same domain for better security (httpOnly cookies work correctly).

This environment will be automatically cleaned up when the PR is closed.

Worker: rushomon-pr-369


💡 Tip: To skip preview deployment, add the skip-preview label to this PR.

SSL validation records (_acme-challenge.*) are only returned when the
certificate is in pending_validation state. The initial CREATE response
may not include them - they appear after Cloudflare starts the certificate
issuance process.

Added a follow-up GET call after creating the custom hostname to fetch
the full hostname details, which includes the validation_records when
the SSL status becomes pending_validation.
@github-actions
Copy link
Copy Markdown

🚀 Ephemeral Environment Deployed (Unified Worker)

Application: https://rushomon-pr-369.piffio.workers.dev

This unified Worker deployment serves both frontend and backend from the same domain for better security (httpOnly cookies work correctly).

This environment will be automatically cleaned up when the PR is closed.

Worker: rushomon-pr-369


💡 Tip: To skip preview deployment, add the skip-preview label to this PR.

The SSL validation records (_acme-challenge.*) from Cloudflare are
returned by the GET endpoint, not just on creation. For non-wildcard
hostnames, CF uses HTTP validation automatically once the CNAME is set,
but falls back to TXT if that fails (showing "Pending Validation (TXT)").
@github-actions
Copy link
Copy Markdown

🚀 Ephemeral Environment Deployed (Unified Worker)

Application: https://rushomon-pr-369.piffio.workers.dev

This unified Worker deployment serves both frontend and backend from the same domain for better security (httpOnly cookies work correctly).

This environment will be automatically cleaned up when the PR is closed.

Worker: rushomon-pr-369


💡 Tip: To skip preview deployment, add the skip-preview label to this PR.

The refresh endpoint was only returning dns_instructions when the
hostname status was not active. But the hostname can be "active" while
the SSL certificate is still "Pending Validation (TXT)" - this is the
common case after CNAME validation succeeds but before SSL completes.

Now when a user clicks Refresh:
- If SSL certificate is still pending (even if hostname is active)
- The response includes both _cf-custom-hostname and _acme-challenge records
- The frontend displays all required TXT records
@github-actions
Copy link
Copy Markdown

🚀 Ephemeral Environment Deployed (Unified Worker)

Application: https://rushomon-pr-369.piffio.workers.dev

This unified Worker deployment serves both frontend and backend from the same domain for better security (httpOnly cookies work correctly).

This environment will be automatically cleaned up when the PR is closed.

Worker: rushomon-pr-369


💡 Tip: To skip preview deployment, add the skip-preview label to this PR.

@github-actions
Copy link
Copy Markdown

🚀 Ephemeral Environment Deployed (Unified Worker)

Application: https://rushomon-pr-369.piffio.workers.dev

This unified Worker deployment serves both frontend and backend from the same domain for better security (httpOnly cookies work correctly).

This environment will be automatically cleaned up when the PR is closed.

Worker: rushomon-pr-369


💡 Tip: To skip preview deployment, add the skip-preview label to this PR.

Changes:
- Added ssl_status field to CustomDomain model with SSL_STATUS_* constants
- Added database migration 0031_ssl_status.sql for ssl_status column
- Updated CustomDomainRepository to include ssl_status in all queries
- Added update_ssl_status() method to persist SSL status from Cloudflare
- Updated refresh endpoint to persist ssl_status from Cloudflare API
- Updated frontend CustomDomain type to include ssl_status
- Updated frontend to auto-fetch DNS instructions on page load if SSL is pending
- Show Refresh button for active domains (not just pending)

Now when a user reloads the page:
- If any domain has ssl_status === "pending", the DNS instructions panel
  automatically shows with the SSL validation TXT records
- Users can click Refresh on active domains to re-check SSL status
@piffio piffio deployed to ephemeral May 15, 2026 20:14 — with GitHub Actions Active
@github-actions
Copy link
Copy Markdown

🚀 Ephemeral Environment Deployed (Unified Worker)

Application: https://rushomon-pr-369.piffio.workers.dev

This unified Worker deployment serves both frontend and backend from the same domain for better security (httpOnly cookies work correctly).

This environment will be automatically cleaned up when the PR is closed.

Worker: rushomon-pr-369


💡 Tip: To skip preview deployment, add the skip-preview label to this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for custom domains

1 participant