Domain DNS Hooks
The ten hooks setting where a domain points: name servers, DNS records, DNSSEC and child name servers.
Overview
What these four surfaces share: they all work live. Reads and writes alike send a real request to the provider, and our record is only a mirror.
Learn the pattern once and all four follow it: a gate in front of a write, an event behind it, and on the read side a filter handing you the list by reference.
Reference
Stopping a name server write
Runs before the name server list is written to the provider, with the input already checked. Both paths, panel and customer panel, pass through here.
ns1..ns4 values, in order.null or an empty string lets it carry on.Hook::add('gate:domain.nameservers_save', 10, function ($service, $dns) {
// Changing name servers mid-transfer breaks the transfer.
if (($service['status'] ?? '') === 'transfer')
return 'Name servers cannot change while a transfer runs.';
return null;
});Following a name server change
Runs after the provider call succeeded, the values landed on the service and the activity was recorded.
Hook::add('action:domain.nameservers_saved', 10, function ($service, $dns) {
DnsMonitor::resync($service['name'] ?? '', $dns);
});Stopping a DNS record change
Runs before a DNS record changes at the provider. Adding, updating and deleting all three pass here, and the third parameter says which.
type, name, value, identity. On an update also ttl and priority.create, update or delete.null or an empty string lets it carry on.Hook::add('gate:domain.dns_record_save', 10, function ($service, $record, $task) {
$type = strtoupper((string) ($record['type'] ?? ''));
// Stop a delete that cuts the mail flow; adds and updates carry on.
if ($task === 'delete' && $type === 'MX')
return 'An MX record cannot be deleted; change the mail settings first.';
return null;
});Following a DNS record change
Runs after the change at the provider finished successfully.
Hook::add('action:domain.dns_record_saved', 10, function ($service, $record, $task) {
Audit::dns($service['name'] ?? '', $task, $record);
});Stopping a DNSSEC record action
Runs before a DS record is created or deleted at the provider.
digest, key_tag, digest_type, algorithm. On a delete also identity.create or delete.null or an empty string lets it carry on.Hook::add('gate:domain.dnssec_save', 10, function ($service, $record, $verb) {
// Deleting the last DS record switches validation off entirely.
if ($verb === 'delete' && Acme::dsCount((int) ($service['id'] ?? 0)) <= 1)
return 'The last DNSSEC record cannot be deleted.';
return null;
});Following a DNSSEC change
Runs after the DS record landed at the provider.
Hook::add('action:domain.dnssec_saved', 10, function ($service, $record, $verb) {
Audit::dnssec($service['name'] ?? '', $verb, $record['key_tag'] ?? '');
});Stopping a child name server action
Runs before a child name server is added at the provider or removed from it.
ns1.example.com.create or delete.null or an empty string lets it carry on.Hook::add('gate:domain.child_ns_save', 10, function ($service, $host, $ip, $verb) {
if ($verb === 'delete') return null;
// Do not glue a name to an address outside your own block.
if (!Acme::ownsIp($ip)) return 'That address is not ours.';
return null;
});Following a child name server change
Runs after the child name server landed at the provider.
Hook::add('action:domain.child_ns_saved', 10, function ($service, $host, $ip, $verb) {
Ipam::glue($verb, $host, $ip);
});Changing the child name server list
Runs after the list read from the provider was normalised, before it reaches the screen.
ns and ip. Keep the shape; the screen draws to this contract.Hook::add('filter:domain.child_ns_list', 10, function (&$list, $service) {
// Sort without breaking the shape: the screen wants ns and ip keys.
usort($list, fn ($a, $b) => strcmp($a['ns'] ?? '', $b['ns'] ?? ''));
});Changing the DNSSEC list
Runs after the DS records read from the provider were normalised.
identity, digest, key_tag, digest_type, algorithm. A delete sends identity back; do not drop it.Hook::add('filter:domain.dnssec_records', 10, function (&$records, $service) {
// KEEP the identity field: a delete targets the record with it.
$records = array_values(array_filter($records,
fn ($r) => (int) ($r['algorithm'] ?? 0) !== 5)); // hide the old algorithm
});Pitfalls
The DNS record gate is the same hook for adding, updating and deleting. Without reading the third parameter, a rule you wrote to "stop deletes" stops adds as well. The same holds for the DNSSEC and child name server gates, where the action sits in the fourth parameter.
Read filters hand you the array the screen draws directly. Renaming a key or dropping identity breaks more than the list: it leaves the delete call without a target, because the record is found by that field.
The name server gate sits on the panel path and the customer path alike. A rule written as "the customer should not" stops the operator too. To tell the paths apart, look at the calling context rather than at the service record.
The event says the provider accepted the change, not that the world sees it. Propagation takes minutes. A listener acting on the new value gets the old answer where it checks straight away.
Related Articles
- Domain Acquisition Hooks
- Domain Hooks
- Writing a Hook Listener
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.