Domain Capability Hooks
The two hooks that hand a single domain capability to a provider other than the registrar.
Overview
A domain service resolves to exactly one module: its registrar. So a provider that is not the registrar has no slot of its own. A DNS server, a forwarding service, a WHOIS privacy provider: none of them can register a domain, and none of them can be the module.
These two hooks open that door. One decides what the screens offer, the other decides who answers the call. The registrar keeps every capability you do not claim.
The capability names are shared: nameservers, child_ns, transfer_lock, auth_code, dns_records, dns_edit, dnssec, email_forwarding, domain_forwarding, whois, whois_privacy.
Use both. Only the matrix gives you a tab with nothing behind it. Only the provider gives you a capability nobody can reach.
Reference
Opening a capability
Runs once the module probe is done and before the tabs are built. Four screens compute this matrix separately and every one of them calls the hook: the customer panel, the customer API, the management panel and the admin API.
name => bool. Raw names on all four screens; the admin payload renames them to has_* afterwards.service the domain record, instance the registrar or null, surface one of website, client_api, admin_panel, admin_api.Hook::add('filter:domain.capabilities', 10, function (&$caps, $context = []) {
$service = $context['service'] ?? [];
if (!MyDns::handles((int) ($service['id'] ?? 0))) return;
$caps['dns_records'] = true;
$caps['dns_edit'] = true;
});Answering the call
Runs when a screen asks for the module instance and names the capability it is about to use. A plain lookup never reaches this hook, so the service lifecycle is untouched.
false. Assign an object and the call goes to it; anything else is ignored.service the domain record, capability the name being asked for, instance the registrar or null.Hook::add('filter:domain.capability_provider', 10, function (&$provider, $context = []) {
if (!in_array((string) ($context['capability'] ?? ''), ['dns_records', 'dns_edit'], true)) return;
$service = $context['service'] ?? [];
if (!MyDns::handles((int) ($service['id'] ?? 0))) return;
$provider = new MyDnsProvider($service);
});What the provider must look like
Your provider carries the same method names the registrar would. Declare them as real methods.
class MyDnsProvider
{
public string $error = '';
public function set_service(int|array $service = []): void {}
public function get_dns_records(): array|false
{
// ... read the zone
return $records;
}
public function add_dns_record($type, $name, $value, $ttl, $priority): bool
{
// ... write one record
return true;
}
}Pitfalls
A magic __call() will not work. Every screen asks method_exists() before it calls anything, and a method served by __call() is invisible to that check. Your provider is then never called and nothing reports an error. Declare each method for real.
One hook alone is not enough. Open the matrix without a provider and the customer opens an empty tab. Hand over a provider without opening the matrix and the tab never shows up.
Four screens, one answer. The matrix is computed separately on each screen. A capability opened in the panel but missed in the API reads as the tab being visible while the endpoint refuses the request.
The lifecycle is not yours. A capability handover only covers the capability. Creation, renewal, transfer and cancellation keep going to the registrar, which is what keeps this safe.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.