Repository: favonia/cloudflare-ddns
Pull request #214 (2022-09-04 23:35:41)

feat(config): accept templates for PROXIED and TTL

codecov[bot]: # [Codecov](https://codecov.io/gh/favonia/cloudflare-ddns/pull/214?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) Report
> Merging [#214](https://codecov.io/gh/favonia/cloudflare-ddns/pull/214?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) (62a9beb) into [main](https://codecov.io/gh/favonia/cloudflare-ddns/commit/a2b2e9b31357194d5fe7e2f0ba6ee14613f2f82d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) (a2b2e9b) will **decrease** coverage by `1.58%`.
> The diff coverage is `78.91%`.

```diff
@@            Coverage Diff             @@
##             main     #214      +/-   ##
==========================================
- Coverage   83.72%   82.14%   -1.59%     
==========================================
  Files          27       28       +1     
  Lines        1702     1747      +45     
==========================================
+ Hits         1425     1435      +10     
- Misses        268      293      +25     
- Partials        9       19      +10     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `82.14% <78.91%> (-1.59%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/favonia/cloudflare-ddns/pull/214?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) | Coverage Δ | |
|---|---|---|
| [cmd/ddns.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/214/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-Y21kL2RkbnMuZ28=) | `0.00% <0.00%> (ø)` | |
| [internal/domain/wildcard.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/214/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvZG9tYWluL3dpbGRjYXJkLmdv) | `89.28% <33.33%> (-10.72%)` | :arrow_down: |
| [internal/domain/template.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/214/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvZG9tYWluL3RlbXBsYXRlLmdv) | `63.15% <63.15%> (ø)` | |
| [internal/api/ttl.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/214/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvYXBpL3R0bC5nbw==) | `90.90% <75.00%> (-9.10%)` | :arrow_down: |
| [internal/config/config.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/214/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvY29uZmlnL2NvbmZpZy5nbw==) | `91.63% <79.01%> (-6.23%)` | :arrow_down: |
| [internal/config/env.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/214/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvY29uZmlnL2Vudi5nbw==) | `98.52% <100.00%> (+0.06%)` | :arrow_up: |
| [internal/domain/domain.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/214/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvZG9tYWluL2RvbWFpbi5nbw==) | `100.00% <100.00%> (ø)` | |
| [internal/domain/fqdn.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/214/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvZG9tYWluL2ZxZG4uZ28=) | `100.00% <100.00%> (ø)` | |
| [internal/setter/setter.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/214/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvc2V0dGVyL3NldHRlci5nbw==) | `100.00% <100.00%> (ø)` | |
| [internal/updater/updater.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/214/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvdXBkYXRlci91cGRhdGVyLmdv) | `100.00% <100.00%> (ø)` | |
| ... and [2 more](https://codecov.io/gh/favonia/cloudflare-ddns/pull/214/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia)


Patch:
diff --git a/.golangci.yaml b/.golangci.yaml
index 6836d84..5d0cabf 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -24,6 +24,7 @@ linters:
     - ifshort # deprecated
     - interfacer # deprecated
     - ireturn # ireturn works poorly for the style with private types and public interfaces
+    - maintidx # It seems the linter never leads to code changes
     - maligned # deprecated, and I value readability over bytes saved by alignment
     - nlreturn # I don't agree with the style enforced by nlreturn
     - nonamedreturns # named returns are needed in the internal updator package
diff --git a/README.markdown b/README.markdown
index 77a9f01..1c4ca6d 100644
--- a/README.markdown
+++ b/README.markdown
@@ -345,12 +345,26 @@ In most cases, `CF_ACCOUNT_ID` is not needed.
 | `PROXIED` | Boolean values, such as `true`, `false`, `0` and `1`. See [strconv.ParseBool](https://pkg.go.dev/strconv#ParseBool) | Whether new DNS records should be proxied by Cloudflare | No | `false`
 | `TTL` | Time-to-live (TTL) values in seconds | The TTL values used to create new DNS records | No | `1` (This means “automatic” to Cloudflare)
 
-🧪 Experimental features: (Please [share your case at this GitHub issue](https://github.com/favonia/cloudflare-ddns/issues/199) so that we can further revise the design. Thanks!)
+> <details>
+> <summary>🧪 Experimental support of Go templates:</summary>
+>
+> Both `PROXIED` and `TTL` can be [Go templates](https://pkg.go.dev/text/template) for per-domain settings. For example, `PROXIED={{not (suffix "example.org")}}` means all domains should be proxied except domains like `www.example.org` and `example.org`. The Go templates are executed with the following two custom functions:
+> - `domain(patterns ...string) bool`
+>
+>   Returns `true` if and only if the target domain matches one of `patterns`. All domains are normalized before comparison; for example, internationalized domain names are converted to Punycode before comparing them.
+> - `suffix(patterns ...string) bool`
+>
+>   Returns `true` if and only if the target domain has one of `patterns` as itself or its parent (or ancestor). Note that labels in domains must fully match; for example, the suffix `b.org` will not match `www.bb.org` because `bb.org` and `b.org` are incomparable, while the suffix `bb.org` will match `www.bb.org`.
+>
+> Some examples:
+> - `TTL={{if suffix "b.c"}} 60 {{else if domain "d.e.f" "a.bb.c" }} 90 {{else}} 120 {{end}}`
+>
+>   For the domain `b.c` and its descendants, the TTL is 60, and for the domains `d.e.f` and `a.bb.c`, the TTL is 90, and then for all other domains, the TTL is 120.
+> - `PROXIED={{and (suffix "b.c") (not (domain "a.b.c"))}}`
+>
+>   Proxy the domain `b.c` and its descendants except for the domain `a.b.c`.
+> </details>
 
-| Name | Valid Values | Meaning | Required? | Default Value |
-| ---- | ------------ | ------- | --------- | ------------- |
-| `PROXIED_DOMAINS` | Comma-separated fully qualified domain names or wildcard domain names | The domains for which the new DNS records should be proxied by Cloudflare, overriding the global setting (`PROXIED`) for these domains | No | `""` (empty)
-| `NON_PROXIED_DOMAINS` | Comma-separated fully qualified domain names or wildcard domain names | The domains for which the new DNS records should **not** be proxied by Cloudflare, overriding the global setting (`PROXIED`) for these domains | No | `""` (empty)
 </details>
 
 <details>
@@ -412,7 +426,7 @@ If you are using Kubernetes, run `kubectl replace -f cloudflare-ddns.yaml` after
 | `cloudflare.authentication.api_key` | ❌ | _Use the newer, more secure [API tokens](https://dash.cloudflare.com/profile/api-tokens)_ |
 | `cloudflare.zone_id` | ✔️ | Not needed; automatically retrieved from the server |
 | `cloudflare.subdomains[].name` | ✔️ | Use `DOMAINS` with **fully qualified domain names** (FQDNs); for example, if your zone is `example.org` and your subdomain is `www`, use `DOMAINS=sub.example.org` |
-| `cloudflare.subdomains[].proxied` | ✔️ | Use `PROXIED=true` or `PROXIED=false` to specify the global proxy setting, and then use `PROXIED_DOMAINS` and `NON_PROXIED_DOMAINS` with **fully qualified domain names** (FQDNs) to override the global setting if desired |
+| `cloudflare.subdomains[].proxied` | ✔️ | Use the experimental support of Go templates in `PROXIED` to specify per-domain settings; see above for the detailed documentation |
 | `a` | ✔️ | Both IPv4 and IPv6 are enabled by default; use `IP4_PROVIDER=none` to disable IPv4 |
 | `aaaa` | ✔️ | Both IPv4 and IPv6 are enabled by default; use `IP6_PROVIDER=none` to disable IPv6 |
 | `proxied` | ✔️ | Use `PROXIED=true` or `PROXIED=false` |
diff --git a/cmd/ddns.go b/cmd/ddns.go
index c49020a..79d84e3 100644
--- a/cmd/ddns.go
+++ b/cmd/ddns.go
@@ -68,7 +68,7 @@ func initConfig(ctx context.Context, ppfmt pp.PP) (*config.Config, api.Handle, s
 	}
 
 	// Get the setter
-	s, ok := setter.New(ppfmt, h, c.TTL)
+	s, ok := setter.New(ppfmt, h)
 	if !ok {
 		bye()
 	}
diff --git a/internal/api/ttl.go b/internal/api/ttl.go
index 33bad94..e896b77 100644
--- a/internal/api/ttl.go
+++ b/internal/api/ttl.go
@@ -1,11 +1,14 @@
 package api
 
 import (
+	"sort"
 	"strconv"
 )
 
 type TTL int
 
+const TTLAuto = 1
+
 func (t TTL) Int() int {
 	return int(t)
 }
@@ -15,8 +18,12 @@ func (t TTL) String() string {
 }
 
 func (t TTL) Describe() string {
-	if t == 1 {
-		return "1 (automatic)"
+	if t == TTLAuto {
+		return "1 (auto)"
 	}
 	return strconv.Itoa(t.Int())
 }
+
+func SortTTLs(s []TTL) {
+	sort.Slice(s, func(i, j int) bool { return int(s[i]) < int(s[j]) })
+}
diff --git a/internal/config/config.go b/internal/config/config.go
index b4cad4c..484a2d2 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -1,6 +1,8 @@
 package config
 
 import (
+	"fmt"
+	"strconv"
 	"strings"
 	"time"
 
@@ -22,9 +24,10 @@ type Config struct {
 	UpdateOnStart    bool
 	DeleteOnStop     bool
 	CacheExpiration  time.Duration
-	TTL              api.TTL
-	DefaultProxied   bool
-	ProxiedByDomain  map[domain.Domain]bool
+	TTLTemplate      string
+	TTL              map[domain.Domain]api.TTL
+	ProxiedTemplate  string
+	Proxied          map[domain.Domain]bool
 	DetectionTimeout time.Duration
 	UpdateTimeout    time.Duration
 	Monitors         []monitor.Monitor
@@ -46,9 +49,10 @@ func Default() *Config {
 		UpdateOnStart:    true,
 		DeleteOnStop:     false,
 		CacheExpiration:  time.Hour * 6, //nolint:gomnd
-		TTL:              api.TTL(1),
-		DefaultProxied:   false,
-		ProxiedByDomain:  map[domain.Domain]bool{},
+		TTLTemplate:      "1",
+		TTL:              map[domain.Domain]api.TTL{},
+		ProxiedTemplate:  "false",
+		Proxied:          map[domain.Domain]bool{},
 		UpdateTimeout:    time.Second * 30, //nolint:gomnd
 		DetectionTimeout: time.Second * 5,  //nolint:gomnd
 		Monitors:         nil,
@@ -164,64 +168,43 @@ func ReadProviderMap(ppfmt pp.PP, field *map[ipnet.Type]provider.Provider) bool
 	return true
 }
 
-func ReadTTL(ppfmt pp.PP, field *api.TTL) bool {
-	if !ReadNonnegInt(ppfmt, "TTL", (*int)(field)) {
-		return false
-	}
+func ParseProxied(ppfmt pp.PP, domain domain.Domain, val string) (bool, bool) {
+	res, err := strconv.ParseBool(strings.TrimSpace(val))
 
-	// According to [API documentation], the valid range is 1 (auto) and [60, 86400].
-	// According to [DNS documentation], the valid range is "Auto" and [30, 86400].
-	// We thus accept the union of both ranges---1 (auto) and [30, 86400].
-	//
-	// [API documentation] https://api.cloudflare.com/#dns-records-for-a-zone-create-dns-record
-	// [DNS documentation] https://developers.cloudflare.com/dns/manage-dns-records/reference/ttl
+	switch {
+	case err != nil:
+		ppfmt.Errorf(pp.EmojiUserError, "Proxy setting of %s (%q) is not a boolean value: %v", domain.Describe(), val, err)
+		return false, false
 
-	if *field != 1 && (*field < 30 || *field > 86400) {
-		ppfmt.Warningf(pp.EmojiUserWarning, "TTL value (%i) should be 1 (automatic) or between 30 and 86400", int(*field))
+	default:
+		return res, true
 	}
-	return true
 }
 
-func ReadProxiedByDomain(ppfmt pp.PP, field *map[domain.Domain]bool) bool {
-	var proxiedDomains, nonProxiedDomains []domain.Domain
-
-	if !ReadDomains(ppfmt, "PROXIED_DOMAINS", &proxiedDomains) ||
-		!ReadDomains(ppfmt, "NON_PROXIED_DOMAINS", &nonProxiedDomains) {
-		return false
-	}
-
-	proxiedDomains = deduplicate(proxiedDomains)
-	nonProxiedDomains = deduplicate(nonProxiedDomains)
-
-	if len(proxiedDomains) > 0 || len(nonProxiedDomains) > 0 {
-		ppfmt.Warningf(pp.EmojiExperimental, "PROXIED_DOMAINS and NON_PROXIED_DOMAINS are experimental features")
-		ppfmt.Warningf(pp.EmojiExperimental, "Please share your case at https://github.com/favonia/cloudflare-ddns/issues/199") //nolint:lll
-		ppfmt.Warningf(pp.EmojiExperimental, "We might remove these features based on your (lack of) feedback")
-	}
+// ParseTTL turns a string into a valid TTL value.
+//
+// According to [API documentation], the valid range is 1 (auto) and [60, 86400].
+// According to [DNS documentation], the valid range is "Auto" and [30, 86400].
+// We thus accept the union of both ranges---1 (auto) and [30, 86400].
+//
+// [API documentation] https://api.cloudflare.com/#dns-records-for-a-zone-create-dns-record
+// [DNS documentation] https://developers.cloudflare.com/dns/manage-dns-records/reference/ttl
+func ParseTTL(ppfmt pp.PP, domain domain.Domain, val string) (api.TTL, bool) {
+	val = strings.TrimSpace(val)
+	res, err := strconv.Atoi(val)
 
-	// the new map to be created
-	m := map[domain.Domain]bool{}
+	switch {
+	case err != nil:
+		ppfmt.Errorf(pp.EmojiUserError, "TTL of %s (%q) is not a number: %v", domain.Describe(), val, err)
+		return 0, false
 
-	// all proxied domains
-	for _, proxiedDomain := range proxiedDomains {
-		m[proxiedDomain] = true
-	}
+	case res != 1 && (res < 30 || res > 86400):
+		ppfmt.Errorf(pp.EmojiUserError, "TTL of %s (%d) should be 1 (auto) or between 30 and 86400", domain.Describe(), res)
+		return 0, false
 
-	// non-proxied domains
-	for _, nonProxiedDomain := range nonProxiedDomains {
-		if proxied, ok := m[nonProxiedDomain]; ok && proxied {
-			ppfmt.Errorf(pp.EmojiUserError,
-				"Domain %q appeared in both PROXIED_DOMAINS and NON_PROXIED_DOMAINS",
-				nonProxiedDomain.Describe())
-			return false
-		}
-
-		m[nonProxiedDomain] = false
+	default:
+		return api.TTL(res), true
 	}
-
-	*field = m
-
-	return true
 }
 
 func describeDomains(domains []domain.Domain) string {
@@ -236,6 +219,22 @@ func describeDomains(domains []domain.Domain) string {
 	return strings.Join(descriptions, ", ")
 }
 
+func inverseMap[K comparable](m map[domain.Domain]K) ([]K, map[K][]domain.Domain) {
+	inverse := map[K][]domain.Domain{}
+
+	for dom, val := range m {
+		inverse[val] = append(inverse[val], dom)
+	}
+
+	keys := make([]K, 0, len(inverse))
+	for val := range inverse {
+		domain.SortDomains(inverse[val])
+		keys = append(keys, val)
+	}
+
+	return keys, inverse
+}
+
 func (c *Config) Print(ppfmt pp.PP) {
 	if !ppfmt.IsEnabledFor(pp.Info) {
 		return
@@ -247,46 +246,49 @@ func (c *Config) Print(ppfmt pp.PP) {
 	inner := ppfmt.IncIndent()
 
 	ppfmt.Infof(pp.EmojiConfig, "Policies:")
-	inner.Infof(pp.EmojiBullet, "IPv4 provider:    %s", provider.Name(c.Provider[ipnet.IP4]))
+	inner.Infof(pp.EmojiBullet, "IPv4 provider:             %s", provider.Name(c.Provider[ipnet.IP4]))
 	if c.Provider[ipnet.IP4] != nil {
-		inner.Infof(pp.EmojiBullet, "IPv4 domains:     %s", describeDomains(c.Domains[ipnet.IP4]))
+		inner.Infof(pp.EmojiBullet, "IPv4 domains:              %s", describeDomains(c.Domains[ipnet.IP4]))
 	}
-	inner.Infof(pp.EmojiBullet, "IPv6 provider:    %s", provider.Name(c.Provider[ipnet.IP6]))
+	inner.Infof(pp.EmojiBullet, "IPv6 provider:             %s", provider.Name(c.Provider[ipnet.IP6]))
 	if c.Provider[ipnet.IP6] != nil {
-		inner.Infof(pp.EmojiBullet, "IPv6 domains:     %s", describeDomains(c.Domains[ipnet.IP6]))
+		inner.Infof(pp.EmojiBullet, "IPv6 domains:              %s", describeDomains(c.Domains[ipnet.IP6]))
 	}
 
 	ppfmt.Infof(pp.EmojiConfig, "Scheduling:")
-	inner.Infof(pp.EmojiBullet, "Timezone:         %s", cron.DescribeLocation(time.Local))
-	inner.Infof(pp.EmojiBullet, "Update frequency: %v", c.UpdateCron)
-	inner.Infof(pp.EmojiBullet, "Update on start?  %t", c.UpdateOnStart)
-	inner.Infof(pp.EmojiBullet, "Delete on stop?   %t", c.DeleteOnStop)
-	inner.Infof(pp.EmojiBullet, "Cache expiration: %v", c.CacheExpiration)
+	inner.Infof(pp.EmojiBullet, "Timezone:                  %s", cron.DescribeLocation(time.Local))
+	inner.Infof(pp.EmojiBullet, "Update frequency:          %v", c.UpdateCron)
+	inner.Infof(pp.EmojiBullet, "Update on start?           %t", c.UpdateOnStart)
+	inner.Infof(pp.EmojiBullet, "Delete on stop?            %t", c.DeleteOnStop)
+	inner.Infof(pp.EmojiBullet, "Cache expiration:          %v", c.CacheExpiration)
 
 	ppfmt.Infof(pp.EmojiConfig, "New DNS records:")
-	inner.Infof(pp.EmojiBullet, "TTL:              %s", c.TTL.Describe())
 	{
-		proxiedMapping := map[bool][]domain.Domain{}
-		proxiedMapping[true] = make([]domain.Domain, 0, len(c.ProxiedByDomain))
-		proxiedMapping[false] = make([]domain.Domain, 0, len(c.ProxiedByDomain))
-		for domain, proxied := range c.ProxiedByDomain {
-			proxiedMapping[proxied] = append(proxiedMapping[proxied], domain)
-		}
-		for b := range proxiedMapping {
-			proxiedMapping[b] = deduplicate(proxiedMapping[b])
+		vals, inverseMap := inverseMap[api.TTL](c.TTL)
+		api.SortTTLs(vals)
+		for _, val := range vals {
+			inner.Infof(
+				pp.EmojiBullet,
+				"%-26s %s",
+				fmt.Sprintf("Domains with TTL=%s:", val.Describe()),
+				describeDomains(inverseMap[val]),
+			)
 		}
-		inner.Infof(pp.EmojiBullet, "Proxied:          %s", describeDomains(proxiedMapping[true]))
-		inner.Infof(pp.EmojiBullet, "Non-proxied:      %s", describeDomains(proxiedMapping[false]))
+	}
+	{
+		_, inverseMap := inverseMap[bool](c.Proxied)
+		inner.Infof(pp.EmojiBullet, "Proxied domains:           %s", describeDomains(inverseMap[true]))
+		inner.Infof(pp.EmojiBullet, "Non-proxied domains:       %s", describeDomains(inverseMap[false]))
 	}
 
 	ppfmt.Infof(pp.EmojiConfig, "Timeouts:")
-	inner.Infof(pp.EmojiBullet, "IP detection:     %v", c.DetectionTimeout)
-	inner.Infof(pp.EmojiBullet, "Record updating:  %v", c.UpdateTimeout)
+	inner.Infof(pp.EmojiBullet, "IP detection:              %v", c.DetectionTimeout)
+	inner.Infof(pp.EmojiBullet, "Record updating:           %v", c.UpdateTimeout)
 
 	if len(c.Monitors) > 0 {
 		ppfmt.Infof(pp.EmojiConfig, "Monitors:")
 		for _, m := range c.Monitors {
-			inner.Infof(pp.EmojiBullet, "%-17s (URL redacted)", m.DescribeService()+":")
+			inner.Infof(pp.EmojiBullet, "%-26s (URL redacted)", m.DescribeService()+":")
 		}
 	}
 }
@@ -304,20 +306,28 @@ func (c *Config) ReadEnv(ppfmt pp.PP) bool { //nolint:cyclop
 		!ReadBool(ppfmt, "UPDATE_ON_START", &c.UpdateOnStart) ||
 		!ReadBool(ppfmt, "DELETE_ON_STOP", &c.DeleteOnStop) ||
 		!ReadNonnegDuration(ppfmt, "CACHE_EXPIRATION", &c.CacheExpiration) ||
-		!ReadTTL(ppfmt, &c.TTL) ||
-		!ReadBool(ppfmt, "PROXIED", &c.DefaultProxied) ||
+		!ReadString(ppfmt, "TTL", &c.TTLTemplate) ||
+		!ReadString(ppfmt, "PROXIED", &c.ProxiedTemplate) ||
 		!ReadNonnegDuration(ppfmt, "DETECTION_TIMEOUT", &c.DetectionTimeout) ||
 		!ReadNonnegDuration(ppfmt, "UPDATE_TIMEOUT", &c.UpdateTimeout) ||
-		!ReadHealthChecksURL(ppfmt, "HEALTHCHECKS", &c.Monitors) ||
-		!ReadProxiedByDomain(ppfmt, &c.ProxiedByDomain) {
+		!ReadHealthChecksURL(ppfmt, "HEALTHCHECKS", &c.Monitors) {
 		return false
 	}
 
 	return true
 }
 
+// NormalizeDomains normalizes the fields Provider, TTL and Proxied.
+// When errors are reported, the original configuration remain unchanged.
+//
 //nolint:funlen,gocognit,cyclop
 func (c *Config) NormalizeDomains(ppfmt pp.PP) bool {
+	// New maps
+	providerMap := map[ipnet.Type]provider.Provider{}
+	ttlMap := map[domain.Domain]api.TTL{}
+	proxiedMap := map[domain.Domain]bool{}
+	activeDomainSet := map[domain.Domain]bool{}
+
 	if ppfmt.IsEnabledFor(pp.Info) {
 		ppfmt.Infof(pp.EmojiEnvVars, "Checking settings . . .")
 		ppfmt = ppfmt.IncIndent()
@@ -328,72 +338,79 @@ func (c *Config) NormalizeDomains(ppfmt pp.PP) bool {
 		return false
 	}
 
-	// change useless policies to none
+	// fill in providerMap and activeDomainSet
 	for ipNet, domains := range c.Domains {
-		if len(domains) == 0 && c.Provider[ipNet] != nil {
-			c.Provider[ipNet] = nil
+		if c.Provider[ipNet] == nil {
+			continue
+		}
+
+		if len(domains) == 0 {
 			ppfmt.Warningf(pp.EmojiUserWarning, "IP%d_PROVIDER was changed to %q because no domains were set for %s",
-				ipNet.Int(), provider.Name(c.Provider[ipNet]), ipNet.Describe())
+				ipNet.Int(), provider.Name(nil), ipNet.Describe())
+
+			continue
+		}
+
+		providerMap[ipNet] = c.Provider[ipNet]
+		for _, domain := range domains {
+			activeDomainSet[domain] = true
 		}
 	}
 
 	// check if all policies are none
-	if c.Provider[ipnet.IP4] == nil && c.Provider[ipnet.IP6] == nil {
+	if providerMap[ipnet.IP4] == nil && providerMap[ipnet.IP6] == nil {
 		ppfmt.Errorf(pp.EmojiUserError, "Both IPv4 and IPv6 are disabled")
 		return false
 	}
 
-	// domainSet is the set of managed domains.
-	domainSet := map[domain.Domain]bool{}
+	// check if some domains are unused
 	for ipNet, domains := range c.Domains {
-		if c.Provider[ipNet] != nil {
-			for _, domain := range domains {
-				domainSet[domain] = true
-			}
+		if providerMap[ipNet] != nil {
+			continue
 		}
-	}
 
-	// check if some domains are unused
-	for ipNet, domains := range c.Domains {
-		if c.Provider[ipNet] == nil {
-			for _, domain := range domains {
-				if !domainSet[domain] {
-					ppfmt.Warningf(pp.EmojiUserWarning,
-						"Domain %q is ignored because it is only for %s but %s is disabled",
-						domain.Describe(), ipNet.Describe(), ipNet.Describe())
-				}
+		for _, domain := range domains {
+			if activeDomainSet[domain] {
+				continue
 			}
+
+			ppfmt.Warningf(pp.EmojiUserWarning,
+				"Domain %q is ignored because it is only for %s but %s is disabled",
+				domain.Describe(), ipNet.Describe(), ipNet.Describe())
 		}
 	}
 
-	// fill in the default "proxied"
-	if c.ProxiedByDomain == nil {
-		c.ProxiedByDomain = map[domain.Domain]bool{}
-		ppfmt.Warningf(pp.EmojiImpossible,
-			"Internal failure: ProxiedByDomain is re-initialized because it was nil",
-		)
-		ppfmt.Warningf(pp.EmojiImpossible,
-			"Please report the bug at https://github.com/favonia/cloudflare-ddns/issues/new",
-		)
-	}
-	for domain := range domainSet {
-		if _, ok := c.ProxiedByDomain[domain]; !ok {
-			c.ProxiedByDomain[domain] = c.DefaultProxied
+	// fill in ttlMap and proxyMap
+	for dom := range activeDomainSet {
+		{
+			ttlString, ok := domain.ExecTemplate(ppfmt, c.TTLTemplate, dom)
+			if !ok {
+				return false
+			}
+			ttl, ok := ParseTTL(ppfmt, dom, ttlString)
+			if !ok {
+				return false
+			}
+			ttlMap[dom] = ttl
 		}
-	}
 
-	// check if some domain-specific "proxied" setting is not used
-	envMap := map[bool]string{true: "PROXIED_DOMAINS", false: "NON_PROXIED_DOMAINS"}
-	if len(c.ProxiedByDomain) > len(domainSet) {
-		for domain, proxied := range c.ProxiedByDomain {
-			if !domainSet[domain] {
-				delete(c.ProxiedByDomain, domain)
-				ppfmt.Warningf(pp.EmojiUserWarning,
-					"Domain %q was listed in %s, but it is ignored because it is not managed by the updater",
-					domain.Describe(), envMap[proxied])
+		{
+			proxiedString, ok := domain.ExecTemplate(ppfmt, c.ProxiedTemplate, dom)
+			if !ok {
+				return false
 			}
+
+			proxied, ok := ParseProxied(ppfmt, dom, proxiedString)
+			if !ok {
+				return false
+			}
+			proxiedMap[dom] = proxied
 		}
 	}
 
+	c.Provider = providerMap
+	c.TTL = ttlMap
+	c.Proxied = proxiedMap
+
 	return true
 }
diff --git a/internal/config/env.go b/internal/config/env.go
index 5147a11..492a1a8 100644
--- a/internal/config/env.go
+++ b/internal/config/env.go
@@ -18,6 +18,17 @@ func Getenv(key string) string {
 	return strings.TrimSpace(os.Getenv(key))
 }
 
+func ReadString(ppfmt pp.PP, key string, field *string) bool {
+	val := Getenv(key)
+	if val == "" {
+		ppfmt.Infof(pp.EmojiBullet, "Use default %s=%s", key, *field)
+		return true
+	}
+
+	*field = val
+	return true
+}
+
 // ReadQuiet reads an environment variable as quiet/verbose.
 func ReadQuiet(key string, ppfmt *pp.PP) bool {
 	val := Getenv(key)
diff --git a/internal/domain/domain.go b/internal/domain/domain.go
index 4107255..ea56b84 100644
--- a/internal/domain/domain.go
+++ b/internal/domain/domain.go
@@ -29,14 +29,24 @@ var (
 // safelyToUnicode takes an ASCII form and returns the Unicode form
 // when the round trip gives the same ASCII form back without errors.
 // Otherwise, the input ASCII form is returned.
-func safelyToUnicode(ascii string) (string, bool) {
+func safelyToUnicode(ascii string) string {
 	unicode, errToA := profileKeepingLeadingDots.ToUnicode(ascii)
 	roundTrip, errToU := profileKeepingLeadingDots.ToASCII(unicode)
 	if errToA != nil || errToU != nil || roundTrip != ascii {
-		return ascii, false
+		return ascii
 	}
 
-	return unicode, true
+	return unicode
+}
+
+// toASCII normalizes a domain with best efforts, ignoring errors.
+func toASCII(domain string) string {
+	normalized, _ := profileDroppingLeadingDots.ToASCII(domain)
+
+	// Remove the final dot for consistency
+	normalized = strings.TrimRight(normalized, ".")
+
+	return normalized
 }
 
 // NewDomain normalizes a domain to its ASCII form and then stores
diff --git a/internal/domain/fqdn.go b/internal/domain/fqdn.go
index 435a73c..9a52408 100644
--- a/internal/domain/fqdn.go
+++ b/internal/domain/fqdn.go
@@ -8,13 +8,7 @@ type FQDN string
 func (f FQDN) DNSNameASCII() string { return string(f) }
 
 func (f FQDN) Describe() string {
-	best, ok := safelyToUnicode(string(f))
-	if !ok {
-		// use the unconverted string if the conversation failed
-		return string(f)
-	}
-
-	return best
+	return safelyToUnicode(string(f))
 }
 
 type FQDNSplitter struct {
diff --git a/internal/domain/template.go b/internal/domain/template.go
new file mode 100644
index 0000000..ee9d158
--- /dev/null
+++ b/internal/domain/template.go
@@ -0,0 +1,53 @@
+package domain
+
+import (
+	"strings"
+	"text/template"
+
+	"github.com/favonia/cloudflare-ddns/internal/pp"
+)
+
+func hasSuffix(s, suffix string) bool {
+	return len(suffix) == 0 || (strings.HasSuffix(s, suffix) && (len(s) == len(suffix) || s[len(s)-len(suffix)-1] == '.'))
+}
+
+// templateFuncs returns the function maps for running the template.
+// "domains" to match a domain in a list, and "suffix" to match a domain with a suffix in a list.
+func templateFuncs(target Domain) template.FuncMap {
+	targetASCII := target.DNSNameASCII()
+
+	return template.FuncMap{
+		"domain": func(rawDomains ...string) (bool, error) {
+			for _, rawDomain := range rawDomains {
+				if targetASCII == toASCII(rawDomain) {
+					return true, nil
+				}
+			}
+			return false, nil
+		},
+		"suffix": func(rawSuffixes ...string) (bool, error) {
+			for _, rawSuffix := range rawSuffixes {
+				if hasSuffix(targetASCII, toASCII(rawSuffix)) {
+					return true, nil
+				}
+			}
+			return false, nil
+		},
+	}
+}
+
+func ExecTemplate(ppfmt pp.PP, tmpl string, target Domain) (string, bool) {
+	t, err := template.New("").Funcs(templateFuncs(target)).Parse(tmpl)
+	if err != nil {
+		ppfmt.Errorf(pp.EmojiUserError, "%q is not a valid template: %v", tmpl, err)
+		return "", false
+	}
+
+	var output strings.Builder
+	if err = t.Execute(&output, nil); err != nil {
+		ppfmt.Errorf(pp.EmojiUserError, "Could not execute the template %q: %v", tmpl, err)
+		return "", false
+	}
+
+	return output.String(), true
+}
diff --git a/internal/domain/wildcard.go b/internal/domain/wildcard.go
index 84ef9b1..5406549 100644
--- a/internal/domain/wildcard.go
+++ b/internal/domain/wildcard.go
@@ -15,13 +15,11 @@ func (w Wildcard) DNSNameASCII() string {
 }
 
 func (w Wildcard) Describe() string {
-	best, ok := safelyToUnicode(string(w))
-	if !ok {
-		// use the unconverted string if the conversation failed
-		return "*." + string(w)
+	if string(w) == "" {
+		return "*"
 	}
 
-	return "*." + best
+	return "*." + safelyToUnicode(string(w))
 }
 
 type WildcardSplitter struct {
diff --git a/internal/setter/base.go b/internal/setter/base.go
index 3470078..48ffa0d 100644
--- a/internal/setter/base.go
+++ b/internal/setter/base.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"net/netip"
 
+	"github.com/favonia/cloudflare-ddns/internal/api"
 	"github.com/favonia/cloudflare-ddns/internal/domain"
 	"github.com/favonia/cloudflare-ddns/internal/ipnet"
 	"github.com/favonia/cloudflare-ddns/internal/pp"
@@ -12,5 +13,13 @@ import (
 //go:generate mockgen -destination=../mocks/mock_setter.go -package=mocks . Setter
 
 type Setter interface {
-	Set(ctx context.Context, ppfmt pp.PP, Domain domain.Domain, IPNetwork ipnet.Type, IP netip.Addr, proxied bool) bool
+	Set(
+		ctx context.Context,
+		ppfmt pp.PP,
+		Domain domain.Domain,
+		IPNetwork ipnet.Type,
+		IP netip.Addr,
+		ttl api.TTL,
+		proxied bool,
+	) bool
 }
diff --git a/internal/setter/setter.go b/internal/setter/setter.go
index b22c9f5..52194d3 100644
--- a/internal/setter/setter.go
+++ b/internal/setter/setter.go
@@ -13,7 +13,6 @@ import (
 
 type setter struct {
 	Handle api.Handle
-	TTL    api.TTL
 }
 
 // partitionRecords partitions record maps into matched and unmatched ones.
@@ -43,17 +42,16 @@ func partitionRecords(rmap map[string]netip.Addr, target netip.Addr) (matchedIDs
 }
 
 // New creates a new Setter.
-func New(_ppfmt pp.PP, handle api.Handle, ttl api.TTL) (Setter, bool) {
+func New(_ppfmt pp.PP, handle api.Handle) (Setter, bool) {
 	return &setter{
 		Handle: handle,
-		TTL:    ttl,
 	}, true
 }
 
 // Set calls the DNS service API to update the API of one domain.
 //
 //nolint:funlen,cyclop,gocognit
-func (s *setter) Set(ctx context.Context, ppfmt pp.PP, domain domain.Domain, ipnet ipnet.Type, ip netip.Addr, proxied bool) bool { //nolint:lll
+func (s *setter) Set(ctx context.Context, ppfmt pp.PP, domain domain.Domain, ipnet ipnet.Type, ip netip.Addr, ttl api.TTL, proxied bool) bool { //nolint:lll
 	recordType := ipnet.RecordType()
 	domainDescription := domain.Describe()
 
@@ -140,7 +138,7 @@ func (s *setter) Set(ctx context.Context, ppfmt pp.PP, domain domain.Domain, ipn
 	// update one of them failed. The last resort is to create a new record with the correct ip.
 	if !uptodate {
 		if id, ok := s.Handle.CreateRecord(ctx, ppfmt,
-			domain, ipnet, ip, s.TTL, proxied); ok {
+			domain, ipnet, ip, ttl, proxied); ok {
 			ppfmt.Noticef(pp.EmojiAddRecord, "Added a new %s record of %q (ID: %s)", recordType, domainDescription, id)
 
 			// Now it's up to date! matchedIDs and unmatchedIDsToUpdate must both be empty at this point
diff --git a/internal/updater/updater.go b/internal/updater/updater.go
index 519ed31..d6f27d6 100644
--- a/internal/updater/updater.go
+++ b/internal/updater/updater.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"net/netip"
 
+	"github.com/favonia/cloudflare-ddns/internal/api"
 	"github.com/favonia/cloudflare-ddns/internal/config"
 	"github.com/favonia/cloudflare-ddns/internal/domain"
 	"github.com/favonia/cloudflare-ddns/internal/ipnet"
@@ -11,20 +12,28 @@ import (
 	"github.com/favonia/cloudflare-ddns/internal/setter"
 )
 
-func getProxied(ppfmt pp.PP, c *config.Config, domain domain.Domain) bool {
-	if proxied, ok := c.ProxiedByDomain[domain]; ok {
-		return proxied
+func getTTL(ppfmt pp.PP, c *config.Config, domain domain.Domain) api.TTL {
+	if ttl, ok := c.TTL[domain]; ok {
+		return ttl
 	}
 
-	c.ProxiedByDomain[domain] = c.DefaultProxied
 	ppfmt.Warningf(pp.EmojiImpossible,
-		"Internal failure: ProxiedByDomain[%s] was not set, and is reset to %t",
-		domain.Describe(), c.DefaultProxied,
+		"TTL[%s] not initialized; please report the bug at https://github.com/favonia/cloudflare-ddns/issues/new",
+		domain.Describe(),
 	)
+	return api.TTLAuto
+}
+
+func getProxied(ppfmt pp.PP, c *config.Config, domain domain.Domain) bool {
+	if proxied, ok := c.Proxied[domain]; ok {
+		return proxied
+	}
+
 	ppfmt.Warningf(pp.EmojiImpossible,
-		"Please report the bug at https://github.com/favonia/cloudflare-ddns/issues/new",
+		"Proxied[%s] not initialized; please report the bug at https://github.com/favonia/cloudflare-ddns/issues/new",
+		domain.Describe(),
 	)
-	return c.DefaultProxied
+	return false
 }
 
 func setIP(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter, ipNet ipnet.Type, ip netip.Addr) bool {
@@ -34,7 +43,9 @@ func setIP(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter,
 		ctx, cancel := context.WithTimeout(ctx, c.UpdateTimeout)
 		defer cancel()
 
-		if !s.Set(ctx, ppfmt, domain, ipNet, ip, getProxied(ppfmt, c, domain)) {
+		if !s.Set(ctx, ppfmt, domain, ipNet, ip,
+			getTTL(ppfmt, c, domain),
+			getProxied(ppfmt, c, domain)) {
 			ok = false
 		}
 	}


Test patch:
diff --git a/internal/api/ttl_test.go b/internal/api/ttl_test.go
index 18b80fc..fa58d9c 100644
--- a/internal/api/ttl_test.go
+++ b/internal/api/ttl_test.go
@@ -15,7 +15,7 @@ func TestTTLDescribe(t *testing.T) {
 		seconds     int
 		description string
 	}{
-		"automatic": {1, "1 (automatic)"},
+		"automatic": {1, "1 (auto)"},
 		"2":         {2, "2"},
 		"30":        {30, "30"},
 		"293":       {293, "293"},
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 1d06537..1b052eb 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -305,138 +305,67 @@ func TestReadDomainMap(t *testing.T) {
 	}
 }
 
-//nolint:paralleltest,funlen // environment vars are global
-func TestReadTTL(t *testing.T) {
-	key := "TTL"
+//nolint:funlen
+func TestParseTTL(t *testing.T) {
+	t.Parallel()
+
+	domain, _ := domain.New("example.io")
 	for name, tc := range map[string]struct {
-		set           bool
 		val           string
-		oldField      api.TTL
-		newField      api.TTL
+		ttl           api.TTL
 		ok            bool
 		prepareMockPP func(*mocks.MockPP)
 	}{
-		"nil": {
-			false, "", 100, 100, true,
-			func(m *mocks.MockPP) {
-				m.EXPECT().Infof(pp.EmojiBullet, "Use default %s=%d", key, 100)
-			},
-		},
 		"empty": {
-			true, "", 100, 100, true,
+			"", 0, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Infof(pp.EmojiBullet, "Use default %s=%d", key, 100)
+				m.EXPECT().Errorf(pp.EmojiUserError, "TTL of %s (%q) is not a number: %v", domain.Describe(), "", gomock.Any())
 			},
 		},
 		"0": {
-			true, "0   ", 100, 0, true,
+			"0   ", 0, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Warningf(pp.EmojiUserWarning, "TTL value (%i) should be 1 (automatic) or between 30 and 86400", 0)
+				m.EXPECT().Errorf(pp.EmojiUserError, "TTL of %s (%d) should be 1 (auto) or between 30 and 86400", domain.Describe(), 0) //nolint:lll
 			},
 		},
 		"-1": {
-			true, "   -1", 100, 100, false,
+			"   -1", 0, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: %d is negative", "-1", gomock.Any())
+				m.EXPECT().Errorf(pp.EmojiUserError, "TTL of %s (%d) should be 1 (auto) or between 30 and 86400", domain.Describe(), -1) //nolint:lll
 			},
 		},
-		"1": {true, "   1   ", 100, 1, true, nil},
+		"1": {"   1   ", 1, true, nil},
 		"20": {
-			true, "   20   ", 100, 20, true,
+			"   20   ", 0, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Warningf(pp.EmojiUserWarning, "TTL value (%i) should be 1 (automatic) or between 30 and 86400", 20)
-			},
-		},
-		"words": {
-			true, "   word   ", 100, 100, false,
-			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: %v", "word", gomock.Any())
+				m.EXPECT().Errorf(pp.EmojiUserError, "TTL of %s (%d) should be 1 (auto) or between 30 and 86400", domain.Describe(), 20) //nolint:lll
 			},
 		},
 		"9999999": {
-			true, "   9999999   ", 100, 9999999, true,
+			"   9999999   ", 0, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Warningf(pp.EmojiUserWarning, "TTL value (%i) should be 1 (automatic) or between 30 and 86400", 9999999)
+				m.EXPECT().Errorf(pp.EmojiUserError, "TTL of %s (%d) should be 1 (auto) or between 30 and 86400", domain.Describe(), 9999999) //nolint:lll
 			},
 		},
-	} {
-		tc := tc
-		t.Run(name, func(t *testing.T) {
-			mockCtrl := gomock.NewController(t)
-
-			set(t, key, tc.set, tc.val)
-
-			field := tc.oldField
-			mockPP := mocks.NewMockPP(mockCtrl)
-			if tc.prepareMockPP != nil {
-				tc.prepareMockPP(mockPP)
-			}
-			ok := config.ReadTTL(mockPP, &field)
-			require.Equal(t, tc.ok, ok)
-			require.Equal(t, tc.newField, field)
-		})
-	}
-}
-
-//nolint:paralleltest // environment vars are global
-func TestReadProxiedByDomain(t *testing.T) {
-	for name, tc := range map[string]struct {
-		proxiedDomains    string
-		nonProxiedDomains string
-		ok                bool
-		expected          map[domain.Domain]bool
-		prepareMockPP     func(*mocks.MockPP)
-	}{
-		"empty": {
-			" ", "   ",
-			true,
-			map[domain.Domain]bool{},
-			nil,
-		},
-		"both": {
-			"  a1, a2", "b1",
-			true,
-			map[domain.Domain]bool{
-				domain.FQDN("a1"): true,
-				domain.FQDN("a2"): true,
-				domain.FQDN("b1"): false,
-			},
-			func(m *mocks.MockPP) {
-				gomock.InOrder(
-					m.EXPECT().Warningf(pp.EmojiExperimental, "PROXIED_DOMAINS and NON_PROXIED_DOMAINS are experimental features"),
-					m.EXPECT().Warningf(pp.EmojiExperimental, "Please share your case at https://github.com/favonia/cloudflare-ddns/issues/199"), //nolint:lll
-					m.EXPECT().Warningf(pp.EmojiExperimental, "We might remove these features based on your (lack of) feedback"),
-				)
-			},
-		},
-		"overlapping": {
-			"  a1 ", "a1",
-			false,
-			nil,
+		"words": {
+			"   word   ", 0, false,
 			func(m *mocks.MockPP) {
-				gomock.InOrder(
-					m.EXPECT().Warningf(pp.EmojiExperimental, "PROXIED_DOMAINS and NON_PROXIED_DOMAINS are experimental features"),
-					m.EXPECT().Warningf(pp.EmojiExperimental, "Please share your case at https://github.com/favonia/cloudflare-ddns/issues/199"), //nolint:lll
-					m.EXPECT().Warningf(pp.EmojiExperimental, "We might remove these features based on your (lack of) feedback"),
-					m.EXPECT().Errorf(pp.EmojiUserError, "Domain %q appeared in both PROXIED_DOMAINS and NON_PROXIED_DOMAINS", "a1"))
+				m.EXPECT().Errorf(pp.EmojiUserError, "TTL of %s (%q) is not a number: %v", domain.Describe(), "word", gomock.Any())
 			},
 		},
 	} {
 		tc := tc
 		t.Run(name, func(t *testing.T) {
-			mockCtrl := gomock.NewController(t)
-
-			store(t, "PROXIED_DOMAINS", tc.proxiedDomains)
-			store(t, "NON_PROXIED_DOMAINS", tc.nonProxiedDomains)
+			t.Parallel()
 
-			var field map[domain.Domain]bool
+			mockCtrl := gomock.NewController(t)
 			mockPP := mocks.NewMockPP(mockCtrl)
 			if tc.prepareMockPP != nil {
 				tc.prepareMockPP(mockPP)
 			}
-			ok := config.ReadProxiedByDomain(mockPP, &field)
+			ttl, ok := config.ParseTTL(mockPP, domain, tc.val)
 			require.Equal(t, tc.ok, ok)
-			require.Equal(t, tc.expected, field)
+			require.Equal(t, tc.ttl, ttl)
 		})
 	}
 }
@@ -482,64 +411,66 @@ func TestPrintDefault(t *testing.T) {
 
 	mockPP := mocks.NewMockPP(mockCtrl)
 	innerMockPP := mocks.NewMockPP(mockCtrl)
-	gomock.InOrder( //nolint:dupl
+	gomock.InOrder(
 		mockPP.EXPECT().IsEnabledFor(pp.Info).Return(true),
 		mockPP.EXPECT().Infof(pp.EmojiEnvVars, "Current settings:"),
 		mockPP.EXPECT().IncIndent().Return(mockPP),
 		mockPP.EXPECT().IncIndent().Return(innerMockPP),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Policies:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv4 provider:    %s", "cloudflare.trace"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv4 domains:     %s", "(none)"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv6 provider:    %s", "cloudflare.trace"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv6 domains:     %s", "(none)"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv4 provider:             %s", "cloudflare.trace"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv4 domains:              %s", "(none)"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv6 provider:             %s", "cloudflare.trace"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv6 domains:              %s", "(none)"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Scheduling:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Timezone:         %s", Some("UTC (UTC+00 now)", "Local (UTC+00 now)")),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Update frequency: %v", cron.MustNew("@every 5m")),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Update on start?  %t", true),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Delete on stop?   %t", false),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Cache expiration: %v", time.Hour*6),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Timezone:                  %s", Some("UTC (UTC+00 now)", "Local (UTC+00 now)")), //nolint:lll
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Update frequency:          %v", cron.MustNew("@every 5m")),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Update on start?           %t", true),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Delete on stop?            %t", false),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Cache expiration:          %v", time.Hour*6),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "New DNS records:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "TTL:              %s", "1 (automatic)"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Proxied:          %s", "(none)"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Non-proxied:      %s", "(none)"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Proxied domains:           %s", "(none)"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Non-proxied domains:       %s", "(none)"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Timeouts:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IP detection:     %v", time.Second*5),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Record updating:  %v", time.Second*30),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IP detection:              %v", time.Second*5),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Record updating:           %v", time.Second*30),
 	)
 	config.Default().Print(mockPP)
 }
 
 //nolint:paralleltest // changing the environment variable TZ
-func TestPrintDomains(t *testing.T) {
+func TestPrintMaps(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 
 	store(t, "TZ", "UTC")
 
 	mockPP := mocks.NewMockPP(mockCtrl)
 	innerMockPP := mocks.NewMockPP(mockCtrl)
-	gomock.InOrder( //nolint:dupl
+	gomock.InOrder(
 		mockPP.EXPECT().IsEnabledFor(pp.Info).Return(true),
 		mockPP.EXPECT().Infof(pp.EmojiEnvVars, "Current settings:"),
 		mockPP.EXPECT().IncIndent().Return(mockPP),
 		mockPP.EXPECT().IncIndent().Return(innerMockPP),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Policies:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv4 provider:    %s", "cloudflare.trace"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv4 domains:     %s", "test4.org, *.test4.org"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv6 provider:    %s", "cloudflare.trace"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv6 domains:     %s", "test6.org, *.test6.org"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv4 provider:             %s", "cloudflare.trace"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv4 domains:              %s", "test4.org, *.test4.org"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv6 provider:             %s", "cloudflare.trace"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv6 domains:              %s", "test6.org, *.test6.org"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Scheduling:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Timezone:         %s", Some("UTC (UTC+00 now)", "Local (UTC+00 now)")),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Update frequency: %v", cron.MustNew("@every 5m")),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Update on start?  %t", true),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Delete on stop?   %t", false),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Cache expiration: %v", time.Hour*6),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Timezone:                  %s", Some("UTC (UTC+00 now)", "Local (UTC+00 now)")), //nolint:lll
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Update frequency:          %v", cron.MustNew("@every 5m")),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Update on start?           %t", true),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Delete on stop?            %t", false),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Cache expiration:          %v", time.Hour*6),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "New DNS records:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "TTL:              %s", "1 (automatic)"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Proxied:          %s", "(none)"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Non-proxied:      %s", "(none)"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "%-26s %s", "Domains with TTL=1 (auto):", "a, c"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "%-26s %s", "Domains with TTL=30000:", "b, d"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Proxied domains:           %s", "a, b"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Non-proxied domains:       %s", "c, d"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Timeouts:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IP detection:     %v", time.Second*5),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Record updating:  %v", time.Second*30),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IP detection:              %v", time.Second*5),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Record updating:           %v", time.Second*30),
+		mockPP.EXPECT().Infof(pp.EmojiConfig, "Monitors:"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "%-26s (URL redacted)", "Healthchecks.io:"),
 	)
 
 	c := config.Default()
@@ -547,94 +478,31 @@ func TestPrintDomains(t *testing.T) {
 	c.Domains[ipnet.IP4] = []domain.Domain{domain.FQDN("test4.org"), domain.Wildcard("test4.org")}
 	c.Domains[ipnet.IP6] = []domain.Domain{domain.FQDN("test6.org"), domain.Wildcard("test6.org")}
 
-	c.Print(mockPP)
-}
-
-//nolint:paralleltest // changing the environment variable TZ
-func TestPrintEmpty(t *testing.T) {
-	mockCtrl := gomock.NewController(t)
-
-	store(t, "TZ", "UTC")
-
-	mockPP := mocks.NewMockPP(mockCtrl)
-	innerMockPP := mocks.NewMockPP(mockCtrl)
-	gomock.InOrder(
-		mockPP.EXPECT().IsEnabledFor(pp.Info).Return(true),
-		mockPP.EXPECT().Infof(pp.EmojiEnvVars, "Current settings:"),
-		mockPP.EXPECT().IncIndent().Return(mockPP),
-		mockPP.EXPECT().IncIndent().Return(innerMockPP),
-		mockPP.EXPECT().Infof(pp.EmojiConfig, "Policies:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv4 provider:    %s", "none"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv6 provider:    %s", "none"),
-		mockPP.EXPECT().Infof(pp.EmojiConfig, "Scheduling:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Timezone:         %s", Some("UTC (UTC+00 now)", "Local (UTC+00 now)")),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Update frequency: %v", cron.Schedule(nil)),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Update on start?  %t", false),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Delete on stop?   %t", false),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Cache expiration: %v", time.Duration(0)),
-		mockPP.EXPECT().Infof(pp.EmojiConfig, "New DNS records:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "TTL:              %s", "0"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Proxied:          %s", "(none)"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Non-proxied:      %s", "(none)"),
-		mockPP.EXPECT().Infof(pp.EmojiConfig, "Timeouts:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IP detection:     %v", time.Duration(0)),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Record updating:  %v", time.Duration(0)),
-	)
-	var cfg config.Config
-	cfg.Print(mockPP)
-}
-
-//nolint:paralleltest // changing the environment variable TZ
-func TestPrintProxiedByDomain(t *testing.T) {
-	mockCtrl := gomock.NewController(t)
+	c.TTL = map[domain.Domain]api.TTL{}
+	c.TTL[domain.FQDN("a")] = 1
+	c.TTL[domain.FQDN("b")] = 30000
+	c.TTL[domain.FQDN("c")] = 1
+	c.TTL[domain.FQDN("d")] = 30000
 
-	store(t, "TZ", "UTC")
+	c.Proxied = map[domain.Domain]bool{}
+	c.Proxied[domain.FQDN("a")] = true
+	c.Proxied[domain.FQDN("b")] = true
+	c.Proxied[domain.FQDN("c")] = false
+	c.Proxied[domain.FQDN("d")] = false
 
-	c := config.Default()
-
-	mockPP := mocks.NewMockPP(mockCtrl)
-	innerMockPP := mocks.NewMockPP(mockCtrl)
-	gomock.InOrder( //nolint:dupl
-		mockPP.EXPECT().IsEnabledFor(pp.Info).Return(true),
-		mockPP.EXPECT().Infof(pp.EmojiEnvVars, "Current settings:"),
-		mockPP.EXPECT().IncIndent().Return(mockPP),
-		mockPP.EXPECT().IncIndent().Return(innerMockPP),
-		mockPP.EXPECT().Infof(pp.EmojiConfig, "Policies:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv4 provider:    %s", "cloudflare.trace"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv4 domains:     %s", "(none)"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv6 provider:    %s", "cloudflare.trace"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv6 domains:     %s", "(none)"),
-		mockPP.EXPECT().Infof(pp.EmojiConfig, "Scheduling:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Timezone:         %s", Some("UTC (UTC+00 now)", "Local (UTC+00 now)")),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Update frequency: %v", cron.MustNew("@every 5m")),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Update on start?  %t", true),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Delete on stop?   %t", false),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Cache expiration: %v", time.Hour*6),
-		mockPP.EXPECT().Infof(pp.EmojiConfig, "New DNS records:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "TTL:              %s", "1 (automatic)"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Proxied:          %s", "a, b"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Non-proxied:      %s", "c, d"),
-		mockPP.EXPECT().Infof(pp.EmojiConfig, "Timeouts:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IP detection:     %v", time.Second*5),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Record updating:  %v", time.Second*30),
-	)
+	m, ok := monitor.NewHealthChecks(mockPP, "http://user:pass@host/path")
+	require.True(t, ok)
+	c.Monitors = []monitor.Monitor{m}
 
-	c.ProxiedByDomain = map[domain.Domain]bool{}
-	c.ProxiedByDomain[domain.FQDN("a")] = true
-	c.ProxiedByDomain[domain.FQDN("b")] = true
-	c.ProxiedByDomain[domain.FQDN("c")] = false
-	c.ProxiedByDomain[domain.FQDN("d")] = false
 	c.Print(mockPP)
 }
 
 //nolint:paralleltest // changing the environment variable TZ
-func TestPrintMonitors(t *testing.T) {
+func TestPrintEmpty(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 
 	store(t, "TZ", "UTC")
 
-	c := config.Default()
-
 	mockPP := mocks.NewMockPP(mockCtrl)
 	innerMockPP := mocks.NewMockPP(mockCtrl)
 	gomock.InOrder(
@@ -643,32 +511,23 @@ func TestPrintMonitors(t *testing.T) {
 		mockPP.EXPECT().IncIndent().Return(mockPP),
 		mockPP.EXPECT().IncIndent().Return(innerMockPP),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Policies:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv4 provider:    %s", "cloudflare.trace"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv4 domains:     %s", "(none)"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv6 provider:    %s", "cloudflare.trace"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv6 domains:     %s", "(none)"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv4 provider:             %s", "none"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IPv6 provider:             %s", "none"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Scheduling:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Timezone:         %s", Some("UTC (UTC+00 now)", "Local (UTC+00 now)")),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Update frequency: %v", cron.MustNew("@every 5m")),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Update on start?  %t", true),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Delete on stop?   %t", false),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Cache expiration: %v", time.Hour*6),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Timezone:                  %s", Some("UTC (UTC+00 now)", "Local (UTC+00 now)")), //nolint:lll
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Update frequency:          %v", cron.Schedule(nil)),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Update on start?           %t", false),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Delete on stop?            %t", false),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Cache expiration:          %v", time.Duration(0)),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "New DNS records:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "TTL:              %s", "1 (automatic)"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Proxied:          %s", "(none)"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Non-proxied:      %s", "(none)"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Proxied domains:           %s", "(none)"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Non-proxied domains:       %s", "(none)"),
 		mockPP.EXPECT().Infof(pp.EmojiConfig, "Timeouts:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IP detection:     %v", time.Second*5),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Record updating:  %v", time.Second*30),
-		mockPP.EXPECT().Infof(pp.EmojiConfig, "Monitors:"),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "%-17s (URL redacted)", "Healthchecks.io:"),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IP detection:              %v", time.Duration(0)),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Record updating:           %v", time.Duration(0)),
 	)
-
-	m, ok := monitor.NewHealthChecks(mockPP, "http://user:pass@host/path")
-	require.True(t, ok)
-
-	c.Monitors = []monitor.Monitor{m}
-	c.Print(mockPP)
+	var cfg config.Config
+	cfg.Print(mockPP)
 }
 
 //nolint:paralleltest // environment vars are global
@@ -709,9 +568,8 @@ func TestReadEnvWithOnlyToken(t *testing.T) {
 		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Use default %s=%t", "UPDATE_ON_START", false),
 		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Use default %s=%t", "DELETE_ON_STOP", false),
 		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Use default %s=%v", "CACHE_EXPIRATION", time.Duration(0)),
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Use default %s=%d", "TTL", 0),
-		innerMockPP.EXPECT().Warningf(pp.EmojiUserWarning, "TTL value (%i) should be 1 (automatic) or between 30 and 86400", 0), //nolint:lll
-		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Use default %s=%t", "PROXIED", false),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Use default %s=%s", "TTL", ""),
+		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Use default %s=%s", "PROXIED", ""),
 		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Use default %s=%v", "DETECTION_TIMEOUT", time.Duration(0)),
 		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Use default %s=%v", "UPDATE_TIMEOUT", time.Duration(0)),
 	)
@@ -743,7 +601,7 @@ func TestReadEnvEmpty(t *testing.T) {
 	require.False(t, ok)
 }
 
-//nolint:funlen,maintidx
+//nolint:funlen
 func TestNormalize(t *testing.T) {
 	t.Parallel()
 
@@ -774,16 +632,9 @@ func TestNormalize(t *testing.T) {
 					ipnet.IP4: {},
 					ipnet.IP6: {},
 				},
-				ProxiedByDomain: map[domain.Domain]bool{},
-			},
-			ok: false,
-			expected: &config.Config{ //nolint:exhaustruct
-				Domains: map[ipnet.Type][]domain.Domain{
-					ipnet.IP4: {},
-					ipnet.IP6: {},
-				},
-				ProxiedByDomain: map[domain.Domain]bool{},
 			},
+			ok:       false,
+			expected: nil,
 			prepareMockPP: func(m *mocks.MockPP) {
 				gomock.InOrder(
 					m.EXPECT().IsEnabledFor(pp.Info).Return(true),
@@ -803,19 +654,24 @@ func TestNormalize(t *testing.T) {
 					ipnet.IP4: {domain.FQDN("a.b.c")},
 					ipnet.IP6: {},
 				},
-				ProxiedByDomain: map[domain.Domain]bool{},
+				TTLTemplate:     "1",
+				ProxiedTemplate: "false",
 			},
 			ok: true,
 			expected: &config.Config{ //nolint:exhaustruct
 				Provider: map[ipnet.Type]provider.Provider{
 					ipnet.IP4: provider.NewCloudflareTrace(),
-					ipnet.IP6: nil,
 				},
 				Domains: map[ipnet.Type][]domain.Domain{
 					ipnet.IP4: {domain.FQDN("a.b.c")},
 					ipnet.IP6: {},
 				},
-				ProxiedByDomain: map[domain.Domain]bool{
+				TTLTemplate: "1",
+				TTL: map[domain.Domain]api.TTL{
+					domain.FQDN("a.b.c"): 1,
+				},
+				ProxiedTemplate: "false",
+				Proxied: map[domain.Domain]bool{
 					domain.FQDN("a.b.c"): false,
 				},
 			},
@@ -833,27 +689,15 @@ func TestNormalize(t *testing.T) {
 		"empty-ip6-none-ip4": {
 			input: &config.Config{ //nolint:exhaustruct
 				Provider: map[ipnet.Type]provider.Provider{
-					ipnet.IP4: nil,
 					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
 				Domains: map[ipnet.Type][]domain.Domain{
 					ipnet.IP4: {domain.FQDN("a.b.c")},
 					ipnet.IP6: {},
 				},
-				ProxiedByDomain: map[domain.Domain]bool{},
-			},
-			ok: false,
-			expected: &config.Config{ //nolint:exhaustruct
-				Provider: map[ipnet.Type]provider.Provider{
-					ipnet.IP4: nil,
-					ipnet.IP6: nil,
-				},
-				Domains: map[ipnet.Type][]domain.Domain{
-					ipnet.IP4: {domain.FQDN("a.b.c")},
-					ipnet.IP6: {},
-				},
-				ProxiedByDomain: map[domain.Domain]bool{},
 			},
+			ok:       false,
+			expected: nil,
 			prepareMockPP: func(m *mocks.MockPP) {
 				gomock.InOrder(
 					m.EXPECT().IsEnabledFor(pp.Info).Return(true),
@@ -869,26 +713,31 @@ func TestNormalize(t *testing.T) {
 		"ignored-ip4-domains": {
 			input: &config.Config{ //nolint:exhaustruct
 				Provider: map[ipnet.Type]provider.Provider{
-					ipnet.IP4: nil,
 					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
 				Domains: map[ipnet.Type][]domain.Domain{
 					ipnet.IP4: {domain.FQDN("a.b.c"), domain.FQDN("d.e.f")},
 					ipnet.IP6: {domain.FQDN("a.b.c"), domain.FQDN("g.h.i")},
 				},
-				ProxiedByDomain: map[domain.Domain]bool{},
+				TTLTemplate:     "1",
+				ProxiedTemplate: "false",
 			},
 			ok: true,
 			expected: &config.Config{ //nolint:exhaustruct
 				Provider: map[ipnet.Type]provider.Provider{
-					ipnet.IP4: nil,
 					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
 				Domains: map[ipnet.Type][]domain.Domain{
 					ipnet.IP4: {domain.FQDN("a.b.c"), domain.FQDN("d.e.f")},
 					ipnet.IP6: {domain.FQDN("a.b.c"), domain.FQDN("g.h.i")},
 				},
-				ProxiedByDomain: map[domain.Domain]bool{
+				TTLTemplate: "1",
+				TTL: map[domain.Domain]api.TTL{
+					domain.FQDN("a.b.c"): 1,
+					domain.FQDN("g.h.i"): 1,
+				},
+				ProxiedTemplate: "false",
+				Proxied: map[domain.Domain]bool{
 					domain.FQDN("a.b.c"): false,
 					domain.FQDN("g.h.i"): false,
 				},
@@ -904,32 +753,36 @@ func TestNormalize(t *testing.T) {
 				)
 			},
 		},
-		"proxied-nil": {
+		"template": {
 			input: &config.Config{ //nolint:exhaustruct
 				Provider: map[ipnet.Type]provider.Provider{
-					ipnet.IP4: nil,
 					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
 				Domains: map[ipnet.Type][]domain.Domain{
-					ipnet.IP4: nil,
-					ipnet.IP6: {domain.FQDN("a.b.c")},
+					ipnet.IP6: {domain.FQDN("a.b.c"), domain.FQDN("a.bb.c"), domain.FQDN("a.d.e.f")},
 				},
-				DefaultProxied:  true,
-				ProxiedByDomain: nil,
+				TTLTemplate:     `{{if suffix "b.c"}} 60 {{else if domain "d.e.f" "a.bb.c" }} 90 {{else}} 120 {{end}}`,
+				ProxiedTemplate: ` {{not (domain "a.bb.c")}} `,
 			},
 			ok: true,
 			expected: &config.Config{ //nolint:exhaustruct
 				Provider: map[ipnet.Type]provider.Provider{
-					ipnet.IP4: nil,
 					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
 				Domains: map[ipnet.Type][]domain.Domain{
-					ipnet.IP4: nil,
-					ipnet.IP6: {domain.FQDN("a.b.c")},
+					ipnet.IP6: {domain.FQDN("a.b.c"), domain.FQDN("a.bb.c"), domain.FQDN("a.d.e.f")},
+				},
+				TTLTemplate: `{{if suffix "b.c"}} 60 {{else if domain "d.e.f" "a.bb.c" }} 90 {{else}} 120 {{end}}`,
+				TTL: map[domain.Domain]api.TTL{
+					domain.FQDN("a.b.c"):   60,
+					domain.FQDN("a.bb.c"):  90,
+					domain.FQDN("a.d.e.f"): 120,
 				},
-				DefaultProxied: true,
-				ProxiedByDomain: map[domain.Domain]bool{
-					domain.FQDN("a.b.c"): true,
+				ProxiedTemplate: ` {{not (domain "a.bb.c")}} `,
+				Proxied: map[domain.Domain]bool{
+					domain.FQDN("a.b.c"):   true,
+					domain.FQDN("a.bb.c"):  false,
+					domain.FQDN("a.d.e.f"): true,
 				},
 			},
 			prepareMockPP: func(m *mocks.MockPP) {
@@ -937,90 +790,116 @@ func TestNormalize(t *testing.T) {
 					m.EXPECT().IsEnabledFor(pp.Info).Return(true),
 					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
 					m.EXPECT().IncIndent().Return(m),
-					m.EXPECT().Warningf(pp.EmojiImpossible,
-						"Internal failure: ProxiedByDomain is re-initialized because it was nil",
-					),
-					m.EXPECT().Warningf(pp.EmojiImpossible,
-						"Please report the bug at https://github.com/favonia/cloudflare-ddns/issues/new",
-					),
 				)
 			},
 		},
-		"proxied-ignored": {
+		"template/invalid/ttl": {
 			input: &config.Config{ //nolint:exhaustruct
 				Provider: map[ipnet.Type]provider.Provider{
-					ipnet.IP4: nil,
 					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
 				Domains: map[ipnet.Type][]domain.Domain{
-					ipnet.IP4: nil,
-					ipnet.IP6: {domain.FQDN("a.b.c")},
-				},
-				ProxiedByDomain: map[domain.Domain]bool{
-					domain.FQDN("a.b.c"): true,
-					domain.FQDN("d.e.f"): true,
-					domain.FQDN("g.h.i"): false,
+					ipnet.IP6: {domain.FQDN("a.b.c"), domain.FQDN("a.bb.c"), domain.FQDN("a.d.e.f")},
 				},
+				TTLTemplate:     `{{if}}`,
+				ProxiedTemplate: ` {{not (domain "a.b.c")}} `,
 			},
-			ok: true,
-			expected: &config.Config{ //nolint:exhaustruct
+			ok:       false,
+			expected: nil,
+			prepareMockPP: func(m *mocks.MockPP) {
+				gomock.InOrder(
+					m.EXPECT().IsEnabledFor(pp.Info).Return(true),
+					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
+					m.EXPECT().IncIndent().Return(m),
+					m.EXPECT().Errorf(pp.EmojiUserError, "%q is not a valid template: %v", "{{if}}", gomock.Any()),
+				)
+			},
+		},
+		"template/error/ttl": {
+			input: &config.Config{ //nolint:exhaustruct
 				Provider: map[ipnet.Type]provider.Provider{
-					ipnet.IP4: nil,
 					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
 				Domains: map[ipnet.Type][]domain.Domain{
-					ipnet.IP4: nil,
 					ipnet.IP6: {domain.FQDN("a.b.c")},
 				},
-				ProxiedByDomain: map[domain.Domain]bool{
-					domain.FQDN("a.b.c"): true,
-				},
+				TTLTemplate:     `not a number`,
+				ProxiedTemplate: `{{not (domain "a.b.c")}}`,
 			},
+			ok:       false,
+			expected: nil,
 			prepareMockPP: func(m *mocks.MockPP) {
-				call0 := m.EXPECT().IsEnabledFor(pp.Info).Return(true)
-				call1 := m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . .").After(call0)
-				call2 := m.EXPECT().IncIndent().Return(m).After(call1)
-				m.EXPECT().Warningf(pp.EmojiUserWarning,
-					"Domain %q was listed in %s, but it is ignored because it is not managed by the updater",
-					"d.e.f", "PROXIED_DOMAINS").After(call2)
-				m.EXPECT().Warningf(pp.EmojiUserWarning,
-					"Domain %q was listed in %s, but it is ignored because it is not managed by the updater",
-					"g.h.i", "NON_PROXIED_DOMAINS").After(call2)
+				gomock.InOrder(
+					m.EXPECT().IsEnabledFor(pp.Info).Return(true),
+					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
+					m.EXPECT().IncIndent().Return(m),
+					m.EXPECT().Errorf(pp.EmojiUserError, "TTL of %s (%q) is not a number: %v", "a.b.c", "not a number", gomock.Any()),
+				)
 			},
 		},
-		"good": {
+		"template/error/ttl/out-of-range": {
 			input: &config.Config{ //nolint:exhaustruct
 				Provider: map[ipnet.Type]provider.Provider{
-					ipnet.IP4: nil,
 					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
 				Domains: map[ipnet.Type][]domain.Domain{
-					ipnet.IP4: nil,
 					ipnet.IP6: {domain.FQDN("a.b.c")},
 				},
-				ProxiedByDomain: map[domain.Domain]bool{
-					domain.FQDN("a.b.c"): true,
-				},
+				TTLTemplate:     `{{if (domain "a.b.c")}} 2 {{end}}`,
+				ProxiedTemplate: `{{not (domain "a.b.c")}}`,
 			},
-			ok: true,
-			expected: &config.Config{ //nolint:exhaustruct
+			ok:       false,
+			expected: nil,
+			prepareMockPP: func(m *mocks.MockPP) {
+				gomock.InOrder(
+					m.EXPECT().IsEnabledFor(pp.Info).Return(true),
+					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
+					m.EXPECT().IncIndent().Return(m),
+					m.EXPECT().Errorf(pp.EmojiUserError, "TTL of %s (%d) should be 1 (auto) or between 30 and 86400", "a.b.c", 2),
+				)
+			},
+		},
+		"template/error/proxied": {
+			input: &config.Config{ //nolint:exhaustruct
 				Provider: map[ipnet.Type]provider.Provider{
-					ipnet.IP4: nil,
 					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
 				Domains: map[ipnet.Type][]domain.Domain{
-					ipnet.IP4: nil,
 					ipnet.IP6: {domain.FQDN("a.b.c")},
 				},
-				ProxiedByDomain: map[domain.Domain]bool{
-					domain.FQDN("a.b.c"): true,
+				TTLTemplate:     `1`,
+				ProxiedTemplate: `{{12345}}`,
+			},
+			ok:       false,
+			expected: nil,
+			prepareMockPP: func(m *mocks.MockPP) {
+				gomock.InOrder(
+					m.EXPECT().IsEnabledFor(pp.Info).Return(true),
+					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
+					m.EXPECT().IncIndent().Return(m),
+					m.EXPECT().Errorf(pp.EmojiUserError, "Proxy setting of %s (%q) is not a boolean value: %v", "a.b.c", "12345", gomock.Any()), //nolint:lll
+				)
+			},
+		},
+		"template/error/proxied/ill-formed": {
+			input: &config.Config{ //nolint:exhaustruct
+				Provider: map[ipnet.Type]provider.Provider{
+					ipnet.IP6: provider.NewCloudflareTrace(),
 				},
+				Domains: map[ipnet.Type][]domain.Domain{
+					ipnet.IP6: {domain.FQDN("a.b.c")},
+				},
+				TTLTemplate:     `1`,
+				ProxiedTemplate: `{{domain 12345}}`,
 			},
+			ok:       false,
+			expected: nil,
 			prepareMockPP: func(m *mocks.MockPP) {
 				gomock.InOrder(
 					m.EXPECT().IsEnabledFor(pp.Info).Return(true),
 					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
 					m.EXPECT().IncIndent().Return(m),
+					m.EXPECT().Errorf(pp.EmojiUserError, "Could not execute the template %q: %v", "{{domain 12345}}", gomock.Any()), //nolint:lll
 				)
 			},
 		},
@@ -1037,7 +916,11 @@ func TestNormalize(t *testing.T) {
 			}
 			ok := cfg.NormalizeDomains(mockPP)
 			require.Equal(t, tc.ok, ok)
-			require.Equal(t, tc.expected, cfg)
+			if tc.ok {
+				require.Equal(t, tc.expected, cfg)
+			} else {
+				require.Equal(t, tc.input, cfg)
+			}
 		})
 	}
 }
diff --git a/internal/config/env_test.go b/internal/config/env_test.go
index 4cb1237..880610c 100644
--- a/internal/config/env_test.go
+++ b/internal/config/env_test.go
@@ -61,6 +61,55 @@ func TestGetenv(t *testing.T) {
 	}
 }
 
+//nolint:paralleltest // environment vars are global
+func TestReadString(t *testing.T) {
+	key := keyPrefix + "STRING"
+	for name, tc := range map[string]struct {
+		set           bool
+		val           string
+		oldField      string
+		newField      string
+		ok            bool
+		prepareMockPP func(*mocks.MockPP)
+	}{
+		"unset": {
+			false, "", "hi", "hi", true,
+			func(m *mocks.MockPP) {
+				m.EXPECT().Infof(pp.EmojiBullet, "Use default %s=%s", key, "hi")
+			},
+		},
+		"empty1": {
+			true, " ", "hello", "hello", true,
+			func(m *mocks.MockPP) {
+				m.EXPECT().Infof(pp.EmojiBullet, "Use default %s=%s", key, "hello")
+			},
+		},
+		"empty2": {
+			true, " \t ", "aloha", "aloha", true,
+			func(m *mocks.MockPP) {
+				m.EXPECT().Infof(pp.EmojiBullet, "Use default %s=%s", key, "aloha")
+			},
+		},
+		"string": {true, "string ", "hey", "string", true, nil},
+	} {
+		tc := tc
+		t.Run(name, func(t *testing.T) {
+			mockCtrl := gomock.NewController(t)
+
+			set(t, key, tc.set, tc.val)
+
+			field := tc.oldField
+			mockPP := mocks.NewMockPP(mockCtrl)
+			if tc.prepareMockPP != nil {
+				tc.prepareMockPP(mockPP)
+			}
+			ok := config.ReadString(mockPP, key, &field)
+			require.Equal(t, tc.ok, ok)
+			require.Equal(t, tc.newField, field)
+		})
+	}
+}
+
 //nolint:paralleltest // environment vars are global
 func TestReadQuiet(t *testing.T) {
 	key := keyPrefix + "QUIET"
@@ -223,7 +272,6 @@ func TestReadNonnegInt(t *testing.T) {
 				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: %v", "word", gomock.Any())
 			},
 		},
-		"9999999": {true, "   9999999   ", 100, 9999999, true, nil},
 	} {
 		tc := tc
 		t.Run(name, func(t *testing.T) {
diff --git a/internal/domain/fqdn_test.go b/internal/domain/fqdn_test.go
index e4c249c..114696c 100644
--- a/internal/domain/fqdn_test.go
+++ b/internal/domain/fqdn_test.go
@@ -20,7 +20,6 @@ func TestFQDNString(t *testing.T) {
 	))
 }
 
-//nolint:dupl
 func TestFQDNDescribe(t *testing.T) {
 	t.Parallel()
 	for _, tc := range [...]struct {
@@ -67,6 +66,7 @@ func TestFQDNDescribe(t *testing.T) {
 	}
 }
 
+//nolint:dupl
 func TestFQDNSplitter(t *testing.T) {
 	t.Parallel()
 	type r = string
diff --git a/internal/domain/wildcard_test.go b/internal/domain/wildcard_test.go
index 1abf0a0..fe2eddc 100644
--- a/internal/domain/wildcard_test.go
+++ b/internal/domain/wildcard_test.go
@@ -23,7 +23,6 @@ func TestWildcardString(t *testing.T) {
 	))
 }
 
-//nolint:dupl
 func TestWildcardDescribe(t *testing.T) {
 	t.Parallel()
 	for _, tc := range [...]struct {
@@ -58,6 +57,7 @@ func TestWildcardDescribe(t *testing.T) {
 		{"xn--mgba3gch31f.de", "*.نامهای.de"},
 		{"xn--mgba3gch31f060k.de", "*.نامه\u200cای.de"},
 		// some other test cases
+		{"", "*"},
 		{"xn--a.xn--a.xn--a.com", "*.xn--a.xn--a.xn--a.com"},
 		{"a.com....", "*.a.com...."},
 		{"a.com", "*.a.com"},
@@ -70,6 +70,7 @@ func TestWildcardDescribe(t *testing.T) {
 	}
 }
 
+//nolint:dupl
 func TestWildcardSplitter(t *testing.T) {
 	t.Parallel()
 	type r = string
diff --git a/internal/provider/dns_test.go b/internal/provider/dns_test.go
index 121934d..a1650b3 100644
--- a/internal/provider/dns_test.go
+++ b/internal/provider/dns_test.go
@@ -81,7 +81,7 @@ func setupServer(t *testing.T, name string, class dnsmessage.Class,
 	}))
 }
 
-//nolint:funlen,maintidx
+//nolint:funlen
 func TestDNSOverHTTPSGetIP(t *testing.T) {
 	t.Parallel()
 
diff --git a/internal/setter/setter_test.go b/internal/setter/setter_test.go
index ed42644..b1ba70e 100644
--- a/internal/setter/setter_test.go
+++ b/internal/setter/setter_test.go
@@ -2,7 +2,6 @@ package setter_test
 
 import (
 	"context"
-	"fmt"
 	"net/netip"
 	"testing"
 
@@ -17,7 +16,7 @@ import (
 	"github.com/favonia/cloudflare-ddns/internal/setter"
 )
 
-//nolint:funlen,maintidx
+//nolint:funlen
 func TestSet(t *testing.T) {
 	t.Parallel()
 
@@ -27,7 +26,6 @@ func TestSet(t *testing.T) {
 		record1   = "record1"
 		record2   = "record2"
 		record3   = "record3"
-		ttl       = api.TTL(100)
 	)
 	var (
 		ip1       = netip.MustParseAddr("::1")
@@ -38,35 +36,43 @@ func TestSet(t *testing.T) {
 	for name, tc := range map[string]struct {
 		ip                netip.Addr
 		ok                bool
+		ttl               api.TTL
+		proxied           bool
 		prepareMockPP     func(m *mocks.MockPP)
-		prepareMockHandle func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle, proxied bool)
+		prepareMockHandle func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle)
 	}{
-		"0-nil": {
+		"0-nil/1-true": {
 			invalidIP,
 			true,
+			1,
+			true,
 			func(m *mocks.MockPP) {
 				m.EXPECT().Infof(pp.EmojiAlreadyDone, "The %s records of %q are already up to date", "AAAA", "sub.test.org")
 			},
-			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle, proxied bool) {
+			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
 				m.EXPECT().ListRecords(ctx, ppfmt, domain, ipNetwork).Return(map[string]netip.Addr{}, true)
 			},
 		},
-		"0": {
+		"0/1-false": {
 			ip1,
 			true,
+			1,
+			false,
 			func(m *mocks.MockPP) {
 				m.EXPECT().Noticef(pp.EmojiAddRecord, "Added a new %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1)
 			},
-			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle, proxied bool) {
+			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
 				gomock.InOrder(
 					m.EXPECT().ListRecords(ctx, ppfmt, domain, ipNetwork).Return(map[string]netip.Addr{}, true),
-					m.EXPECT().CreateRecord(ctx, ppfmt, domain, ipNetwork, ip1, ttl, proxied).Return(record1, true),
+					m.EXPECT().CreateRecord(ctx, ppfmt, domain, ipNetwork, ip1, api.TTL(1), false).Return(record1, true),
 				)
 			},
 		},
-		"1unmatched": {
+		"1unmatched/300-false": {
 			ip1,
 			true,
+			300,
+			false,
 			func(m *mocks.MockPP) {
 				m.EXPECT().Noticef(pp.EmojiUpdateRecord,
 					"Updated a stale %s record of %q (ID: %s)",
@@ -75,57 +81,65 @@ func TestSet(t *testing.T) {
 					record1,
 				)
 			},
-			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle, proxied bool) {
+			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
 				gomock.InOrder(
 					m.EXPECT().ListRecords(ctx, ppfmt, domain, ipNetwork).Return(map[string]netip.Addr{record1: ip2}, true),
 					m.EXPECT().UpdateRecord(ctx, ppfmt, domain, ipNetwork, record1, ip1).Return(true),
 				)
 			},
 		},
-		"1unmatched-updatefail": {
+		"1unmatched-updatefail/300-false": {
 			ip1,
 			true,
+			300,
+			false,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
 					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1),
 					m.EXPECT().Noticef(pp.EmojiAddRecord, "Added a new %s record of %q (ID: %s)", "AAAA", "sub.test.org", record2),
 				)
 			},
-			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle, proxied bool) {
+			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
 				gomock.InOrder(
 					m.EXPECT().ListRecords(ctx, ppfmt, domain, ipNetwork).Return(map[string]netip.Addr{record1: ip2}, true),
 					m.EXPECT().UpdateRecord(ctx, ppfmt, domain, ipNetwork, record1, ip1).Return(false),
 					m.EXPECT().DeleteRecord(ctx, ppfmt, domain, ipNetwork, record1).Return(true),
-					m.EXPECT().CreateRecord(ctx, ppfmt, domain, ipNetwork, ip1, ttl, proxied).Return(record2, true),
+					m.EXPECT().CreateRecord(ctx, ppfmt, domain, ipNetwork, ip1, api.TTL(300), false).Return(record2, true),
 				)
 			},
 		},
-		"1unmatched-nil": {
+		"1unmatched-nil/300-false": {
 			invalidIP,
 			true,
+			300,
+			false,
 			func(m *mocks.MockPP) {
 				m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1)
 			},
-			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle, proxied bool) {
+			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
 				gomock.InOrder(
 					m.EXPECT().ListRecords(ctx, ppfmt, domain, ipNetwork).Return(map[string]netip.Addr{record1: ip1}, true),
 					m.EXPECT().DeleteRecord(ctx, ppfmt, domain, ipNetwork, record1).Return(true),
 				)
 			},
 		},
-		"1matched": {
+		"1matched/300-false": {
 			ip1,
 			true,
+			300,
+			false,
 			func(m *mocks.MockPP) {
 				m.EXPECT().Infof(pp.EmojiAlreadyDone, "The %s records of %q are already up to date", "AAAA", "sub.test.org")
 			},
-			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle, proxied bool) {
+			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
 				m.EXPECT().ListRecords(ctx, ppfmt, domain, ipNetwork).Return(map[string]netip.Addr{record1: ip1}, true)
 			},
 		},
-		"2matched": {
+		"2matched/300-false": {
 			ip1,
 			true,
+			300,
+			false,
 			func(m *mocks.MockPP) {
 				m.EXPECT().Noticef(
 					pp.EmojiDelRecord,
@@ -135,27 +149,31 @@ func TestSet(t *testing.T) {
 					record2,
 				)
 			},
-			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle, proxied bool) {
+			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
 				gomock.InOrder(
 					m.EXPECT().ListRecords(ctx, ppfmt, domain, ipNetwork).Return(map[string]netip.Addr{record1: ip1, record2: ip1}, true), //nolint:lll
 					m.EXPECT().DeleteRecord(ctx, ppfmt, domain, ipNetwork, record2).Return(true),
 				)
 			},
 		},
-		"2matched-deletefail": {
+		"2matched-deletefail/300-false": {
 			ip1,
 			true,
+			300,
+			false,
 			nil,
-			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle, proxied bool) {
+			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
 				gomock.InOrder(
 					m.EXPECT().ListRecords(ctx, ppfmt, domain, ipNetwork).Return(map[string]netip.Addr{record1: ip1, record2: ip1}, true), //nolint:lll
 					m.EXPECT().DeleteRecord(ctx, ppfmt, domain, ipNetwork, record2).Return(false),
 				)
 			},
 		},
-		"2unmatched": {
+		"2unmatched/300-false": {
 			ip1,
 			true,
+			300,
+			false,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
 					m.EXPECT().Noticef(
@@ -168,7 +186,7 @@ func TestSet(t *testing.T) {
 					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record2),
 				)
 			},
-			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle, proxied bool) {
+			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
 				gomock.InOrder(
 					m.EXPECT().ListRecords(ctx, ppfmt, domain, ipNetwork).Return(map[string]netip.Addr{record1: ip2, record2: ip2}, true), //nolint:lll
 					m.EXPECT().UpdateRecord(ctx, ppfmt, domain, ipNetwork, record1, ip1).Return(true),
@@ -176,9 +194,11 @@ func TestSet(t *testing.T) {
 				)
 			},
 		},
-		"2unmatched-updatefail": {
+		"2unmatched-updatefail/300-false": {
 			ip1,
 			true,
+			300,
+			false,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
 					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1),
@@ -191,7 +211,7 @@ func TestSet(t *testing.T) {
 					),
 				)
 			},
-			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle, proxied bool) {
+			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
 				gomock.InOrder(
 					m.EXPECT().ListRecords(ctx, ppfmt, domain, ipNetwork).Return(map[string]netip.Addr{record1: ip2, record2: ip2}, true), //nolint:lll
 					m.EXPECT().UpdateRecord(ctx, ppfmt, domain, ipNetwork, record1, ip1).Return(false),
@@ -200,9 +220,11 @@ func TestSet(t *testing.T) {
 				)
 			},
 		},
-		"2unmatched-updatefailtwice": {
+		"2unmatched-updatefailtwice/300-false": {
 			ip1,
 			true,
+			300,
+			false,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
 					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1),
@@ -210,21 +232,23 @@ func TestSet(t *testing.T) {
 					m.EXPECT().Noticef(pp.EmojiAddRecord, "Added a new %s record of %q (ID: %s)", "AAAA", "sub.test.org", record3),
 				)
 			},
-			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle, proxied bool) {
+			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
 				gomock.InOrder(
 					m.EXPECT().ListRecords(ctx, ppfmt, domain, ipNetwork).Return(map[string]netip.Addr{record1: ip2, record2: ip2}, true), //nolint:lll
 					m.EXPECT().UpdateRecord(ctx, ppfmt, domain, ipNetwork, record1, ip1).Return(false),
 					m.EXPECT().DeleteRecord(ctx, ppfmt, domain, ipNetwork, record1).Return(true),
 					m.EXPECT().UpdateRecord(ctx, ppfmt, domain, ipNetwork, record2, ip1).Return(false),
 					m.EXPECT().DeleteRecord(ctx, ppfmt, domain, ipNetwork, record2).Return(true),
-					m.EXPECT().CreateRecord(ctx, ppfmt, domain, ipNetwork, ip1, ttl, proxied).Return(record3, true),
+					m.EXPECT().CreateRecord(ctx, ppfmt, domain, ipNetwork, ip1, api.TTL(300), false).Return(record3, true),
 				)
 			},
 		},
 		//nolint:dupl
-		"2unmatched-updatefail-deletefail-updatefail": {
+		"2unmatched-updatefail-deletefail-updatefail/300-false": {
 			ip1,
 			false,
+			300,
+			false,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
 					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record2),
@@ -232,21 +256,23 @@ func TestSet(t *testing.T) {
 					m.EXPECT().Errorf(pp.EmojiError, "Failed to complete updating of %s records of %q; records might be inconsistent", "AAAA", "sub.test.org"), //nolint:lll
 				)
 			},
-			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle, proxied bool) {
+			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
 				gomock.InOrder(
 					m.EXPECT().ListRecords(ctx, ppfmt, domain, ipNetwork).Return(map[string]netip.Addr{record1: ip2, record2: ip2}, true), //nolint:lll
 					m.EXPECT().UpdateRecord(ctx, ppfmt, domain, ipNetwork, record1, ip1).Return(false),
 					m.EXPECT().DeleteRecord(ctx, ppfmt, domain, ipNetwork, record1).Return(false),
 					m.EXPECT().UpdateRecord(ctx, ppfmt, domain, ipNetwork, record2, ip1).Return(false),
 					m.EXPECT().DeleteRecord(ctx, ppfmt, domain, ipNetwork, record2).Return(true),
-					m.EXPECT().CreateRecord(ctx, ppfmt, domain, ipNetwork, ip1, ttl, proxied).Return(record3, true),
+					m.EXPECT().CreateRecord(ctx, ppfmt, domain, ipNetwork, ip1, api.TTL(300), false).Return(record3, true),
 				)
 			},
 		},
 		//nolint:dupl
-		"2unmatched-updatefailtwice-createfail": {
+		"2unmatched-updatefailtwice-createfail/300-false": {
 			ip1,
 			false,
+			300,
+			false,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
 					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1),
@@ -254,37 +280,41 @@ func TestSet(t *testing.T) {
 					m.EXPECT().Errorf(pp.EmojiError, "Failed to complete updating of %s records of %q; records might be inconsistent", "AAAA", "sub.test.org"), //nolint:lll
 				)
 			},
-			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle, proxied bool) {
+			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
 				gomock.InOrder(
 					m.EXPECT().ListRecords(ctx, ppfmt, domain, ipNetwork).Return(map[string]netip.Addr{record1: ip2, record2: ip2}, true), //nolint:lll
 					m.EXPECT().UpdateRecord(ctx, ppfmt, domain, ipNetwork, record1, ip1).Return(false),
 					m.EXPECT().DeleteRecord(ctx, ppfmt, domain, ipNetwork, record1).Return(true),
 					m.EXPECT().UpdateRecord(ctx, ppfmt, domain, ipNetwork, record2, ip1).Return(false),
 					m.EXPECT().DeleteRecord(ctx, ppfmt, domain, ipNetwork, record2).Return(true),
-					m.EXPECT().CreateRecord(ctx, ppfmt, domain, ipNetwork, ip1, ttl, proxied).Return(record3, false),
+					m.EXPECT().CreateRecord(ctx, ppfmt, domain, ipNetwork, ip1, api.TTL(300), false).Return(record3, false),
 				)
 			},
 		},
-		"listfail": {
+		"listfail/300-false": {
 			ip1,
 			false,
+			300,
+			false,
 			func(m *mocks.MockPP) {
 				m.EXPECT().Errorf(pp.EmojiError, "Failed to retrieve the current %s records of %q", "AAAA", "sub.test.org")
 			},
-			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle, proxied bool) {
+			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
 				m.EXPECT().ListRecords(ctx, ppfmt, domain, ipNetwork).Return(nil, false)
 			},
 		},
-		"impossible-invalidIP-records": {
+		"impossible-invalidIP-records/300-false": {
 			invalidIP,
 			true,
+			300,
+			false,
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
 					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record1),
 					m.EXPECT().Noticef(pp.EmojiDelRecord, "Deleted a stale %s record of %q (ID: %s)", "AAAA", "sub.test.org", record2),
 				)
 			},
-			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle, proxied bool) {
+			func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) {
 				gomock.InOrder(
 					m.EXPECT().ListRecords(ctx, ppfmt, domain, ipNetwork).Return(map[string]netip.Addr{record1: ip1, record2: invalidIP}, true), //nolint:lll
 
@@ -295,28 +325,26 @@ func TestSet(t *testing.T) {
 		},
 	} {
 		tc := tc
-		for _, proxied := range [...]bool{true, false} {
-			t.Run(fmt.Sprintf("%s-%t", name, proxied), func(t *testing.T) {
-				t.Parallel()
-				mockCtrl := gomock.NewController(t)
+		t.Run(name, func(t *testing.T) {
+			t.Parallel()
+			mockCtrl := gomock.NewController(t)
 
-				ctx := context.Background()
+			ctx := context.Background()
 
-				mockPP := mocks.NewMockPP(mockCtrl)
-				if tc.prepareMockPP != nil {
-					tc.prepareMockPP(mockPP)
-				}
-				mockHandle := mocks.NewMockHandle(mockCtrl)
-				if tc.prepareMockHandle != nil {
-					tc.prepareMockHandle(ctx, mockPP, mockHandle, proxied)
-				}
+			mockPP := mocks.NewMockPP(mockCtrl)
+			if tc.prepareMockPP != nil {
+				tc.prepareMockPP(mockPP)
+			}
+			mockHandle := mocks.NewMockHandle(mockCtrl)
+			if tc.prepareMockHandle != nil {
+				tc.prepareMockHandle(ctx, mockPP, mockHandle)
+			}
 
-				s, ok := setter.New(mockPP, mockHandle, ttl)
-				require.True(t, ok)
+			s, ok := setter.New(mockPP, mockHandle)
+			require.True(t, ok)
 
-				ok = s.Set(ctx, mockPP, domain, ipNetwork, tc.ip, proxied)
-				require.Equal(t, tc.ok, ok)
-			})
-		}
+			ok = s.Set(ctx, mockPP, domain, ipNetwork, tc.ip, tc.ttl, tc.proxied)
+			require.Equal(t, tc.ok, ok)
+		})
 	}
 }
diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go
index 87fcb11..44280d6 100644
--- a/internal/updater/updater_test.go
+++ b/internal/updater/updater_test.go
@@ -8,6 +8,7 @@ import (
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/require"
 
+	"github.com/favonia/cloudflare-ddns/internal/api"
 	"github.com/favonia/cloudflare-ddns/internal/config"
 	"github.com/favonia/cloudflare-ddns/internal/domain"
 	"github.com/favonia/cloudflare-ddns/internal/ipnet"
@@ -16,7 +17,7 @@ import (
 	"github.com/favonia/cloudflare-ddns/internal/updater"
 )
 
-//nolint:funlen,paralleltest,maintidx // updater.IPv6MessageDisplayed is a global variable
+//nolint:funlen,paralleltest // updater.IPv6MessageDisplayed is a global variable
 func TestUpdateIPs(t *testing.T) {
 	domain4 := domain.FQDN("ip4.hello")
 	domain6 := domain.FQDN("ip6.hello")
@@ -41,60 +42,71 @@ func TestUpdateIPs(t *testing.T) {
 	provider4 := func(ppfmt pp.PP, m *mocks.MockProvider) { m.EXPECT().GetIP(gomock.Any(), ppfmt, ipnet.IP4).Return(ip4) }
 	provider6 := func(ppfmt pp.PP, m *mocks.MockProvider) { m.EXPECT().GetIP(gomock.Any(), ppfmt, ipnet.IP6).Return(ip6) }
 
+	type mockttl = map[domain.Domain]api.TTL
+	ttlAuto := mockttl{domain4: api.TTLAuto, domain6: api.TTLAuto}
+
 	type mockproxied = map[domain.Domain]bool
 	proxiedNone := mockproxied{domain4: false, domain6: false}
 	proxiedBoth := mockproxied{domain4: true, domain6: true}
 
 	for name, tc := range map[string]struct {
-		proxiedByDomain      mockproxied
+		ttl                  mockttl
+		proxied              mockproxied
 		ok                   bool
 		MessageShouldDisplay map[ipnet.Type]bool
 		prepareMockPP        func(m *mocks.MockPP)
 		prepareMockProvider  mockproviders
 		prepareMockSetter    func(ppfmt pp.PP, m *mocks.MockSetter)
 	}{
-		"none": {proxiedBoth, true, map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true}, nil, mockproviders{}, nil},
+		"none": {
+			ttlAuto, proxiedBoth, true, map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true}, nil, mockproviders{}, nil,
+		},
 		"ip4only": {
+			ttlAuto,
 			proxiedNone,
 			true,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			pp4only,
 			mockproviders{ipnet.IP4: provider4},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, false).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), false).Return(true)
 			},
 		},
 		"ip4only/setfail": {
+			ttlAuto,
 			proxiedBoth,
 			false,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			pp4only,
 			mockproviders{ipnet.IP4: provider4},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, true).Return(false)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), true).Return(false)
 			},
 		},
 		"ip6only": {
+			ttlAuto,
 			proxiedNone,
 			true,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			pp6only,
 			mockproviders{ipnet.IP6: provider6},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, false).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTL(1), false).Return(true)
 			},
 		},
 		"ip6only/setfail": {
+			ttlAuto,
 			proxiedBoth,
 			false,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			pp6only,
 			mockproviders{ipnet.IP6: provider6},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, true).Return(false)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTL(1), true).Return(false)
 			},
 		},
 		"both": {
+			ttlAuto,
 			proxiedNone,
 			true,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
@@ -102,12 +114,13 @@ func TestUpdateIPs(t *testing.T) {
 			mockproviders{ipnet.IP4: provider4, ipnet.IP6: provider6},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
 				gomock.InOrder(
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, false).Return(true),
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, false).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), false).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTL(1), false).Return(true),
 				)
 			},
 		},
 		"both/setfail1": {
+			ttlAuto,
 			proxiedBoth,
 			false,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
@@ -115,12 +128,13 @@ func TestUpdateIPs(t *testing.T) {
 			mockproviders{ipnet.IP4: provider4, ipnet.IP6: provider6},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
 				gomock.InOrder(
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, true).Return(false),
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, true).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), true).Return(false),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTL(1), true).Return(true),
 				)
 			},
 		},
 		"both/setfail2": {
+			ttlAuto,
 			proxiedNone,
 			false,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
@@ -128,12 +142,13 @@ func TestUpdateIPs(t *testing.T) {
 			mockproviders{ipnet.IP4: provider4, ipnet.IP6: provider6},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
 				gomock.InOrder(
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, false).Return(true),
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, false).Return(false),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), false).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTL(1), false).Return(false),
 				)
 			},
 		},
 		"ip4fails": {
+			ttlAuto,
 			proxiedBoth,
 			false,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
@@ -151,10 +166,11 @@ func TestUpdateIPs(t *testing.T) {
 				ipnet.IP6: provider6,
 			},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, true).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTL(1), true).Return(true)
 			},
 		},
 		"ip6fails": {
+			ttlAuto,
 			proxiedNone,
 			false,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
@@ -174,10 +190,11 @@ func TestUpdateIPs(t *testing.T) {
 				},
 			},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, false).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), false).Return(true)
 			},
 		},
 		"ip6fails/again": {
+			ttlAuto,
 			proxiedBoth,
 			false,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: false},
@@ -194,10 +211,11 @@ func TestUpdateIPs(t *testing.T) {
 				},
 			},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, true).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), true).Return(true)
 			},
 		},
 		"bothfail": {
+			ttlAuto,
 			proxiedNone,
 			false,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
@@ -222,6 +240,7 @@ func TestUpdateIPs(t *testing.T) {
 			nil,
 		},
 		"ip4only-proxied-nil": {
+			mockttl{},
 			mockproxied{},
 			true,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
@@ -229,17 +248,18 @@ func TestUpdateIPs(t *testing.T) {
 				gomock.InOrder(
 					m.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4),
 					m.EXPECT().Warningf(pp.EmojiImpossible,
-						"Internal failure: ProxiedByDomain[%s] was not set, and is reset to %t",
-						domain4.Describe(), false,
+						"TTL[%s] not initialized; please report the bug at https://github.com/favonia/cloudflare-ddns/issues/new",
+						"ip4.hello",
 					),
 					m.EXPECT().Warningf(pp.EmojiImpossible,
-						"Please report the bug at https://github.com/favonia/cloudflare-ddns/issues/new",
+						"Proxied[%s] not initialized; please report the bug at https://github.com/favonia/cloudflare-ddns/issues/new",
+						"ip4.hello",
 					),
 				)
 			},
 			mockproviders{ipnet.IP4: provider4},
 			func(ppfmt pp.PP, m *mocks.MockSetter) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, false).Return(true)
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTL(1), false).Return(true)
 			},
 		},
 	} {
@@ -249,7 +269,8 @@ func TestUpdateIPs(t *testing.T) {
 			ctx := context.Background()
 			conf := config.Default()
 			conf.Domains = domains
-			conf.ProxiedByDomain = tc.proxiedByDomain
+			conf.TTL = tc.ttl
+			conf.Proxied = tc.proxied
 			mockPP := mocks.NewMockPP(mockCtrl)
 			if tc.prepareMockPP != nil {
 				tc.prepareMockPP(mockPP)
@@ -285,18 +306,24 @@ func TestClearIPs(t *testing.T) {
 
 	type mockproviders = map[ipnet.Type]bool
 
+	type mockttl = map[domain.Domain]api.TTL
+	ttlAuto := mockttl{domain4: api.TTLAuto, domain6: api.TTLAuto}
+
 	type mockproxied = map[domain.Domain]bool
 	proxiedNone := mockproxied{domain4: false, domain6: false}
-	proxiedBoth := mockproxied{domain4: true, domain6: true}
 
 	for name, tc := range map[string]struct {
+		ttl                  mockttl
+		proxied              mockproxied
 		ok                   bool
 		MessageShouldDisplay map[ipnet.Type]bool
 		prepareMockPP        func(m *mocks.MockPP)
 		prepareMockProvider  mockproviders
-		prepareMockSetter    func(ppfmt pp.PP, m *mocks.MockSetter, proxied bool)
+		prepareMockSetter    func(ppfmt pp.PP, m *mocks.MockSetter)
 	}{
 		"none": {
+			ttlAuto,
+			proxiedNone,
 			true,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			nil,
@@ -304,106 +331,119 @@ func TestClearIPs(t *testing.T) {
 			nil,
 		},
 		"ip4only": {
+			ttlAuto,
+			proxiedNone,
 			true,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			nil,
 			mockproviders{ipnet.IP4: true},
-			func(ppfmt pp.PP, m *mocks.MockSetter, proxied bool) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, proxied).Return(true)
+			func(ppfmt pp.PP, m *mocks.MockSetter) {
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, api.TTL(1), false).Return(true)
 			},
 		},
 		"ip4only/setfail": {
+			ttlAuto,
+			proxiedNone,
 			false,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			nil,
 			mockproviders{ipnet.IP4: true},
-			func(ppfmt pp.PP, m *mocks.MockSetter, proxied bool) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, proxied).Return(false)
+			func(ppfmt pp.PP, m *mocks.MockSetter) {
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, api.TTL(1), false).Return(false)
 			},
 		},
 		"ip6only": {
+			ttlAuto,
+			proxiedNone,
 			true,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			nil,
 			mockproviders{ipnet.IP6: true},
-			func(ppfmt pp.PP, m *mocks.MockSetter, proxied bool) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, proxied).Return(true)
+			func(ppfmt pp.PP, m *mocks.MockSetter) {
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, api.TTL(1), false).Return(true)
 			},
 		},
 		"ip6only/setfail": {
+			ttlAuto,
+			proxiedNone,
 			false,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			nil,
 			mockproviders{ipnet.IP6: true},
-			func(ppfmt pp.PP, m *mocks.MockSetter, proxied bool) {
-				m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, proxied).Return(false)
+			func(ppfmt pp.PP, m *mocks.MockSetter) {
+				m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, api.TTL(1), false).Return(false)
 			},
 		},
 		"both": {
+			ttlAuto,
+			proxiedNone,
 			true,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			nil,
 			mockproviders{ipnet.IP4: true, ipnet.IP6: true},
-			func(ppfmt pp.PP, m *mocks.MockSetter, proxied bool) {
+			func(ppfmt pp.PP, m *mocks.MockSetter) {
 				gomock.InOrder(
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, proxied).Return(true),
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, proxied).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, api.TTL(1), false).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, api.TTL(1), false).Return(true),
 				)
 			},
 		},
 		"both/setfail1": {
+			ttlAuto,
+			proxiedNone,
 			false,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			nil,
 			mockproviders{ipnet.IP4: true, ipnet.IP6: true},
-			func(ppfmt pp.PP, m *mocks.MockSetter, proxied bool) {
+			func(ppfmt pp.PP, m *mocks.MockSetter) {
 				gomock.InOrder(
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, proxied).Return(false),
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, proxied).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, api.TTL(1), false).Return(false),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, api.TTL(1), false).Return(true),
 				)
 			},
 		},
 		"both/setfail2": {
+			ttlAuto,
+			proxiedNone,
 			false,
 			map[ipnet.Type]bool{ipnet.IP4: true, ipnet.IP6: true},
 			nil,
 			mockproviders{ipnet.IP4: true, ipnet.IP6: true},
-			func(ppfmt pp.PP, m *mocks.MockSetter, proxied bool) {
+			func(ppfmt pp.PP, m *mocks.MockSetter) {
 				gomock.InOrder(
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, proxied).Return(true),
-					m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, proxied).Return(false),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain4, ipnet.IP4, netip.Addr{}, api.TTL(1), false).Return(true),
+					m.EXPECT().Set(gomock.Any(), ppfmt, domain6, ipnet.IP6, netip.Addr{}, api.TTL(1), false).Return(false),
 				)
 			},
 		},
 	} {
 		tc := tc
-		for proxied, proxiedByDomain := range map[bool]mockproxied{false: proxiedNone, true: proxiedBoth} {
-			t.Run(name, func(t *testing.T) {
-				mockCtrl := gomock.NewController(t)
-				ctx := context.Background()
-				conf := config.Default()
-				conf.Domains = domains
-				conf.ProxiedByDomain = proxiedByDomain
-				mockPP := mocks.NewMockPP(mockCtrl)
-				if tc.prepareMockPP != nil {
-					tc.prepareMockPP(mockPP)
+		t.Run(name, func(t *testing.T) {
+			mockCtrl := gomock.NewController(t)
+			ctx := context.Background()
+			conf := config.Default()
+			conf.Domains = domains
+			conf.TTL = tc.ttl
+			conf.Proxied = tc.proxied
+			mockPP := mocks.NewMockPP(mockCtrl)
+			if tc.prepareMockPP != nil {
+				tc.prepareMockPP(mockPP)
+			}
+			for _, ipnet := range [...]ipnet.Type{ipnet.IP4, ipnet.IP6} {
+				updater.MessageShouldDisplay[ipnet] = tc.MessageShouldDisplay[ipnet]
+				if !tc.prepareMockProvider[ipnet] {
+					conf.Provider[ipnet] = nil
+					continue
 				}
-				for _, ipnet := range [...]ipnet.Type{ipnet.IP4, ipnet.IP6} {
-					updater.MessageShouldDisplay[ipnet] = tc.MessageShouldDisplay[ipnet]
-					if !tc.prepareMockProvider[ipnet] {
-						conf.Provider[ipnet] = nil
-						continue
-					}
 
-					conf.Provider[ipnet] = mocks.NewMockProvider(mockCtrl)
-				}
-				mockSetter := mocks.NewMockSetter(mockCtrl)
-				if tc.prepareMockSetter != nil {
-					tc.prepareMockSetter(mockPP, mockSetter, proxied)
-				}
-				ok := updater.ClearIPs(ctx, mockPP, conf, mockSetter)
-				require.Equal(t, tc.ok, ok)
-			})
-		}
+				conf.Provider[ipnet] = mocks.NewMockProvider(mockCtrl)
+			}
+			mockSetter := mocks.NewMockSetter(mockCtrl)
+			if tc.prepareMockSetter != nil {
+				tc.prepareMockSetter(mockPP, mockSetter)
+			}
+			ok := updater.ClearIPs(ctx, mockPP, conf, mockSetter)
+			require.Equal(t, tc.ok, ok)
+		})
 	}
 }