Skip to content

How Salieno registrar extensions work

A registrar extension is code only — one PHP driver plus a salieno.json manifest — that registers, transfers, renews and manages domains. Search stays registrar-free.

10 min readUpdated Aug 15, 2026
On this page

A registrar extension teaches Salieno Core to register, transfer, renew and manage domains through a registrar's API — Namecheap, Enom, ResellerClub, or anything else with one. This is the front-door article for the series: read it to build the mental model, then follow the links at the end into the parts you need.

A registrar is the domain-side analogue of a panel extension — same trust model, same distribution path — but it differs in one load-bearing way, so we start there.

Search is registrar-free#

This is the defining difference from panels: domain availability search never touches your registrar. Core answers "is example.com available?" itself — before any registrar is involved, and even on an install with none configured.

It does this through App\Services\DomainAvailabilityService, which checks a name three ways, cheapest first:

  1. RDAP — the modern registration-data protocol, queried at the TLD's authoritative server.
  2. WHOIS — a direct socket to the registry on port 43 when RDAP has no answer.
  3. DNS probe — a nameserver lookup as the last-resort signal.

The consequence: the storefront search box works on a fresh install with zero registrars, and a slow, rate-limited or down registrar API can never gate it. Your driver is called only later — to register or transfer a name at checkout, and to manage that domain's lifecycle afterwards (renew, nameservers, lock, privacy, contacts, EPP, sync).

So there is no `checkAvailability` method in the contract, and availability is not a capability — don't add one; Core would not call it. A registrar is for buying and managing, never for searching.

Code, not screens#

A registrar extension is code only. It is two files: one PHP driver class and a salieno.json manifest.

code
your-registrar/
  salieno.json       # the manifest
  YourRegistrar.php  # the driver class (+ any helper classes under your namespace)

There are no views, no templates, no CSS, nothing to style. Core owns every screen — the checkout register/transfer step, the client domain manager, the admin domain page, the Domain Registrars config form — and renders them generically from what your driver returns and declares. You never touch a Blade file, and you never edit Core.

That is the whole reason the model works: because Core does the rendering, a brand-new registrar plugs in and lights up everywhere it applies without a single change to Core. Your job is to implement the operations your registrar supports and return the exact shapes Core expects. Core does the rest.

The easiest way to write the driver is to extend App\DomainRegistrars\AbstractRegistrar. It ships safe "not supported" defaults for every method plus a trait of helpers for talking to a registrar API, so you override only the methods you actually implement. The one thing you must always add is capabilities(). The driver contract covers the method shapes in detail.

A driver is stateless: the registry instantiates it once with no constructor and passes the Domain to every method, so you read credentials and context per call, never from instance state. Trait helpers do the work — registrarOf($domain) (the connection profile behind the domain), getParam($registrar, 'api_key') (one stored credential), isSandbox($registrar), parseDomain($domain) (sld/tld), http() (a pre-configured client), and the success(...) / error(...) envelopes.

Where a registrar shows up#

Once a registrar is installed and a connection profile is configured, the same driver drives four surfaces, and Core draws all of them:

  • Checkout — the register-or-transfer step when a customer buys a domain. register provisions a new name; transfer starts an inbound transfer (and needs an EPP/auth code).
  • The client domain manager — auto-renew, transfer lock, ID/WHOIS protection, nameserver delegation, private (child) nameservers, contact information, and EPP retrieval, each shown only if the registrar backs it.
  • The admin domain page — the operator's operations on a single domain: register, renew, manage nameservers, edit contacts, toggle ID protection, transfer, get EPP, lock/unlock, poll transfer status, and sync.
  • Domain Registrars config + Test connection — the settings screen where an operator enters API credentials into a connection profile and clicks Test connection to verify them against the registrar.

Core never enumerates registrar names. Every one of those controls is gated on capabilities(), not on "is this Namecheap?" — so a registrar that can't retrieve EPP codes, or has no transfer lock, simply doesn't show those controls. You supply data and behaviour; Core supplies pixels.

Capabilities: you declare what you do#

Not every registrar does everything. Namecheap has no API to fetch an EPP code; a lean registrar might not host DNS zones. So Core never assumes — it asks.

Your driver's capabilities() method returns a subset of the fourteen known keys. Before Core renders any control, it checks whether you declared the matching capability. Declare it and the control appears; omit it and the control is hidden. A button is never shown-and-failing — if it is on screen, your registrar backs it.

php
namespace Salieno\Registrar\Namecheap;

use App\DomainRegistrars\AbstractRegistrar;
use App\DomainRegistrars\RegistrarCapability;
use App\Models\Domain;

class Namecheap extends AbstractRegistrar
{
    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 for your registrar
        ];
    }

    public function register(Domain $domain, array $nameservers): array
    {
        $registrar = $this->registrarOf($domain);
        // ...call your registrar's API with $this->getParam($registrar, ...) + $this->http()...
        return $this->success('Domain registered successfully.');
    }

    // ...override only the methods you support...
}

Extending AbstractRegistrar declares nothing by default — capabilities() returns [], so every control stays hidden until you opt in. That is deliberate: a half-built registrar is safe, not broken. Here is what each of the fourteen capabilities lights up:

CapabilityLights up
registerprovisioning a domain on a paid order + admin "Register"
transferinbound transfer at checkout + admin "Transfer" (needs an EPP/auth code)
renewbilling-driven renewal + admin "Renew"
nameserversclient + admin "Manage Nameservers" (delegation)
child_nameserversclient Private Nameservers card (glue hosts)
dnsregistrar-hosted DNS zone-record management
epp_codeadmin "Get EPP" via API — omit it and the client sees manual-retrieval instructions
registrar_lockclient + admin transfer-lock toggle
id_protectionclient + admin WHOIS/ID-privacy toggle
auto_renewclient + admin auto-renew toggle
contactsclient Contact Information + admin Contacts editor
syncadmin "Sync" (pull status / expiry / nameservers back)
transfer_statusadmin "Transfer Status" polling
price_syncadmin TLD-pricing import

A crucial detail: the capabilities array in salieno.json is what Core reads to gate the UI without loading your driver — a cheap, signature-checked lookup. The authoritative source at runtime is the driver's capabilities() method. Keep the two identical; Core trusts the code. The capabilities guide goes surface by surface.

Worked example: registrars/namecheap declares eleven capabilities, omitting epp_code (Namecheap has no API for it — it emails the code to the registrant, so Core shows manual instructions), dns, and price_sync. Declaring a capability you can't truly back is the wrong move — it puts a control on screen that fails when clicked.

What the manifest carries#

The manifest is small but load-bearing. Its slug is the identity that ties everything together: it is both the marketplace product slug and the registrar_slug a connection profile stores, and the two must match. Beyond identity (schema, kind, name, version, namespace, driver, entry, requires_core), it declares two things that let a registrar onboard with no core edits:

  • capabilities — the informational copy of your driver's list, so the marketplace and UI can gate without loading code.
  • credentials — a list of {key, label, type, required, help} fields. Core renders the Domain Registrars config modal straight from it (type is text or password), so adding a registrar needs no core edits. The sandbox toggle is Core's own; read it in the driver with $this->isSandbox($registrar).

The manifest must satisfy two validators — the marketplace (kind, name, version, requires_core, entry) and the Core installer (schema, kind, slug, namespace, driver) — so include all of them. The full field reference lives in the manifest article, and the credential fields are covered in credentials & connection.

Distribution: marketplace-only, signed#

Registrars are distributed only through marketplace.salieno.com, and only in signed form. There is no local upload path. A folder copied onto a server will not run — not because of a config flag you could flip, but because of how Core resolves a driver.

Installed registrar code lives at storage/app/registrars/{slug}/, a runtime artifact that is gitignored and deliberately off the normal autoload path. Dropping a folder there loads nothing. Every time Core resolves a driver, App\Services\Registrars\RegistrarRegistry runs three gates in order:

  1. Registered — there is an enabled registrar_extensions row for the slug. A hand-copied folder has no row, so it never gets this far.
  2. Genuine — the stored signed artifact re-verifies against Core's pinned Ed25519 marketplace key, and each on-disk class file's SHA-256 matches the hash recorded from that verified artifact at install time. You cannot forge the signature, and you cannot edit the code after install without breaking the hash check.
  3. Entitled — the activated licence still owns this registrar, as decided by the marketplace (not by anything on the box). This is what makes "a copied folder won't run" true even when the folder is genuine: the same signed code copied onto an install that never bought it fails here. The check is cached briefly; if the marketplace is unreachable, Core falls back to the last confirmation within a grace window, then fails closed.

Only after all three gates pass does Core admit the slug to a scoped autoloader that requires only that extension's hash-matching files. Nothing else can trigger the load, and a slug that resolves to nothing falls back to a NullRegistrar — so a register, renew or transfer fails safely instead of running unverified code. The practical consequence for you as an author: never write state into your registrar's own folder — it would break the per-file hash check. Keep all state in what Core passes your methods and in the domain record. The security & trust model explains each gate in full.

Publishing follows from this. You submit the package; a reviewer approves it; on approval the marketplace signs the artifact with its key. Pricing is set at creation — free or a paid one-time purchase — and a buyer's entitlement covers every version. Operators install from the admin Extensions library (Domain Registrars → Registrar Extensions), which lists only the registrars their licence owns; on install Core auto-provisions an unconfigured connection profile to fill in and "Test connection". See publishing & updates for the flow.

Because a registrar is a stateless artifact, updates are painless. You publish a higher semver; installs show an "update available" badge and apply it with one click through the same signed path, an atomic code swap. No data is lost — connection profiles and every registered domain live in the database keyed by the registrar slug, which an update never changes; and since the entitlement covers all versions, updating a paid registrar never charges again.

Where to go next#

The rest of the series builds on this model:

  • [Quickstart](/registrar-development/registrar-quickstart) — scaffold a working driver and get it resolving.
  • [The manifest](/registrar-development/registrar-manifest) — every salieno.json field, explained.
  • [The driver contract](/registrar-development/registrar-contract) — each method's arguments and exact return shape.
  • [Capabilities](/registrar-development/registrar-capabilities) — the capability system in depth, surface by surface.
  • [Credentials & connection](/registrar-development/registrar-credentials) — the config form and sandbox toggle your manifest drives.
  • [Testing](/registrar-development/registrar-testing) — how to exercise a driver against a real registrar before you ship.
  • [Publishing & updates](/registrar-development/registrar-publish) — submit, get signed, release new versions.
  • [Security & trust](/registrar-development/registrar-security) — the three gates in full, and what they mean for how you write code.

Namecheap ships in the repo as the worked, full-featured example — eleven capabilities, honest about the three it omits — and is worth reading alongside these articles. Start with the quickstart when you are ready to write code.

Was this article helpful?
Still stuck?Contact support
How Salieno registrar extensions work