Domains

Domains, subdomains, aliases and parked domains of each hosting account, with ownership verification.

In preview domains version 0.1.0 Part of Websites and PHP

Actions

5 actions, callable from the panel, the command palette and the API as POST /api/v1/a/<id>. Internal actions used between modules are not listed.

ActionWhat it doesRiskPreview
domains.create Add a domain low
domains.list List domains (read) low No dry run
domains.get Get a domain (read) low No dry run
domains.verify Check domain ownership low No dry run
domains.delete Remove a domain high

Permissions and limits

Permissions

  • domains.manage Add and remove domains
  • domains.view View domains

Plan limits

  • domains.count Domains (incl. addon and parked)
  • domains.subdomains Subdomains

Engineering notes

Generated from modules/domains/docs.md at build d90e9e2. These are the notes the engineers keep next to the code: precise, technical, and honest about what is not done yet.

Owns domain names and who they belong to; web, dns, mail, ssl and cdn reference domains by id and react to domains.created / domains.deleted through manifest subscriptions (ADR 0011 §3).

Patterns on top of modules/accounts (copy these):

  • Dry run: domains.create and domains.delete declare dry_run: true; when c.DryRun the handler validates everything, changes nothing and returns a module.Plan (POST /api/v1/a/domains.create?dry_run=1).
  • Inverse: domains.create declares inverse: domains.delete for the change timeline / assistant rollback.
  • Resources + scoped grants: resources: [{kind: domain}]; sub-users see/manage only domains granted to them (c.Can("domains.view", "domain", id)).
  • Durable subscription: subscriptions: [{event: accounts.deleted, action: domains.on_account_deleted}]; the action is internal: true (hidden from the API) and idempotent (a redelivery finds nothing left).
  • Cross-module checks: a reseller's access to an account is checked by calling accounts.get (which applies the subtree rule) instead of duplicating it.
  • Names are normalised with IDNA (UTS-46) -> stored as lower-case punycode, shown as Unicode.

Verified in the lab (2026-10-09): dry run returns a plan and creates nothing; Bücher.lab.test stored as xn--bcher-kva.lab.test; invalid label rejected; internal action -> 404 from the API; deleting the account removed its domain via the event within seconds.

Next: UI; per-plan auto-provisioning (website + zone + mail domain on create) lives in those modules' subscriptions.