Skip to content

Capabilities and the UI

A registrar's declared capabilities decide which controls Core draws — at checkout, in the client domain manager and admin page. Declare one to show it; omit it to hide it.

9 min readUpdated Aug 15, 2026
On this page

A registrar extension never renders a screen of its own. Core owns every surface a domain touches — the checkout domain step, the admin domain operations page, the client domain manager, the Domain Registrars connection form — and decides what to draw from one thing your driver declares: its capabilities. Declare a capability and Core shows the control. Omit it and Core hides it. There is no third state where a button appears and then fails when clicked. Core never enumerates registrar names; every surface asks whether the registrar backing this domain supports the action, and draws accordingly.

This article covers the RegistrarCapability vocabulary, exactly what each capability lights up across the three domain surfaces, and the "honest capabilities" principle that makes a partial registrar first-class: Core asks whether you support an action before it ever shows the control.

The capability vocabulary#

Capabilities are string constants on App\DomainRegistrars\RegistrarCapability. There are fourteen, grouped by what they govern:

php
namespace App\DomainRegistrars;

final class RegistrarCapability
{
    // Provision / transact
    public const REGISTER = 'register';            // register a new domain at checkout
    public const TRANSFER = 'transfer';            // inbound transfer (needs an EPP/auth code)
    public const RENEW = 'renew';                  // renew for N years

    // Delegation + naming
    public const NAMESERVERS = 'nameservers';              // change delegated nameservers
    public const CHILD_NAMESERVERS = 'child_nameservers';  // private (glue) nameservers
    public const DNS = 'dns';                              // registrar-hosted DNS zone records

    // Transfer + security controls
    public const EPP_CODE = 'epp_code';            // retrieve the EPP/auth code via API
    public const REGISTRAR_LOCK = 'registrar_lock';// get/set the transfer lock
    public const ID_PROTECTION = 'id_protection';  // enable/disable WHOIS/ID privacy
    public const AUTO_RENEW = 'auto_renew';        // toggle registrar-side auto-renew

    // Records + introspection
    public const CONTACTS = 'contacts';            // get/update registrant/admin/tech contacts
    public const SYNC = 'sync';                    // pull status + expiry + nameservers back
    public const TRANSFER_STATUS = 'transfer_status'; // poll an in-progress transfer

    // Admin-only
    public const PRICE_SYNC = 'price_sync';        // import TLD pricing from the registrar

    public const ALL = [ /* all fourteen, in the order above */ ];
}

RegistrarCapability::ALL is the full set — the sensible declaration for a registrar that genuinely does everything. Your capabilities() method returns some subset of it.

Before the table, the one thing that makes a registrar different from a panel. Domain availability search is deliberately not a capability. Core checks availability itself — RDAP first, then WHOIS on port 43, then a DNS probe — through App\Services\DomainAvailabilityService. There is no checkAvailability in the contract and nothing in RegistrarCapability for it. So search works on a fresh install with no registrar configured, and one slow or rate-limited registrar API can never gate the storefront's search box. Your driver is consulted only to register or transfer a domain at checkout, and to manage its lifecycle afterwards — everything below is one of those two jobs.

What each capability lights up#

Each capability maps to one or more concrete controls. Declare only the capabilities behind which you have a working method, because Core will surface every control in this table the moment you list its capability.

CapabilityConstantWhat it lights up in Core
registerREGISTERProvisioning a domain on a paid order, plus the admin Register action
transferTRANSFERInbound transfer at checkout, plus the admin Transfer action (needs an EPP/auth code)
renewRENEWBilling-driven renewal, plus the admin Renew action
nameserversNAMESERVERSClient and admin Manage Nameservers (delegation; changeNameservers)
child_nameserversCHILD_NAMESERVERSThe client Private Nameservers card (glue hosts; registerNameserver / modifyNameserver / deleteNameserver / getNameservers)
dnsDNSRegistrar-hosted DNS zone-record management
epp_codeEPP_CODEThe admin Get EPP button (fetch via API). Omit it and the client EPP control self-adapts — see below
registrar_lockREGISTRAR_LOCKClient and admin transfer-lock toggle (lockDomain / unlockDomain)
id_protectionID_PROTECTIONClient and admin WHOIS/ID-privacy toggle (enableIdProtection / disableIdProtection)
auto_renewAUTO_RENEWClient and admin auto-renew toggle (setRenewInfo)
contactsCONTACTSClient Contact Information and admin Contacts editor (getContactInfo / updateContactInfo)
syncSYNCAdmin Sync — pull status, expiry and nameservers back (sync)
transfer_statusTRANSFER_STATUSAdmin Transfer Status polling (getTransferStatus)
price_syncPRICE_SYNCThe admin TLD-pricing import (syncPricing)

The same declarations drive three surfaces. At checkout, register and transfer decide whether a domain can be bought at all through this registrar — one provisions a paid domain order, the other runs the inbound transfer and collects the customer's EPP/auth code. In the client domain manager, the customer sees only the self-service controls their registrar backs: delegation, the private-nameserver card, the lock/privacy/auto-renew toggles and the contact editor. On the admin domain operations page, an operator additionally gets the reconciliation tools — Sync, Transfer Status and TLD-pricing import. Anything you do not declare is simply not on any of them.

One control is worth calling out as the standout registrar-specific behaviour. The client EPP-retrieval control is always visible; only the admin API button is gated. Declare epp_code and the admin gets a Get EPP button that calls your API. Omit it — as a registrar that emails the auth code to the registrant must — and the client EPP control does not disappear: it self-adapts, showing manual retrieval instructions instead of an API fetch. The customer always has a way to start a transfer out; the capability only decides whether that way is an API call or a set of instructions.

Core asks before it shows#

Every domain surface routes its decision through one seam, RegistrarResolver::supports():

php
public function supports(Domain $domain, string $capability): bool
{
    return in_array($capability, $this->capabilitiesFor($domain), true);
}

The interesting part is capabilitiesFor(). It does not load your driver to answer — it reads the capability list off the installed extension's signed manifest (a cheap, signature-checked lookup) and runs it through RegistrarCapability::sanitize().

Two properties follow. First, because gating reads the manifest and never executes driver code, a bug in your driver can never break a control's visibility — the UI has already decided what to draw before any of your methods run. Second, a domain whose configured registrar profile has no matching installed, entitled extension resolves to NullRegistrar, whose capabilities() is []. Either way an unresolved registrar declares nothing, so the domain degrades to a read-only view rather than throwing.

The payoff is the design rule that runs through the whole registrar system: an undeclared action is hidden, never shown-and-failing. Customers and operators only ever see controls that lead to a real registrar API call.

Declaring capabilities in your driver#

There are two ways a driver ends up with a capability set, and they have opposite defaults. Pick deliberately.

Extend `AbstractRegistrar` (recommended). Its capabilities() returns an empty array by default, so nothing is declared until you opt in. You override the method to list exactly what you implement:

php
use App\DomainRegistrars\AbstractRegistrar;
use App\DomainRegistrars\RegistrarCapability;

class MyRegistrar extends AbstractRegistrar
{
    public function capabilities(): array
    {
        return [
            RegistrarCapability::REGISTER,
            RegistrarCapability::RENEW,
            RegistrarCapability::NAMESERVERS,
            RegistrarCapability::SYNC,
        ];
    }

    // ...override only the methods for the capabilities above
}

This is the safe path for any registrar that does not do everything: because the default hides all controls, a half-built registrar is safe rather than broken — a control can only appear once you have both declared its capability and written its method.

Implement `RegistrarInterface` directly. If you pull in RegistrarModuleTrait yourself instead of extending AbstractRegistrar, the trait's default capabilities() returns RegistrarCapability::ALL — the right choice only for a registrar that genuinely implements every capability. AbstractRegistrar starts you at nothing and makes you add; the trait starts you at everything and makes you subtract. A partial registrar should extend AbstractRegistrar so its default is honest.

Whichever path you choose, the manifest's `capabilities` array must match your driver's `capabilities()` exactly. Core reads the manifest to gate the UI (see above) and trusts the driver at runtime; if they disagree, a control can appear that the driver refuses, or vice versa. One safety net applies to both paths and both sources: RegistrarCapability::sanitize() intersects whatever you declare with the recognised set, so a typo like 'auto_renwe' is silently dropped rather than lighting up a control. A misspelled capability is a missing capability, never an error.

Honest capabilities: the Namecheap example#

The worked example in the repo is deliberately not a full-house declaration. Namecheap declares eleven of the fourteen capabilities — and that is the correct, first-class answer, not a shortcut.

php
public function capabilities(): array
{
    return [
        RegistrarCapability::REGISTER, RegistrarCapability::TRANSFER, RegistrarCapability::RENEW,
        RegistrarCapability::NAMESERVERS, RegistrarCapability::CHILD_NAMESERVERS,
        RegistrarCapability::REGISTRAR_LOCK, RegistrarCapability::ID_PROTECTION,
        RegistrarCapability::AUTO_RENEW, RegistrarCapability::CONTACTS,
        RegistrarCapability::SYNC, RegistrarCapability::TRANSFER_STATUS,
        // omit EPP_CODE, DNS, PRICE_SYNC -> Core hides those controls
    ];
}

It omits three, and each omission is principled:

  • `epp_code` — Namecheap has no API command to retrieve the auth code; for security it is emailed to the registrant after a manual dashboard request. Declaring epp_code would put an admin Get EPP button in front of an operation that can only fail. By omitting it, Core hides that admin button and the client EPP control self-adapts to manual instructions, which is the truth.
  • `dns` — this driver does nameserver delegation and child (glue) hosts, not registrar-hosted DNS zone records. Declaring dns would surface a zone-record editor with no backing call.
  • `price_sync` — TLD-pricing import is not wired; operators set Core's pricing themselves. Declaring it would offer an admin import that does nothing.

The shape holds for any registrar: one with a registrar-hosted DNS API, an EPP-retrieval endpoint and a pricing feed would earn all fourteen. The failure mode to avoid is the opposite — declaring a capability whose semantics do not fit your registrar, in the hope of looking complete. That produces exactly the shown-and-failing button the whole system exists to prevent.

A partial registrar is first-class#

The capability system rewards honesty. There is no penalty for a smaller declaration: a registrar that only registers, renews and syncs — and nothing else — is a completely valid, shippable extension, and no core edits are ever needed to onboard it. Core simply renders a leaner domain page, with only the controls it can honour.

Declare what you actually implement, back each declared capability with the method behind it, keep the manifest and the driver in step, and let Core adapt every surface around your honest answer. Next, wire those declarations to real methods — the method contract specifies the exact return shape for each one; the overview shows how a registrar plugs into Core; and the quickstart walks a driver from empty class to installed extension.

Was this article helpful?
Still stuck?Contact support
Capabilities and the UI · Salieno Docs