DNS Handling implements the ACME protocol (RFC 8555) DNS-01 challenge type using theDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/plutoploy/dns-handling/llms.txt
Use this file to discover all available pages before exploring further.
acmez/v3 library to automatically issue TLS certificates from Let’s Encrypt or any compatible ACME certificate authority. The process is entirely asynchronous: the API returns immediately after starting an order and a background goroutine handles the polling, challenge completion, and certificate storage without blocking the caller.
Key data structures
Two structs ininternal/acme/acme.go represent the ACME state that is persisted to the database during issuance:
Challenge.TXTValue is the value that must appear in DNS — it is computed by acmez/v3 via dnsChallenge.DNS01KeyAuthorization() and is distinct from the domain ownership verification token used in the earlier pending → verified step.
Full issuance flow
The entire flow is triggered by a single API call:POST /domains/{id}/issue-certificate. The domain must already be in verified status.
SetupAccount — resolve or create the ACME account
The handler calls
acmeProv.SetupAccount(). This method first checks the database for an existing ACME account record. If one exists, it decodes the stored PEM-encoded RSA private key and returns the key along with the saved Key ID (KID).If no account exists, it generates a new RSA-2048 account key, registers a new account with the ACME directory using the configured contact email address, and saves the resulting KID and PEM-encoded private key to the database so future calls reuse the same account.StartOrder — create the ACME order and extract the DNS-01 challenge
acmeProv.StartOrder() creates a new ACME order identifying the target domain, fetches the first authorization object, and iterates over the authorization’s challenges to find the one with type == "dns-01".The required DNS TXT value is then computed from the challenge token and the account key using acmez/v3:Order and Challenge records are persisted to the database and returned to the handler.Transition domain to certificate_pending
Before launching the background goroutine, the handler calls
domainSvc.SetCertificatePending(), which enforces that the domain is in verified status and then writes status = "certificate_pending" to the database.Return immediately to the caller
The handler responds with HTTP 202 Accepted before the certificate has been issued. The response body provides everything the caller needs to update their DNS record:The caller must update (or create) the TXT record at
challenge_domain with expected_txt_value as soon as possible.Background polling — wait for the TXT record to appear
A goroutine launched with If the context deadline is reached before the record appears,
go h.pollACME(...) runs independently with its own context that carries a 5-minute timeout (config.PollTimeout). Every 10 seconds (config.PollInterval) it calls dns.LookupTXT() on _acme-challenge.<domain>. and checks whether any returned record matches challenge.TXTValue. DNS lookup errors during polling are logged at debug level and silently retried on the next tick.domainSvc.SetFailed() is called and the goroutine exits.CompleteOrder — validate the challenge and finalize
Once the expected TXT value is detected in DNS, the goroutine calls
acmeProv.CompleteOrder(), which performs the remaining ACME protocol steps in sequence:- InitiateChallenge — notifies the ACME server that the DNS record is ready for validation.
- PollAuthorization — waits for the ACME server to confirm the challenge is
valid. If it fails, the order and challenge are markedfailedin the database. - Generate certificate key — a new RSA-2048 private key is created specifically for the certificate (separate from the account key).
- Create CSR — a Certificate Signing Request is constructed for the domain name.
- FinalizeOrder — submits the CSR to the ACME server to complete the order.
- GetCertificateChain — downloads the issued certificate chain PEM from the ACME server.
Store certificate and mark domain active
Back in the polling goroutine,
certSvc.Store() persists the certificate PEM, private key PEM, issued_at, and expires_at timestamps to the database. Finally, domainSvc.SetActive() advances the domain to status = "active". The certificate is then accessible via GET /domains/{id}/certificate.Polling configuration
Both timing values are hard-coded ininternal/config/config.go and apply globally:
| Parameter | Value | Description |
|---|---|---|
PollInterval | 10 seconds | How often the background goroutine checks DNS |
PollTimeout | 5 minutes | Maximum wall-clock time before the goroutine gives up and sets failed |
DNSTimeout | 10 seconds | Per-query timeout applied to each individual DNS lookup |
Checking the result
Because issuance is asynchronous, pollGET /domains/{id} to watch the status change:
active, retrieve the certificate metadata:
The default ACME directory configured in
config.go points to the Let’s Encrypt staging environment (https://acme-staging-v02.api.letsencrypt.org/directory). Staging certificates are signed by a test CA and are not trusted by browsers. Set the ACME_DIRECTORY environment variable to https://acme-v02.api.letsencrypt.org/directory to use the Let’s Encrypt production environment and obtain publicly trusted certificates. Note that the production environment enforces rate limits.