Custom domain verification for Cloudflare SaaS apps. Handles DNS verification, SSL certificate provisioning, and ongoing monitoring via Cloudflare Custom Hostnames.
Zero runtime dependencies. Works in Cloudflare Workers, Node.js, Deno, and Bun.
npm install @systemoperator/domainsimport { DomainVerifier } from '@systemoperator/domains';
const verifier = new DomainVerifier({
cfApiToken: env.CF_API_TOKEN,
cfZoneId: env.CF_ZONE_ID,
store: myDomainStore, // you implement this
verification: {
onSSLActive: async (hostname) => {
await sendEmail(user, `${hostname} is live!`);
},
onFailed: async (hostname, reason) => {
await logError(hostname, reason);
},
onStatusLost: async (hostname) => {
await alertUser(hostname, 'DNS records may have been removed');
},
},
});
// add a domain
await verifier.addDomain('blog.example.com', { wwwEnabled: true });
// run verification (in a Cloudflare Workflow)
await verifier.verify('blog.example.com', step);
// run verification (standalone, no workflow)
await verifier.verify('blog.example.com');
// recheck active domains (from a cron job)
const { checked, lost } = await verifier.recheckAll();You bring your own persistence. Implement this interface with your database of choice (Drizzle, Prisma, D1, raw SQL, etc):
import type { DomainStore, DomainRecord, DomainStatusUpdate } from '@systemoperator/domains';
const store: DomainStore = {
async save(domain: DomainRecord) { /* INSERT into your DB */ },
async get(hostname: string) { /* SELECT by hostname */ },
async updateStatus(hostname: string, update: DomainStatusUpdate) { /* UPDATE fields */ },
async listActive() { /* SELECT WHERE sslStatus = 'active' */ },
};The verify() method accepts an optional Cloudflare WorkflowStep for durable execution with automatic retries and sleep:
import { DomainVerifier } from '@systemoperator/domains';
export class DomainWorkflow extends WorkflowEntrypoint {
async run(event, step) {
const verifier = new DomainVerifier({ /* ... */ });
await verifier.verify(event.payload.hostname, step);
}
}Without a step, verify() uses setTimeout for waits, suitable for cron jobs or manual checks.
All helpers are exported individually for use without the DomainVerifier class:
import {
// DNS
lookupDNS, verifyCNAME, verifyDNS, checkSSL, normalizeDomain,
// Cloudflare API
createCustomHostname, getCustomHostname, deleteCustomHostname, listCustomHostnames, getSSLStatus,
// Domain helpers
isValidDomain, generateDNSRecords, isDomainReady, getDomainStatusSummary, shouldRedirect, extractSubdomain,
} from '@systemoperator/domains';const verifier = new DomainVerifier({
cfApiToken: '...',
cfZoneId: '...',
store: myStore,
verification: {
// retry settings
dnsRetries: 10, // default: 10
dnsRetryInterval: 300000, // default: 5 min
sslRetries: 20, // default: 20
sslRetryInterval: 600000, // default: 10 min
initialDelay: 300000, // default: 5 min (DNS propagation wait)
// lifecycle hooks
onDNSVerified: async (hostname) => { },
onSSLActive: async (hostname) => { },
onFailed: async (hostname, reason) => { },
onStatusLost: async (hostname) => { }, // was active, now broken
onRetry: async (hostname, step, attempt) => { },
},
});MIT - System Operator LLC