Feature Specification: Registrar providers of the operator path

Created: 2026-11-08 Status: Partially implemented Input: The use of the openprovider OpenTofu provider and of the Openprovider API in core/effects/tf/incus/operator/, core/effects/tf/openprovider/ and core/components/hosting-provider/, at d1cfa620c, issue #1908.

The tf-incus-operator method (tf-incus-operator.md) registers, transfers and keeps the domains of operator deployments at one registrar account. This spec gives the capabilities that a registrar MUST have before the operator path can use it. It comes from what the operator path needed from Openprovider, which is the only registrar of that path. A capability that only Openprovider has is not in this contract, unless the operator path needs it.

This spec does not select a second registrar, and it does not define the seam in the graph that selects a registrar. The registrar effects of the apex (registrars.md) are a different and smaller contract.

The contract has two parts:

  • A graph capability is what a plan or an apply of the operator graph needs. The OpenTofu provider of the registrar gives it, or a read beside the provider gives it.
  • An account operation is what a front end starts while no apply runs. The OpenTofu provider of the registrar can give it, or a command that talks to the registrar API can give it. The answers of the command do not change with the registrar, so a front end does not know which registrar the account is at.

User Scenarios & Testing (mandatory)

User Story 1 - Register a free domain under a cost limit (Priority: P1)

An operator submits a domain that no one holds. The graph registers the domain at the registrar account, and the registration costs no more than the limit of the hosting provider.

Why this priority: This is the only step of the operator path that spends money without a person who checks the price.

Independent Test: Apply the graph against a fake of the registrar API that gives a quote, one time below the limit and one time above it, and read the orders that the fake received.

Acceptance Scenarios:

  1. Given a free domain and a live quote at or below the limit, When the graph applies, Then the provider orders the registration one time, and the state has the cost, the expiry and the status.
  2. Given a live quote above the limit, When the graph applies, Then the apply fails, names the two amounts and orders nothing.
  3. Given a quote in a different currency than the limit, or no quote, When the graph applies, Then the apply fails and orders nothing.
  4. Given a premium domain, When the graph plans, Then the plan fails and orders nothing.
  5. Given a completed registration, When the graph applies again, Then the provider orders nothing.

User Story 2 - Delegate and sign a domain (Priority: P1)

The graph points the domain at the nameservers of the deployment, publishes glue when the nameservers are under the domain, and gives the registry the DNSSEC keys of the zone.

Why this priority: A registered domain that has no delegation serves nothing. A delegation that has wrong DNSSEC data makes the zone bogus, and a validating resolver then gives no answer.

Independent Test: Apply the graph against a fake of the registrar API with keys, then with an empty key list, and read the delegation and the keys that the fake holds.

Acceptance Scenarios:

  1. Given a set of nameservers, When the graph applies, Then the registration uses these nameservers.
  2. Given nameservers under the domain, When the graph applies, Then the registrar publishes glue with the IPv4 and IPv6 addresses of each nameserver.
  3. Given the keys of a signed zone, When the graph applies, Then the registry receives these keys in the form that the registrar takes.
  4. Given a domain with keys and a configuration with an empty key list, When the graph applies, Then the registrar removes the keys.
  5. Given a configuration that does not state keys, When the graph applies, Then the keys of the domain stay.

User Story 3 - Keep a domain that the account holds (Priority: P1)

The account holds the domain already, or a deployment that holds it ends. The graph adopts the domain without a new order, and a destroy does not delete it.

Why this priority: A wrong decision here deletes a live name or pays for it two times.

Independent Test: Import a domain from a fake of the registrar API, apply, then destroy, and read the calls that the fake received.

Acceptance Scenarios:

  1. Given a domain that the account holds, When the graph imports it by its name, Then the import succeeds without a warning about an auth code, and the next plan orders nothing.
  2. Given a change of the renewal, the nameservers or the admin and tech contacts, When the graph applies, Then the provider changes the domain in place.
  3. Given a deployment that ends, When the graph destroys it, Then the registrar keeps the domain, and the renewal goes off.
  4. Given a registrar read that fails, When the provider refreshes the domain, Then the domain stays in the state and the refresh fails.
  5. Given a nameserver group that different deployments share, When one deployment ends, Then the group stays at the registrar.

User Story 4 - Transfer a domain in (Priority: P2)

An operator has the auth code of a domain at a different account. The graph transfers the domain to the registrar account, under the same cost limit.

Why this priority: Migration between hosting providers needs it, but a deployment can start without it.

Independent Test: Apply the graph against a fake of the registrar API that answers with an in-progress status, then a completed status, then a refused status.

Acceptance Scenarios:

  1. Given an auth code and a quote at or below the limit, When the graph applies, Then the provider orders the transfer one time, with the nameservers, the contacts, the DNSSEC keys and the renewal of a registration.
  2. Given a transfer in progress, When the graph applies, Then the apply fails before the wait for the delegation, and the provider does not order again.
  3. Given a transfer that the registry refuses, When the graph applies, Then the apply fails and orders nothing, and an account operation can send the transfer again without a new order.

User Story 5 - Let a domain leave the account (Priority: P2)

An operator moves to a different hosting provider. A front end gets the auth code of the domain, and can make that code invalid when the domain stays.

Why this priority: An operator must be able to leave with their domain, but an operator seldom needs it.

Independent Test: Run the account operations against a fake of the registrar API, and read the lock, the code and the DNSSEC keys that the fake holds.

Acceptance Scenarios:

  1. Given a domain that the account holds, When a front end releases it, Then the domain has no lock, it has no DNSSEC keys, and the front end receives the auth code and a marker of this release.
  2. Given a registry that keeps no auth code, When a front end releases the domain, Then the registrar makes a new code first.
  3. Given a released domain, When a front end retakes it, Then the auth code changes, the lock is on and the marker is gone.
  4. Given a domain that the account does not hold, or that has a deleted status, When a front end runs an operation, Then the answer says that the account does not hold the domain, and nothing changes.

User Story 6 - Name the contacts of a domain (Priority: P2)

A registration names contacts by a handle. The contact holds personal data, and the state of a deployment does not.

Why this priority: The registries refuse a registration with wrong contacts, and the state of a deployment must not hold personal data.

Independent Test: Apply the contact against a fake of the registrar API, register a domain with its handle, and search the state of the deployment for the personal data.

Acceptance Scenarios:

  1. Given an extension with admin and tech contacts, and an extension without them, When the graph registers a domain in each, Then each registration names only the roles of its extension.
  2. Given a handle that the account does not have, When the graph plans, Then the plan fails before an order.
  3. Given a contact that the registry of the extension does not allow as owner, When the graph plans, Then the plan fails before an order.
  4. Given a change of the name of a contact, on a registrar that does not let a handle change its name, When the contact applies, Then the result is a new handle, and domains that use the old handle stay valid.
  5. Given a change of owner that the registrar lets through at no fee, When a front end runs it, Then the owner changes and the lock, the DNSSEC keys and the other contacts stay.
  6. Given a change of owner that needs a trade or a fee, When a front end runs it, Then the answer says that the change is not allowed, and nothing changes.

User Story 7 - Watch the domains of the account (Priority: P3)

The hosting provider sees when a registration expires, what its renewal costs, and when a domain leaves the account.

Why this priority: The renewal is automatic. The watch only makes a wrong renewal or a lost domain visible.

Independent Test: Run the account listing against a fake of the registrar API with more domains than one page holds.

Acceptance Scenarios:

  1. Given an account with many domains, When the watch runs, Then it receives the status, the expiry, the renewal state and the owner of each domain, over all pages.
  2. Given an active domain with its renewal on, When the watch runs, Then it receives the price of a renewal of one year and the currency.
  3. Given a registrar read that fails, When the watch runs, Then the watch fails and does not report that the domains left the account.

Edge Cases

  • A registrar can answer an order without the values of the order. The provider keeps the planned values and does not report a change after the apply.
  • A value can be unknown at plan time, such as the period or the nameserver group. The provider accepts the unknown value.
  • A domain that the account deleted, or that a transfer moved away, can stay in the listing. The graph and the watch do not count that domain as held.
  • A listing can match more than the exact name. The graph compares the full name.
  • A name below a registrable domain is not for sale. The check tells this before the registrar refuses the name.
  • An extension can need registry terms, or data about the owner, such as an acceptance flag. The order carries this data, and a later change of it does not make a new order.
  • An extension can have more than one label. The graph takes the longest suffix that the registrar knows.
  • A release that keeps the DNSSEC keys can leave DS records of the old zone. The release removes the keys.

Requirements (mandatory)

Functional Requirements

Account and configuration:

  • FR-001: The provider MUST take the credentials and the API address of the registrar from the environment. Credentials MUST NOT reach the state, the plan or the configuration.
  • FR-002: The API address MUST be configurable, so that a test can use a fake of the registrar API.
  • FR-003: The provider MUST be usable at an exact version, with its schema committed to the repository.

Availability and holdings (graph capabilities):

  • FR-004: The graph MUST be able to read, for a domain, if it is available and if it is premium.
  • FR-005: The graph MUST be able to read the domains of the account by full name, with the status of each. The graph MUST be able to tell apart these conditions: not held, active, in progress, refused and deleted.
  • FR-006: The graph MUST be able to read which contact roles an extension has, and if a name is registrable.
  • FR-007: A failed read MUST fail the plan. A failed read MUST NOT count as a domain that is free, not held, or absent.

Registration and transfer in (graph capabilities):

  • FR-008: The provider MUST register a domain with a period, contacts by handle, nameservers and DNSSEC keys.
  • FR-009: The provider MUST take a cost limit on a registration and on a transfer in, in minor units of a stated currency. It MUST compare a live quote to the limit before it orders. It MUST fail and order nothing on a quote above the limit, a quote in a different currency, or no quote.
  • FR-010: The provider MUST report the cost, the expiry and the status of the domain.
  • FR-011: The provider MUST transfer a domain in with an auth code. The attribute MUST be sensitive, the provider MUST also take the code as a write-only attribute that does not reach the plan or the state, and a transfer in MUST take the same data as a registration.
  • FR-012: The provider MUST order a registration or a transfer at most one time for a domain. A later apply, a transfer in progress and an import MUST NOT order again.
  • FR-013: The provider MUST let the order carry the data of the extension, such as registry terms. A later change of this data MUST NOT make a new order.

Delegation and DNSSEC (graph capabilities):

  • FR-014: The provider MUST set the nameservers of a domain, as a list or as a named group. A provider with a named group MUST keep the group when a deployment that uses it ends, and MUST let the graph adopt a group that exists.
  • FR-015: The provider MUST publish glue for a nameserver under the domain, with IPv4 and IPv6 addresses.
  • FR-016: The provider MUST set the DNSSEC keys of a domain, as DS or as DNSKEY records. An empty list MUST remove the keys, and an unset value MUST keep them.
  • FR-017: The graph MUST run the steps in the order that the registrar needs, for example a registration before the glue.

Lifecycle (graph capabilities):

  • FR-018: The provider MUST import a domain by its name without an auth code and without a warning.
  • FR-019: The provider MUST set the renewal of a domain, and MUST change it in place.
  • FR-020: A destroy MUST NOT delete a domain at the registrar. A destroy MUST either leave the domain as it is, or turn its renewal off, as the configuration states.
  • FR-021: The provider MUST change the admin and tech contacts in place. A change of the domain name MUST be a planned replacement that the graph can refuse.
  • FR-022: The provider MUST keep a domain in the state until a successful read confirms that it is gone.
  • FR-023: The provider MUST accept unknown values at plan time, and MUST keep the planned values when the answer of the registrar omits them.

Contacts:

  • FR-024: The domain resource MUST name contacts by a reference. The domain resource MUST NOT take the personal data of a contact.
  • FR-025: The graph MUST be able to read if a handle exists at the account, and if a contact can own a domain of an extension. Only the answers MUST reach the state, and not the data of the contact.
  • FR-026: The account MUST be able to make a contact and change it. The state of a contact MUST be separate from the state of a deployment.

Account operations:

  • FR-027: A front end MUST be able to read if the account holds a domain, if the domain has a lock, and if the registry refused its transfer.
  • FR-028: A front end MUST be able to release a domain: remove the lock, remove the DNSSEC keys, and get the auth code. When the registry keeps no code, the operation MUST first get a new one.
  • FR-029: A front end MUST be able to retake a domain: reset the auth code and set the lock.
  • FR-030: A front end MUST be able to tell that no different release or retake came after a release. The registrar can keep a marker for this, or a different store can keep it.
  • FR-031: An auth code that a front end receives MUST NOT reach the state of a deployment.
  • FR-032: A front end MUST be able to send a refused transfer to the registry again, without a new order.
  • FR-033: A front end MUST be able to change the renewal of a domain.
  • FR-034: A front end MUST be able to ask for a change of owner. The operation MUST NOT accept a fee. When the registrar needs a trade or a fee, the answer MUST say that the change is not allowed.
  • FR-035: A registrar operation MUST give the same answer format for each registrar.
  • FR-036: The watch MUST be able to list all domains of the account with the status, the expiry, the renewal state and the owner, and MUST be able to read the price of a renewal of one year.

Limits:

  • FR-037: The operator path MUST use only the resource types of the registrar that this contract needs. It MUST NOT use a resource type of the registrar for DNS records or for certificate orders.
  • FR-038: A registrar that cannot satisfy a requirement of the graph capabilities MUST NOT be selectable on the operator path.

Key Entities

  • Registrar account: The login at the registrar that holds the domains of a hosting provider.
  • Domain: A registration in the account, with its status, expiry, renewal, lock, nameservers, DNSSEC keys and contacts.
  • Quote: The live price of an operation on a domain, with its currency.
  • Contact handle: The reference at the registrar to a contact. The personal data stays at the registrar and in the state of the contact.
  • Auth code: The secret that lets a different account transfer a domain.
  • Marker: The value that tells which release is the last one.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: An apply against a fake of the registrar API orders nothing at a quote above the limit, at a quote in a different currency, or with no quote.
  • SC-002: A second apply of an unchanged configuration orders nothing.
  • SC-003: The state of a deployment holds no personal data of a contact and no credential of the registrar.
  • SC-004: A destroy of a deployment sends no call that deletes a domain.
  • SC-005: For each requirement of the graph capabilities, a selectable registrar has a test against a fake of its API.
  • SC-006: The output of each account operation has the same format for each selectable registrar.

Assumptions

  • A hosting provider uses one registrar account on the operator path.
  • A registrar has an API that gives a live quote, a status and an auth code. A registrar without these cannot be on the operator path.
  • The claims of a domain in the KV store do not depend on the registrar.
  • The status words of a registrar differ. The contract gives conditions, and each registrar maps its own words to these conditions.

Known Deviations

  • Only Openprovider is on the operator path. OPERATOR_REGISTRAR selects the registrar, and registrar-status.json maps its status words to the conditions of FR-005, but Openprovider is the only value that it accepts. The graph still names the Openprovider resources directly, so no seam selects other resources yet. Issue #1908 tracks this.
  • FR-006 and the owner rule of FR-025 go around the provider. A read beside the provider calls the Openprovider API. The provider reads an extension only by its exact name, which does not find the extension of a name, and the owner rule needs personal data that must not reach the state.
  • The account operations of FR-027 to FR-034 and the watch of FR-036 use the Openprovider API directly, and they do not use the provider. The commands that a front end runs do not change with the registrar, but no second registrar implements them.
  • The graph ignores a change of owner, so that a deploy keeps the owner that the account operation of FR-034 set.
  • The contact state uses the customer resource of the Openprovider provider. Openprovider does not let a handle change its name, so a change of a name makes a new handle. A destroy of a contact only removes it from the state.
  • The pin of the provider is a branch of a fork, which merges open upstream pull requests on the upstream main, and not a release.

Current providers

The committed schemas hold resources only, and they do not hold data sources. Thus the capabilities that use a read (FR-004, FR-005, the auth code) are not known for Porkbun and INWX from these schemas.

Capability Openprovider Porkbun INWX
Cost limit on a registration Yes Yes No
Transfer in with an auth code Yes No No
Contacts by a reference Yes, by handle No contact inputs Yes, by numeric ID
Nameservers Named group List per domain List on the domain
Glue Yes Yes Yes
DNSSEC keys DNSKEY, in the domain DS, per record DNSKEY, per key
Lock Set Read only Set
Renewal Set Read only Set
Expiry Yes Yes No