Skip to content

systemoperators/domains

Repository files navigation

@systemoperator/domains

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.

Install

npm install @systemoperator/domains

Quick start

import { 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();

DomainStore interface

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' */ },
};

Cloudflare Workflow integration

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.

Standalone utilities

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';

Configuration

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) => { },
  },
});

License

MIT - System Operator LLC

About

Custom domain verification for apps deployed on Cloudflare

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published