Repository navigation
Hosted DNS, Cluster Domain and wildcard certificates - #1061
Merged
Merged
Conversation
added 13 commits
September 25, 2026 04:39
Adds organization_cluster_domain (token encrypted under APP_ENCRYPTION_SECRET, optional wildcard certificate columns), PLOYZ_HOSTED_DNS_URL and optional PLOYZ_HOSTED_DNS_MINT_KEY config, and a small Hosted DNS client. Founder enrollment completion reserves the name with the Organization slug and never fails on a Hosted DNS error. Server Settings shows the name or Not reserved yet, with Publish now reserving a missing name. Organization teardown releases the name once after the Organization row is deleted, ignoring failure.
Deploy compile reads the row (one inline reserve when missing, else a named cluster_domain_unreserved failure pointing to Server Settings) and sends managed hostnames to the daemon as explicit prefix.name routes. PLOYZ_PUBLIC_DOMAIN, the Service drawer, variables tab and reference targets read the name from the Org Store, so they render with no Server connected. Cloud no longer reads hosted_dns_hostname from the runtime frame, and the up.ployz.dev placeholder is gone.
…main One Inngest function, sync-cluster-domain, runs per Organization on cluster-domain/sync.requested. An hourly cron fans that event out to every founded pairing. The sync reserves a missing name, probes ingress Servers with a public IP on /.ployz-verify, PUTs the reachable set and renews the lease. A missing frame or an empty set writes no records. A reaped name (404) is reserved again; 410 and 401 fail as ClusterDomainUnusable without retries. Publish now, founder enrollment completion and machine removal request the sync. Server Settings shows when records were published and each ingress Server's place in the set, which needs the new unreachable column.
Derive the route id as a v5-shaped UUID over the hostname's SHA-1, so two compiles of the same config send identical routes. Core validation requires route ids to be UUIDs; the test lowers the expanded config to prove it.
Add the frozen PublishCertificateMaterial RPC (Cluster capability ployz.certificates.publish.v1): set validates chain, key match and leaf coverage; clear removes published material and returns the hostname to ACME. Certificate rows gain an optional published flag that ACME never orders, renews or overwrites. Rows key by CertificateHost (hostname or *.x); a published wildcard suppresses ACME for covered hostnames and lends them its material in Caddy. SDK: Client.publishCertificateMaterial.
Rebuild the SDK for the CertificateHost wire type, and let managed hostnames take certificate evidence from a covering published wildcard.
sync-cluster-domain gains ensure-certificate and publish-certificate after renew-lease. With no certificate or under 30 days left, Cloud generates a P-256 key and a CSR for exactly name and *.name (empty subject), obtains the chain from Hosted DNS through withClusterDomain, and stores the key encrypted under APP_ENCRYPTION_SECRET with the chain and not-after. A Hosted DNS failure, 429 included, keeps the current certificate. Every sync republishes *.name through PublishCertificateMaterial, which is idempotent, so a re-paired Cluster gets the material back. The Hosted DNS client timeout is now per call; the certificate call allows 15 minutes. Server Settings shows the wildcard's expiry.
Cloud now owns the Cluster Domain end to end, so core only knows explicit Ingress Hostnames. Deletes the Hosted DNS client and cluster row, the four DNS RPCs, hosted_dns_hostname on the Runtime Watch frame, IngressHostname::ClusterDomain and its label type, managedHostnames lowering (now refused unexpanded), the CLI --no-dns/--dns-endpoint flags, machine add/remove DNS refresh, the ingress probe and the Hosted DNS layer-3 suite. Keeps /.ployz-verify, ClusterDnsVerdict and the .internal DNS server. Closes #1024
…enum PortPublication::Ingress holds IngressHost directly; SDK payloads regenerated. Published Certificate Material is its own row variant, so ACME writes go through one acme_row guard, and SAN coverage, the wanted set and the ingress projection share CertificateHost::covers via published_cover. Store row helpers take CertificateHost and publish_certificate_material takes material by value.
Session RPC wrappers share one unary helper; bind collects the apply-set with map; the warned-failure test uses the message the CLI emits.
added 4 commits
September 25, 2026 06:36
The hourly sync fans out over every organization_cluster_domain row, so an Organization with no Cluster still renews its lease. The sync reserves once up front; withClusterDomain only loads the row, and a name Hosted DNS reaped (404) or retired (410) is replaced by a fresh reservation with its old wildcard cleared from the Cluster. The lease route is called only when no records were PUT. The published column becomes record_addresses, the two unreleased migrations are one, and the settings copy says Cluster Domain.
…spense managedHostname(prefix, domain) builds every generated hostname; expandManagedHostnames moves to environment-design and fails with a typed ClusterDomainRequired. useClusterDomainName reads the Org Store with Suspense, and the settings tab test covers the covering-wildcard certificate status.
acme_row hands back the row's CertificateHost key so ACME writers stop rebuilding it, and a row marked published without valid material is refused as corrupt.
requireClusterDomain already refuses a deploy without a Cluster Domain, so expansion takes the name directly. The sync's records flag is recordsPut.
nick-potts
marked this pull request as ready for review
September 25, 2026 06:48
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
added 3 commits
September 25, 2026 07:04
Conflicts: ingress.rs, caddy_tests.rs and contract.rs imports (main deleted Ingress Proxy Fragments; this branch collapsed IngressHostname), caddy_tests keeps main's caddyfile_for with CertificateHost keys; config, .env.example and vitest.setup take main's single Polar product and drop installer config, keeping the Hosted DNS URL and mint key; tests use main's testConfigEnvironment. SDK payloads, config-wasm and the organization_cluster_domain migration are regenerated after main's migrations.
Takes the Build Method rename in ServiceSettingsTab.test.tsx, regenerates config-wasm, and regenerates the organization_cluster_domain migration after main's build_method migration.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Managed hostnames move out of the daemon and become a Cloud feature, served by the Ployz-run Hosted DNS service (getployz/hosted-dns#1). Each Organization gets one Cluster Domain and one wildcard certificate that covers every generated hostname.
Core loses all Hosted DNS and gains one frozen RPC:
managedHostnamesstays in the authored environment document, but core lowering refuses it. Cloud expands it to explicit hostnames before lowering (spec amended). The daemon never lets ACME touch published Certificate Material. A published*.xcovers matching hostnames, so ACME orders nothing for them. Custom domains keep Let's Encrypt HTTP-01 exactly as before.Evidence
After: the daemon holds no generated-domain state.
rg -i 'hosted_dns|hostedDns|ReserveDomain|no-dns|dns-endpoint' core/returns nothing.Final run, at
eaeb4ca4, after the four-axis review:Four-axis review: Standards, Spec, Maintainability and Simplicity all pass after 3 rounds.
New tests that pin the behaviour:
Merge Danger
Door: one-way
PublishCertificateMaterialbecomes a frozen RPC under the Stable promise once 0.2.0 ships. Deleting the four DNS RPCs and thehosted_dnsrow is fine only because nothing has been released yet. The rest is two-way: one additive table and Cloud-side code.Blast Radius: ingress
hosted_dns_hostnameis gone, and the certificate keys are nowCertificateHost. The SDK payloads are regenerated.https://dns.ployz.app(Deploy Hosted DNS to production and set Cloud's default endpoint #1018, smoke run passed). The mint key is already set on Ployz Dashboardwebandworker, so hosted Cloud mints without hitting the per-IP limit.Closes #1015
Closes #1018
Closes #1019
Closes #1020
Closes #1021
Closes #1022
Closes #1023
Closes #1024