OpenCoreDev has released Domain SDK version 0.2.0, a TypeScript library that standardises how developers add, verify, and remove customer domains across five hosting platforms.
In this article
The scope
The tool handles Vercel, Cloudflare for SaaS, Railway, Render, and Netlify. It does not register domains, host DNS, or store tenant data. Your application retains all authorisation logic. The provider remains the source of truth.
The package runs server-side only to keep credentials out of browser code. It requires Node version 20 or higher and supports Bun. The distribution is ESM-only with a single runtime dependency: tldts.
The client interface
The API consists of seven methods available on a stateless client returned by createDomainClient(): add, get, refresh, list, verify, remove, and waitUntilActive. Every method accepts an AbortSignal. The add and remove functions are idempotent, meaning retries are safe.
Each platform is a separate entry point: ./vercel, ./cloudflare, ./railway, ./render, and ./netlify. A testing adapter is also included. Switching platforms requires changing the import statement, not the workflow.
Status is granular
Because DNS propagation, ownership checks, and TLS certificate issuance happen at different speeds, the SDK models these states separately. DomainStatus contains eight values:
pending, pending_dns, pending_verification, pending_certificate, active, misconfigured, failed, and unknown.
A Domain object also holds separate fields for verification (pending | verified | failed | unknown), certificate (pending | active | expiring | failed | unknown), and an issues array. Each issue includes a code, message, an optional record, and a retryable flag.
Record types follow the same pattern. DnsRecord lists type, name, value, an optional ttl, purpose, required, status, and an optional description. The purpose field is routing, ownership, certificate, or other. The status field is pending, valid, invalid, or unknown.
Consequently, a Vercel hostname surfaces three distinct records:
| Type | Name | Purpose |
|---|---|---|
| CNAME | app.customer.com | routing |
| TXT | _vercel.app.customer.com | ownership |
| TXT | _acme-challenge.app.customer.com | certificate |
Platform limits
While the lifecycle is normalised, platform restrictions remain:
| Capability | Vercel | Cloudflare SaaS | Railway | Render | Netlify |
|---|---|---|---|---|---|
| List domains | ✓ | ✓ | ✓ | ✓ | ✓ |
| Explicit verification | ✓ | — | — | ✓ | — |
| Managed certificates | ✓ | ✓ | ✓ | ✓ | ✓ |
| Apex domains | ✓ | — | ✓ | ✓ | ✓ |
| Wildcard domains | — | — | — | ✓ | — |
| Automatic customer DNS | — | — | — | — | — |
Cloudflare for SaaS requires a CNAME target, so the adapter does not claim apex support. Render alone accepts wildcard hostnames. No adapter edits customer DNS automatically.
Instead of hard-coding these limits, read client.capabilities at runtime. It exposes list, explicitVerification, managedCertificates, apexDomains, and wildcardDomains.
Working with the client
The following snippet type-checks and runs against 0.2.0:
import { createDomainClient } from "@opencoredev/domain-sdk";
import { vercel } from "@opencoredev/domain-sdk/vercel";
export const domains = createDomainClient({
provider: vercel({
token: process.env.VERCEL_TOKEN!,
projectId: process.env.VERCEL_PROJECT_ID!,
}),
});
const domain = await domains.add("app.customer.com");
// Render every required record in your customer-facing DNS instructions.
const required = domain.records.filter((item) => item.required);
const active = await domains.waitUntilActive(domain.hostname, {
timeoutMs: 300_000, // default
intervalMs: 5_000, // default, minimum 250
onStatus: (current) => console.log(current.status),
});
console.log(active.certificate.status, active.verification.status);Polling is sequential. It defaults to a 300,000 ms timeout and a 5,000 ms interval. Intervals below 250 ms are rejected, and each state is reported through onStatus. When a provider returns retryAfter, the client waits at least that long before retrying.
Testing and agent support
For CI, an in-memory adapter never calls a real provider:
import { createDomainClient } from "@opencoredev/domain-sdk";
import { memoryProvider } from "@opencoredev/domain-sdk/testing";
const memory = memoryProvider();
const domains = createDomainClient({ provider: memory });
await domains.add("app.customer.com"); // status: pending_dns
memory.activate("app.customer.com");
const latest = await domains.refresh("app.customer.com");
console.log(latest.status === "active"); // trueIt also supports transition(), injected latencyMs, per-operation failures, and a calls log. createMockDomain, createMockDnsRecord, and createFailingProvider sit alongside it.
Separately, an agent skill installs the workflow into Codex, Claude Code, Cursor, and other compatible agents:
npx skills add opencoredev/domain-sdk --skill domain-sdkUse cases
- Source Read original →




