Library / SDK
opencoredev/domain-sdk avatar
opencoredev/domain-sdk

domain-sdk normalizes six hosting providers and leaves tenancy to you

Add, verify, monitor, and remove customer domains with one TypeScript API.

372 stars19 forksTypeScriptMIT

At a glance

What is it?
A TypeScript client for adding, verifying and removing customer domains across Vercel, Cloudflare for SaaS, Railway, Render, Netlify and bunny.net, where the provider decides readiness and your application keeps ownership of who a tenant is.
Who is it for?
domain-sdk suits a SaaS that provisions a hostname per tenant and currently has one code path per hosting provider, since the add, list, verify and remove sequence stops being six integrations. It does not suit anyone who needs wildcard coverage on a provider that does not support it, or who wants the tenancy model solved for them, because the application remains responsible for tenant ownership and storing unique labels.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • opencoredev/domain-sdk372 ★ · +11
Star history
Repository2026-09-172026-11-13
opencoredev/domain-sdk361372

11 Stars in the last 57 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

One lifecycle across six providers, with their details intact

The problem this solves is narrow and real: a platform that gives each customer their own hostname has to add a domain, tell the customer which DNS records to create, know when it has actually taken effect, and eventually remove it, and each hosting provider exposes that under a different API with different record shapes. This client puts one lifecycle around adding, reading, listing, verifying and removing, across Vercel, Cloudflare for SaaS, Railway, Render, Netlify and bunny.net.

Normalization stops at the lifecycle boundary. Each provider lives behind its own entry point and the platform-specific DNS and verification details are preserved, because a customer-facing instruction screen cannot be generic: a CNAME on one platform and a TXT verification record on another are not interchangeable strings.

Installation is one dependency, and the constraint on it is explicit: server-side only, Node 20 or newer or Bun, with provider credentials kept out of browser code.

bash
npm install @opencoredev/domain-sdk

That last line is the security boundary of the whole library. It holds tokens for six hosting providers, so a copy of it in a client bundle is a copy of your infrastructure credentials.

The required flag is what a customer sees on screen

Adding a domain returns a record list, and each record carries a `required` flag. That flag is the whole point of the shape:

ts
import { createDomainClient } from "@opencoredev/domain-sdk";
import { vercel } from "@opencoredev/domain-sdk/vercel";

const domains = createDomainClient({
  provider: vercel({
    token: process.env.VERCEL_TOKEN!,
    projectId: process.env.VERCEL_PROJECT_ID!,
  }),
});

const domain = await domains.add("app.customer.com");
const active = await domains.waitUntilActive(domain.hostname);

A UI can render type, name and value for the records that matter and stay quiet about the rest, instead of showing a customer a wall of DNS entries and hoping they pick the right ones. The same call hands back the hostname you then wait on, so the add step and the wait step share an identifier rather than requiring you to reconstruct the domain string.

The errors on this path are normalized and retry-aware, and add and remove are idempotent, which matters more than it sounds: a customer clicking the confirm button twice should not produce two domains or an error on the second attempt.

Readiness comes from the provider, and polling is sequential

`waitUntilActive` is where most homegrown implementations get the semantics wrong, so it is worth reading the description rather than the name. Verification and certificate status are provider-authoritative, and the stated goal is no false readiness: the client does not declare a domain live because DNS resolves somewhere, it waits until the provider agrees.

The polling is sequential, with timeouts, cancellation, callbacks and provider backoff. Sequential is the honest choice when a provider rate-limits status checks, and backoff is what keeps a pending domain from consuming your quota while it waits. Callbacks are the part to plan for: you have somewhere to report progress between the add call and the active state, which is often minutes of certificate issuance that a plain HTTP handler has no good way to express.

The practical consequence for an application is that this is not a request-scoped operation. If you provision during a signup request, you need the callback path or a background job, and the cancellation hook is what lets a user abandon a half-finished domain without leaving an orphaned record.

Reserved labels exist, tenant ownership does not

For subdomains under a parent domain you already control, there is a second client that takes a namespace and a list of labels it must not hand out:

ts
import { createSubdomainClient } from "@opencoredev/domain-sdk";

const subdomains = createSubdomainClient({
  domainClient: domains,
  baseDomain: "mydomain.com",
  reservedLabels: ["www", "api", "admin"],
});

subdomains.toHostname("customer"); // customer.mydomain.com

The reserved list is a guardrail against a tenant claiming `admin.yourdomain.com`, and it is worth reading as exactly that: a list you maintain, not a policy engine. The library states the boundary itself, saying your application remains responsible for tenant ownership and unique label storage. Nothing here prevents two tenants being assigned the same label, or stops a label that was freed earlier from being reused.

Wildcards are the other branch. `provisionWildcard()` is called once where the provider supports wildcards, and `add()` with a bare label provisions per hostname where it does not. That difference decides how your DNS instructions are worded, since a wildcard covers every label and a specific record covers one.

The testing adapter never touches a provider

One of the six providers is an in-memory adapter that never calls a real provider, and for a library whose failure modes are DNS timing and provider-specific record shapes, that is the entry point to start from.

Tests written against it run without credentials and without network, which means the normalized lifecycle, the required-flag filtering and the error normalisation can all be exercised in ordinary unit tests. What it cannot exercise is the part that breaks first, since real providers disagree about what counts as active and how long a certificate takes.

So the honest division is this: the adapter proves your code calls the SDK correctly, and only a staging domain proves it works against the provider. Both are needed, and the SDK's own claims about verification come from the second kind of test, not the first.

Three Node floors live in one repository

The runtime requirements are not uniform, which is the sort of thing that surfaces on a release day rather than on a first install. The published SDK targets Node 20 or newer, or Bun. The release tooling requires Node.js 24 or newer. The development toolchain is pinned to Bun through `packageManager` at `[email protected]`.

The repository is a Turborepo monorepo whose root `package.json` is private and named `domain-sdk`, with `apps/*` and `packages/*` workspaces, while the published package is `@opencoredev/domain-sdk`. Release commands are filtered to that package by name, so the filters are the part to copy correctly if you fork this.

The docs site runs through a sudo-free proxy called Portless on port 1355, with `PORTLESS=0 bun run dev` to bypass it. That is a small convenience for local HTTPS, and worth knowing before you conclude your own certificate tooling is broken.

One release command, and a lock it keeps when publishing fails

Releasing is described as a changelog plus a command. You add a Tegami changelog entry under `.tegami/` and run:

bash
bun run release

Underneath, that single command runs the SDK's lint, type check and tests, versions it with Tegami, builds the package, publishes to npm, and then removes the completed publish lock. The failure handling is the part worth borrowing: if publishing fails, Tegami keeps the lock in place so `bun run tegami publish` can retry safely, rather than leaving a half-finished release that the next person cannot tell apart from a clean one.

The local development loop is the usual set, `bun install` then `bun run dev`, `bun run test`, `bun run check-types`, `bun run lint` and `bun run build`. There is also a shadcn-style component registry in `registry.json`, validated and built through the shadcn CLI, which is how the documentation site's components are assembled.

The licence is MIT, the repository is not archived, and the last push was on 2026-09-24 with no GitHub releases, so npm rather than the repository is where versions are published.

Editorial conclusion

domain-sdk suits a SaaS that provisions a hostname per tenant and currently has one code path per hosting provider, since the add, list, verify and remove sequence stops being six integrations. It does not suit anyone who needs wildcard coverage on a provider that does not support it, or who wants the tenancy model solved for them, because the application remains responsible for tenant ownership and storing unique labels. Verify first that your provider is on the list, that your DNS instructions can be driven from the returned records rather than hardcoded, and which Node version your release pipeline has, since the SDK runs on 20 or newer while the release tooling needs 24.

Frequently asked questions

Which hosting providers does domain-sdk support?

Vercel, Cloudflare for SaaS, Railway, Render, Netlify and bunny.net, plus an in-memory testing adapter that never calls a real provider. Each provider sits behind its own entry point and keeps its platform-specific DNS and verification details.

How do I install domain-sdk and what does it need?

`npm install @opencoredev/domain-sdk`, with Node 20 or newer or Bun. It is server-side only, and provider credentials must stay out of browser code.

What does the required flag on a DNS record do?

It marks the records a customer has to create, so an interface can print type, name and value for those and ignore the rest. That is how the returned records from `add()` become a customer-facing DNS instruction rather than a raw provider payload.

Does domain-sdk handle tenant ownership and subdomains?

It handles the subdomain namespace and a `reservedLabels` list so you can block labels like www, api or admin, and it exposes `provisionWildcard()` where the provider allows it. Tenant ownership and unique label storage stay the responsibility of your application.

How does domain-sdk decide a domain is ready?

Verification and certificate status come from the provider rather than from local DNS resolution, so readiness is not reported early. `waitUntilActive` polls sequentially with timeouts, cancellation, callbacks and provider backoff.

Official sources

  1. Issues
  2. License: MIT
  3. opencoredev/domain-sdk on GitHub
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/opencoredev-domain-sdk.svg)](https://hysenlabs.com/projects/opencoredev-domain-sdk)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/opencoredev-domain-sdk.svg)](https://hysenlabs.com/projects/opencoredev-domain-sdk)

Was this useful?