1---
2title: DNS
3description: Hetzner Cloud DNS with Alchemy — Zones and RecordSets as resources, plus ReadDns/WriteDns bindings for querying and mutating records from Effects.
4---
5
6Hetzner Cloud DNS is part of the same project and the same
7`HCLOUD_TOKEN` as your servers — no separate account, console, or
8token. Alchemy models it two ways: **resources** (`Zone`,
9`RecordSet`) for records that belong to your stack, and **bindings**
10(`ReadDns`/`WriteDns`) for code that queries or mutates records
11dynamically.
12
13## Zones
14
15A [`Zone`](/providers/hetzner/zone) is an apex domain you own —
16`name` must be a real registrable domain (no subdomains, no
17trailing dot):
18
19```typescript
20import * as Hetzner from "alchemy/Hetzner";
21
22export const Dns = Hetzner.Zone("dns", {
23 name: "example.com",
24 ttl: 300,
25});
26```
27
28Changing `name` replaces the Zone. The resolved resource exposes
29`assignedNameservers` — set those at your registrar to delegate the
30domain — plus `delegationStatus` to check whether that's done.
31
32## RecordSets
33
34A [`RecordSet`](/providers/hetzner/recordset) is one `(name, type)`
35pair and **all** of its records. Values accept resource outputs, so
36pointing your domain at a Load Balancer is a reference, not a
37copy-pasted IP:
38
39```typescript
40const www = yield* Hetzner.RecordSet("www", {
41 zone: Dns,
42 name: "www",
43 type: "A",
44 ttl: 300,
45 records: [{ value: lb.ipv4.as<string>() }],
46});
47```
48
49`name` is relative to the zone (`@` for the apex). Changing `zone`,
50`name`, or `type` replaces the RecordSet; `records` and `ttl`
51update in place. Because a RecordSet owns the whole `(name, type)`
52pair, keep each pair in exactly one resource.
53
54## Query & mutate from Effects
55
56For DNS that isn't part of the declared stack — health-dependent
57records, dynamic entries, lookups — bind a typed client to a zone.
58Three bindings, each with a matching layer:
59
60| Binding | Layer | Client |
61| --- | --- | --- |
62| `Hetzner.ReadDns(zone)` | `Hetzner.ReadDnsHttp` | `getRecordSet`, `listRecordSets` |
63| `Hetzner.WriteDns(zone)` | `Hetzner.WriteDnsHttp` | `createRecordSet`, `setRecordSetRecords`, `addRecordSetRecords`, `removeRecordSetRecords`, `updateRecordSet`, `deleteRecordSet`, `changeRecordSetTtl`, … |
64| `Hetzner.ReadWriteDns(zone)` | `Hetzner.ReadWriteDnsHttp` | both |
65
66The zone is fixed when you bind — pass the Zone declaration
67directly, and calls never pass a zone id:
68
69```typescript
70import * as Alchemy from "alchemy";
71import * as Hetzner from "alchemy/Hetzner";
72import * as Effect from "effect/Effect";
73import { Dns } from "./dns.ts";
74
75const SyncRecords = Alchemy.Action(
76 "SyncRecords",
77 Effect.gen(function* () {
78 const dns = yield* Hetzner.ReadWriteDns(Dns);
79
80 return Effect.fn(function* () {
81 const listed = yield* dns.listRecordSets({ type: ["A"] });
82
83 const { action } = yield* dns.setRecordSetRecords("api", "A", {
84 records: [{ value: "203.0.113.20" }],
85 });
86 yield* Hetzner.waitForZoneAction(action);
87 });
88 }).pipe(Effect.provide(Hetzner.ReadWriteDnsHttp)),
89);
90```
91
92:::caution[DNS mutations are asynchronous]
93Every mutating RRSet call returns an `action` that Hetzner applies
94in the background — a read right after may not see the change.
95Always `yield* Hetzner.waitForZoneAction(action)` before depending
96on the result. (Zone actions have their own API namespace — use
97`waitForZoneAction`, not the compute `waitForAction`.)
98:::
99
100Writes also require your `HCLOUD_TOKEN` to be a **Read & Write**
101token — a read-only token fails mutations with a typed `Forbidden`.
102
103## Put it together: a domain on your Load Balancer
104
105```typescript
106const cert = yield* Hetzner.Certificate("cert", {
107 type: "managed",
108 domainNames: ["example.com", "www.example.com"],
109});
110
111yield* Hetzner.RecordSet("apex", {
112 zone: Dns,
113 name: "@",
114 type: "A",
115 records: [{ value: lb.ipv4.as<string>() }],
116});
117```
118
119The managed [Certificate](/hetzner/networking#tls-termination) is
120issued via this same DNS, so the Zone must exist (and be delegated)
121first — Alchemy orders that for you through the references.
122
123## Where next
124
125- [Networking](/hetzner/networking) — the Load Balancer these
126 records usually point at.
127- [`Zone`](/providers/hetzner/zone) and
128 [`RecordSet`](/providers/hetzner/recordset) references.