Custom Domains API

The custom domains API manages per-organization custom domain mappings. Organizations on the Enterprise plan can serve ProBeya under their own branded domain (e.g., ops.mycompany.com) instead of the default {slug}.probeya.com subdomain. All procedures require organization-level authentication (orgProcedure).

Domain Lifecycle

Custom domains follow a strict status progression:

pending_dns --> dns_verified --> ssl_provisioning --> active
StatusDescription
pending_dnsDomain registered, awaiting DNS CNAME configuration
dns_verifiedDNS CNAME verified via server-side lookup
ssl_provisioningSSL certificate issuance in progress (ACME/Let’s Encrypt)
activeDomain is fully configured and serving traffic

customDomains.list

List all custom domains configured for the organization. Returns domains ordered by creation date descending (newest first).

Type: Query

Input: None (uses organization context from session)

Output:

Array<{
  id: string;              // cuid2 record identifier
  organizationId: string;
  domain: string;          // e.g., "obeya.yourcompany.com"
  status: string;          // "pending_dns" | "dns_verified" | "ssl_provisioning" | "active"
  cnameTarget: string;     // e.g., "acme-corp.custom.probeya.com"
  verifiedAt: Date | null;
  createdAt: Date;
  updatedAt: Date;
}>

customDomains.create

Register a new custom domain for the organization. Validates platform-wide uniqueness (a domain can only belong to one organization), generates the CNAME target ({orgSlug}.custom.probeya.com), and returns setup instructions for the administrator.

Type: Mutation

Input Schema:

{
  domain: string;  // Fully qualified domain name (e.g., "obeya.yourcompany.com")
}

Output:

{
  id: string;
  domain: string;
  status: "pending_dns";
  cnameTarget: string;       // e.g., "acme-corp.custom.probeya.com"
  instructions: string;      // Human-readable CNAME setup instructions
  createdAt: Date;
}

Error Codes:

CodeCondition
CONFLICTDomain is already registered by another organization
FORBIDDENCaller is not an admin or owner
NOT_FOUNDOrganization not found

Side Effects:

  • Logs custom_domain.created activity with domain and CNAME target metadata.

customDomains.verify

Trigger a server-side DNS CNAME lookup to verify the administrator has correctly configured their DNS. The server performs its own lookup using dns.promises.resolveCname rather than trusting client-reported results.

Type: Mutation

Input Schema:

{
  domainId: string;  // cuid2 identifier of the domain record
}

Output (success):

{
  verified: true;
  domain: { ... };    // Updated domain record with status "dns_verified"
  message: "DNS verification successful. You can now provision an SSL certificate.";
}

Output (failure):

{
  verified: false;
  message: string;    // Explains what went wrong (wrong target, lookup failed, etc.)
}

Error Codes:

CodeCondition
NOT_FOUNDDomain not found or belongs to another organization

Side Effects:

  • On success: updates domain status to dns_verified, sets verifiedAt timestamp.
  • On success: logs custom_domain.verified activity.

customDomains.provisionSsl

Initiate SSL certificate provisioning for a DNS-verified domain. The domain must be in dns_verified status before SSL can be provisioned. In production, this triggers a background ACME job; the domain status updates to active when the certificate is issued.

Type: Mutation

Input Schema:

{
  domainId: string;  // cuid2 identifier of the domain record
}

Output:

{
  domain: { ... };   // Updated domain record with status "ssl_provisioning"
  message: "SSL provisioning has been initiated. This process typically takes 1-5 minutes.";
}

Error Codes:

CodeCondition
NOT_FOUNDDomain not found or belongs to another organization
PRECONDITION_FAILEDDomain is not in dns_verified status

Side Effects:

  • Updates domain status to ssl_provisioning.
  • Logs custom_domain.ssl_provisioned activity with previous and new status.

customDomains.delete

Permanently remove a custom domain registration. This is a destructive operation that can break existing bookmarks and links if the domain was active. Requires owner role (stricter than create, which allows admin) because of the higher blast radius.

Type: Mutation

Input Schema:

{
  domainId: string;  // cuid2 identifier of the domain record
}

Output:

{
  deleted: true;
  message: "Custom domain \"ops.mycompany.com\" has been removed.";
}

Error Codes:

CodeCondition
NOT_FOUNDDomain not found or belongs to another organization
FORBIDDENCaller is not an owner (admin is not sufficient for deletion)

Side Effects:

  • Logs custom_domain.deleted activity with domain and status at time of deletion.

Permissions

ProcedureRequired Role
customDomains.listAny authenticated member
customDomains.createAdmin or Owner
customDomains.verifyAny authenticated member
customDomains.provisionSslAny authenticated member
customDomains.deleteOwner only

Security

  • All procedures are scoped via orgProcedure for tenant isolation.
  • Domain uniqueness is enforced platform-wide: no two organizations can claim the same domain.
  • DNS verification is performed server-side to prevent spoofing.
  • Tenant isolation is checked on every operation: the domain’s organizationId must match the caller’s context.

DNS Configuration Guide

After creating a custom domain, configure the following DNS record:

Record TypeHostTarget
CNAMEobeya.yourcompany.com{orgSlug}.custom.probeya.com

DNS propagation typically takes 5-30 minutes but can take up to 48 hours in some cases. Use the verify endpoint to check whether the CNAME has propagated.