OpenCoreDev Releases Domain SDK 0.2.0: One TypeScript API to Add, Verify, and Remove Customer Domains Across Five Platforms


Custom domains are a standard SaaS feature. Yet every hosting platform exposes a different API for them. OpenCoreDev has published Domain SDK, a TypeScript client that normalizes that work. Version 0.2.0 reached npm a day after the first release.

What is Domain SDK?

Domain SDK covers Vercel, Cloudflare for SaaS, Railway, Render, and Netlify. You add a hostname, show the exact DNS records, then track provider state until it is ready.

It does not register domains, host DNS, deploy apps, proxy traffic, or store tenant data. Your application keeps tenant authorization and state. The provider stays the source of truth.

The package is server-side only, so credentials never reach browser code. engines.node is >=20, and Bun is supported. It is ESM-only and ships one runtime dependency, tldts.

The client surface

Within that scope, the API is small. createDomainClient() returns a stateless client with seven members: add, get, refresh, list, verify, remove, and waitUntilActive. Every method accepts an AbortSignal. Add and remove are idempotent, so retries are safe.

Each adapter is a separate entry point: ./vercel, ./cloudflare, ./railway, ./render, ./netlify, and ./testing. Changing platforms means changing the import, not the workflow.

Status is a union, not a boolean

Because DNS, ownership, and TLS finish at different times, the SDK models them separately. DomainStatus carries eight values:

pending, pending_dns, pending_verification, pending_certificate, active, misconfigured, failed, unknown.

A Domain also holds verification (pending | verified | failed | unknown), certificate (pending | active | expiring | failed | unknown), and an issues array. Each issue has a code, message, optional record, and a retryable flag.

Records are typed the same way. DnsRecord has type, name, value, optional ttl, purpose, required, status, and optional description. purpose is routing, ownership, certificate, or other. status is pending, valid, invalid, or unknown.

Consequently, a Vercel hostname surfaces three separate records:

TypeNamePurpose
CNAMEapp.customer.comrouting
TXT_vercel.app.customer.comownership
TXT_acme-challenge.app.customer.comcertificate

Provider compatibility

While the lifecycle is normalized, platform limits are not:

CapabilityVercelCloudflare SaaSRailwayRenderNetlify
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.

Rather than hard-coding those 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"); // true

It 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-sdk

Use cases

  • Multi-tenant SaaS on Vercel: A customer maps app.customer.com. Your settings page renders required CNAME and TXT records with live status.
  • Cloudflare for SaaS at scale: Custom Hostnames live inside one zone. The adapter keeps that scoping explicit.
  • Tenant subdomains you own: createSubdomainClient maps labels under a base domain. reservedLabels blocks www, api, and admin.
  • Provider migration: Swap the adapter import and keep the same lifecycle calls.

Interactive Explainer



Source link

  • Related Posts

    PrismML Releases Bonsai 27B: 1-bit and Ternary Builds of Qwen3.6-27B That Run on Laptops and Phones

    ” style=”width:100%;border:0;height:600px;display:block;overflow:hidden;” scrolling=”no” loading=”lazy” title=”Bonsai 27B interactive explainer”> That architecture shapes the compression method below. How the Compression Works Each weight is a code, with one shared FP16 scale per…

    Mistral Vibe for Code vs Claude Code vs Cursor vs Codex: Four Agents Scored on One Scaffold-to-PR Task

    Coding agents are the most contested category in developer tooling right now. Four names dominate the shortlist: Mistral Vibe for Code, Claude Code, Cursor, and OpenAI Codex. Each claims to…

    Leave a Reply

    Your email address will not be published. Required fields are marked *