@saastemly/better-dns
v0.1.0
Published
DNS as part of the deployment — declare records, plan the diff, apply it. Cloudflare and Simply.com.
Maintainers
Readme
better-dns
DNS as part of the deployment, not a console tab.
Standing up a shop means pointing a domain at it: an apex record for the storefront, a CNAME for the API worker, and whatever the mail provider asks for so order confirmations do not land in spam. Every one of those is done by hand, in a registrar's UI, and it is the reason "deployed" and "live" are two different days.
import { dns } from "@saastemly/better-dns";
import { cloudflareDns } from "@saastemly/better-dns/providers/cloudflare";
import { simplyDns } from "@saastemly/better-dns/providers/simply";
dns({
providers: {
cloudflare: cloudflareDns({ apiToken: env.CLOUDFLARE_DNS_TOKEN }),
simply: simplyDns({ apiKey: env.SIMPLY_API_KEY }),
},
zones: { "example.dk": "simply" },
records: [
{ key: "api", zone: "example.dk", name: "api", type: "CNAME", value: "app.workers.dev" },
],
})POST /dns/plan { zone } # what a sync would do. writes nothing
POST /dns/sync { zone, dryRun?, prune? } # make it so
GET /dns/records?zone=… # desired state
POST /dns/records # an operator adds one
GET /dns/syncs # what changed, and whenThe safety property
A zone is not this plugin's to own. It holds the customer's MX records, their domain verification TXT, the CNAME their newsletter tool asked for. Deleting one takes down something unrelated to the shop, and the damage is invisible from here — mail simply stops arriving.
So:
- A sync never removes a record at a name the app has not declared. The
whole zone is read precisely so those records can be identified and left
alone; they are reported as
untouched. pruneis off by default, and even on it only reaches names the app declares. An extra record at a managed name is reported as aconflictrather than deleted.- Every apply can be a dry run first, and it is the same computation — a plan an operator approved and an apply that did something else would make the approval meaningless.
zonesis an allow-list. A zone that is not in it is refused rather than falling through to whichever provider happens to be first, which on a typo would write your records into somebody else's registrar account and report success.- Removing a record from the desired state does not remove it from the zone. Deleting from a list and deleting from the internet should not be the same button.
Providers
| | auth | zone is addressed by |
|---|---|---|
| Cloudflare | API token, Zone:DNS:Edit (+ Zone:Zone:Read, or pass zoneIds) | an opaque id, looked up once and cached |
| Simply.com | the API key alone, as a bearer token — or accountName for Basic | the domain name itself |
Four methods to add another: listRecords, createRecord, updateRecord,
deleteRecord. Zone discovery lives inside the provider on purpose, because
every provider identifies a zone differently and the plugin should not know
which one it is talking to.
What the plan is careful about
Real zones make several things look like drift when they are not, and a sync that rewrote a record every time would eventually rewrite the wrong one:
- TXT quoting. Providers disagree; compared unquoted.
- Trailing dots and case on CNAME/MX/NS targets.
- Cloudflare's automatic TTL — it stores
1for anything proxied, whatever was asked for. - The proxy flag is only compared when the declaration mentions it, since most providers have no such concept.
- Record sets — three MX at one name are matched by value, not by position, so declaring one does not rewrite the backup mail server into the primary.
