# DNS providers

Every fact here exists so you learn it from this page, not by damaging a
zone. Read it before you touch a real one.

## A provider is a package, not a built-in vendor

Talking to Hostinger or Cloudflare is an ordinary package: `devmachine
packages add hostinger`, run on the machine, never on your computer.
There is no vendor list inside the CLI binary.

That gives one safety property: **a provider nobody installed cannot be
written to.** No code path reaches a registrar's write API unless its
package is on the lock file for that machine. Adding a provider is a
decision with a diff.

## What the provider gets from the shell

`PATH`, `HOME`, and the one token its own credential declares — nothing
else. The CLI sources `/etc/devmachine/<credential>/env` right before
running the entrypoint, so nothing else leaks in. A provider that reads a
variable its credential does not declare works by accident on one
machine and breaks everywhere else.

The token never appears in a command argument, where `ps` would show it
to every account on the machine. Only the credential's name does, and
that name is not a secret.

## Hostinger

### RRsets, not records

Hostinger's API has no idea of a single record. It has **RRsets**: one
entry per `(name, type)`, holding a list of values. `www A` is one RRset
that can carry several addresses at once. `list` splits each RRset into
one entry per value, but every write has to work in RRset terms.

### `overwrite: false` appends, and the default is `true`

Hostinger's `PUT` takes a whole RRset and an `overwrite` flag. With
`false`, the request's values are **added** to what is already there —
pointing an existing name at a new address this way leaves both, a
round-robin nobody asked for. Leave the field out and Hostinger's own
default is `overwrite: true`, the destructive mode, so the provider
always sends it explicitly, decided by the code, never left to
Hostinger's default.

### Replacing a value: delete, then append — not one call

`upsert` must make a name hold **exactly** the new value. Hostinger gives
two ways: one `PUT` with `overwrite: true`, or a `DELETE` of the RRset
then a `PUT` with `overwrite: false`. The provider uses the second, at
the cost of two calls: Hostinger's own docs say `overwrite: true`
replaces every record in the payload, so a one-RRset `PUT` would empty
the rest of the zone. The cost of the two-call path is real — **the name
holds nothing for the gap between them**, so a resolver asking in that
window gets NXDOMAIN — but that beats a write whose blast radius is the
whole zone.

### A DNS-only token needs its zones configured

Hostinger's DNS API answers about one zone at a time and has no
list-zones endpoint. Provider selection normally reads the account's
domains from the Domains portfolio endpoint, but a least-privilege DNS
token gets a 403 there even though it can read its own zone. Set
`hostinger.zones` on the machine for that case.

### A delete filter takes the whole RRset

Hostinger's `DELETE` removes an entire `(name, type)`, not one value
inside it. Deleting one value out of several means: read the RRset,
delete it whole, then `PUT` back the values you meant to keep.

### Writes are asynchronous

A successful `PUT` or `DELETE` means "request accepted," not "done." A
`list` called right after can still show the old state — which is why
the provider never checks its own writes immediately. Give it a moment
before checking with `dns status` or `dns list`.

## Cloudflare

### `PUT` clears every field it was not given

Cloudflare's DNS record `PUT` replaces the whole record: send only
`content` and `ttl`, and `proxied`, `comment` and `tags` reset to
defaults. The provider always uses `PATCH` instead, which touches only
the fields in the request.

### No upsert: every write past a create needs an id

Cloudflare has no "make this name hold this value" call. Creating a
record returns an id; changing or deleting one needs that id, found by
listing first. So `upsert` always lists before it writes: create on no
match, `PATCH` by id on one match, refuse to guess on more than one
(`ambiguous`).

### An invisible zone is a 200, not a 404

Ask Cloudflare for a zone the token cannot see, and the answer is `200`
with an empty `result` array, not an error. The provider treats an empty
result as `zone_not_found` itself.

### `proxied` is always `false`

Certificates on this machine renew through a challenge that needs the
request to reach the machine directly. A proxied record answers from
Cloudflare's edge instead, and the challenge fails.

## Neither provider retries a rate limit

A `429` is reported as `rate_limited` and the provider stops. Retrying
inside the entrypoint would hide how throttled the registrar really is,
and could turn one throttled request into a silent hang. Cloudflare
blocks a token for five minutes after a single `429`, so retrying would
not even help.