DNS
Hetzner Cloud DNS is part of the same project and the same
HCLOUD_TOKEN as your servers — no separate account, console, or
token. Alchemy models it two ways: resources (Zone,
RecordSet) for records that belong to your stack, and bindings
(ReadDns/WriteDns) for code that queries or mutates records
dynamically.
A Zone is an apex domain you own —
name must be a real registrable domain (no subdomains, no
trailing dot):
import * as Hetzner from "alchemy/Hetzner";
export const Dns = Hetzner.Zone("dns", { name: "example.com", ttl: 300,});Changing name replaces the Zone. The resolved resource exposes
assignedNameservers — set those at your registrar to delegate the
domain — plus delegationStatus to check whether that’s done.
RecordSets
Section titled “RecordSets”A RecordSet is one (name, type)
pair and all of its records. Values accept resource outputs, so
pointing your domain at a Load Balancer is a reference, not a
copy-pasted IP:
const www = yield* Hetzner.RecordSet("www", { zone: Dns, name: "www", type: "A", ttl: 300, records: [{ value: lb.ipv4.as<string>() }],});name is relative to the zone (@ for the apex). Changing zone,
name, or type replaces the RecordSet; records and ttl
update in place. Because a RecordSet owns the whole (name, type)
pair, keep each pair in exactly one resource.
Query & mutate from Effects
Section titled “Query & mutate from Effects”For DNS that isn’t part of the declared stack — health-dependent records, dynamic entries, lookups — bind a typed client to a zone. Three bindings, each with a matching layer:
| Binding | Layer | Client |
|---|---|---|
Hetzner.ReadDns(zone) |
Hetzner.ReadDnsHttp |
getRecordSet, listRecordSets |
Hetzner.WriteDns(zone) |
Hetzner.WriteDnsHttp |
createRecordSet, setRecordSetRecords, addRecordSetRecords, removeRecordSetRecords, updateRecordSet, deleteRecordSet, changeRecordSetTtl, … |
Hetzner.ReadWriteDns(zone) |
Hetzner.ReadWriteDnsHttp |
both |
The zone is fixed when you bind — pass the Zone declaration directly, and calls never pass a zone id:
import * as Alchemy from "alchemy";import * as Hetzner from "alchemy/Hetzner";import * as Effect from "effect/Effect";import { Dns } from "./dns.ts";
const SyncRecords = Alchemy.Action( "SyncRecords", Effect.gen(function* () { const dns = yield* Hetzner.ReadWriteDns(Dns);
return Effect.fn(function* () { const listed = yield* dns.listRecordSets({ type: ["A"] });
const { action } = yield* dns.setRecordSetRecords("api", "A", { records: [{ value: "203.0.113.20" }], }); yield* Hetzner.waitForZoneAction(action); }); }).pipe(Effect.provide(Hetzner.ReadWriteDnsHttp)),);Writes also require your HCLOUD_TOKEN to be a Read & Write
token — a read-only token fails mutations with a typed Forbidden.
Put it together: a domain on your Load Balancer
Section titled “Put it together: a domain on your Load Balancer”const cert = yield* Hetzner.Certificate("cert", { type: "managed", domainNames: ["example.com", "www.example.com"],});
yield* Hetzner.RecordSet("apex", { zone: Dns, name: "@", type: "A", records: [{ value: lb.ipv4.as<string>() }],});The managed Certificate is issued via this same DNS, so the Zone must exist (and be delegated) first — Alchemy orders that for you through the references.
Where next
Section titled “Where next”- Networking — the Load Balancer these records usually point at.
ZoneandRecordSetreferences.